ENGINE STATUS NOMINAL MISSION CUSTODY ZERO CLASS ADVISORY
UTC --:--:--
TOOLS //GTRE SANDBOX //OPERATION MANUAL

Operation manual

Geodesic Trajectory & Results Explorer

Every control, what each number means, how to run a test from the panels or from a written plan, and how to tell a real result from an artefact of your own settings. Written to be read beside the tool — open the sandbox in another tab.

1What this is, and its three limits

GTRE is a trajectory and results explorer that runs entirely in a browser tab. It loads a Monte Carlo ensemble or a results table, plays a long arc back at a rate you can think in, lets you inject a disturbance and solve the correction for it, and lets you write the whole exercise down as a plan and run it.

Three limits govern everything below. None of them is a temporary gap.

Limit 1 — it is not the GDS Physics engine

The propagator here is a JavaScript RK4 with two-body gravity, an optional J2 term and an optional static 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 shape a problem and build intuition. When you need a result a reviewer will accept, call the API.

Limit 2 — it explores results, it does not produce them

Load a table from a run made elsewhere and GTRE will plot it, difference it and summarise it. It will not re-run the check, re-derive a number, or validate a claim. A value you read here is only ever as good as the run that wrote the file.

Limit 3 — nothing is saved

There is no autosave and no undo. A refresh discards everything, which is why the browser asks before you do it. That is the price of the guarantee in §12, and it was chosen deliberately over keeping your file in browser storage.

2Quick start — first run

You do not need a file. The fastest way to see the tool work is to let it build an ensemble and run the plan it ships with.

  1. Open the sandbox. Nothing is loaded; the viewport reads NO ENSEMBLE LOADED.
  2. Scroll to Plan console at the bottom. It opens with the CAM rehearsal example already in the editor.
  3. Press Run plan (or Ctrl+Enter in the editor).
  4. Watch the panels above move while it runs — the plan drives the same controls you would click. It takes about twenty seconds.
  5. Read the verdict bar under the transcript. A clean run reports PASS · 8 checks · 8 passed · 0 failed and the time it took.
  6. Press Save transcript to write the plan and its output to your disk as one text file.
  7. Press Download results — the example records a row per case, so you get a CSV of Δv against burn cost and before/after dispersion. Drop that file back on the page and it plots, because it is written in the same shape the reader parses. See §7.

If you would rather drive it by hand, press Generate ensemble in the Synthesise panel instead, then follow §6.

3The console, panel by panel

Three columns. Data and configuration on the left, the viewport and transport in the middle, operations and readouts on the right. Below them, the plan console. On a narrow screen the columns stack in that order.

Source — left column

ControlWhat it does
Drop zoneClick or drag a .csv onto it. The header decides whether it is read as an ephemeris or as a channel series — see §4.
Keep every NthDecimation on load. Set above 1 to read a large file at coarser resolution. Applied to rows as they stream, so it lowers memory as well as detail.
Cap MiBWorking-set limit. Ingest aborts with a clear message rather than exhausting the tab. Default 512.
ManifestAppears once something is loaded. Every field is explained in §10.
Reference runWhich run of the ensemble counts as “the plan”. Dispersion, the RIC view and the correction are all measured against it. Default 0, which for a synthesised set is the undispersed nominal.
Download format templateWrites the reference run to your disk as a CSV in the exact shape the reader expects. The quickest way to see the format.
Purge everything nowDrops every array immediately and disarms the refresh warning. A deliberate discard.

Synthesise — build an ensemble with no file

FieldDefaultMeaning
Runs32Members of the ensemble. Run 0 is always the undispersed nominal.
Arc d400Length of the arc in days.
Step s20Integration step. This is an accuracy control, not a speed control — see §10.
Sample s3600How often a state is stored. Independent of the step: the integrator runs fine and records coarse.
SMA km6878Semi-major axis. 6878 km is roughly a 500 km circular orbit.
Ecc0.001Eccentricity.
Inc °51.6Inclination.
σ pos m50Per-axis position dispersion applied to every run except run 0.
σ vel m/s0.01Per-axis velocity dispersion. This dominates long-arc spread — a velocity error becomes a period error, and a period error grows without bound along track.
Seed20260811Same seed, same ensemble, on any machine. A dispersion you cannot reproduce is an anecdote.

Force model

  • J2 oblateness — on by default. It is the dominant perturbation in LEO by two orders of magnitude; a long arc without it is not worth looking at.
  • Atmospheric drag — off by default. When on, uses 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 gives a plausible shape, not a prediction.
  • Ballistic coeffCd·A/m in m²/kg. Only active with drag on. Default 0.02.
  • Integration step — the step used for disturbance and correction arcs, separately from the synthesis step. Default 20 s.

