GTRE Sandbox
Geodesic Trajectory & Results Explorer
Load a Monte Carlo ensemble or synthesise one, play a 400-day arc back at whatever rate you can think in, inject a disturbance part-way through, and solve the correction that pulls it back. Drop in a verification table instead — step size against error, a reference column beside the one being checked, residuals over time — and every column becomes a channel you can plot, difference and read off. Everything runs in this tab. The file you open is never uploaded, never stored, and gone the moment you refresh.
The propagator on this page is JavaScript written for interactivity: an RK4 integrator with two-body gravity, an optional J2 term and an optional static exponential atmosphere. It is not the compiled core engine, it has not been through the stress campaign, and nothing it produces carries an execution certificate. Use it to build intuition and to shape a problem. When you need a result you can hand to a reviewer, call the API and take the certificate with it.
It explores results; it does not produce them. When you load a table from a run made somewhere else, this page plots it and differences it — it does not re-run the check, re-derive a number, or validate a claim. A value you read off here is only ever as good as the run that wrote the file. The same holds in the other direction: a trajectory this page generates is its own arithmetic, not a measurement of anything, and no result shown here has been through the stress campaign the engines have.
Nothing you load here leaves the browser. This page is served with
connect-src 'none', which means the browser itself refuses every
outbound request this document could try to make — read the policy in this page’s
<meta http-equiv> tag, or watch devtools block a request, if you would
rather check than be told. There is no
localStorage, no IndexedDB and no cookie: the samples live
in memory and a refresh drops them.
File.stream() a large ensemble would have to be read into a
single string. Current Firefox, Chrome, Edge and Safari all support both.
Source
CSV- source
- --
- runs
- --
- samples/run
- --
- arc
- --
- cadence
- --
- rows
- --
- units
- --
- velocity
- --
- resident
- --
- ingest
- --
- integrator
- --
Synthesise
NO FILE NEEDEDForce model
SANDBOXChannels
SERIESDisturbance
ΔvCorrection
IDLE- offset now
- --
- burn 1
- --
- total budget
- --
Readout
AT PLAYHEAD- elapsed
- --
- sample
- --
- altitude
- --
- radius
- --
- speed
- --
- |Δr| median
- --
- |Δr| p95
- --
- |Δr| max
- --
- full arc plays in
- --
Event log
Two file shapes
The console reads the header and decides which of two things it has. It does not go by the file extension, which says nothing.
- Ephemeris —
x,y,zplus a time column, optionally velocity and a run index. One trajectory or a whole Monte Carlo ensemble. Gets the orbital views, the transport and the disturbance chain. - Series — an x column and any number of named numeric channels. This is what a verification run emits: step size against error, a reference column beside the column being checked, RIC residuals, a density profile. Gets the channel explorer.
Anything that is not an ephemeris falls through to the series reader rather than
being refused. Being refused for lacking x,y,z was the single
biggest limit on what this page could be used for.
Ephemeris files
One row per sample. Columns are matched by name, not position, because every tool that exports an ephemeris orders them differently and reading a velocity into a position slot produces a plausible-looking wrong answer rather than an error.
run,t,x,y,z,vx,vy,vz
0,0.0,6878.000,0.000,0.000,0.000000,7.6127,0.000000
0,3600.0,-6683.4,1560.2,0.0,-1.72808,-7.39942,0.0
1,0.0,6878.140,0.062,-0.031,0.000021,7.6126,0.000004
...
- Time —
t,time,time_s,seconds,elapsed, or an ISO-8601epoch/utccolumn, which is converted to seconds from the first row. - Position —
x,y,z,rx,ry,rz,pos_x…, or with a_km/_msuffix. Without a suffix the magnitude decides: an Earth orbit is either ~6.8e3 or ~6.8e6 and there is no third possibility. - Velocity —
vx,vy,vzorx_dot,y_dot,z_dot(the OPM spelling). Optional; if absent it is recovered by central difference, which is reported in the manifest rather than done quietly. - Run —
run,case,trial,mc,member. Absent means a single trajectory.
Rows may be grouped by run or by epoch. Runs with a different sample count are linearly resampled onto the longest run's grid, and the count of runs that needed it appears in the log.
Series files — exploring a verification run
If you have run a check somewhere else and kept the numbers, this is where they go. Drop the table in and every column becomes a channel you can plot, compare and read off.
step_s,err_rk4,err_order8
600,5.000000e-03,5.000000e-03
300,3.125000e-04,1.953125e-05
150,1.953125e-05,7.629395e-08
...
- Any x column. A time-like name is used if present, otherwise the first column. It may ascend or descend — a convergence sweep is conventionally written with the step size falling, and that plots with the coarse end on the left. A column that is not ordered at all is demoted to the row index and the log says so.
- Log axis on |y|. An error column spanning 1e-19 to 1e-3 is a flat line against a linear axis. Sixteen decades is normal in this material.
- A − B. The question a comparison asks is never "what are these
two columns" but "how far apart are they". Pick the pair and the difference
is drawn in white with its
|max|and RMS beside it. - Gaps stay gaps. A blank or non-numeric cell becomes a break in the line, not a zero. A missing measurement must never read as a measurement of zero.
Series channels are held in float64, where ensemble samples are float32. That is not an inconsistency. The whole content of a comparison test can be a difference of 6.66e-16 between two numbers near 1, or a difference of exactly zero for a bit-exactness claim. Stored as float32 those two collapse onto the same value and the difference reads as 0 for the wrong reason — a false pass, which is worse than having no feature. Verified: two columns four ULP apart come back as 8.9e-15, not 0.
This explores results; it does not produce them. Nothing here re-runs a check or re-derives a number, and the sandbox is not the GDS Physics engine, so a value plotted here is only ever as good as the run that made the file. What the page adds is the ability to see it.
The plan console
Clicking through the panels is fine for one case. A rehearsal is not one case — it is a sweep, run the same way twice, with the outcome written down. So the whole plan goes in the editor and runs in one press.
A plan is a document: seeded, so it reproduces; asserted, so it has a verdict rather than a wall of numbers; and saveable, so the plan and the transcript it produced land on disk together. A result without the plan that produced it is not evidence of anything.
- Setup —
seed(n),forces(j2=, drag=, bc=, step_s=) - Data —
synthesize(runs=, days=, sma=, inc=, …),purge() - Operations —
inject(ric_ms=[r,i,c], at_day=, model=),solve(at_day=, horizon_rev=, two_burn=),fly() - Measurement —
dispersion(at_day=),status(), and for series fileschannels(),stat(name),diff(a, b) - Verdict —
check(label, condition)andreport() - Results —
record(name=value, …)collects a row per call; Download results writes them as a CSV in the same shape this page reads, so a sweep can be dropped straight back in and plotted
Every verb sets the same inputs a human would set and calls the same function the button calls. There is no second implementation of synthesise or of the targeting solver, so a plan and a click cannot disagree — and the panels visibly move while a plan runs, which is the difference between trusting the output and watching it happen.
Why the plan language is not quite Python
It is an interpreter for a subset of Python's syntax, written in JavaScript and running in this tab. It is not CPython, and the page says so rather than letting you find out.
The obvious alternative was Pyodide — real CPython compiled to
WebAssembly. It is genuinely good, and it is unavailable here for a reason that
is not negotiable: Pyodide fetches its own runtime and standard library over
the network at startup, and this page is served with
connect-src 'none' precisely so that it cannot make a request.
Shipping it would mean relaxing the one guarantee the sandbox is built around,
and downloading roughly ten megabytes to do it. The scripting is worth a lot;
it is not worth that.
Supported: variables, numbers, strings, lists, indexing (including
negative), arithmetic including ** and //,
comparisons, and / or / not with
short-circuit evaluation, for … in, if /
elif / else, break /
continue, calls with keyword arguments, attribute access on
results, pass, comments, and line continuation inside brackets.
Not supported: import, def, classes,
while, dictionaries, tuples, slicing, f-strings,
try/except, generators. The keywords are caught by name
and say what to use instead; dictionaries, tuples, slicing and f-strings are
rejected as a parse error rather than by name — while points you at
for … in range(), assert points you at
check(), import tells you every verb is already in
scope. None of them fails as a puzzling syntax error.
A misspelled keyword argument is an error, not a shrug. A plan that runs,
passes, and quietly did not do what its author wrote is the worst outcome
available here, so inject(ric_ms=…) spelled
ric_m_s stops and lists what it accepts.
HDF5 — pending
.h5 is not readable in this build, and it says so rather than half
working. A browser-side HDF5 reader means shipping a ~700 KB WebAssembly
build of the C library and adding 'wasm-unsafe-eval' to this page's
script policy — a real weakening of the one guarantee this page is built
around. That trade is worth making deliberately, not by default.
Until then, one line converts the file, and it stays on your machine:
# pandas, from an HDF5 table to the CSV above
import pandas as pd
pd.read_hdf("ensemble.h5").to_csv("ensemble.csv", index=False)
If your ensemble lives in a plain array rather than a table, h5py
plus numpy.savetxt does the same job in three lines. Tell us which
shape yours is in and it will inform whether the WASM reader is worth the policy
change.
What the physics does and does not include
- Two-body gravity at GM = 398600.4418 km³/s², the same constant the OPM examples use.
- J2 oblateness, optional and on by default. It is the dominant perturbation in LEO by two orders of magnitude and a 400-day arc without it is not worth looking at.
- Drag, optional, from a static piecewise-exponential atmosphere with no solar-activity term. Density at 400 km moves by more than an order of magnitude across a solar cycle, so this produces a plausible shape, not a prediction. The API's decay engine does not work this way.
- Not modelled: third-body gravity, solar radiation pressure, tides, higher-order geopotential terms, mass depletion. A burn here is constant-mass.
The step size is not a performance knob, it is an accuracy one, and the console measures the consequence rather than assuming it. Every ensemble reports its steps per revolution and the secular semi-major-axis error its energy drift implies. On a 94-minute orbit over 400 days — about 6,100 revolutions — a 120 s step loses 6% of the orbital energy and the satellite spirals in by 400 km from truncation error alone, which on screen is indistinguishable from atmospheric decay. That is why it is called out in red rather than left for you to find.
Smaller is not monotonically better. Below roughly period/280
(~20 s here) the truncation error drops under the floating-point round-off
floor, and taking more steps then accumulates more round-off: measured drift
bottoms out near 130 ppm and rises again at 15 s and 10 s. The
defaults sit at that minimum.
The Linear (CW) propagation option is not a faster approximation to be preferred — it is there to be compared against. Run a small burn both ways over ten days and the two are indistinguishable; run a large one over two hundred days and the linear answer is wrong by hundreds of kilometres. Watching where the agreement fails is worth more than being told that it does.
The correction, and where it breaks
Targeting is Clohessy–Wiltshire: given the relative state in RIC and a
horizon T, the impulse that drives relative position to zero at
t+T is -inv(Φrv)·Φrr·δr, minus
whatever relative velocity you already had.
The in-plane determinant reduces to
8(1-cos nT) - 3nT·sin nT, which is zero at exactly one
orbital period. Target a full revolution ahead and no impulse can null the
offset, because after one period every in-plane impulse has returned you to where
you started. Real targeters hit this and emit an enormous garbage burn; this one
detects it, refuses, and offers three horizons that are well conditioned. The
cross-track block goes singular at every half period for the same reason.
The solved burn is commanded from one estimate and then flown by every run in the ensemble, which is what an operator actually does. That is why the dispersion after the correction does not collapse to zero — and why the residual is the number worth looking at.
Both burns are flown when the two-burn box is ticked. Unticking it is instructive rather than cheaper: one impulse drives relative position to zero at the horizon and says nothing about relative velocity, so the spacecraft arrives at the plan already moving away from it and diverges faster afterwards than if nothing had been done.
Why the in-track case is the hard one
Set the disturbance to pure cross-track and the correction is well behaved: the out-of-plane motion is bounded and oscillatory, and it costs roughly what the disturbance cost — measured here, 0.058 m/s to undo 0.05 m/s.
Set it to pure in-track and everything changes. An in-track impulse changes the orbital period, and a period error integrates into an along-track offset that grows linearly and without bound: 0.05 m/s becomes ~39 km in three days and ~260 km in twenty. Worse, nulling that offset inside one revolution demands a large radial impulse — about 3 m/s, sixty times the disturbance that caused it. The manoeuvre does work: the console measures 12.75 km reduced to 416 m at the target epoch, a 97% cut. But the remaining 3% is the linearisation error on a 3 m/s burn, and that residual regrows.
So repeating a short-horizon correction does not converge — each pass fires another large impulse carrying another few percent of error. That is not a defect in the solver, and the zero-disturbance case confirms it: with nothing to correct, the solver asks for 0.0038 m/s. It is the reason real operations phase an along-track error out over many revolutions with a small in-track burn instead of braking against it. Lengthen the horizon here and watch the budget fall.
Where your file goes
Nowhere. Specifically:
- The page is served with
connect-src 'none'. Even a compromised script on this page could not open a socket — the browser refuses before the request is made. Check it in devtools rather than taking our word. - Parsing happens in a Web Worker in this tab. The
Filehandle is read withFile.stream(); the bytes go into typed arrays and nowhere else. - A refresh is irreversible, so the browser asks first. Because nothing here is saved, there is no autosave to fall back on and no undo — a 400-day ensemble that took six seconds to parse is simply gone. Whenever an ensemble is loaded, leaving or reloading raises the browser's own confirmation. It is armed only when there is something to lose, and the Purge button disarms it, because a deliberate discard needs no warning. We chose the prompt over the obvious alternative: keeping your file across a refresh would mean copying it into browser storage or holding a handle to it, and the guarantee below is worth more than the convenience.
- This page writes no
localStorage,sessionStorage,IndexedDB, cookie or cache entry. A refresh, a tab close or the Purge button drops every array. For completeness, because you can see it in devtools: the rest of the site stores one key,gds:zoom, holding a page-size preference. This page neither loads the script that sets it nor reads it — which is also why the size control in the status rail is absent here. A fixed-layout console is sized against a real pixel budget, and scaling the document would reflow it rather than magnify it. - Download format template writes a file to your own disk through a
blob:URL. That is a local write, not a send.
This is the same posture as the rest of the platform, arrived at from the other direction: the API holds nothing because it computes and discards, and this page holds nothing because it never had anywhere to put it.
Reproducibility and keys
Every dispersion is drawn from a seeded generator and the seed is on screen. Same seed, same ensemble, on any machine — a dispersion you cannot reproduce is an anecdote. Execution error on the burns is seeded separately and derived from the same value, so changing the seed moves both together.
Samples are held as float32, about a metre of resolution at LEO
radius, which halves the memory of a large ensemble. Integration, the RIC
transforms and the targeting solve are all double precision. The only place the
choice is visible is a miss-distance readout below a few metres.
No API key is involved anywhere on this page, because no request is made. When you want a signed result, the endpoints and the certificate format are in the technical documentation, and access is arranged through the developer portal.