Status: Core architecture principle enforced in models/core.py
Last Updated: February 22, 2026
Authority: Kernel-centric architecture (single state mutation point)
The MASTER DATA MODEL defines immutable state containers that form the complete contract between the kernel and all physics modules.
Key Principle: The kernel is the single authority for state. Physics modules are pure functions.
Kernel (state manager) ← → Physics Modules (pure functions)
↑
└── Physics modules NEVER communicate directly
└── All interaction flows ONLY through kernel
The complete spacecraft state snapshot. Every physics module receives this.
@dataclass(frozen=True)
class SpacecraftState:
# PRIMARY DYNAMICS
position: Vector3 # [m] inertial frame
velocity: Vector3 # [m/s] inertial frame
# ATTITUDE
attitude_quaternion: Quaternion # [q0, q1, q2, q3] normalized
angular_velocity: Vector3 # [rad/s] inertial frame
# SPACECRAFT PROPERTIES
mass: float # [kg] total spacecraft mass
power_available: float # [W] available electrical power
# INTERNAL STATES
thermal_state: Dict[str, float] # [K] subsystem temperatures
structural_modes: Tuple[float] # structural deformation modes
# PROPULSION
propulsion: PropulsionState # thrust capability
# METADATA
epoch_sec: float # [s] absolute time since J2000
mission_elapsed_sec: float # [s] time since mission start
valid: bool # state is physically validInterface Contract:
- All vectors in inertial frame unless noted
- All quantities in SI units
- Frozen (immutable) to prevent accidental mutations
- Kernel is the only component that creates new SpacecraftState instances
Backward Compatibility:
The implementation maintains references (r, v, q, omega) to old field names for legacy code compatibility.
The external environment affecting the spacecraft. Populated by environment models (solar cycle, CME, radiation).
@dataclass(frozen=True)
class EnvironmentState:
solar_flux: float # [W/m²] solar irradiance
magnetic_field: Vector3 # [T] inertial frame field
solar_wind_density: float # [kg/m³] plasma density
radiation_index: float # [0-10] radiation belt (Kp-like)
epoch_sec: float # [s] absolute timeResponsibility Chain:
- Environment models (solar_cycle.py, cme.py, radiation_belts.py) compute conditions
- Kernel packages these into EnvironmentState
- All physics modules receive the same EnvironmentState snapshot
- Result: Consistent environment across all modules for a given time step
Propulsion system capability and performance.
@dataclass(frozen=True)
class PropulsionState:
thrust_vector: Vector3 # [N] thrust in inertial frame
mass_flow_rate: float # [kg/s] propellant consumption
efficiency: float # [0-1] thruster efficiency
fuel_remaining_kg: float # [kg] remaining propellantManagement:
- Part of SpacecraftState
- Updated by propulsion module or GNC logic
- Propagated through engine for fuel tracking
The output of physics computation. Represents dr/dt, dv/dt, etc.
@dataclass(frozen=True)
class StateDerivative:
velocity_derivative: Vector3 # dr/dt = v [m/s]
acceleration: Vector3 # dv/dt [m/s²]
angular_acceleration: Vector3 # dω/dt [rad/s²]
power_derivative: float # dP/dt [W]
mass_derivative: float # dm/dt [kg/s]
thermal_derivatives: Dict[str] # dT/dt [K/s] per subsystemIntegration:
- ODE solver combines StateDerivatives from all modules
- Kernel advances state:
state_new = state_old + dt * combined_derivatives - Physics modules never see intermediate updates
Every physics module implements this strict contract:
class PhysicsModule(ABC):
"""All modules MUST implement this interface."""
def compute(self, physics_input: PhysicsInput) -> PhysicsOutput:
"""Pure function: (state, environment, config) → output"""
pass@dataclass(frozen=True)
class PhysicsInput:
state: SpacecraftState # complete immutable state snapshot
environment: EnvironmentState # consistent environment snapshot
t_epoch: float # absolute time [s]
params: Dict[str, Any] # module-specific configurationConstraint: Input is immutable. Module cannot modify it.
@dataclass(frozen=True)
class PhysicsOutput:
state_derivative: StateDerivative # change in state (for integration)
diagnostics: Dict[str, float] # scalar measurements (log these)
events: List[Event] # discrete occurrences detected
valid: bool # computation succeededConstraint: No side effects. Output is purely functional.
How the kernel orchestrates physics modules:
Step t:
├─ state_t = current spacecraft state (immutable)
├─ environment_t = current environment (immutable)
│
├─ For each physics module M:
│ ├─ input = PhysicsInput(state_t, environment_t, t, params_M)
│ ├─ output_M = M.compute(input)
│ └─ collect output_M
│
├─ Combine all state_derivatives
│ └─ combined = sum of all module derivatives
│
└─ ODE integration:
└─ state_{t+dt} = integrate(state_t, combined, dt)
└─ state_{t+dt} is a NEW immutable SpacecraftState
Advantages:
- Composable: Add/remove modules without code changes
- Testable: Each module tested independently
- Deterministic: Same input → identical output (with seed)
- Parallelizable: All modules can run in parallel (read-only state)
- Auditable: Complete state history, no hidden mutations
All data containers use @dataclass(frozen=True):
try:
state.position = (1.0, 2.0, 3.0) # AttributeError!
except FrozenInstanceError:
print("Cannot mutate frozen dataclass")# This is a type error (mypy will catch it):
output: PhysicsOutput = module.compute(input)
output.state_derivative = new_derivative # Type mismatch!
# Correct usage:
output = PhysicsOutput(
state_derivative=new_derivative,
diagnostics={...},
events=[...],
valid=True
)The kernel enforces that modules cannot create new SpacecraftState instances:
# In kernel/engine.py
def _integrate_step(self, state: SpacecraftState, dt: float):
# 1. Get module outputs
outputs = [m.compute(PhysicsInput(...)) for m in self.modules]
# 2. Combine derivatives (modules cannot interfere)
derivatives = [out.state_derivative for out in outputs]
combined = self._combine_derivatives(derivatives)
# 3. KERNEL creates new state (modules never do this)
new_state = self._advance_state(state, combined, dt)
return new_state # Return new immutable snapshotEnforcement: Modules receive SpacecraftState but cannot instantiate new ones.
from models.core import (
PhysicsModule, PhysicsInput, PhysicsOutput,
StateDerivative, PhysicsModuleType
)
class TwoBodyModule(PhysicsModule):
"""Orbital mechanics: r̈ = -μ/r³ * r"""
def __init__(self, mu: float):
self.mu = mu # gravitational parameter [m³/s²]
def compute(self, input: PhysicsInput) -> PhysicsOutput:
# Read immutable inputs
pos = input.state.position
vel = input.state.velocity
# Compute gravitational acceleration
r_mag = (pos[0]**2 + pos[1]**2 + pos[2]**2) ** 0.5
a_mag = self.mu / (r_mag ** 3)
accel = (-a_mag * pos[0], -a_mag * pos[1], -a_mag * pos[2])
# Construct derivative (immutable)
derivative = StateDerivative(
velocity_derivative=vel, # dr/dt = v
acceleration=accel, # dv/dt = a
angular_acceleration=(0, 0, 0), # no torque
)
# Return output (immutable)
return PhysicsOutput(
state_derivative=derivative,
diagnostics={"r_mag_m": r_mag},
events=[], # no special events
valid=True
)
def get_module_type(self) -> PhysicsModuleType:
return PhysicsModuleType.ORBITAL_MECHANICSKey Points:
- Module receives immutable
PhysicsInput - Reads freely from
state,environment,params - Computes new StateDerivative (doesn't modify input)
- Returns immutable
PhysicsOutput - Kernel integrates the derivative to produce new state
# Old: PhysicsInput with optional environment dict
class PhysicsInput:
state: SpacecraftState
t_epoch: float
params: Dict
environment: Optional[Dict[str, float]] = None # ❌ untyped
# Old: PhysicsOutput with separate fields
class PhysicsOutput:
acceleration: Vector3
torque: Vector3
diagnostics: Dict
state_derivative: Optional[Dict] = None
valid: bool# New: EnvironmentState is a formal data class
class PhysicsInput:
state: SpacecraftState
environment: EnvironmentState # ✓ typed, structured
t_epoch: float
params: Dict
# New: StateDerivative is explicit
class PhysicsOutput:
state_derivative: StateDerivative # ✓ complete derivative
diagnostics: Dict
events: List[Event]
valid: bool# 1. Update compute() signature (old → new)
def compute(self, input: PhysicsInput) -> PhysicsOutput:
# 2. Access environment as structured data
solar_flux = input.environment.solar_flux # typed
# 3. Create StateDerivative explicitly
derivative = StateDerivative(
velocity_derivative=vel,
acceleration=accel,
angular_acceleration=ang_accel,
power_derivative=power_rate,
# ... other derivatives
)
# 4. Return PhysicsOutput
return PhysicsOutput(
state_derivative=derivative,
diagnostics=diags,
events=events,
valid=True
)def test_immutability():
state = SpacecraftState(
position=(1, 2, 3),
velocity=(4, 5, 6),
attitude_quaternion=(1, 0, 0, 0),
angular_velocity=(0, 0, 0),
mass=1000,
power_available=500,
thermal_state={},
structural_modes=()
)
# Cannot mutate
with pytest.raises(FrozenInstanceError):
state.position = (7, 8, 9)def test_determinism():
module = TwoBodyModule(mu=1.327e20)
input1 = PhysicsInput(state=state, environment=env, t_epoch=0, params={})
output1 = module.compute(input1)
output2 = module.compute(input1) # Same input
assert output1.state_derivative.acceleration == output2.state_derivative.accelerationdef test_no_side_effects():
module = TwoBodyModule(mu=1.327e20)
original_state = state
module.compute(PhysicsInput(state=original_state, ...))
# State unchanged
assert original_state.position == (1, 2, 3)
assert original_state.velocity == (4, 5, 6)| Aspect | Guarantee |
|---|---|
| State Mutation | Kernel only (no modules) |
| Module Interface | (state, env, config) → output |
| Data Mutability | Frozen dataclasses (immutable) |
| Module Communication | No direct; all through kernel |
| Determinism | Same input → identical output |
| Parallelization | All modules can run simultaneously |
| Testability | Each module tested independently |
| Auditability | Complete immutable state history |
Result: A production-grade, composable aerospace simulation framework.