Disturbance — right column

ControlDefaultMeaning
RIC / ECIRICFrame the vector is given in. RIC is radial / in-track / cross-track, which is what operators think in.
R, I, C m/s0, 0.05, 0The commanded impulse. In-track is the interesting one: it changes the orbital period, and a period error integrates into an unbounded along-track offset.
σR, σI, σC0.002 eachPer-axis execution error, drawn per run. A perfectly executed burn is not a Monte Carlo, and this is most of what the residual after a correction is made of.
Inject at day10% of arcSet from the loaded arc, not a fixed number.
Sustained, not impulsiveoffSpreads the same total Δv over a burn window as constant acceleration — a stuck thruster rather than a commanded impulse.
Burn duration s600Only active when sustained is ticked.
RK4 / Linear (CW)RK4Full integration, or the closed-form Clohessy–Wiltshire propagation. The linear option is there to be compared against, not preferred — see §10.
Inject and propagateApplies the impulse to every run and reintegrates to the end of the arc. Switches the viewport to RIC dispersion.

Correction

ControlDefaultMeaning
Burn at dayinject + 2 dWhen the correction is commanded. Must be after the injection; the field nudges itself if you set an earlier injection.
Horizon h0.75 revWhen the offset should be nulled. Set from the orbit’s own period on load. Snapped to a whole sample on solve.
Reference / Ensemble meanReferenceWhich estimate the burn is computed from. One burn is commanded and then flown by every run, which is what an operator actually does.
Second burn at arrivalonCancels the arrival velocity. Unticking is instructive rather than cheaper — see the note below.
Solve correctionComputes the burn and reports it. Changes nothing.
Fly itApplies the solved burn (both burns if solved that way) to every run and reintegrates.
Why solve and fly are separate

Solving tells you what a manoeuvre would cost. Flying changes the ensemble. If one button did both, every exploratory solve would destroy the arc you were studying.

Readout and Event log

Readout shows the reference run at the playhead — elapsed time, sample index, altitude, radius, speed, its position vector, and the ensemble’s |Δr| median, p95 and max against the plan. Full arc plays in tells you how long the current rate will take end to end.

Event log is the running commentary: what was loaded, what was measured, and every warning. It is worth reading rather than dismissing — most of the traps in §10 announce themselves here first.

4Loading a file

Drop a CSV on the Source panel. The reader inspects the header and takes one of two shapes. It does not go by the file extension, which says nothing. Columns are matched by name, not position, because every tool orders them differently and reading a velocity into a position slot produces a plausible-looking wrong answer rather than an error.

Shape A — ephemeris

Recognised when x, y, z and a time column are all present.

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
...
  • Timet, time, time_s, seconds, elapsed, or an ISO-8601 epoch / utc column converted to seconds from the first row.
  • Positionx,y,z, rx,ry,rz, pos_x…, with or without a _km / _m suffix. Without one the magnitude decides: an Earth orbit is either ~6.8e3 or ~6.8e6 and there is no third possibility.
  • Velocityvx,vy,vz or x_dot,y_dot,z_dot (the OPM spelling). Optional; if absent it is recovered by central difference and the manifest says so.
  • Runrun, 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 that needed it appears in the log.

Shape B — series

Anything else. An x column plus any number of named numeric channels — which is what a verification run emits.

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
...
  • The x axis is a time-like column if one is named, otherwise the first column. It may ascend or descend — a convergence sweep is conventionally written with the step size falling.
  • A column that is not ordered at all is demoted to the row index, and the log says so.
  • 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.
Why series channels are float64 and ensemble samples are float32

Ensemble samples are display data; a metre of resolution at LEO radius is below anything that can be drawn, and float32 halves the memory of a large set.

A verification series is the opposite. The entire content of a comparison test can be a difference of 6.66e-16 between two numbers near 1, or exactly zero for a bit-exactness claim. In float32 those collapse onto the same value and the difference reads 0 for the wrong reason — a false pass, which is worse than having no feature at all.

HDF5

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 guarantee in §12. 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)

5Views and transport

Views

  • Inertial — Earth, the planned trace and the ensemble at the playhead. Drag to rotate, wheel to zoom. Only about three revolutions of trace are drawn: a 400-day arc is ~6,100 revolutions and the trace would be a solid annulus.
  • RIC dispersion — the ensemble in the reference’s rotating frame, in-track horizontal and radial vertical, with 1σ and 3σ ellipses computed from the runs on screen. If the cloud is not Gaussian the ellipse will visibly fail to contain it, which is information.
  • Channels — replaces the orbital views when a series file is loaded. Hover anywhere to read every selected channel at that x.

