Plan Serialisation ================== COASTSim can save an observation plan produced by a DITL run to a portable JSON file and reload it later. The serialisation layer is built on `Pydantic v2`_ and lives in :mod:`conops.targets.plan_schema`. Convenience methods are also available directly on :class:`~conops.targets.Plan` so you rarely need to import the schema classes explicitly. Overview -------- Two Pydantic models handle the conversion: * :class:`~conops.targets.plan_schema.PlanEntrySchema` — represents a single observation entry (a :class:`~conops.targets.PlanEntry` or :class:`~conops.targets.Pointing`). * :class:`~conops.targets.plan_schema.PlanSchema` — top-level container that bundles metadata (version, timestamps, entry count) with the list of entries. Both models support ``model_validate(..., from_attributes=True)``, so they accept plain Python objects produced by the scheduler without any intermediate conversion step. Quick Start ----------- **Save a plan after a DITL run** The simplest approach is to call :meth:`~conops.targets.Plan.save` directly on the plan: .. code-block:: python # `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}") You can also go via :class:`~conops.targets.plan_schema.PlanSchema` if you need access to the metadata fields before writing: .. code-block:: python from conops.targets import PlanSchema schema = PlanSchema.from_plan(ditl.plan) print(f"Saving {schema.num_entries} entries (version {schema.version})") schema.save("plan_20251201.json") **Load it back** :meth:`~conops.targets.Plan.load` is a class method on :class:`~conops.targets.Plan` that returns a :class:`~conops.targets.plan_schema.PlanSchema` (preserving all metadata): .. code-block:: python schema = Plan.load("plan_20251201.json") print(schema.version) # schema format version (integer) print(schema.num_entries) # number of plan entries print(schema.entries[0].name) # first target name Or equivalently via :class:`~conops.targets.plan_schema.PlanSchema` directly: .. code-block:: python from conops.targets import PlanSchema schema = PlanSchema.load("plan_20251201.json") **Round-trip via model_validate** .. code-block:: python schema = PlanSchema.model_validate(ditl.plan, from_attributes=True) JSON File Format ---------------- The JSON file contains a metadata envelope followed by the entry list. .. code-block:: json { "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, "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" } ] } Metadata Fields ~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 :widths: 20 15 65 * - Field - Type - Description * - ``version`` - 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 :ref:`auto-versioning` below). * - ``coast_sim_version`` - string - COASTSim package version that produced the file (e.g. ``"0.1.3"``). * - ``created_at`` - string - ISO-8601 UTC timestamp of when the :class:`~conops.targets.plan_schema.PlanSchema` instance was created/validated (not updated by :meth:`~conops.targets.plan_schema.PlanSchema.save`). * - ``start`` - string - ISO-8601 UTC timestamp of the first entry's ``begin`` time (``"1970-01-01T00:00:00+00:00"`` if the plan is empty). * - ``end`` - string - ISO-8601 UTC timestamp of the last entry's ``end`` time (``"1970-01-01T00:00:00+00:00"`` if the plan is empty). * - ``num_entries`` - int - Total number of entries in the file. Entry Fields ~~~~~~~~~~~~ .. list-table:: :header-rows: 1 :widths: 20 15 65 * - Field - Type - Description * - ``name`` - string - Human-readable target name (e.g. ``"Crab Nebula"``). * - ``ra`` - float - Right ascension in degrees (J2000). * - ``dec`` - float - Declination in degrees (J2000). * - ``roll`` - float - Spacecraft roll angle in degrees (``-1`` = unset). * - ``begin`` - string - Start of the observation window (ISO-8601 UTC). * - ``end`` - string - End of the observation window (ISO-8601 UTC). * - ``merit`` - float - Scheduler merit/priority figure of merit. * - ``slewtime`` - int - Slew duration in seconds. * - ``insaa`` - int - Time spent in the South Atlantic Anomaly during the window (seconds). * - ``obsid`` - int - Numeric observation identifier. * - ``obstype`` - string - Observation type. Valid values: ``"AT"`` (Astronomical Target), ``"PPT"`` (Preprogrammed Target), ``"TOO"`` (Target of Opportunity), ``"SAFE"`` (Safe mode pointing), ``"CHARGE"`` (Emergency charging), ``"GSP"`` (Ground Station Pass). * - ``slewdist`` - float - Angular slew distance in degrees. * - ``ss_min`` - float - Minimum Sun-spacecraft separation angle encountered (degrees). * - ``ss_max`` - float - Maximum Sun-spacecraft separation angle encountered (degrees). * - ``exptime`` - int - Exposure time in seconds (may be shorter than original if interrupted). * - ``exporig`` - int - Originally requested exposure time in seconds. * - ``isat`` - bool - ``true`` if the detector was saturated during the exposure. * - ``done`` - bool - ``true`` if the observation completed successfully. * - ``exposure`` - int - Net science exposure time: ``end − begin − slewtime − insaa`` (seconds). * - ``station`` - string | null - Ground station code for ``GSP`` (Ground Station Pass) entries. Only present for ``obstype="GSP"``. Identifies which station is used for the commanded pass. * - ``contact_begin`` - string | null - ISO-8601 UTC timestamp of when the ground station contact window begins. Only present for ``obstype="GSP"``. This is the actual pass start time; ``begin`` is the reservation time (which may include slew preparation). * - ``contact_end`` - string | null - ISO-8601 UTC timestamp of when the ground station contact window ends. Only present for ``obstype="GSP"``. Typically matches ``end``. 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 :class:`~conops.ditl.queue_ditl.QueueDITL` when the spacecraft enters a ground station visibility window. * **Pass reservation**: The ``begin`` time marks when the spacecraft reserves the window (potentially including slew preparation), while ``contact_begin`` marks the actual start of the ground station contact. * **Metadata fields**: The ``station``, ``contact_begin``, and ``contact_end`` fields are only present for GSP entries (``null`` or omitted for other observation types). * **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 :func:`~conops.visualization.mpl.ditl_timeline.plot_ditl_timeline` and :func:`~conops.visualization.plotly.ditl_timeline.plot_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:** .. code-block:: json { "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", "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``). .. _auto-versioning: Auto-versioning ~~~~~~~~~~~~~~~ When ``path`` is a directory (or ends with ``/``), :meth:`~conops.targets.Plan.save` scans the directory for existing files matching ``plan___v.json`` and sets ``version`` to ``max(N) + 1`` (or ``0`` if no matching files exist). Saving to an explicit file path leaves ``version`` unchanged. Backward Compatibility ---------------------- :meth:`~conops.targets.plan_schema.PlanSchema.save` creates any missing parent directories automatically, so you can pass a nested path without creating it first. :meth:`~conops.targets.plan_schema.PlanSchema.load` accepts files written by older versions of COASTSim that predate ``PlanSchema``. 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 :class:`~datetime.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. API Reference ------------- .. automethod:: conops.targets.plan.Plan.save .. automethod:: conops.targets.plan.Plan.load .. autoclass:: conops.targets.plan_schema.PlanEntrySchema :members: :undoc-members: :show-inheritance: .. autoclass:: conops.targets.plan_schema.PlanSchema :members: :undoc-members: :show-inheritance: .. _Pydantic v2: https://docs.pydantic.dev/latest/