Skip to content
The Parsimoni tap does not publish space-sim, so the tool this page drives cannot be installed, though the simulator itself works and its transcripts below are executed on every build.

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.

The scenario propagates a published element set against a mission clock to place the spacecraft on a ground track. A scene within half a swath of the track is what the camera returns, and a downlink is accepted only while a ground station is above the horizon.

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 way space is and named separately. The tap ships space and space-ground today, so this is the step that blocks the rest.
  • A payload app that calls Device.Camera.capture and Device.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:

Terminal window
$ space-sim scenes --scenario scenarios/park-fire/scenario.json
parkfire_post_nir EPSG:32610 257x257 west=-121.7706 south=39.9571 east=-121.6793 north=40.0273
parkfire_pre_nir EPSG:32610 257x257 west=-121.7706 south=39.9571 east=-121.6793 north=40.0273

Ask where the spacecraft is at the scenario’s start epoch:

Terminal window
$ space-sim where --scenario scenarios/park-fire/scenario.json
2026-07-16T18:50:53Z lat=40.271 lon=-122.777 alt=707.8km v=7.51km/s

Find 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:

Terminal window
$ space-sim overpass --scenario scenarios/park-fire/scenario.json --within 30
2026-07-16T18:50:53Z parkfire_post_nir in view, 95km off the ground track

Write 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:

Terminal window
$ space-sim capture --scenario scenarios/park-fire/scenario.json --out burn.tif
w=128,h=128,lat=40.009740,lon=-121.747141,len=32768,bits=16,acq=parkfire_post_nir

Serve the payload on the bus

The bus serves until you stop it, so this one runs in its own terminal:

Terminal window
space-sim run --scenario scenarios/park-fire/scenario.json

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

Terminal window
space build . -t burn-index:ci
space run burn-index:ci --headless --timeout 120
space status --json > report.json

result.state is ok when no workload failed, and failed with the reason otherwise:

Terminal window
jq -e '.result.state == "ok"' report.json

downlink.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:

Terminal window
jq -e '.downlink.state == "available"' report.json

A 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.

Terminal window
$ space-sim tasking --fleet scenarios/fleet.json \
> --area=-121.78,39.95,-121.67,40.03 --from 1784217600 --hours 240
provider sim, 3 satellite(s), 240h from 2026-07-16T16:00:00Z
landsat-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.