The timeline is the scrub bar

One strip, not two. The question you ask while dragging a playhead across 400 days is how big the error is at that point, so the dispersion envelope (p05–p95 band, median, max, on a log axis) is drawn under the cursor. Injection and correction epochs are marked. Click or drag anywhere on it to seek.

Rate

Expressed as sim-time per wall-second. The slider is continuous; the presets are 1 min : 1 s, 1 hr : 1 s, 1 day : 10 s (the default), 1 day : 1 s and 10 d : 1 s. At the default a 400-day arc plays in 66 minutes, which the full arc plays in readout tells you before you start.

Keyboard

KeyAction
SpacePlay / pause
Step one sample
Shift + Step fifty samples
HomeReturn to the start of the arc
1 / 2Inertial / RIC dispersion
Tab in the plan editorIndents four spaces instead of leaving the field
Ctrl+Enter in the plan editorRun the plan

6Running a test by hand

The full disturbance-and-correction exercise, from an empty console. Every step here has a plan-console equivalent in §9.

  1. Set the seed before anything else. Everything dispersed downstream derives from it, so changing it later invalidates the comparison you were making.
  2. Choose the force model. Leave J2 on. Leave drag off unless you specifically want decay behaviour — with drag off, any orbit decay you then observe is the integrator, which is a useful thing to be able to say.
  3. Press Generate ensemble (or drop a CSV). Wait for the manifest. A 400-day, 32-run set takes about six seconds and reports its own throughput.
  4. Read the integrator line in the manifest before going further. It should be green. If it is red, stop and fix it — see §10. Every number downstream inherits this.
  5. Set the disturbance. Frame, the three components, and the per-axis execution error. Set Inject at day early enough that there is arc left to watch afterwards.
  6. Press Inject and propagate. The view switches to RIC dispersion at the injection epoch and the timeline fills with the envelope.
  7. Scrub forward and watch the cloud grow. For an in-track disturbance it stretches along track and keeps stretching — that is the physics, not a drawing artefact.
  8. Set the correction epoch and horizon, then press Solve correction. Read the status pill: SOLVED, OUT OF REGIME or SINGULAR. Nothing has changed yet.
  9. Press Fly it. Both burns are flown if the second-burn box is ticked. The playhead moves to the burn epoch.
  10. Compare the |Δr| readouts at the same late epoch, before and after. The honest comparison is at one fixed day, not at whatever the playhead happens to be showing.
Expect the in-track case to look wrong

Correcting an along-track offset over a short horizon usually leaves the ensemble worse off at the end of the arc. That is real. Nulling a phase error inside one revolution needs a large radial impulse, and the linearisation error on a burn that size exceeds the offset it was sent to remove.

Cross-track behaves the way you would hope: bounded, and correctable for roughly what the disturbance cost. Set the disturbance to pure C and compare. Then lengthen the horizon on the in-track case and watch the budget fall.

7The 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. 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 its transcript land on disk together. A result without the plan that produced it is not evidence of anything.

Controls

  • Example — loads one of three ready plans. This replaces the editor contents.
  • Run plan / Ctrl+Enter — executes from the top. Disabled while running.
  • Stop — halts at the next statement. A verb already in flight finishes first.
  • Download results — writes the rows collected by record() as a CSV. Disabled until a plan records something; the label shows the row count.
  • Save transcript — writes the plan, the transcript and the verdict to your disk as one text file.

Recording results

record() collects one row per call, with columns you name. Put it at the end of a sweep iteration and each case adds a row.

for dv in [0.01, 0.05, 0.20]:
    inject(ric_ms=[0, dv, 0], at_day=20)
    before = dispersion(at_day=115)
    s = solve(at_day=22, horizon_rev=0.75, two_burn=True)
    fly()
    after = dispersion(at_day=115)

    record(dv_ms=dv, burn_ms=s.burn_ms,
           before_km=before.median_km, after_km=after.median_km)

Press Download results and you get:

# GTRE Sandbox — plan results
# build 2026-08-11
# sandbox arithmetic, not the GDS Physics engine, no execution certificate
# 8 checks, 8 passed, 0 failed
dv_ms,burn_ms,before_km,after_km
0.01,0.06601634992336892,14.48779888692292,18.71241722069512
0.05,0.28284001744899867,16.625360960017577,17.769083317217667
It round-trips

