Skip to content

Latest commit

 

History

History
345 lines (273 loc) · 10.2 KB

File metadata and controls

345 lines (273 loc) · 10.2 KB

"""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.

Overview

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 value

Pattern 1: Simple Function Wrapping

If 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,
        )

Pattern 2: Object-Wrapper (Recommended)

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)

Pattern 3: Composition (Multiple Sub-modules)

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,
        )

Pattern 4: Async/Batch (Future-Ready)

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 ...
        pass

Integration Checklist

For 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

Example: Wrapping sail-physics Package

# 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) ** 2

Running with Wrapped Modules

from 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()

Backward Compatibility

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.acceleration

Testing Wrappers

Unit 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

Migration Timeline

  1. Week 1: Wrap orbital-mechanics, sail-physics (highest priority)
  2. Week 2: Wrap propulsion-hybrid, structural-dynamics
  3. Week 3: Wrap core-math integrators, GNC controllers
  4. Week 4: Wrap environment-models, mission-optimizer
  5. 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. """