astrodyn 0.1.1

Pipeline orchestration, VehicleBuilder, and recipes — single API surface for ECS adapters
Documentation
// JEOD_INV: TS.01 — `<SelfRef>` / `<SelfPlanet>` are runtime-resolved storage-boundary wildcards; see `docs/JEOD_invariants.md` row TS.01 and the lint at `tests/self_ref_self_planet_discipline.rs`.
//! Vehicle-level configuration types.
//!
//! [`VehicleConfig`] is the user-facing description of a single simulated
//! vehicle: initial state plus all physics configuration. Mission code passes
//! one to `SimulationBuilder::add_body` (or to a Bevy spawn helper in Phase 9).
//!
//! Phase 6 of #101 relocated [`VehicleConfig`] and its companion option
//! structs out of `astrodyn_runner`; the runner and the future Bevy adapter both
//! consume this single description.

use glam::DMat3;

use crate::integrator::IntegratorType;
use crate::interactions::FlatPlateState;
use crate::EulerSequence;
use astrodyn_gravity::GravityControls;
use astrodyn_interactions::DragConfig;

use astrodyn_dynamics::state::TranslationalStateTyped;
use astrodyn_dynamics::{MassPropertiesTyped, RotationalStateTyped};
use astrodyn_quantities::frame::{RootInertial, SelfRef};

// ── Frame switching ─────────────────────────────────────────────────────

/// Trigger condition for a frame switch.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwitchSense {
    /// Switch when the body approaches the target frame origin.
    OnApproach,
    /// Switch when the body departs from the current frame origin.
    OnDeparture,
}

/// Configuration for a distance-based integration frame switch.
///
/// Port of JEOD's `DynBodyFrameSwitch` body action. When triggered, the
/// body's integration frame is reparented to the target source's inertial
/// frame in the frame tree, and gravity controls are flipped to make the
/// target source non-differential (central body).
///
/// Generic over `SourceId` to mirror [`astrodyn_gravity::GravityControls`]:
/// `astrodyn_runner::Simulation` uses the default `SourceId = usize` (sources
/// are identified by their registration order); the Bevy adapter uses
/// `SourceId = bevy::ecs::entity::Entity` (sources are identified by
/// their ECS entity). The lifted
/// [`crate::evaluate_and_apply_frame_switch`] helper is generic over the
/// same type so both consumers share one implementation.
#[derive(Debug, Clone)]
pub struct FrameSwitchConfig<SourceId = usize> {
    /// Identifier of the gravity source whose inertial frame to switch to.
    /// On switch, this source becomes non-differential and all others become
    /// differential, matching JEOD's `GravityInteraction::set_integ_frame()`.
    pub target_source: SourceId,
    /// Whether to switch on approach or departure.
    pub switch_sense: SwitchSense,
    /// Distance threshold (meters).
    pub switch_distance: f64,
    /// Whether this switch is active.
    pub active: bool,
}

// ── Solar radiation pressure ────────────────────────────────────────────

/// Solar radiation pressure model — mutually exclusive variants.
///
/// The `FlatPlate` variant carries `FlatPlateState<SelfRef>`: this
/// adapter-neutral struct is the runtime-resolved boundary where the
/// vehicle phantom is `SelfRef` (the per-entity adapter knows the
/// concrete vehicle at runtime). The underlying
/// [`astrodyn_interactions::FlatPlate<V>`] is `<V: Vehicle>`-parametric so
/// mission code that pins a concrete vehicle (e.g.
/// `FlatPlateState<Iss>`) can demonstrate cross-vehicle compile-time
/// blocking before lowering through the runner; the runner-facing
/// `VehicleConfig` always lands at `<SelfRef>`.
#[derive(Debug, Clone)]
pub enum SrpModel {
    /// Per-plate modeling with thermal emission.
    FlatPlate(FlatPlateState<astrodyn_quantities::frame::SelfRef>),
    /// Simple cannonball model.
    Cannonball {
        /// Effective cross-section area (m²).
        cx_area: f64,
        /// Surface albedo.
        albedo: f64,
        /// Diffuse reflection fraction.
        diffuse: f64,
    },
}