That file is in exactly the shape the series reader parses, so you can drop it straight back on this page and plot the columns against each other — burn against Δv on a log axis, before against after as a difference. The # lines are comments the reader skips, so the provenance travels with the file without disturbing the parse.

Numbers are written at full precision rather than rounded, because rounding here would discard exactly what the float64 series store exists to preserve.

Two things to know

Text columns will not plot. record(note="in-track") is fine and lands in the CSV, but on re-read it becomes an empty channel, because the reader wants numbers. The log warns you by name when you export one. Useful for a spreadsheet; not for the plot.

There is no timestamp in the file, on purpose. Everything else here is built so the same seed and the same plan give the same numbers — stamping the wall clock would be the one thing making two identical runs differ. The build date is recorded instead.

Reading the transcript

Every line is stamped with elapsed seconds. PASS and FAIL lines come from check(). A failure at the end names the line number. The verdict bar summarises: state, check count, passes, failures, and total time.

The plan drives the real console

Every verb sets the same inputs you 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.

8Plan language reference

The plan language uses Python’s syntax for the part a test plan is made of. It is not CPython, and the page says so rather than letting you find out.

Why not real Python

The obvious answer is Pyodide — 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 the sandbox page is served with connect-src 'none' precisely so that it cannot make a request. Adopting 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 great deal; it is not worth that.

Supported

  • Variables and assignment; numbers, strings, True, False, None
  • Lists, and indexing including negative indices
  • Arithmetic + - * / // % **, with ** right-associative and binding tighter than unary minus
  • Comparisons == != < <= > >=
  • and, or, not, with short-circuit evaluation
  • for … in over a list or range(); break, continue, pass
  • if / elif / else
  • Calls with positional and keyword arguments; attribute access on results
  • # 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.

# what you get, rather than a puzzling syntax error
import numpy   -> "import" is not supported in a plan — there is nothing
                  to import; every verb is already in scope
while True:    -> "while" is not supported — use "for k in range(n)"
assert x == 1  -> "assert" is not supported — use check(label, condition)
A misspelled keyword argument is an error

inject(ric_m_s=[0,1,0]) stops and lists what it accepts. A plan that runs, passes, and quietly did not do what its author wrote is the worst outcome available here, so unknown keywords are never ignored.

9Verb reference

Setup

VerbEffect
seed(n)Sets the seed. Call it first.
forces(j2=, drag=, bc=, step_s=)Force model and integration step. All arguments optional.

Data

VerbEffect
synthesize(runs=, days=, step_s=, cadence_s=, sma=, ecc=, inc=, sigma_pos_m=, sigma_vel_ms=)Builds an ensemble and returns a status object.
purge()Drops everything.

Operations

VerbEffect
inject(ric_ms=[r,i,c], at_day=, model=, sigma_ms=, sustained_s=)Applies a disturbance. Use eci_ms= instead of ric_ms= for the inertial frame. model is "rk4" or "linear".
solve(at_day=, horizon_rev=, two_burn=, against=)Solves the correction. Use horizon_h= for hours instead of revolutions. against is "ref" or "mean".
fly()Applies the solved burn or burns.

Measurement

VerbReturns
dispersion(at_day=).median_km .p95_km .max_km .day .runs
status().loaded .runs .samples .arc_days .steps_per_rev .sma_drift_km .period_min .disturbed .solved .burn_ms .burn2_ms .corr_status
channels()List of channel names in a loaded series file
stat(name).min .max .abs_max .rms .n
diff(a, b).abs_max .rms .n for channel a minus channel b

synthesize(), inject(), solve() and fly() all return the same object status() does, so you can read a result straight off the call.

Verdict and utilities

VerbEffect
record(name=value, …)Adds one row to the results table, columns named by you. Keyword arguments only — a column without a name is not a column. Numbers, strings and booleans accepted; booleans become 1 and 0. Export with Download results.
check(label, condition, detail)Records PASS or FAIL. detail is optional and prints beside the result. Returns the boolean.
report()Prints the summary and re-lists every failure with its line number.
print(…)Writes a stamped line to the transcript.
str abs sqrt round min max len rangeAs you would expect. round(x, n) takes an optional digit count.

A complete plan

# Cross-track corrects cleanly; in-track does not. Show both.

seed(20260811)
forces(j2=True, drag=False, step_s=20)

ens = synthesize(runs=16, days=120, cadence_s=3600)
check("integrator adequate", ens.steps_per_rev > 100, ens.steps_per_rev)
check("no false decay", ens.sma_drift_km < 2, ens.sma_drift_km)

