"""Module Integration Guide for Heliosail-RX Production Architecture
This guide shows how to wrap existing physics packages (orbital-mechanics, sail-physics, propulsion-hybrid, etc.) as production-grade PhysicsModule implementations.
Every existing physics package can be adapted to the new interface with minimal changes:
Before (prototype style):
# From orbital_mech package
from orbital_mech import TwoBodyProblem
a = TwoBodyProblem(r=(7e9, 0, 0), t_epoch=0, mu=1.327e20)After (production style, backward compatible):
# Wrapped in PhysicsModule interface
from api_wrappers import OrbitMechModule
module = OrbitMechModule(mu=1.327e20)
output = module.compute(PhysicsInput(state, t_epoch, params))
# output.acceleration → same a valueIf your module is a pure function:
# EXISTING: core-math/core_math/ode_rk4.py
def rk4_step(state, dt, accel_fn):
\"\"\"Compute one RK4 step.\"\"\"
# ... integrator code ...
return new_state
# WRAPPER: models/wrappers/integrators.py
from models import PhysicsInput, PhysicsOutput
from core_math import rk4_step
class RK4IntegratorModule:
def compute(self, inp: PhysicsInput) -> PhysicsOutput:
# inp.state is current state
# inp.params has 'dt', 'accel_fn' key
dt = inp.params.get('dt', 10.0)
# Call existing function
new_state = rk4_step(inp.state, dt, accel_fn=...)
return PhysicsOutput(
acceleration=(0, 0, 0), # RK4 doesn't compute forces
diagnostics={'dt': dt},
valid=True,
)Create a thin wrapper around existing module:
# WRAPPER: models/wrappers/orbital_mechanics.py
from models import PhysicsInput, PhysicsOutput, Event, EventType
from orbital_mech import TwoBodyProblem, PerturbationStack
class OrbitMechModule:
def __init__(self, mu: float = 1.327e20, enable_perturbations: bool = True):
self.mu = mu
self.enable_perturbations = enable_perturbations
self.two_body = TwoBodyProblem(mu)
if enable_perturbations:
self.pert_stack = PerturbationStack()
def compute(self, inp: PhysicsInput) -> PhysicsOutput:
# Extract state
r = inp.state.r
v = inp.state.v
# Compute 2-body
a_2body = self.two_body.acceleration(r)
# Compute perturbations if enabled
a_pert = [0, 0, 0]
if self.enable_perturbations:
a_pert = self.pert_stack.perturbations(
r=r,
v=v,
t_epoch=inp.t_epoch,
params=inp.params,
)
# Sum
a_total = tuple(a_2body[i] + a_pert[i] for i in range(3))
# Diagnostics from both
diags = {
'a_2body_mag': sum(x**2 for x in a_2body) ** 0.5,
'a_pert_mag': sum(x**2 for x in a_pert) ** 0.5,
'r_mag': sum(x**2 for x in r) ** 0.5,
}
# Events (e.g., escape hyperbolic)
events = []
if self._is_hyperbolic(v, inp.state.r):
events.append(Event(
type=EventType.MILESTONE,
t_epoch=inp.t_epoch,
description="Achieved escape velocity",
))
return PhysicsOutput(
acceleration=a_total,
diagnostics=diags,
events=events,
valid=True,
)
def _is_hyperbolic(self, v, r):
v_sq = sum(x**2 for x in v)
r_mag = sum(x**2 for x in r) ** 0.5
v_esc_sq = 2 * self.mu / r_mag
return v_sq > v_esc_sq
# Usage:
module = OrbitMechModule(enable_perturbations=True)
mission.add_physics_module("orbital_mechanics", module)Combine multiple existing modules:
# WRAPPER: models/wrappers/full_dynamics.py
from models import PhysicsInput, PhysicsOutput
from orbital_mech import OrbitalMechanics
from sail_physics import SRPTensor, MembraneThermal
from propulsion_hybrid import PowerBudget, Thruster
class FullDynamicsModule:
def __init__(self):
self.orbital = OrbitalMechanics()
self.srp = SRPTensor()
self.thermal = MembraneThermal()
self.power = PowerBudget()
self.thruster = Thruster()
def compute(self, inp: PhysicsInput) -> PhysicsOutput:
# Compute each sub-module in order
a_orbital = self.orbital.acceleration(inp.state.r)
a_srp = self.srp.compute(inp.state, inp.t_epoch, inp.params)
q_thermal = self.thermal.absorbed_power(inp.state, inp.t_epoch)
power_available = self.power.available(inp.state.power_state)
# Thruster might fire based on power and guidance
a_thrust = [0, 0, 0]
if power_available > 100: # Watts
a_thrust = self.thruster.acceleration(power=100, direction=...)
# Sum all accelerations
a_total = tuple(
a_orbital[i] + a_srp[i] + a_thrust[i]
for i in range(3)
)
return PhysicsOutput(
acceleration=a_total,
diagnostics={
'a_orbital': sum(x**2 for x in a_orbital) ** 0.5,
'a_srp': sum(x**2 for x in a_srp) ** 0.5,
'q_absorbed': q_thermal,
'power': power_available,
},
valid=True,
)For modules that could benefit from parallelization:
# WRAPPER: models/wrappers/async_module.py
import asyncio
from models import PhysicsInput, PhysicsOutput
class AsyncPhysicsModule:
def __init__(self, num_workers: int = 4):
self.num_workers = num_workers
async def compute_async(self, inp: PhysicsInput) -> PhysicsOutput:
# This can run in thread pool if needed
return await self._compute_internal(inp)
def compute(self, inp: PhysicsInput) -> PhysicsOutput:
# Synchronous wrapper for kernel
loop = asyncio.get_event_loop()
return loop.run_until_complete(self.compute_async(inp))
async def _compute_internal(self, inp):
# ... actual computation ...
passFor each physics package (e.g., orbital-mechanics):
- Create wrapper file:
models/wrappers/orbital_mech_wrapper.py - Implement
compute(inp: PhysicsInput) -> PhysicsOutput - Export from
models/wrappers/__init__.py - Write unit test comparing old & new interface
- Add example to
examples/showing usage - Update this document with the wrapper pattern used
# models/wrappers/sail_physics_wrapper.py
from models import PhysicsInput, PhysicsOutput
from sail_physics.srp_core import solar_radiation_pressure
from sail_physics.thermal import solve_membrane_heat
class SailPhysicsModule:
def __init__(self, sail_area_m2: float = 196.0):
self.A = sail_area_m2
def compute(self, inp: PhysicsInput) -> PhysicsOutput:
# SRP acceleration
F_srp = solar_radiation_pressure(
r=inp.state.r,
normal=self._sail_normal(inp.state),
area=self.A,
R=inp.params.get('reflectivity', 0.85),
)
# Account for spacecraft mass
mass = inp.params.get('mass_kg', 260.0)
a_srp = tuple(F_srp[i] / mass for i in range(3))
# Membrane heat (if enabled)
q_absorbed = 0.0
if inp.params.get('thermal_enabled', True):
q_absorbed = solve_membrane_heat(
irradiance=self._solar_irradiance(inp.state.r),
T_space=inp.params.get('T_space_K', 300.0),
)
return PhysicsOutput(
acceleration=a_srp,
diagnostics={
'srp_force_N': sum(x**2 for x in F_srp) ** 0.5,
'q_absorbed_W': q_absorbed,
},
valid=True,
)
def _sail_normal(self, state):
# Simplified: point toward sun
r = state.r
r_mag = sum(x**2 for x in r) ** 0.5
return tuple(-r[i] / r_mag for i in range(3))
def _solar_irradiance(self, r):
AU = 1.496e11
irr_1au = 1361.0
r_mag = sum(x**2 for x in r) ** 0.5
return irr_1au * (AU / r_mag) ** 2from api import Mission
from models.wrappers import (
OrbitMechModule,
SailPhysicsModule,
PropulsionModule,
)
mission = Mission(config)
mission.add_physics_module("orbital_mechanics", OrbitMechModule())
mission.add_physics_module("sail_physics", SailPhysicsModule(sail_area_m2=196))
mission.add_physics_module("propulsion", PropulsionModule())
result = mission.run()Wrapped modules remain compatible with old code:
# Old way still works:
from orbital_mech import TwoBodyProblem
a = TwoBodyProblem(...).acceleration(r)
# New way:
module = OrbitMechModule()
inp = PhysicsInput(state, t_epoch, params)
output = module.compute(inp)
a = output.accelerationUnit test against original implementation:
# tests/test_wrappers.py
from models import SpacecraftState, PhysicsInput
from models.wrappers import OrbitMechModule
from orbital_mech import TwoBodyProblem
def test_orbit_mech_wrapper():
state = SpacecraftState(r=(7e9, 0, 0), ...)
inp = PhysicsInput(state=state, t_epoch=0, params={})
# New way
module = OrbitMechModule()
out_new = module.compute(inp)
# Old way
old_module = TwoBodyProblem()
a_old = old_module.acceleration(state.r)
# Should match
for i in range(3):
assert abs(out_new.acceleration[i] - a_old[i]) < 1e-6- Week 1: Wrap orbital-mechanics, sail-physics (highest priority)
- Week 2: Wrap propulsion-hybrid, structural-dynamics
- Week 3: Wrap core-math integrators, GNC controllers
- Week 4: Wrap environment-models, mission-optimizer
- Week 5+: Polish, testing, documentation
Status: Pattern library ready. Awaiting physics module owners to create wrappers.
Next: Create models/wrappers/ directory and implement first set of wrappers.
"""