Mission Configuration ===================== The :class:`~conops.config.MissionConfig` class is the central configuration object in COASTSim. It aggregates all spacecraft subsystem configurations into a single, serializable Pydantic model that can be passed to DITL simulations. Overview -------- ``MissionConfig`` serves as the complete definition of your spacecraft, including: * Spacecraft bus properties (power, attitude control, communications) * Solar panel configuration and power generation * Payload instruments and data generation * Battery specifications * Pointing constraints (Sun, Moon, Earth limb avoidance) * Ground station network * Onboard data recorder * Fault management system * Visualization settings Creating a Configuration ------------------------ There are several ways to create a ``MissionConfig``: **1. Default Configuration** Create a configuration with all default values: .. code-block:: python from conops.config import MissionConfig config = MissionConfig() **2. Programmatic Configuration** Build a configuration by specifying individual components: .. code-block:: python from conops.config import ( MissionConfig, SpacecraftBus, SolarPanelSet, SolarPanel, Payload, Instrument, Battery, Constraint, GroundStationRegistry, GroundStation, OnboardRecorder, FaultManagement, ) config = MissionConfig( name="My Space Telescope", spacecraft_bus=SpacecraftBus(...), solar_panel=SolarPanelSet(...), payload=Payload(...), battery=Battery(...), constraint=Constraint(...), ground_stations=GroundStationRegistry(...), recorder=OnboardRecorder(...), fault_management=FaultManagement(...), ) **3. Load from JSON File** Load a pre-defined configuration from a JSON file: .. code-block:: python config = MissionConfig.from_json_file("spacecraft_config.json") **4. Load from YAML File** Load a pre-defined configuration from a YAML file (typically more human-readable than JSON): .. code-block:: python config = MissionConfig.from_yaml_file("spacecraft_config.yaml") **5. Save to JSON File** Save a configuration to JSON for version control or sharing: .. code-block:: python config.to_json_file("spacecraft_config.json") **6. Save to YAML File** Save a configuration to YAML with helpful annotations explaining units and purpose: .. code-block:: python config.to_yaml_file("spacecraft_config.yaml") The YAML output includes comprehensive comments explaining: - Default units for physical quantities (power in Watts, time in seconds, etc.) - Purpose and meaning of configuration settings - Valid ranges or constraints where applicable This makes YAML configurations particularly useful for: - Human review and editing - Documentation and configuration examples - Onboarding new team members - Version control with readable diffs Configuration Components ------------------------ name ~~~~ A human-readable name for the mission configuration. .. code-block:: python name: str = "Default Config" Example: .. code-block:: python config = MissionConfig(name="STROBE-X Observatory") random_seed ~~~~~~~~~~~ An optional integer seed for stochastic planning decisions. .. code-block:: python random_seed: int | None = None When set, COASTSim uses this value to seed the random-number generators that break scheduling ties and select ground-station pass opportunities. Identical seeds produce identical plans across runs with the same inputs, making simulation results reproducible and regression-testable. When ``None`` (the default), system-time entropy is used, so each run may produce different orderings when targets have equal merit or multiple ground stations are visible simultaneously. Example: .. code-block:: python config = MissionConfig(name="Reproducible Run", random_seed=42) attitude_constraint_policy ~~~~~~~~~~~~~~~~~~~~~~~~~~ Controls which constraints are checked against the executed attitude timeline during post-run plan validation. .. code-block:: python attitude_constraint_policy: dict[str, AttitudeConstraintPolicy] = **Background** At the end of every :meth:`~conops.ditl.queue_ditl.QueueDITL.run`, COASTSim compares the exported plan against the ACS telemetry it actually produced — a step called *plan execution validation*. One part of that validation scans every attitude sample in the telemetry and checks whether the spacecraft was pointing somewhere it shouldn't have been. Not all modes are held to the same standard. During ``SCIENCE`` mode a pointing violation is unambiguously a simulation bug. During ``SLEWING`` the spacecraft is actively rotating between targets and may briefly sweep through a soft-constraint zone by design — flagging that as an error would produce false positives. The ``attitude_constraint_policy`` map lets you express, per ACS mode, exactly how strict you want that check to be. If any sample fails its policy check, a :class:`~conops.ditl.queue_ditl.PlanExecutionMismatch` is recorded and :meth:`~conops.ditl.queue_ditl.QueueDITL.run` raises :exc:`~conops.ditl.queue_ditl.PlanExecutionMismatchError` before returning. The error message includes the first few violations — mode, obsid, RA/Dec/roll, and the constraint name — to aid debugging. **Hard keep-outs vs. full mission constraints** COASTSim's constraint system has two tiers: * **Full mission constraints** — the complete set of keep-outs the spacecraft must respect during normal science operations: minimum Sun angle, Moon and Earth limb avoidance, solar-panel illumination requirement, and the star-tracker, radiator, and telescope hard constraints listed below. Violating any of these during science would degrade or invalidate the observation. * **Hard keep-outs** — a strict health-and-safety subset that must never be violated regardless of mode: star tracker hard constraint (sensor blinding risk), radiator hard constraint (thermal damage risk), and telescope hard constraint (structural or optical damage risk). These are always instrument-protection limits, not science-quality limits. The ``HARD_KEEPOUT`` policy enforces the second tier only, which is appropriate for transient modes (slewing, SAA, safe) where soft science-quality limits legitimately do not apply. ``IDLE`` uses this policy by default, and the ACS also uses the configured ``IDLE`` policy at runtime when deciding whether an idle hold must be moved to a safer attitude before telemetry is recorded. **Policy values** (:class:`~conops.config.AttitudeConstraintPolicy`): .. list-table:: :header-rows: 1 :widths: 25 75 * - Value - Behaviour * - ``"full_mission"`` - The complete mission constraint (Sun, Moon, Earth limb, star tracker, radiator, telescope) is evaluated. Any violation is flagged. * - ``"hard_keepout"`` - Only the three hard keep-outs are checked: star tracker hard constraint, radiator hard constraint, and telescope hard constraint. Science-quality soft constraints (Sun angle, Moon, Earth limb, etc.) are intentionally ignored. * - ``"none"`` - No constraint checking is performed for this mode. Use this when the mode genuinely has no pointing restrictions (rare) or when you need to suppress false positives during development. **Default policy per ACS mode:** .. list-table:: :header-rows: 1 :widths: 20 30 50 * - ACS Mode - Default Policy - Rationale * - ``SCIENCE`` - ``full_mission`` - Science observations must satisfy all pointing constraints. * - ``CHARGING`` - ``full_mission`` - Emergency charging still obeys the full mission keep-out. * - ``SLEWING`` - ``hard_keepout`` - Slews may briefly sweep through soft-constraint zones by design; hard health-and-safety limits are still enforced. * - ``PASS`` - ``hard_keepout`` - Ground-station tracking may transit soft zones during the arc; hard limits apply. * - ``SAA`` - ``hard_keepout`` - SAA transits relax science constraints but not instrument-protection limits. * - ``SAFE`` - ``hard_keepout`` - Safe-mode pointing relaxes science constraints but not instrument-protection limits. * - ``IDLE`` - ``hard_keepout`` - Idle holds may persist, so hard health-and-safety limits are enforced; missions that require science-quality idle holds can override this to ``full_mission``. **Overriding the policy for individual modes:** Pass a partial dict — only the modes you specify are overridden; all others keep their defaults. Keys can be :class:`~conops.common.ACSMode` enum members, their integer values, or their string names. .. code-block:: python from conops.config import MissionConfig, AttitudeConstraintPolicy from conops.common import ACSMode config = MissionConfig( attitude_constraint_policy={ # Relax SLEWING to no check when using a separate, less restrictive # slew constraint already enforced by the ACS slew algorithm. ACSMode.SLEWING: AttitudeConstraintPolicy.NONE, # Promote SAFE mode to full validation for a strict mission context. "SAFE": AttitudeConstraintPolicy.FULL_MISSION, } ) **YAML configuration:** When loading a config from YAML, use the mode name string as the key and the policy string as the value. Omitted modes retain their defaults. .. code-block:: yaml attitude_constraint_policy: SLEWING: none SAFE: full_mission spacecraft_bus ~~~~~~~~~~~~~~ The :class:`~conops.config.SpacecraftBus` defines the spacecraft bus subsystems. **Attributes:** * ``name`` (str): Bus identifier * ``power_draw`` (:class:`~conops.config.PowerDraw`): Power consumption characteristics * ``attitude_control`` (:class:`~conops.config.AttitudeControlSystem`): ACS configuration * ``communications`` (:class:`~conops.config.CommunicationsSystem`): Optional comms system * ``heater`` (:class:`~conops.config.Heater`): Optional thermal heater * ``data_generation`` (:class:`~conops.config.DataGeneration`): Bus-level data generation * ``star_trackers`` (:class:`~conops.config.StarTrackerConfiguration`): Optional star tracker configuration * ``radiators`` (:class:`~conops.config.RadiatorConfiguration`): Optional body-mounted radiator configuration **AttitudeControlSystem Configuration:** The :class:`~conops.config.AttitudeControlSystem` defines slew performance and path algorithms: * ``slew_acceleration`` (float): Maximum angular acceleration in deg/s² * ``max_slew_rate`` (float): Maximum slew rate in deg/s * ``slew_accuracy`` (float): Pointing accuracy after slew completion in degrees * ``settle_time`` (float): Time to settle after slew completion in seconds * ``slew_algorithm`` (:class:`~conops.common.enums.SlewAlgorithm`): Algorithm for computing slew paths: - ``QUATERNION`` (default): Full 3-DOF SLERP coupling pointing and roll changes - ``CONSTRAINT_AVOIDING``: Routes around any configured constraints (Sun, Earth, Moon, etc.) * ``slew_constraint`` (ConstraintConfig | None): Optional rust-ephem constraint for slew path planning. When set and ``slew_algorithm`` is ``CONSTRAINT_AVOIDING``, this constraint is used instead of the spacecraft's general pointing constraint. This allows different safety margins for slewing vs. science pointing. .. code-block:: python from conops.config import SpacecraftBus, PowerDraw, AttitudeControlSystem, Heater from conops.common.enums import SlewAlgorithm import rust_ephem spacecraft_bus = SpacecraftBus( name="Observatory Bus", power_draw=PowerDraw( nominal_power=50.0, # Watts - normal operations peak_power=300.0, # Watts - maximum draw power_mode={0: 70.0, 1: 100.0}, # Mode-specific power eclipse_power=75.0, # Power draw during eclipse ), attitude_control=AttitudeControlSystem( slew_acceleration=0.01, # deg/s² - angular acceleration max_slew_rate=0.3, # deg/s - maximum slew rate slew_accuracy=0.01, # deg - pointing accuracy settle_time=10.0, # seconds - time to settle after slew slew_algorithm=SlewAlgorithm.CONSTRAINT_AVOIDING, # Avoid all constraints slew_constraint=( # Optional: separate constraint for slewing rust_ephem.SunConstraint(min_angle=30.0) # Relaxed Sun angle during slews | rust_ephem.EarthLimbConstraint(min_angle=20.0) # Relaxed Earth limb during slews ), ), heater=Heater( name="Bus Heaters", power_draw=PowerDraw( nominal_power=25.0, eclipse_power=75.0, # Higher power in eclipse ), ), ) solar_panel ~~~~~~~~~~~ The :class:`~conops.config.SolarPanelSet` defines the solar array configuration. **SolarPanelSet Attributes:** * ``name`` (str): Array identifier * ``panels`` (list[SolarPanel]): List of individual panel configurations * ``conversion_efficiency`` (float): Default array-level efficiency (0-1) **SolarPanel Attributes:** * ``name`` (str): Panel identifier * ``gimbled`` (bool): Whether the panel can track the Sun * ``normal`` (tuple[float, float, float]): Panel normal vector in spacecraft body frame - +x is the spacecraft pointing direction (boresight) - +y is the spacecraft "up" direction - +z completes the right-handed coordinate system - Should be a unit vector for proper illumination calculations * ``max_power`` (float): Maximum power output at full illumination (Watts) * ``conversion_efficiency`` (float | None): Per-panel efficiency override .. code-block:: python from conops.config import SolarPanelSet, SolarPanel solar_panel = SolarPanelSet( name="Main Solar Array", panels=[ SolarPanel( name="Panel +Y", gimbled=False, normal=(0.0, 1.0, 0.0), # Side-mounted max_power=400.0, conversion_efficiency=0.94, ), SolarPanel( name="Panel -Y", gimbled=False, normal=(0.0, -1.0, 0.0), # Opposite side max_power=400.0, conversion_efficiency=0.94, ), ], conversion_efficiency=0.95, ) Solar Panel Vector Helper Function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Defining panel normal vectors manually can be error-prone. The :func:`~conops.config.solar_panel.create_solar_panel_vector` helper function simplifies this by generating unit normal vectors based on mount type and cant angles. **Supported Mount Types:** * ``'sidemount'``: Panel faces +Y (spacecraft "up") * ``'aftmount'``: Panel faces -X (spacecraft "back") * ``'boresight'``: Panel faces +X (spacecraft forward/pointing direction) **Cant Angles:** * ``cant_z``: Rotation around Z-axis (yaw) in degrees * ``cant_perp``: Rotation around perpendicular axis (pitch) in degrees - For 'sidemount': rotates around X-axis - For 'aftmount': rotates around Y-axis - For 'boresight': rotates around Y-axis **Examples:** .. code-block:: python from conops.config import SolarPanelSet, SolarPanel from conops.config.solar_panel import create_solar_panel_vector # Simple side-mounted panel (no cant) normal = create_solar_panel_vector('sidemount') # Result: (0.0, 1.0, 0.0) # Side-mounted panel with 30° yaw normal = create_solar_panel_vector('sidemount', cant_z=30.0) # Aft-mounted panel with 45° pitch backward slant normal = create_solar_panel_vector('aftmount', cant_perp=-45.0) # Boresight panel tilted backward 45° (forward-facing with backward slant) normal = create_solar_panel_vector('boresight', cant_perp=-45.0) # Complex orientation: boresight with 30° yaw left and 45° backward pitch normal = create_solar_panel_vector('boresight', cant_z=30.0, cant_perp=-45.0) # Use in panel definition solar_panel = SolarPanelSet( name="Main Solar Array", panels=[ SolarPanel( name="Panel +Y", gimbled=False, normal=create_solar_panel_vector('sidemount', cant_z=10.0), max_power=400.0, conversion_efficiency=0.94, ), SolarPanel( name="Panel Boresight Aft", gimbled=False, normal=create_solar_panel_vector('boresight', cant_perp=-30.0), max_power=200.0, conversion_efficiency=0.94, ), ], conversion_efficiency=0.95, ) star_tracker ~~~~~~~~~~~~ The :class:`~conops.config.StarTrackerConfiguration` configures the star tracker system. Star trackers provide attitude determination by identifying star fields and are subject to avoidance constraints (e.g., never look within N° of the Sun). **StarTrackerConfiguration Attributes:** * ``star_trackers`` (list[StarTracker]): Individual star tracker configurations * ``min_functional_trackers`` (int): Minimum trackers not in soft violation for pointing to be valid (science-quality check). Hard constraints are always enforced regardless of this value. * ``modes_require_lock`` (list[ACSMode] | None): ACS modes in which star tracker **soft** constraints are enforced as a science-quality check. ``None`` (default) enforces soft constraints in all modes. An empty list disables soft constraint checks entirely. For example, ``[ACSMode.SCIENCE]`` only applies soft constraints during science observations. .. note:: **Hard** constraints are absolute health-and-safety keep-outs (e.g. sensor blinding) and are *always* enforced in every mode, regardless of ``modes_require_lock``. ``modes_require_lock`` only gates the science-quality **soft** constraint checks. **StarTracker Attributes:** * ``name`` (str): Tracker identifier * ``orientation`` (:class:`~conops.config.StarTrackerOrientation`): Boresight direction in spacecraft body frame * ``hard_constraint`` (optional): Constraint defining regions where the tracker *cannot* operate (e.g. Sun avoidance). Always enforced. Violations are recorded in the ``star_tracker_hard_violations`` telemetry field. * ``soft_constraint`` (optional): Constraint defining regions of degraded performance (science-quality check). Enforced only in modes listed in ``StarTrackerConfiguration.modes_require_lock``. Violations are recorded in ``star_tracker_soft_violations``. **StarTrackerOrientation Attributes:** * ``boresight`` (tuple[float, float, float]): Boresight direction as a unit vector in spacecraft body frame - +x is the spacecraft pointing direction (forward/boresight) - +y is the spacecraft "up" direction - +z completes the right-handed coordinate system Star Tracker Vector Helper Function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :func:`~conops.config.star_tracker.create_star_tracker_vector` helper converts roll, pitch, and yaw Euler angles to a boresight vector, mirroring the solar panel helper: .. code-block:: python from conops.config.star_tracker import create_star_tracker_vector # Star tracker pointing along spacecraft boresight (+X) boresight = create_star_tracker_vector(roll_deg=0, pitch_deg=0, yaw_deg=0) # Result: (1.0, 0.0, 0.0) # Star tracker rotated 90° in pitch to point "up" (+Z) boresight = create_star_tracker_vector(roll_deg=0, pitch_deg=90, yaw_deg=0) # Star tracker angled 45° to the side boresight = create_star_tracker_vector(roll_deg=0, pitch_deg=0, yaw_deg=45) Euler angles use the ZYX convention: yaw about Z, then pitch about Y, then roll about X. **Example:** .. code-block:: python from conops.config import ( SpacecraftBus, StarTracker, StarTrackerConfiguration, StarTrackerOrientation, ) from conops.config.star_tracker import create_star_tracker_vector from conops.common import ACSMode from rust_ephem import SunConstraint, EarthLimbConstraint # Build two star trackers at different orientations st1 = StarTracker( name="ST1", orientation=StarTrackerOrientation( boresight=create_star_tracker_vector(pitch_deg=45), # 45° off boresight ), hard_constraint=SunConstraint(min_angle=30.0), # Always enforced: never look within 30° of Sun soft_constraint=SunConstraint(min_angle=45.0), # Science-quality: degraded within 45° of Sun ) st2 = StarTracker( name="ST2", orientation=StarTrackerOrientation( boresight=create_star_tracker_vector(pitch_deg=-45), # Opposite side ), hard_constraint=SunConstraint(min_angle=30.0), ) star_trackers = StarTrackerConfiguration( star_trackers=[st1, st2], min_functional_trackers=1, # At least one must satisfy soft constraint modes_require_lock=[ACSMode.SCIENCE], # Enforce soft constraints in science mode only ) # Attach to spacecraft bus spacecraft_bus = SpacecraftBus( name="Observatory Bus", star_trackers=star_trackers, # ... other bus fields ... ) The ACS monitors star tracker constraints at each timestep and records: * ``star_tracker_hard_violations``: Number of trackers violating their hard constraint (always monitored) * ``star_tracker_soft_violations``: Whether any tracker is in its soft constraint zone * ``star_tracker_functional_count``: Number of functional (hard-constraint-clear) trackers Hard violations are health-and-safety events and always cause a pointing-invalid result. Soft violations only affect pointing validity in modes listed in ``StarTrackerConfiguration.modes_require_lock``. When star trackers are configured, ``MissionConfig.init_fault_management_defaults()`` automatically adds a ``star_tracker_functional_count`` threshold (``direction="below"``, both yellow and red set to ``num_trackers - 1``) so that any hard violation immediately triggers a RED fault alert. radiators ~~~~~~~~~ The :class:`~conops.config.RadiatorConfiguration` models body-mounted radiators used for thermal rejection. Radiators in COASTSim have: * Hard keep-out constraints (optional) for invalid orientations. * Continuous Sun/Earth exposure metrics. * A first-order net heat-flow estimate used for optimization and analysis. Unlike star trackers, radiators do **not** use soft constraints or functional-count logic. A radiator always exists physically; geometry changes its net thermal behavior. **RadiatorConfiguration Attributes:** * ``radiators`` (list[Radiator]): Individual radiator panels **Radiator Attributes:** * ``name`` (str): Radiator identifier * ``width_m`` (float): Radiator width in meters * ``height_m`` (float): Radiator height in meters * ``orientation`` (:class:`~conops.config.RadiatorOrientation`): Outward radiator normal in spacecraft body frame * ``subsystem`` (str): Subsystem served by radiator (for example ``"payload"`` or ``"spacecraft_bus"``) * ``efficiency`` (float): Thermal efficiency factor [0, 1] * ``emissivity`` (float): Surface emissivity [0, 1] * ``absorptivity`` (float): Effective absorptivity [0, 1] * ``radiator_temperature_k`` (float): Representative radiator temperature in Kelvin * ``sink_temperature_k`` (float): Thermal sink/background temperature in Kelvin * ``solar_constant_w_per_m2`` (float): Solar constant term (W/m²), default 1361 * ``earth_ir_flux_w_per_m2`` (float): Earth IR loading term (W/m²), default 237 * ``dissipation_coefficient_w_per_m2`` (float): Emitted flux cap for model calibration/backward compatibility * ``sun_loading_factor`` (float): Weighting factor applied to Sun exposure term * ``earth_loading_factor`` (float): Weighting factor applied to Earth exposure term * ``hard_constraint`` (optional): Keep-out constraint for invalid radiator pointing **RadiatorOrientation Attributes:** * ``normal`` (tuple[float, float, float]): Unit vector in spacecraft body frame - +x is spacecraft forward/boresight - +y is spacecraft "up" - +z completes right-handed frame **Example:** .. code-block:: python import rust_ephem from conops.config import ( MissionConfig, Radiator, RadiatorConfiguration, RadiatorOrientation, ) config = MissionConfig() bus_radiator = Radiator( name="BusRad+Y", width_m=1.2, height_m=0.8, orientation=RadiatorOrientation(normal=(0.0, 1.0, 0.0)), subsystem="spacecraft_bus", efficiency=0.9, emissivity=0.85, absorptivity=0.2, radiator_temperature_k=300.0, sink_temperature_k=3.0, solar_constant_w_per_m2=1361.0, earth_ir_flux_w_per_m2=237.0, dissipation_coefficient_w_per_m2=220.0, sun_loading_factor=0.7, earth_loading_factor=0.3, hard_constraint=rust_ephem.SunConstraint(min_angle=20.0), ) payload_radiator = Radiator( name="PayloadRad-X", width_m=0.6, height_m=0.6, orientation=RadiatorOrientation(normal=(-1.0, 0.0, 0.0)), subsystem="payload", ) config.spacecraft_bus.radiators = RadiatorConfiguration( radiators=[bus_radiator, payload_radiator] ) Scheduler Optimization Weights ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Queue scheduling can combine target merit with configurable optimization weights. All weights default to ``0.0``. When every weight is zero, the queue keeps the legacy fast path: targets are sorted by merit and the first visible target is selected without the extra scoring pass. When any weight is non-zero, visible candidates are scored as: .. code-block:: text score = target.merit - slew_distance_weight * slew_distance_degrees - slew_time_weight * slew_minutes + collection_time_weight * collection_minutes - radiator_sun_exposure_weight * sun_exposure - radiator_earth_exposure_weight * earth_exposure The available weights are: * ``config.targets.slew_distance_weight`` - Merit penalty per degree of slew. This favors nearby pointings when candidates have similar target merit. * ``config.targets.slew_time_weight`` - Merit penalty per minute of estimated slew time. ``QueueDITL`` candidate scoring uses a lightweight quaternion attitude-distance estimate and the configured ACS slew-time curve, without materializing the full slew path for every candidate. Once a target is selected, ``QueueDITL`` computes the full attitude-aware :class:`~conops.simulation.Slew` before enqueueing the command. * ``config.targets.collection_time_weight`` - Merit reward per minute of useful science collection. Collection time is capped by the target visibility window, remaining target exposure, and ``ss_max``. In ``QueueDITL`` it is also capped by the next science deadline, such as the next ground-station pass handoff or the simulation end. If that downstream deadline leaves less than ``ss_min`` of collection time, the candidate is skipped. * ``config.targets.radiator_sun_exposure_weight`` - Merit penalty per unit of radiator Sun exposure. * ``config.targets.radiator_earth_exposure_weight`` - Merit penalty per unit of radiator Earth exposure. The radiator exposure values are area-weighted fractions in ``[0, 1]`` when radiators are configured. Higher values steer scheduling away from pointings that increase thermal loading on radiators. All weights operate in merit units, so tune them relative to the target merit scale. For example, if normal target merits are around ``100``, then ``collection_time_weight = 1.0`` gives a ten-minute candidate a ``10`` point reward, while ``slew_time_weight = 1.0`` gives a ten-minute slew a ``10`` point penalty. Radiator Net Heat Model ^^^^^^^^^^^^^^^^^^^^^^^ COASTSim uses a first-order net-radiative model for each radiator: .. math:: Q_{net} = A\,\eta\,[\min(\epsilon\sigma(T_r^4 - T_s^4), C_d) - \alpha(S_0 f_{sun}E_{sun} + F_{earth} f_{earth}E_{earth})] Where: * :math:`S_0` is ``solar_constant_w_per_m2`` * :math:`F_{earth}` is ``earth_ir_flux_w_per_m2`` * :math:`E_{sun}, E_{earth}` are geometric exposure factors in [0, 1] * :math:`f_{sun}, f_{earth}` are loading weights * :math:`\alpha` is ``absorptivity`` and :math:`\epsilon` is ``emissivity`` * :math:`C_d` is ``dissipation_coefficient_w_per_m2`` (emission cap) * :math:`\sigma` is the Stefan-Boltzmann constant * :math:`A` is radiator area and :math:`\eta` is ``efficiency`` Positive :math:`Q_{net}` means net heat rejection (dumping heat). Negative :math:`Q_{net}` means net absorbed external radiative load. payload ~~~~~~~ The :class:`~conops.config.Payload` contains the science instruments. **Payload Attributes:** * ``instruments`` (list[Instrument]): List of :class:`~conops.config.Instrument` (or subclass) instances **Instrument Attributes:** * ``name`` (str): Instrument identifier * ``power_draw`` (:class:`~conops.config.PowerDraw`): Power consumption * ``heater`` (:class:`~conops.config.Heater`): Optional thermal heater * ``data_generation`` (:class:`~conops.config.DataGeneration`): Data output characteristics .. code-block:: python from conops.config import Payload, Instrument, PowerDraw, DataGeneration payload = Payload( instruments=[ Instrument( name="X-ray Detector", power_draw=PowerDraw( nominal_power=100.0, peak_power=150.0, power_mode={0: 120.0, 1: 80.0}, ), data_generation=DataGeneration( rate_gbps=0.001, # 1 Mbps continuous ), ), Instrument( name="Fine Guidance Sensor", power_draw=PowerDraw(nominal_power=15.0), data_generation=DataGeneration( per_observation_gb=0.01, # Fixed per observation ), ), ] ) Telescope Instruments ^^^^^^^^^^^^^^^^^^^^^ :class:`~conops.config.Telescope` is a subclass of :class:`~conops.config.Instrument` that adds optical configuration via a nested :class:`~conops.config.TelescopeConfig`. A ``Telescope`` can be placed directly in ``Payload.instruments`` alongside any other ``Instrument``. **Telescope Attributes** (in addition to Instrument fields): * ``boresight`` (tuple[float, float, float]): Unit vector in spacecraft body frame giving the direction the telescope points. Defaults to ``(1, 0, 0)`` (spacecraft forward/primary boresight). Must be a unit vector (magnitude within 1 %). * ``optics`` (:class:`~conops.config.TelescopeConfig`): Optical configuration **TelescopeConfig Attributes:** * ``aperture_m`` (float | None): Clear aperture diameter in metres * ``focal_length_m`` (float | None): Effective focal length in metres * ``f_number`` (float | None): Focal ratio — auto-derived as ``focal_length_m / aperture_m`` when both are provided and ``f_number`` is omitted; validated for consistency when all three are supplied * ``telescope_type`` (:class:`~conops.config.TelescopeType`): Optical design family * ``tube_length_m`` (float | None): Physical tube length in metres (useful for folded or catadioptric designs where tube length differs from focal length) **TelescopeType values:** .. list-table:: :header-rows: 1 :widths: 40 60 * - Enum member - Value * - ``NEWTONIAN`` - ``"Newtonian"`` * - ``CASSEGRAIN`` - ``"Cassegrain"`` * - ``RITCHEY_CHRETIEN`` - ``"Ritchey-Chrétien"`` * - ``SCHMIDT_CASSEGRAIN`` - ``"Schmidt-Cassegrain"`` * - ``MAKSUTOV_CASSEGRAIN`` - ``"Maksutov-Cassegrain"`` * - ``KORSCH`` - ``"Korsch"`` * - ``GREGORIAN`` - ``"Gregorian"`` * - ``DALL_KIRKHAM`` - ``"Dall-Kirkham"`` * - ``TMA`` - ``"Three Mirror Anastigmat"`` * - ``REFRACTOR`` - ``"Refractor"`` * - ``OTHER`` - ``"Other"`` *(default)* ``TelescopeConfig`` also exposes a read-only ``plate_scale_arcsec_per_um`` property (``206265 / focal_length_m_in_um``) for convenience, returning ``None`` when ``focal_length_m`` is not set. .. code-block:: python from conops.config import ( Payload, Instrument, Telescope, TelescopeConfig, TelescopeType, PowerDraw, DataGeneration, ) import math # f_number is derived automatically from aperture and focal length primary = Telescope( name="Primary Telescope", boresight=(1.0, 0.0, 0.0), # aligned with spacecraft boresight (default) power_draw=PowerDraw(nominal_power=80.0, peak_power=120.0), data_generation=DataGeneration(rate_gbps=0.5), optics=TelescopeConfig( aperture_m=0.6, focal_length_m=6.0, # f_number → 10.0 (auto-derived) telescope_type=TelescopeType.RITCHEY_CHRETIEN, tube_length_m=1.2, ), ) # Off-axis telescope: boresight 30° off spacecraft +X, rotated toward +Y off_axis_boresight = (math.cos(math.radians(30)), math.sin(math.radians(30)), 0.0) secondary = Telescope( name="Secondary Telescope", boresight=off_axis_boresight, optics=TelescopeConfig(aperture_m=0.15, focal_length_m=1.5), ) print(primary.optics.f_number) # 10.0 print(primary.optics.plate_scale_arcsec_per_um) # ~3.44e-5 arcsec/µm # Mix Telescope and plain Instrument in the same payload payload = Payload( instruments=[ primary, Instrument(name="Fine Guidance Sensor", power_draw=PowerDraw(nominal_power=15.0)), ] ) JSON Configuration ^^^^^^^^^^^^^^^^^^ ``Telescope`` serialises cleanly to/from JSON. The ``optics`` block maps directly to the ``TelescopeConfig`` fields: .. code-block:: json { "payload": { "instruments": [ { "name": "Primary Telescope", "boresight": [1.0, 0.0, 0.0], "power_draw": { "nominal_power": 80.0, "peak_power": 120.0, "power_mode": {} }, "data_generation": { "rate_gbps": 0.5, "per_observation_gb": 0.0 }, "optics": { "aperture_m": 0.6, "focal_length_m": 6.0, "f_number": 10.0, "telescope_type": "Ritchey-Chrétien", "tube_length_m": 1.2 } } ] } } battery ~~~~~~~ The :class:`~conops.config.Battery` models the spacecraft battery system. **Attributes:** * ``name`` (str): Battery identifier * ``amphour`` (float): Battery capacity in amp-hours * ``voltage`` (float): Battery voltage (Volts) * ``watthour`` (float): Total energy capacity (Watt-hours, auto-calculated) * ``max_depth_of_discharge`` (float): Maximum allowed DoD (0-1) * ``recharge_threshold`` (float): SOC level to end emergency recharge (0-1) * ``charge_level`` (float): Current charge in Watt-hours .. code-block:: python from conops.config import Battery battery = Battery( name="Primary Battery", amphour=20.0, voltage=28.0, watthour=560.0, # Optional, calculated from amphour * voltage max_depth_of_discharge=0.4, # Allow 40% discharge recharge_threshold=0.95, # Recharge until 95% SOC ) constraint ~~~~~~~~~~ The :class:`~conops.config.Constraint` defines pointing constraints for the spacecraft. **Attributes:** * ``sun_constraint``: Minimum angle from the Sun * ``anti_sun_constraint``: Maximum angle from anti-Sun direction * ``moon_constraint``: Minimum angle from the Moon * ``earth_constraint``: Minimum angle from Earth limb * ``panel_constraint``: Solar panel pointing constraint * ``ephem``: Ephemeris object (set at runtime, not serialized) .. code-block:: python import rust_ephem from conops.config import Constraint constraint = Constraint( sun_constraint=rust_ephem.SunConstraint(min_angle=45.0), moon_constraint=rust_ephem.MoonConstraint(min_angle=20.0), earth_constraint=rust_ephem.EarthLimbConstraint(min_angle=15.0), panel_constraint=( rust_ephem.SunConstraint(min_angle=45.0, max_angle=135.0) & ~rust_ephem.EclipseConstraint() ), ) # Set ephemeris at runtime constraint.ephem = ephemeris ground_stations ~~~~~~~~~~~~~~~ The :class:`~conops.config.GroundStationRegistry` contains all ground stations. **GroundStationRegistry Methods:** * ``add(station)``: Add a ground station * ``get(code)``: Get station by code * ``codes()``: List all station codes * ``default()``: Get pre-populated registry **GroundStation Attributes:** * ``code`` (str): Short identifier (e.g., "MAL", "SGS") * ``name`` (str): Human-readable name * ``latitude_deg`` (float): Latitude in degrees * ``longitude_deg`` (float): Longitude in degrees * ``elevation_m`` (float): Elevation in meters * ``min_elevation_deg`` (float): Minimum pass elevation * ``schedule_probability`` (float): Probability of scheduling (0-1) * ``bands`` (list[BandCapability]): Supported frequency bands * ``gain_db`` (float | None): Antenna gain in dB .. code-block:: python from conops.config import GroundStationRegistry, GroundStation, BandCapability ground_stations = GroundStationRegistry( stations=[ GroundStation( code="MAL", name="Malindi", latitude_deg=-3.22, longitude_deg=40.12, elevation_m=0.0, min_elevation_deg=10.0, schedule_probability=1.0, bands=[ BandCapability( band="S", uplink_rate_mbps=2.0, downlink_rate_mbps=10.0, ), ], ), GroundStation( code="SGS", name="Svalbard", latitude_deg=78.229, longitude_deg=15.407, min_elevation_deg=5.0, schedule_probability=0.8, bands=[ BandCapability(band="X", downlink_rate_mbps=150.0), ], ), ] ) recorder ~~~~~~~~ The :class:`~conops.config.OnboardRecorder` simulates onboard data storage. **Attributes:** * ``name`` (str): Recorder identifier * ``capacity_gb`` (float): Maximum capacity in Gigabits * ``current_volume_gb`` (float): Current stored data in Gigabits * ``yellow_threshold`` (float): Warning threshold (fraction, 0-1) * ``red_threshold`` (float): Critical threshold (fraction, 0-1) .. code-block:: python from conops.config import OnboardRecorder recorder = OnboardRecorder( name="Solid State Recorder", capacity_gb=128.0, current_volume_gb=0.0, yellow_threshold=0.7, # 70% full warning red_threshold=0.9, # 90% full critical ) fault_management ~~~~~~~~~~~~~~~~ The :class:`~conops.config.FaultManagement` monitors parameters and triggers safe mode. **Attributes:** * ``thresholds`` (list[FaultThreshold]): Parameter thresholds * ``red_limit_constraints`` (list[FaultConstraint]): Pointing constraints * ``safe_mode_on_red`` (bool): Trigger safe mode on RED conditions **FaultThreshold Attributes:** * ``name`` (str): Parameter name to monitor * ``yellow`` (float): Yellow (warning) threshold * ``red`` (float): Red (critical) threshold * ``direction`` (str): "below" or "above" .. code-block:: python from conops.config import FaultManagement, FaultThreshold import rust_ephem fault_management = FaultManagement( thresholds=[ FaultThreshold( name="battery_level", yellow=0.5, red=0.4, direction="below", # Alert when value drops below threshold ), FaultThreshold( name="recorder_fill_fraction", yellow=0.7, red=0.9, direction="above", # Alert when value exceeds threshold ), ], safe_mode_on_red=True, ) # Add red limit constraint for Sun avoidance fault_management.add_red_limit_constraint( name="Sun Avoidance", constraint=rust_ephem.SunConstraint(min_angle=20.0) & ~rust_ephem.EclipseConstraint(), time_threshold_seconds=120.0, description="Avoid pointing within 20° of Sun for more than 2 minutes", ) observation_categories ~~~~~~~~~~~~~~~~~~~~~~ The :class:`~conops.config.ObservationCategories` defines how observations are categorized based on their target ID (obsid) for visualization purposes. **Attributes:** * ``categories`` (list[ObservationCategory]): Category definitions * ``default_name`` (str): Default category name * ``default_color`` (str): Default visualization color .. code-block:: python from conops.config import ObservationCategories, ObservationCategory categories = ObservationCategories( categories=[ ObservationCategory( name="Science", obsid_min=10000, obsid_max=30000, color="tab:blue", ), ObservationCategory( name="Calibration", obsid_min=90000, obsid_max=91000, color="gray", ), ], default_name="Other", default_color="tab:purple", ) # Or use defaults categories = ObservationCategories.default_categories() visualization ~~~~~~~~~~~~~ The :class:`~conops.config.VisualizationConfig` controls plot appearance. This field is excluded from JSON serialization. **Attributes:** * ``mode_colors`` (dict): Colors for ACS modes * ``font_family`` (str): Font for plots * ``title_font_size`` (int): Title font size * ``label_font_size`` (int): Axis label font size * ``figsize`` (tuple): Default figure size * ``timeline_figsize`` (tuple): DITL timeline figure size * ``data_telemetry_figsize`` (tuple): Data management plot size .. code-block:: python from conops.config import VisualizationConfig visualization = VisualizationConfig( mode_colors={ "SCIENCE": "green", "SLEWING": "orange", "SAA": "purple", "PASS": "cyan", "CHARGING": "yellow", "SAFE": "red", }, font_family="DejaVu Sans", title_font_size=14, figsize=(16, 10), ) Complete Programmatic Example ----------------------------- Here is a complete example of creating a ``MissionConfig`` programmatically: .. code-block:: python from conops.config import ( MissionConfig, SpacecraftBus, SolarPanelSet, SolarPanel, Payload, Instrument, Battery, Constraint, GroundStationRegistry, GroundStation, OnboardRecorder, FaultManagement, PowerDraw, DataGeneration, BandCapability, AttitudeControlSystem, Heater, ) from conops.common import ACSMode import rust_ephem # Create fault management with custom thresholds fault_management = FaultManagement() fault_management.add_threshold("battery_level", yellow=0.5, red=0.4, direction="below") # Create the complete configuration config = MissionConfig( name="Example Observatory", spacecraft_bus=SpacecraftBus( name="Observatory Bus", power_draw=PowerDraw( nominal_power=50.0, peak_power=300.0, power_mode={0: 70.0, 1: 100.0}, eclipse_power=75.0 ), attitude_control=AttitudeControlSystem( slew_acceleration=0.01, max_slew_rate=0.3, slew_accuracy=0.01, settle_time=10.0 ), heater=Heater( name="Bus Heaters", power_draw=PowerDraw( nominal_power=25.0, eclipse_power=75.0 ) ) ), solar_panel=SolarPanelSet( name="Solar Array", panels=[ SolarPanel( name="Panel A", gimbled=False, sidemount=True, max_power=500.0, conversion_efficiency=0.94 ) ], conversion_efficiency=0.95 ), payload=Payload( instruments=[ Instrument( name="Main Instrument", power_draw=PowerDraw(nominal_power=100.0, peak_power=150.0), data_generation=DataGeneration(rate_gbps=0.001) ) ] ), battery=Battery( amphour=20.0, voltage=28.0, max_depth_of_discharge=0.4, recharge_threshold=0.95 ), constraint=Constraint( sun_constraint=rust_ephem.SunConstraint(min_angle=45.0), moon_constraint=rust_ephem.MoonConstraint(min_angle=20.0), earth_constraint=rust_ephem.EarthLimbConstraint(min_angle=15.0) ), ground_stations=GroundStationRegistry( stations=[ GroundStation( code="GND", name="Ground Station", latitude_deg=35.0, longitude_deg=-106.0, min_elevation_deg=10.0, bands=[BandCapability(band="S", downlink_rate_mbps=10.0)] ) ] ), recorder=OnboardRecorder( capacity_gb=64.0, yellow_threshold=0.7, red_threshold=0.9 ), fault_management=fault_management ) Automatic Fault Thresholds -------------------------- When a ``MissionConfig`` is created, it automatically initializes default fault thresholds based on the battery and recorder configuration: 1. **battery_level**: Yellow at ``1.0 - max_depth_of_discharge``, Red 10% below that 2. **recorder_fill_fraction**: Uses the recorder's ``yellow_threshold`` and ``red_threshold`` 3. **star_tracker_functional_count**: When star trackers are configured, both yellow and red are set to ``num_trackers - 1`` with ``direction="below"``. This fires the moment any tracker enters a hard constraint zone (``functional_count`` drops from ``num_trackers`` to ``num_trackers - 1``), making any hard violation immediately critical. No threshold is added if no star trackers are configured. You can override these by adding custom thresholds to the ``FaultManagement`` instance before calling ``init_fault_management_defaults()`` (defaults are skipped if a threshold for that parameter already exists). .. code-block:: python from conops.config import MissionConfig, FaultManagement from conops.common import ACSMode # Create config (automatically adds default thresholds on first use) config = MissionConfig() # Override the auto-configured battery threshold config.fault_management.add_threshold( "battery_level", yellow=0.35, red=0.25, direction="below", ) Using with DITL Simulation -------------------------- Pass the configuration to DITL for simulation: .. code-block:: python from conops.ditl import DITL, QueueDITL from datetime import datetime, timedelta # Create or load configuration config = MissionConfig.from_json_file("config.json") # Set ephemeris on constraint (required at runtime) config.constraint.ephem = ephemeris # Run simulation ditl = DITL( config=config, ephem=ephemeris, begin=datetime(2025, 1, 1), end=datetime(2025, 1, 2), ) ditl.calc() # Or use QueueDITL for target-driven simulation queue_ditl = QueueDITL( config=config, ephem=ephemeris, begin=datetime(2025, 1, 1), end=datetime(2025, 1, 2), ) queue_ditl.run() API Reference ------------- For detailed API documentation, see: * :class:`~conops.config.MissionConfig` * :class:`~conops.config.AttitudeConstraintPolicy` * :class:`~conops.config.SpacecraftBus` * :class:`~conops.config.SolarPanelSet` * :class:`~conops.config.SolarPanel` * :class:`~conops.config.Payload` * :class:`~conops.config.Instrument` * :class:`~conops.config.Telescope` * :class:`~conops.config.TelescopeConfig` * :class:`~conops.config.TelescopeType` * :class:`~conops.config.Battery` * :class:`~conops.config.Constraint` * :class:`~conops.config.GroundStationRegistry` * :class:`~conops.config.GroundStation` * :class:`~conops.config.OnboardRecorder` * :class:`~conops.config.FaultManagement` * :class:`~conops.config.AttitudeControlSystem` * :class:`~conops.config.PowerDraw` * :class:`~conops.config.DataGeneration` * :class:`~conops.config.CommunicationsSystem` * :class:`~conops.config.ObservationCategories` * :class:`~conops.config.VisualizationConfig`