for axis in ["in-track", "cross-track"]:
    if axis == "in-track":
        inject(ric_ms=[0, 0.05, 0], at_day=20)
    else:
        inject(ric_ms=[0, 0, 0.05], at_day=20)

    before = dispersion(at_day=115)
    s = solve(at_day=22, horizon_rev=0.75, two_burn=True)
    fly()
    after = dispersion(at_day=115)

    print(axis, "burn", round(s.burn_ms, 3), "m/s |",
          round(before.median_km, 1), "->", round(after.median_km, 1), "km")
    check(axis + " solver converged", s.corr_status != "SINGULAR", s.corr_status)

report()

10Reading the diagnostics

This is the section worth reading twice. Most ways of getting a wrong answer here announce themselves, and this is what they mean.

The integrator line

The manifest reports N steps/rev · a drift X km. Green is fine, red is not. Both numbers are measured on the run you just made, not assumed.

  • steps/rev — integration steps per orbital revolution. RK4 needs roughly 100 to hold energy. Below 50 the orbit spirals in from truncation error alone.
  • a drift — the secular change in semi-major axis implied by the energy drift, measured over one whole orbit at each end of the arc so that J2’s short-period oscillation cancels.
The trap this exists to catch

A too-coarse step does not fail loudly. It produces a smooth, plausible, entirely fictional decay that is indistinguishable on screen from atmospheric drag. With drag switched off, any decay you see is arithmetic.

Measured on a 94-minute orbit over 120 days: a 300 s step drifts 410 km, 120 s drifts 98 km, 60 s drifts 2.9 km, and 20 s drifts 0.03 km. The default is 20 s.

Smaller is not monotonically better

Below roughly period/280 the truncation error drops under the floating-point round-off floor, and taking more steps then accumulates more round-off. Drift bottoms out near 20 s on the default orbit and stops improving. The defaults sit at that minimum.

Correction status

StatusMeaning and what to do
SOLVEDA well-conditioned solution inside the model’s regime.
OUT OF REGIMEA solution exists but Clohessy–Wiltshire is being used outside where it is valid — the separation exceeds about 1% of the orbit radius, or the horizon is more than three revolutions. The log names which. The number is still returned; treat it as indicative.
SINGULARNo impulse can null the offset at that horizon. Targeting exactly one orbital period ahead is singular, because after one period every in-plane impulse has returned you to where you started; the cross-track block goes singular at every half period. The log offers three horizons that are well conditioned.

Log warnings you will actually see

  • “velocity derived by central difference” — your file had no velocity columns. RIC orientation and the interpolation between samples both depend on it, so this is worth knowing.
  • “run(s) had a different sample count and were linearly resampled” — a real approximation, reported rather than done quietly.
  • “Horizon snapped from X to Y” — burns can only be placed where the ensemble has samples. The solve uses the snapped value so both burns agree with each other; a finer sample cadence places it closer.
  • “the x column does not increase monotonically” — a series file whose x column is unordered. The row index is plotted instead.
  • “most of the burn is radial” — you are nulling a phase error by brute force. Lengthen the horizon.

Linear (CW) versus RK4

The linear 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.

11What it cannot tell you

GTRE cannot run any of the platform’s stress tests, and no amount of feature work would change that. A stress test verifies the GDS Physics engine against a known answer. This is a JavaScript reimplementation; running the same scenario here proves something about this page, not about the engine.

What it can do is let you explore the phenomenon behind several of them — convergence order and the round-off floor, the fact that conservative models cannot decay an orbit, that burning early and along-track is cheaper, and that execution error can make the cure worse than the disease.

And it can explore the output of a run you made elsewhere, which is what shape B exists for. That is a genuinely different thing from producing the output, and the distinction is the whole of limit 2.

The endpoints, the certificate format and the stress-test evidence are in the technical documentation.

12Where your data goes

Nowhere. Specifically, and checkably:

  • The sandbox page is served with connect-src 'none'. The browser refuses every outbound request the page could attempt, before it is made. Open devtools and read the policy rather than taking our word.
  • Parsing happens in a Web Worker in your tab. The file is read with File.stream(); the bytes go into typed arrays and nowhere else.
  • No localStorage, sessionStorage, IndexedDB, cookie or cache write. A refresh, a tab close or Purge drops every array.
  • Three controls write files, and they are the only three: Download format template, Save transcript and Download results. All go to your own disk through a blob: URL — a local write, not a send. Nothing is uploaded as a side effect of any of them.
  • No API key is involved anywhere, because no request is made.

This manual page is ordinary site content and carries the site’s normal policy. The sandbox itself is the page with connect-src 'none', because it is the one that touches your files.

← Open the sandbox