ENGINE STATUS NOMINAL MISSION CUSTODY ZERO CLASS ADVISORY
UTC --:--:--
TOOLS //GTRE SANDBOX //OFFLINE //BUILD --

GTRE Sandbox

Geodesic Trajectory & Results Explorer

Operation manual →

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.

Sandbox arithmetic — not the flight engine

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.

Source

CSV
Drop a file / click .csv · comma, tab or space

Synthesise

NO FILE NEEDED

Force model

SANDBOX
DRAG ROTATE WHEEL ZOOM SPACE PLAY ←→ STEP
reference run
ensemble
planned trace (first ~3 revs)
NO ENSEMBLE LOADED
Drop a CSV on the left, or press Generate ensemble to build a 400-day, 64-run dispersion here in about a second.
0d 00:00:00 mission elapsed
Rate 1 day : 10.0 s

Disturbance

Δv

Correction

IDLE
Δv R--
Δv I--
Δv C--
offset now
--
burn 1
--
total budget
--

Readout

AT PLAYHEAD
elapsed
--
sample
--
altitude
--
radius
--
speed
--
X km--
Y km--
Z km--
|Δr| median
--
|Δr| p95
--
|Δr| max
--
full arc plays in
--

Event log

Plan console

PYTHON SUBSET Runs in this tab. No interpreter is downloaded and no request is made.
ready

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.

  • Ephemerisx,y,z plus 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
...
  • Timet, time, time_s, seconds, elapsed, or an ISO-8601 epoch / utc column, which is converted to seconds from the first row.
  • Positionx,y,z, rx,ry,rz, pos_x…, or with a _km / _m suffix. Without a suffix 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, which is reported in the manifest rather than done quietly.
  • 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 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.

  • Setupseed(n), forces(j2=, drag=, bc=, step_s=)
  • Datasynthesize(runs=, days=, sma=, inc=, …), purge()
  • Operationsinject(ric_ms=[r,i,c], at_day=, model=), solve(at_day=, horizon_rev=, two_burn=), fly()
  • Measurementdispersion(at_day=), status(), and for series files channels(), stat(name), diff(a, b)
  • Verdictcheck(label, condition) and report()
  • Resultsrecord(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 File handle is read with File.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.

← Back to TOOLS