SVF Equipment Library¶
Status: v0.5 Last updated: 2026-05 Author: lipofefeyt
Overview¶
Every spacecraft model in SVF is an Equipment - a Python class with named IN/OUT ports, a do_step() physics implementation, and SRDB-canonical parameter names. This document defines the interface contract for each reference model.
The contract is stable. If you replace a reference model with a higher-fidelity implementation or a hardware-in-the-loop adapter, only the wiring YAML changes - nothing else.
Fidelity Levels¶
All SVF equipment models declare a fidelity level. The level determines what the model is validated against and what test plan is appropriate.
| Level | Name | Definition | Validated Against |
|---|---|---|---|
| F1 | Functional | Correct port names and directions; trivial or stub physics (fixed values, pass-through). Used for wiring tests and OBC integration. | Port contract only |
| F2 | Behavioural | Correct dynamics structure - noise, bias, saturation, parametric hardware profiles. Sufficient for OBSW algorithm testing. | Domain physics knowledge |
| F3 | High-fidelity | Detailed physical modelling - nonlinear effects, thermal coupling, dynamic power budgets. Appropriate for mission performance analysis. | Flight heritage data or simulation benchmark |
| F4 | Validated | Parameters derived from acceptance test measurements or hardware-in-the-loop runs. Suitable for qualification evidence. | Hardware test data |
All reference models currently ship at F2. F3/F4 models are supplied by the mission team or via HIL adapters.
Equipment Contract Summary¶
| Equipment | Factory / Class | Subsystem | Bus Interface | Fidelity | Milestone |
|---|---|---|---|---|---|
| OBC | ObcEquipment |
DHS | 1553 BC | F2 | M7/M8 |
| OBC Stub | ObcStub |
DHS | - | F1 | M10 |
| OBC Emulator | OBCEmulatorAdapter |
DHS | binary pipe | F4 | M11 |
| TTC | TtcEquipment |
TTC | software | F2 | M7 |
| YAMCS Bridge | YamcsBridge |
GND | TCP | F2 | M12 |
| KDE Dynamics | make_kde_equipment() |
Dynamics | FMI 2.0 | F3 | M11.5 |
| Magnetometer | make_magnetometer() |
AOCS | - | F2 | M11.5 |
| Magnetorquer | make_magnetorquer() |
AOCS | - | F2 | M11.5 |
| Gyroscope | make_gyroscope() |
AOCS | - | F2 | M11.5 |
| CSS | make_css() |
AOCS | - | F2 | M11.5 |
| B-dot Controller | make_bdot_controller() |
AOCS | - | F2 | M11.5 |
| Reaction Wheel | make_reaction_wheel() |
AOCS | 1553 RT | F2 | M6/M8 |
| Star Tracker | make_star_tracker() |
AOCS | SpW/1553 | F2 | M8 |
| Thruster | make_thruster() |
AOCS/Prop | discrete | F2 | M17 |
| GPS Receiver | make_gps() |
NAV | UART/SPI | F2 | M17 |
| Thermal Model | make_thermal() |
THM | - | F2 | M17 |
| Solar Array | make_solar_array() |
EPS | - | F2 | M26 |
| Battery | make_battery() |
EPS | - | F2 | M26 |
| PCDU | make_pcdu() |
EPS | 1553/CAN | F2 | M9 |
EPS FMU (
FmuEquipment(EpsFmu)) was removed in M26. EPS is now fully covered bymake_solar_array(),make_battery(), andmake_pcdu().
F3/F4 upgrade paths¶
| Equipment | What F3 needs | What F4 needs |
|---|---|---|
| KDE Dynamics | Already F3 via FMI (6-DOF rigid body) | IMU mounting misalignment + flex modes from modal test |
| Reaction Wheel | Speed-dependent friction model | Measured friction curve from bearing test |
| Battery | Electro-chemical (e.g. SPKF) model | Discharge curves from acceptance test |
| Thermal Model | Multi-node radiative coupling (Gebhart) | MLI effective emittance from thermal vacuum test |
| GPS Receiver | Ionospheric/tropospheric delay model | Measured position noise from sky test |
Promoting a model from F2 to F3 with CalibrationCurve (M31)¶
The SRDB supports polynomial and piecewise-linear calibration curves on TM
parameters. Adding a CalibrationCurve to a parameter signals that raw ADC
counts are converted to engineering units before being written to
ParameterStore.
# srdb/baseline/aocs_sensors.yaml
parameters:
mag.field_x:
description: "Magnetometer X field"
unit: T
dtype: float
classification: TM
domain: AOCS
model_id: mag
calibration:
type: polynomial
coefficients: [0.0, 4.882813e-7] # raw counts → Tesla (16-bit ADC, ±16 Gauss)
checkcov detects inconsistencies: if a model is listed as F2 in
EQUIPMENT_FIDELITY but its SRDB parameters have CalibrationCurve entries,
the tool reports an INCONSISTENCY error and exits 1. Update the fidelity
level in tools/check_coverage.py when you add calibration data.
Port Name Convention¶
All AOCS sensor and actuator ports follow the pattern:
The equipment_id is the instance name passed to the factory (e.g. "rw1", "rw2", "str_front"). This lets you instantiate the same model type multiple times without port collisions.
Exception - shared truth ports: aocs.truth.rate_x/y/z are written by the KDE dynamics model and shared by all sensors. They use a fixed prefix because there is only one dynamics model per simulation.
Exception - GPS: GPS ports use <equipment_id>.<signal> (no aocs. prefix) to match the NAV subsystem naming convention.
Hardware Profile Support¶
All make_* AOCS factories accept equipment_id, hardware_profile, and hardware_dir parameters:
# Use built-in defaults, default id
rw = make_reaction_wheel(sync, store, cmd_store)
# Named instance - ports become aocs.rw_pitch.*
rw = make_reaction_wheel(sync, store, cmd_store,
equipment_id="rw_pitch")
# Load physics constants from a hardware profile
rw = make_reaction_wheel(sync, store, cmd_store,
equipment_id="rw_pitch",
hardware_profile="rw_sinclair_rw003",
hardware_dir="mission_mysat1/hardware_profiles")
Profiles live in mission_mysat1/hardware_profiles/. If hardware_dir is omitted, the loader searches the built-in profile path.
Available profiles:
| Profile | Type | Key Parameters |
|---|---|---|
rw_default |
reaction_wheel | 6000 rpm, 0.2 Nm |
rw_sinclair_rw003 |
reaction_wheel | 5000 rpm, 30 mNm |
mtq_default |
magnetorquer | 10 Am², 5 Ω |
mag_default |
magnetometer | 1×10⁻⁷ T noise |
gyro_default |
gyroscope | ARW 1×10⁻⁴ rad/s/√Hz |
str_default |
star_tracker | 30° sun exclusion, 10 s acquisition |
thr_default |
thruster | 1 N, Isp=70s (cold gas) |
thr_moog_monarc_1 |
thruster | 1 N, Isp=220s (hydrazine) |
gps_default |
gps | 5 m position noise |
gps_novatel_oem7 |
gps | 1.5 m position noise |
thermal_default |
thermal_node | 3-node (panels + internal) |
1. OBC Equipment¶
File: src/svf/models/obc.py / obc_stub.py / obc_emulator.py
Three drop-in implementations via ObcInterface:
# Simulated OBC
obc = ObcEquipment(config, sync, store, cmd_store)
# Rule-based OBSW simulator
obc = ObcStub(config, sync, store, cmd_store, rules=[...])
# Real OBSW binary under test
obc = OBCEmulatorAdapter(sim_path="obsw_sim", ...)
SRDB version handshake: OBCEmulatorAdapter reads SRDB version from obsw_sim stderr at startup and compares against installed obsw-srdb package. Logs WARNING on mismatch.
Ports¶
| Port | Direction | Description |
|---|---|---|
dhs.obc.mode_cmd |
IN | Mode command (0=SAFE, 1=NOMINAL) |
dhs.obc.mode |
OUT | Current FSM mode |
dhs.obc.obt |
OUT (s) | On-board time |
dhs.obc.watchdog_status |
OUT | 0=nominal, 1=warning, 2=reset |
dhs.obc.health |
OUT | 0=nominal, 1=degraded, 2=failed |
2. TTC Equipment + YAMCS Bridge¶
File: src/svf/models/ttc.py, src/svf/yamcs_bridge.py
# Without YAMCS
ttc = TtcEquipment(obc, sync, store, cmd_store)
# With YAMCS ground station
bridge = YamcsBridge(store)
bridge.start()
ttc = TtcEquipment(obc, sync, store, cmd_store, yamcs_bridge=bridge)
3. KDE Dynamics¶
File: src/svf/models/kde_equipment.py
6-DOF spacecraft physics via FMI 2.0 FMU. Provides truth state to all sensor models.
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.mtq.torque_x/y/z |
IN | Nm | MTQ torques |
aocs.truth.rate_x/y/z |
OUT | rad/s | True angular velocity → GYRO, CSS |
aocs.mag.true_x/y/z |
OUT | T | True B-field → MAG |
aocs.attitude.quaternion_w/x/y/z |
OUT | - | True attitude → ST |
4. AOCS Sensor Models¶
Magnetometer¶
make_magnetometer(sync, store, cmd_store,
equipment_id="mag", seed=None,
hardware_profile=None, hardware_dir=None)
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.<id>.power_enable |
IN | - | Power on/off |
aocs.<id>.true_x/y/z |
IN | T | True B-field from truth model |
aocs.<id>.field_x/y/z |
OUT | T | Measured field (noise + bias drift) |
aocs.<id>.status |
OUT | - | 0=off, 1=nominal |
Gyroscope¶
make_gyroscope(sync, store, cmd_store,
equipment_id="gyro", seed=None,
hardware_profile=None, hardware_dir=None)
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.<id>.power_enable |
IN | - | Power on/off |
aocs.truth.rate_x/y/z |
IN | rad/s | True angular rate (shared truth port) |
aocs.<id>.rate_x/y/z |
OUT | rad/s | Measured rate (noise + ARW + bias) |
aocs.<id>.status |
OUT | - | 0=off, 1=nominal |
Coarse Sun Sensor (CSS)¶
make_css(sync, store, cmd_store,
equipment_id="css", seed=None,
hardware_profile=None, hardware_dir=None)
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.<id>.power_enable |
IN | - | Power on/off |
aocs.truth.rate_x/y/z |
IN | rad/s | True angular rate (shared truth port) |
aocs.<id>.sun_x/y/z |
OUT | - | Estimated sun unit vector |
aocs.<id>.eclipse |
OUT | - | 1=eclipse |
aocs.<id>.status |
OUT | - | 0=off, 1=nominal, 2=eclipse |
B-dot Controller (validation oracle)¶
make_bdot_controller(sync, store, cmd_store,
equipment_id="bdot",
gain=1e4, max_dipole=10.0,
mag_id="mag", mtq_id="mtq")
Note: This is a Python validation oracle - not flight code. The flight b-dot runs in openobsw.
mag_id and mtq_id select which magnetometer and magnetorquer instance to read/command. Port prefixes resolve to aocs.<mag_id>.field_* and aocs.<mtq_id>.dipole_*.
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.<id>.enable |
IN | - | Enable control (0=off, 1=on) |
aocs.<mag_id>.field_x/y/z |
IN | T | MAG measurement |
aocs.<mtq_id>.dipole_x/y/z |
OUT | Am² | MTQ dipole commands |
aocs.<id>.bdot_x/y/z |
OUT | T/s | Estimated B-dot (telemetry) |
aocs.<id>.active |
OUT | - | 1=controller active |
5. Magnetorquer¶
make_magnetorquer(sync, store, cmd_store,
equipment_id="mtq",
hardware_profile=None, hardware_dir=None)
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.<id>.power_enable |
IN | - | Power on/off |
aocs.<id>.dipole_x/y/z |
IN | Am² | Dipole commands |
aocs.<id>.b_field_x/y/z |
IN | T | Local B-field for torque calculation |
aocs.<id>.torque_x/y/z |
OUT | Nm | Torque = m × B |
aocs.<id>.status |
OUT | - | 0=off, 1=nominal |
aocs.<id>.power_w |
OUT | W | Power consumption |
6. Reaction Wheel¶
make_reaction_wheel(sync, store, cmd_store,
equipment_id="rw1",
hardware_profile=None, hardware_dir=None)
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.<id>.torque_cmd |
IN | Nm | Torque command |
aocs.<id>.speed |
OUT | rpm | Wheel speed |
aocs.<id>.temperature |
OUT | °C | Bearing temperature |
aocs.<id>.status |
OUT | - | 0=off, 1=nominal, 2=over-temp |
Multiple wheels: pass different equipment_id values ("rw_x", "rw_y", "rw_z", "rw_skew").
7. Star Tracker¶
make_star_tracker(sync, store, cmd_store,
equipment_id="str1", seed=None,
hardware_profile=None, hardware_dir=None)
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.<id>.power_enable |
IN | - | Power on/off |
aocs.<id>.sun_angle |
IN | deg | Sun angle for blinding check |
aocs.<id>.quaternion_w/x/y/z |
OUT | - | Attitude quaternion (noise added) |
aocs.<id>.validity |
OUT | - | 1=valid measurement |
aocs.<id>.mode |
OUT | - | 0=off, 1=acquiring, 2=tracking |
8. Thruster¶
make_thruster(sync, store, cmd_store,
equipment_id="thr1",
hardware_profile=None, hardware_dir=None)
Physics: propellant consumption via rocket equation (Δm = F / (Isp × g₀) × dt).
| Port | Direction | Unit | Description |
|---|---|---|---|
aocs.<id>.enable |
IN | - | Fire command (1=fire) |
aocs.<id>.thrust_cmd |
IN | N | Commanded thrust |
aocs.<id>.thrust |
OUT | N | Actual thrust |
aocs.<id>.temperature |
OUT | °C | Thruster temperature |
aocs.<id>.propellant |
OUT | kg | Remaining propellant mass |
aocs.<id>.status |
OUT | - | 0=off 1=nominal 2=low_prop 3=empty 4=over_temp |
Status codes are exported as STATUS_OFF, STATUS_NOMINAL, STATUS_LOW_PROP, STATUS_EMPTY, STATUS_OVER_TEMP from svf.models.aocs.thruster.
9. GPS Receiver¶
make_gps(sync, store, cmd_store,
equipment_id="gps", seed=None,
hardware_profile=None, hardware_dir=None)
Truth state from KDE (position/velocity). Gaussian noise added per axis. GPS ports use <equipment_id>.<signal> (no aocs. prefix).
| Port | Direction | Unit | Description |
|---|---|---|---|
<id>.power_enable |
IN | - | Power on/off |
<id>.truth.pos_x/y/z |
IN | m | True ECI position from KDE |
<id>.truth.vel_x/y/z |
IN | m/s | True ECI velocity from KDE |
<id>.eclipse |
IN | - | Eclipse flag from CSS |
<id>.position_x/y/z |
OUT | m | Measured ECI position |
<id>.velocity_x/y/z |
OUT | m/s | Measured ECI velocity |
<id>.fix |
OUT | - | 1=valid fix |
<id>.altitude_km |
OUT | km | Altitude above sphere |
<id>.status |
OUT | - | 0=off 1=acquiring 2=fix 3=eclipse_outage |
Status codes exported as STATUS_OFF, STATUS_ACQUIRING, STATUS_FIX, STATUS_ECLIPSE_OUTAGE from svf.models.aocs.gps.
10. Thermal Model¶
make_thermal(sync, store, cmd_store, hardware_profile=None)
N-node configurable thermal network. Node count and properties from hardware profile.
| Port | Direction | Unit | Description |
|---|---|---|---|
thermal.solar_illumination |
IN | - | 0=eclipse, 1=sun |
thermal.equipment_power_w |
IN | W | Equipment dissipation |
thermal.{node_id}.temp_degc |
OUT | °C | Per-node temperature |
thermal.cavity.temp_degc |
OUT | °C | Internal cavity temperature |
thermal.min_temp_degc |
OUT | °C | Coldest node |
thermal.max_temp_degc |
OUT | °C | Hottest node |
Default 3 nodes: panel_plus_x, panel_minus_x, internal.
11. S-Band Transponder¶
make_sbt(sync, store, cmd_store)
| Port | Direction | Unit | Description |
|---|---|---|---|
ttc.sbt.uplink_signal_level |
IN | dBm | Signal level |
ttc.sbt.uplink_lock |
OUT | - | 1=locked |
ttc.sbt.rx_bitrate |
OUT | bps | Uplink bit rate |
ttc.sbt.tx_bitrate |
OUT | bps | Downlink bit rate |
12. PCDU¶
make_pcdu(sync, store, cmd_store)
| Port | Direction | Unit | Description |
|---|---|---|---|
eps.solar_array.generated_power |
IN | W | Solar power |
eps.pcdu.lcl{1-8}.enable |
IN | - | Per-LCL enable |
eps.pcdu.total_load |
OUT | W | Total load |
eps.pcdu.uvlo_active |
OUT | - | 1=UVLO active |
Adding a New Equipment Model¶
def make_my_sensor(sync, store, cmd_store,
equipment_id="mysensor",
hardware_profile=None, hardware_dir=None):
# Physics constants as factory locals (not module globals)
noise_std = 0.01
if hardware_profile is not None:
from svf.config.hardware_profile import load_hardware_profile
p = load_hardware_profile(hardware_profile, hardware_dir)
noise_std = p.get("noise_std", noise_std)
_pfx = f"aocs.{equipment_id}"
def _step(eq, t, dt):
val = eq.read_port(f"{_pfx}.input")
eq.write_port(f"{_pfx}.output", val + rng.gauss(0, noise_std))
return NativeEquipment(
equipment_id=equipment_id,
ports=[
PortDefinition(f"{_pfx}.input", PortDirection.IN),
PortDefinition(f"{_pfx}.output", PortDirection.OUT),
],
step_fn=_step,
sync_protocol=sync, store=store, command_store=cmd_store,
)
Key rules:
- Define _step inside the factory so physics constants are captured as closure variables. Module-level step functions break multi-instance support.
- Use _pfx = f"aocs.{equipment_id}" for all port names. Hard-coding equipment names in port strings prevents running two instances simultaneously.
- Load profile values with p.get("key", local_default) and assign back to the local constant. Calling load_hardware_profile without using the result is a silent no-op bug.
Add a hardware profile in mission_mysat1/hardware_profiles/mysensor_default.yaml and declare the equipment in spacecraft.yaml.