Test a payload against a simulated pass
A payload app is written against a spacecraft you cannot have yet. space-sim
supplies one on your workstation. It propagates a published element set against
a mission clock, answers the payload’s camera with the acquisition beneath the
ground track, and accepts a downlink only while a ground station is in view.
The downlink constraint is what makes the test worth running. An app that can downlink everything never has to decide anything, and deciding what is worth sending is the reason to run inference on a spacecraft instead of on the ground.
Prerequisites
- The Space CLI on your computer.
space-sim. It is going to be published as its own formula in the Parsimoni tap, installed the wayspaceis and named separately. The tap shipsspaceandspace-groundtoday, so this is the step that blocks the rest.- A payload app that calls
Device.Camera.captureandDevice.Comms.downlink.
Inspect the world before you fly it
A scenario names an element set, a start epoch, ground stations, and a scene
directory. The park-fire scenario below is the worked example in the
simulator’s source distribution; write your own to fly a different orbit. Read a
scenario without starting a bus.
List the scenes the scenario can serve:
$ space-sim scenes --scenario scenarios/park-fire/scenario.jsonparkfire_post_nir EPSG:32610 257x257 west=-121.7706 south=39.9571 east=-121.6793 north=40.0273parkfire_pre_nir EPSG:32610 257x257 west=-121.7706 south=39.9571 east=-121.6793 north=40.0273Ask where the spacecraft is at the scenario’s start epoch:
$ space-sim where --scenario scenarios/park-fire/scenario.json2026-07-16T18:50:53Z lat=40.271 lon=-122.777 alt=707.8km v=7.51km/sFind when the ground track first crosses each scene. Use this to choose a start epoch that puts a scene under the spacecraft early in the run:
$ space-sim overpass --scenario scenarios/park-fire/scenario.json --within 302026-07-16T18:50:53Z parkfire_post_nir in view, 95km off the ground trackWrite out the exact picture the camera service would serve at a given mission time, as a GeoTIFF, so you can see what your app will be handed. The line names the acquisition the pixels came from:
$ space-sim capture --scenario scenarios/park-fire/scenario.json --out burn.tifw=128,h=128,lat=40.009740,lon=-121.747141,len=32768,bits=16,acq=parkfire_post_nirServe the payload on the bus
The bus serves until you stop it, so this one runs in its own terminal:
space-sim run --scenario scenarios/park-fire/scenario.jsonThe app now calls Device.Camera.capture and gets a 16-bit crop of what is
underneath. Device.Comms.downlink is refused until a station in the scenario
is above the horizon.
An instrument is not a pinhole under the spacecraft. Landsat’s swath is 185 km wide, so a scene is in view when it falls within half a swath of the ground track. Off-nadir geometry is not modelled: a real off-nadir look foreshortens and resamples the ground, and these pixels do not.
Assert on the app’s output in CI
Run the payload headless with a deadline, then check what it produced. Assert on the output: a workload that downlinks nothing exits cleanly. The headless boot and the report both use the local runner, so the CI job needs the local context.
space build . -t burn-index:cispace run burn-index:ci --headless --timeout 120space status --json > report.jsonresult.state is ok when no workload failed, and failed with the reason
otherwise:
jq -e '.result.state == "ok"' report.jsondownlink.state is available once the app has produced something to send, and
idle when it has not. Assert on it, so a run where the app captured nothing
and sent nothing fails the build:
jq -e '.downlink.state == "available"' report.jsonA run whose contact window never opens is a passing test of the wrong thing.
Use overpass to pick a start epoch that puts both a scene and a station inside
the window you give --timeout.
Task a fleet instead of flying one spacecraft
space-sim run answers the question a payload asks: what is underneath me now.
A buyer asks a different question, about supply: who can photograph this
region, when, and at what resolution.
$ space-sim tasking --fleet scenarios/fleet.json \> --area=-121.78,39.95,-121.67,40.03 --from 1784217600 --hours 240provider sim, 3 satellite(s), 240h from 2026-07-16T16:00:00Zlandsat-8 oli 2026-07-16T18:50:50Z..2026-07-16T18:50:56Z 94km off nadir 0.00 EUR (simulated)sentinel-2b msi 2026-07-17T19:03:06Z..2026-07-17T19:03:46Z 51km off nadir 0.00 EUR (simulated)sentinel-2a msi 2026-07-24T19:03:20Z..2026-07-24T19:04:00Z 52km off nadir 0.00 EUR (simulated)landsat-8 oli 2026-07-25T18:44:34Z..2026-07-25T18:44:58Z 36km off nadir 0.00 EUR (simulated)The orbits and the windows are real. The prices are not: every row costs
0.00 EUR and is marked (simulated). A tasking provider is a contract, and
the simulated fleet is one implementation of it, so replacing it with a
satellite operator’s tasking service changes the provider and leaves the caller
unchanged.