// ── Shadow body ─────────────────────────────────────────────────────────

/// Shadow-casting body for SRP eclipse computation.
#[derive(Debug, Clone, Copy)]
pub struct ShadowBody {
    /// Index into the gravity source table.
    pub source_idx: usize,
    /// Body radius (m) for eclipse geometry.
    pub radius: f64,
}

// ── Geodetic computation ────────────────────────────────────────────────

/// Geodetic computation configuration.
#[derive(Debug, Clone, Copy)]
pub struct GeodeticConfig {
    /// Gravity source index (must have `t_inertial_pfix` for planet-fixed rotation).
    pub source_idx: usize,
    /// Equatorial radius (m).
    pub r_eq: f64,
    /// Polar radius (m).
    pub r_pol: f64,
}

// ── Earth lighting ──────────────────────────────────────────────────────

/// Earth lighting computation configuration.
#[derive(Debug, Clone, Copy)]
pub struct EarthLightingConfig {
    /// Earth mean radius (m) for eclipse geometry.
    pub earth_radius: f64,
    /// Moon mean radius (m) for eclipse geometry.
    pub moon_radius: f64,
    /// Sun mean radius (m) for eclipse geometry.
    pub sun_radius: f64,
}

// ── Derived state requests ──────────────────────────────────────────────

/// All derived-state requests for a vehicle, grouped in one place.
#[derive(Debug, Clone, Default)]
pub struct DerivedStateConfig {
    /// Gravity source index for orbital elements. `None` = skip.
    pub orbital_elements_source: Option<usize>,
    /// Euler angle decomposition sequence. `None` = skip.
    pub euler_sequence: Option<EulerSequence>,
    /// Whether to compute LVLH frame each step.
    pub lvlh: bool,
    /// Geodetic computation config. `None` = skip.
    pub geodetic: Option<GeodeticConfig>,
    /// Whether to compute solar beta angle. Requires `sun_source` on Simulation.
    pub solar_beta: bool,
    /// Earth lighting config. Requires `sun_source` and `moon_source`.
    pub earth_lighting: Option<EarthLightingConfig>,
}

// ── Vehicle configuration ───────────────────────────────────────────────

/// User-facing vehicle configuration.
///
/// Passed to `SimulationBuilder::add_body` to create a simulated
/// vehicle. Contains initial state plus all physics configuration.
/// `VehicleConfig` is adapter-neutral: it has no output fields, and
/// results are read back via the adapter's own output view (the
/// standalone runner exposes one; the Bevy adapter reads components).
pub struct VehicleConfig {
    // ── Initial state ──
    /// Translational state in the root-inertial frame, typed
    /// end-to-end. The runner re-tags as `<IntegrationFrame>` at
    /// `SimBody::new`; the Bevy adapter relabels to
    /// `<PlanetInertial<P>>` via the `From<TranslationalStateTyped<RootInertial>>`
    /// component impl. Mission code that constructs `VehicleConfig`
    /// directly via struct literal can pass an untyped
    /// `TranslationalState` via `.into()` (the
    /// `From<TranslationalState> for TranslationalStateTyped<F>` impl
    /// in `astrodyn_dynamics` lifts at the boundary).
    pub trans: TranslationalStateTyped<RootInertial>,
    /// Rotational state (typed). `None` for 3-DOF bodies. The vehicle
    /// phantom is the runtime-resolved wildcard `<SelfRef>` (JEOD_INV
    /// `TS.01`); the runner / Bevy adapter drops to raw at the
    /// construction boundary. Mission code can pass an untyped
    /// `RotationalState` via `.into()` (the
    /// `From<RotationalState> for RotationalStateTyped<V>` impl in
    /// `astrodyn_dynamics` lifts at the boundary).
    pub rot: Option<RotationalStateTyped<SelfRef>>,
    /// Mass properties (typed). `None` for massless test particles
    /// (gravity-only). Phantom is `<SelfRef>` (JEOD_INV `TS.01`);
    /// mission code can pass an untyped `MassProperties` via `.into()`.
    pub mass: Option<MassPropertiesTyped<SelfRef>>,

