Plan Serialisation
COASTSim can save an observation plan produced by a DITL run to a portable JSON file and
reload it later. Plan and PlanEntry
are directly serializable Pydantic v2 models. A completed DITL run also attaches
executed attitude and orbit-state timelines, which save()
writes as sibling JSON files.
Overview
The primary models are:
PlanEntry— a single observation or ground-station-pass entry.Plan— the top-level plan, including its entries, file metadata, and optional execution timeseries.
The former PlanSchema and
PlanEntrySchema names remain as compatibility exports.
PlanSchema.from_plan(plan) also remains available for callers that need it, but new code
should use Plan and PlanEntry directly.
Quick Start
Save a plan after a DITL run
The simplest approach is to call save() directly on the plan:
# `ditl` is a QueueDITL (or similar) instance that has already been run
saved_path = ditl.plan.save("plan_20251201.json")
print(f"Saved to {saved_path}")
Plan exposes its file metadata before writing:
print(f"Saving {ditl.plan.num_entries} entries (version {ditl.plan.version})")
ditl.plan.save("plan_20251201.json")
Load it back
load() is a class method on Plan that
returns a Plan with its serialized metadata and entries:
from conops.targets import Plan
plan = Plan.load("plan_20251201.json")
print(plan.version) # plan-file revision (integer)
print(plan.num_entries) # number of plan entries
print(plan.entries[0].name) # first target name
Runtime-only configuration and ephemeris references are intentionally absent from
the JSON. DITL restores them when the loaded plan is
passed to the constructor or assigned before calc():
ditl = DITL(config=config, ephem=ephem, plan=plan)
ditl.calc()
The compatibility PlanSchema class can still load a plan when preserving that type is
important to existing callers:
from conops.targets import PlanSchema
legacy_schema = PlanSchema.load("plan_20251201.json")
Round-trip via model_validate
plan = Plan.model_validate(ditl.plan, from_attributes=True)
JSON File Format
The JSON file contains a metadata envelope followed by the entry list.
{
"version": 3,
"coast_sim_version": "0.1.3",
"created_at": "2025-12-01T00:00:00+00:00",
"start": "2025-12-01T00:00:00+00:00",
"end": "2025-12-01T23:59:00+00:00",
"num_entries": 42,
"attitude_timeseries_file": "plan_20251201_attitude_timeseries.json",
"orbit_state_timeseries_file": "plan_20251201_orbit_state_timeseries.json",
"roll_axis": "+X",
"roll_convention": "right_handed_body_rotation",
"roll_reference_axis": "+Z",
"roll_reference": "projected_celestial_north",
"metadata": {
"ephemeris": {
"source": "TLE",
"norad_id": 25544
}
},
"entries": [
{
"name": "TEST_001",
"ra": 83.82,
"dec": -5.39,
"roll": 0.0,
"begin": "2025-12-01T00:00:00+00:00",
"end": "2025-12-01T00:16:40+00:00",
"merit": 95.0,
"slewtime": 120,
"insaa": 0,
"obsid": 1001,
"obstype": "AT",
"slewdist": 10.3,
"ss_min": 300.0,
"ss_max": 1000000.0,
"exptime": 880,
"exporig": 1000,
"isat": false,
"done": true,
"exposure": 880
},
{
"name": "SGS_PASS",
"ra": 120.0,
"dec": 45.0,
"roll": 0.0,
"begin": "2025-12-01T00:18:00+00:00",
"end": "2025-12-01T00:28:00+00:00",
"merit": 101.0,
"slewtime": 120,
"insaa": 0,
"obsid": 65535,
"obstype": "GSP",
"slewdist": 5.2,
"ss_min": 45.0,
"ss_max": 180.0,
"exptime": 480,
"exporig": 600,
"isat": false,
"done": true,
"exposure": 480,
"station": "SGS",
"contact_begin": "2025-12-01T00:20:00+00:00",
"contact_end": "2025-12-01T00:28:00+00:00",
"track_start_ra": 120.0,
"track_start_dec": 45.0,
"track_end_ra": 231.67,
"track_end_dec": -0.38
}
]
}
Metadata Fields
Field |
Type |
Description |
|---|---|---|
|
int |
Integer plan-file revision counter. Starts at 0 and is incremented automatically each time a new plan is saved to the same directory for the same time window (see Auto-versioning below). |
|
string |
COASTSim package version that produced the file (e.g. |
|
string |
ISO-8601 UTC timestamp of when the |
|
string |
ISO-8601 UTC timestamp of the first entry’s |
|
string |
ISO-8601 UTC timestamp of the last entry’s |
|
int |
Total number of entries in the file. |
|
string | null |
Filename (no path) of the sibling attitude-timeseries JSON file written alongside
this plan, or |
|
string | null |
Filename (no path) of the sibling GCRS orbit-state JSON file written alongside this
plan, or |
|
string |
By default, plan roll is a right-handed physical spacecraft rotation about
body |
|
string |
At zero roll, body |
|
object | null |
Optional producer provenance. COASTSim reserves the |
Initialization State
metadata.ephemeris.initialization_state provides a single Cartesian state
for initializing an external simulation before plan execution. The block
includes an explicit UTC epoch, GCRS frame, Earth-center origin, position
in meters, velocity in meters per second, and the propagator, gravity model,
and SGP4 operation mode reported by the source TLEEphemeris. Callers
cannot override those provenance fields. Producers should generate a dedicated
ephemeris sample at the requested initialization epoch instead of
extrapolating plan-start orbital elements.
Entry Fields
Field |
Type |
Description |
|---|---|---|
|
string |
Human-readable target name (e.g. |
|
float |
Science-target right ascension in degrees (J2000). For a mounted telescope,
this is the instrument |
|
float |
Declination in degrees (J2000). |
|
float |
Right-handed roll in degrees ( |
|
string | null |
Telescope whose frame defines |
|
array[float, float, float] | null |
Physical spacecraft body- |
|
string |
Start of the observation window (ISO-8601 UTC). |
|
string |
End of the observation window (ISO-8601 UTC). |
|
float |
Scheduler merit/priority figure of merit. |
|
int |
Slew duration in seconds. |
|
int |
Time spent in the South Atlantic Anomaly during the window (seconds). |
|
int |
Numeric observation identifier. |
|
string |
Observation type. Valid values: |
|
float |
Angular slew distance in degrees. |
|
float |
Minimum Sun-spacecraft separation angle encountered (degrees). |
|
float |
Maximum Sun-spacecraft separation angle encountered (degrees). |
|
int |
Exposure time in seconds (may be shorter than original if interrupted). |
|
int |
Originally requested exposure time in seconds. |
|
bool |
|
|
bool |
|
|
int |
Net science exposure time in seconds. For |
|
string | null |
Ground station code for |
|
string | null |
ISO-8601 UTC timestamp of when the ground station contact window begins. Only present
for |
|
string | null |
ISO-8601 UTC timestamp of when the ground station contact window ends. Only present
for |
|
float | null |
Ground-station tracking right ascension at |
|
float | null |
Ground-station tracking declination at |
|
float | null |
Ground-station tracking roll at |
|
float | null |
Ground-station tracking right ascension used by ACS at pass end, in degrees. This is
derived from the final tracking sample and matches |
|
float | null |
Ground-station tracking declination used by ACS at pass end, in degrees. This is
derived from the final tracking sample and matches |
|
float | null |
Ground-station tracking roll used by ACS at pass end, in degrees. This is derived
from the final tracking sample and matches |
|
float | null |
Ground-station geodetic latitude and longitude in degrees, and altitude in metres.
Present for exported |
|
object | null |
Commanded fixed target attitude for |
Ground Station Pass (GSP) Entries
Ground station pass entries (obstype="GSP") represent commanded communication windows
with ground stations. Unlike science observations, these entries capture the reservation
and execution of data downlink passes.
Key characteristics of GSP entries:
Automatic creation: Created by
QueueDITLwhen the spacecraft enters a ground station visibility window.Pass reservation: The
begintime marks when the spacecraft reserves the window (potentially including slew preparation), whilecontact_beginmarks the actual start of the ground station contact.Metadata fields: The
station,contact_begin, andcontact_endfields are only present for GSP entries (nullor omitted for other observation types).Tracking attitude: GSP entries track the ground station through the contact. The generic
ra,dec, androllfields are the pass-start attitude, matchingtrack_start_ra,track_start_dec, andtrack_start_roll. Usetrack_end_ra,track_end_dec, andtrack_end_rollto inspect the spacecraft attitude at the end of the pass. The end fields are derived from the final tracking sample, matching ACSPass.attitude_at(contact_end)behavior.Deconfliction: When multiple ground stations are visible simultaneously, COASTSim automatically selects the pass with the highest expected data volume (downlink rate × duration). Dropped overlapping opportunities are logged but not exported to the plan.
Visualization: GSP entries are excluded from the science observation bands in
plot_ditl_timeline()andplot_ditl_timeline(). Ground station passes are still shown in the timeline’s dedicated “Ground Contact” row.Safe mode: No GSP entries are created when the spacecraft is in SAFE mode.
Example GSP entry:
{
"name": "TRO_PASS",
"obstype": "GSP",
"station": "TRO",
"begin": "2025-12-01T12:00:00+00:00",
"contact_begin": "2025-12-01T12:02:00+00:00",
"contact_end": "2025-12-01T12:12:00+00:00",
"end": "2025-12-01T12:12:00+00:00",
"track_start_ra": 120.0,
"track_start_dec": 45.0,
"track_start_roll": 12.4,
"track_end_ra": 231.67,
"track_end_dec": -0.38,
"track_end_roll": 18.2,
"slewtime": 120,
"exptime": 600,
"obsid": 65535
}
In this example, the spacecraft begins reserving the pass window at 12:00:00 (begin),
uses 2 minutes for slew preparation (slewtime), and the actual ground station contact
runs from 12:02:00 to 12:12:00 (contact_begin to contact_end).
Plan Provenance
Use attach_tle_plan_metadata() to store the TLE provenance used to
create a plan. The helper records the source, TLE filename and lines, epoch, NORAD ID, and
TLE mean elements under the plan’s metadata.ephemeris object.
Use attach_osculating_elements_metadata() separately to record
instantaneous osculating elements derived from the propagated GCRS position and velocity.
The requested epoch must match exactly one ephemeris timestamp. Both helpers preserve
metadata keys already present. DITL and
QueueDITL call this helper automatically at the
simulation start epoch when attaching the orbit-state timeseries.
Orbit-state attachment also records metadata.ephemeris.earth_orientation
provenance for the exact JPL EOP2 text loaded by rust-ephem’s UT1 and
polar-motion providers. Each provider reports availability, source URL,
SHA-256, download/cache source, and whether stale-cache fallback was used.
Unused or unavailable providers remain explicit with available: false.
from conops.targets import attach_tle_plan_metadata
# tle_record is the rust_ephem.TLERecord used to build the ephemeris.
attach_tle_plan_metadata(
ditl.plan,
tle_record,
tle_file="examples/example.tle",
)
ditl.plan.save("plan_20251201.json")
The resulting metadata.ephemeris object keeps the two element sets distinct:
tle_mean_elements is associated with the TLE epoch, while osculating_elements
includes the selected state epoch, frame: "GCRS", origin: "Earth center", and
the gravitational parameter used for the conversion. Element angles are normalized to
[0, 360) degrees.
rust-ephem owns the Cartesian-state conversion and selection of the central-body
gravitational parameter; its current default is the WGS-72 Earth value used by SGP4.
Coast does not select or implement a gravity model. It serializes the exact value returned
by rust-ephem as GravitationalParameter_m3_s2 so consumers can reproduce the
conversion.
For validation and programmatic construction, use
PlanMetadata and EphemerisMetadata.
Plan.metadata itself remains a JSON-compatible dictionary so that mission tools can add
producer-specific provenance alongside the typed ephemeris object.
Auto-versioning
When path is a directory (or ends with /), save()
scans the directory for existing files matching
plan_<start>_<end>_v<N>.json and sets version to max(N) + 1
(or 0 if no matching files exist). Saving to an explicit file path
leaves version unchanged.
Attitude Timeseries
Both DITL and QueueDITL
automatically attach the executed spacecraft attitude timeline to the plan at the end of
calc()/run(). When the plan is then saved, this timeseries is written to a sibling
JSON file alongside the main plan file.
File naming: the sibling file is placed in the same directory as the plan and named
<plan_stem>_attitude_timeseries.json. For example, if the plan is saved as
plan_20251201T000000Z_20251201T235900Z_v3.json the attitude file is
plan_20251201T000000Z_20251201T235900Z_v3_attitude_timeseries.json.
The plan file records the sibling filename in the attitude_timeseries_file metadata
field so that consumers can locate it without scanning the directory.
Attitude Timeseries File Format
{
"version": 0,
"coast_sim_version": "0.6.1",
"created_at": "2025-12-01T00:00:00+00:00",
"plan_file": "plan_20251201T000000Z_20251201T235900Z_v3.json",
"plan_version": 3,
"plan_start": "2025-12-01T00:00:00+00:00",
"plan_end": "2025-12-01T23:59:00+00:00",
"frame": "GCRS",
"body_frame": "COAST_BODY",
"roll_axis": "+X",
"roll_convention": "right_handed_body_rotation",
"roll_reference_axis": "+Z",
"roll_reference": "projected_celestial_north",
"representation": "quaternion",
"direction": "inertial_to_body",
"order": "wxyz",
"quaternion_product": "hamilton",
"vector_action": "q_v_q_conjugate",
"num_samples": 2,
"samples": [
{
"utime": 1748736000.0,
"timestamp": "2025-12-01T00:00:00+00:00",
"ra": 83.82,
"dec": -5.39,
"roll": 0.0,
"mode": "SCIENCE",
"obsid": 1001,
"quat_w": 0.9998,
"quat_x": 0.0050,
"quat_y": -0.0120,
"quat_z": 0.0150
},
{
"utime": 1748736060.0,
"timestamp": "2025-12-01T00:01:00+00:00",
"ra": 83.82,
"dec": -5.39,
"roll": 0.0,
"mode": "SCIENCE",
"obsid": 1001,
"quat_w": 0.9998,
"quat_x": 0.0051,
"quat_y": -0.0121,
"quat_z": 0.0151
}
]
}
Attitude Timeseries Metadata Fields
Field |
Type |
Description |
|---|---|---|
|
int |
Schema format version (currently always |
|
string |
COASTSim package version that produced the file. |
|
string |
ISO-8601 UTC timestamp of when the file was created. |
|
string | null |
Filename of the associated plan JSON file. |
|
int | null |
|
|
string | null |
ISO-8601 UTC timestamp matching the plan’s |
|
string | null |
ISO-8601 UTC timestamp matching the plan’s |
|
string |
Source and destination frames for every sample quaternion: |
|
string |
Roll is a right-handed physical body rotation about |
|
string |
At zero roll, body |
|
string |
Every sample uses an |
|
string |
Every sample uses the Hamilton product and transforms inertial vectors into body
coordinates as |
|
int |
Number of attitude samples in |
Attitude Sample Fields
Each element of samples is an AttitudeSampleSchema:
Field |
Type |
Description |
|---|---|---|
|
float |
Unix timestamp (seconds since epoch, UTC). |
|
string |
ISO-8601 UTC timestamp — human-readable form of |
|
float | null |
Right ascension of the spacecraft boresight in degrees (J2000). |
|
float | null |
Declination of the spacecraft boresight in degrees (J2000). |
|
float | null |
Right-handed physical spacecraft rotation about body |
|
string | null |
ACS mode name at this sample (e.g. |
|
int | null |
Observation ID active at this sample. |
|
float | null |
Spacecraft attitude quaternion W component. |
|
float | null |
Spacecraft attitude quaternion X component. |
|
float | null |
Spacecraft attitude quaternion Y component. |
|
float | null |
Spacecraft attitude quaternion Z component. |
Loading the Attitude Timeseries
from conops.targets import AttitudeTimeseriesSchema, Plan
from pathlib import Path
plan = Plan.load("plan_20251201T000000Z_20251201T235900Z_v3.json")
if plan.attitude_timeseries_file:
plan_dir = Path("plan_20251201T000000Z_20251201T235900Z_v3.json").parent
timeseries_path = plan_dir / plan.attitude_timeseries_file
raw = __import__("json").loads(timeseries_path.read_text())
timeseries = AttitudeTimeseriesSchema.model_validate(raw)
print(f"Loaded {timeseries.num_samples} attitude samples")
Orbit-State Timeseries
When the ephemeris provides GCRS position and velocity samples, completed
DITL and QueueDITL runs attach an
orbit-state timeline to the plan. save() writes it beside the plan
as <plan_stem>_orbit_state_timeseries.json and stores that basename in
orbit_state_timeseries_file.
The file has the same common metadata fields as the attitude sidecar (version,
coast_sim_version, created_at, plan_file, plan_version, plan_start,
plan_end, and num_samples). It also declares its coordinate conventions:
frame is "GCRS", origin is "Earth center", positions are in kilometres, and
velocities are in kilometres per second. Each sample contains:
{
"utime": 1748736000.0,
"timestamp": "2025-12-01T00:00:00+00:00",
"position_km": [6524.834, 6862.875, 6448.296],
"velocity_km_s": [4.902, 5.533, -1.976]
}
Load the sidecar through OrbitStateTimeseriesSchema:
import json
from pathlib import Path
from conops.targets import OrbitStateTimeseriesSchema, Plan
plan_path = Path("plan_20251201T000000Z_20251201T235900Z_v3.json")
plan = Plan.load(plan_path)
if plan.orbit_state_timeseries_file:
raw = json.loads(
(plan_path.parent / plan.orbit_state_timeseries_file).read_text()
)
orbit_states = OrbitStateTimeseriesSchema.model_validate(raw)
print(f"Loaded {orbit_states.num_samples} orbit-state samples")
Backward Compatibility
save() creates any missing parent directories
automatically, so you can pass a nested path without creating it first.
load() accepts files written by older versions of
COASTSim. Fields not present in the file (e.g. created_at,
num_entries) are filled with schema defaults; num_entries is always recomputed from
the actual entry list after loading. Legacy files that store start, end, begin,
or end as numeric Unix timestamps (float/int) are accepted and converted to
datetime objects internally — only the on-disk format changed to ISO-8601.
Legacy files that stored version as a semantic-version string (e.g. "0.1.3") are
coerced to 0. Legacy files must already use the field names documented above (including
exporig); there is currently no automatic renaming or aliasing of deprecated keys.
The from_attributes=True model configuration means the schema can also validate against
any object that exposes the expected attributes, not just plain dicts.