    // ── Dynamics ──
    /// Integration method. Defaults to `IntegratorType::Rk4`.
    pub integrator: IntegratorType,
    /// Structural-to-body rotation matrix. `DMat3::IDENTITY` when structure = body.
    pub t_struct_body: DMat3,

    // ── Gravity ──
    /// Gravity controls referencing sources by index.
    pub gravity_controls: GravityControls<usize>,
    /// Whether to compute gravity gradient (needed for gravity torque).
    pub compute_gravity_gradient: bool,

    // ── Interactions ──
    /// Drag configuration. `None` disables drag.
    pub drag: Option<DragConfig>,
    /// Solar radiation pressure model. `None` disables SRP.
    pub srp: Option<SrpModel>,
    /// Shadow-casting body for SRP eclipse. `None` = full illumination.
    pub shadow_body: Option<ShadowBody>,

    // ── Derived state requests ──
    /// Derived state computation requests.
    pub derived: DerivedStateConfig,

    // ── External loads ──
    /// External force in the root-inertial frame, typed end-to-end.
    pub external_force:
        astrodyn_quantities::aliases::Force<astrodyn_quantities::frame::RootInertial>,
    /// External torque in the body frame, typed against the wildcard
    /// vehicle phantom `<SelfRef>` at this storage boundary
    /// (per-vehicle phantom is runtime-resolved by the runner / Bevy
    /// adapter; documented under JEOD_INV `TS.01`).
    pub external_torque: astrodyn_quantities::aliases::Torque<
        astrodyn_quantities::frame::BodyFrame<astrodyn_quantities::frame::SelfRef>,
    >,

    // ── Frame switching ──
    /// Gravity source whose inertial frame is used for integration.
    /// `None` means the root frame (Earth.inertial). `Some(idx)` means
    /// the inertial frame of the source at that index.
    ///
    /// **Non-root caveat (issue #263).** When `Some(...)`, the
    /// integrated translational state is integ-frame-relative rather
    /// than root-inertial, but downstream Bevy storage
    /// (`TranslationalStateC<RootInertial>`) is still tagged
    /// `RootInertial` — issue #263 Section A.1. Derived-state
    /// consumers that read the state as absolute root-inertial
    /// (geodetic vs. another planet, solar-beta, SRP relative to a Sun
    /// position not in the integ frame) will silently produce wrong
    /// answers. Until #263 closes, mission code should either avoid
    /// non-root integration or restrict derived states to ones
    /// evaluated in the same source's frame.
    pub integ_source: Option<usize>,
    /// Distance-based frame switch triggers.
    pub frame_switches: Vec<FrameSwitchConfig>,
}

impl Default for VehicleConfig {
    fn default() -> Self {
        Self {
            trans: TranslationalStateTyped::<RootInertial>::default(),
            rot: None,
            mass: None,
            integrator: IntegratorType::default(),
            t_struct_body: DMat3::IDENTITY,
            gravity_controls: GravityControls::default(),
            compute_gravity_gradient: false,
            drag: None,
            srp: None,
            shadow_body: None,
            derived: DerivedStateConfig::default(),
            external_force: astrodyn_quantities::aliases::Force::<
                astrodyn_quantities::frame::RootInertial,
            >::zero(),
            external_torque: astrodyn_quantities::aliases::Torque::<
                astrodyn_quantities::frame::BodyFrame<astrodyn_quantities::frame::SelfRef>,
            >::zero(),
            integ_source: None,
            frame_switches: Vec::new(),
        }
    }
}