Skip to content

Architecture

Architecture

日本語版: ARCHITECTURE.ja.md

1. Overview

orts is split into a Rust workspace (simulation core + CLI + plugin SDK) and a TypeScript side (real-time 3D viewer + streaming charts). The two talk over WebSocket when running live, or over files (RRD / CSV) for replay.

flowchart TB
  subgraph rust["Rust workspace"]
    utsuroi["utsuroi<br/>ODE solvers"]
    arika["arika<br/>frames / time / ephemeris"]
    tobari["tobari<br/>Earth environment"]
    orts["orts<br/>orbit + attitude + spacecraft"]
    cli["orts-cli<br/>run / serve / replay / convert"]
    sdk["orts-plugin-sdk<br/>WASM guest SDK"]
    rrdwasm["rrd-wasm<br/>RRD decoder (wasm)"]
  end
  subgraph ts["TypeScript packages"]
    uneri["uneri<br/>DuckDB-wasm + uPlot"]
    viewer["orts-viewer<br/>React + @react-three/fiber"]
  end
  wasm[(WASM plugins<br/>Component Model)]

  arika --> tobari
  arika --> orts
  utsuroi --> orts
  tobari --> orts
  orts --> cli
  sdk -. implements WIT world .-> orts
  wasm -. loaded by .-> orts
  cli -- WebSocket :9001 --> viewer
  rrdwasm --> viewer
  uneri --> viewer

2. Rust workspace layering

LayerCrateResponsibility
FoundationutsuroiGeneric ODE solvers (RK4, DOP853, Dormand-Prince, Störmer-Verlet, Yoshida). Exposes OdeState, DynamicalSystem.
FoundationarikaTyped coordinate frames (ECI / ECEF / IAU), time scales (UTC / TT / TDB / TAI), Meeus analytic ephemerides, JPL Horizons fetcher, WGS-84, EOP.
EnvironmenttobariAtmosphere models (Exponential, Harris-Priester, NRLMSISE-00), geomagnetic field (IGRF-14, tilted-dipole), space-weather providers (CSSI, GFZ).
SimulationortsOrbitalState / AttitudeState / SpacecraftState, unified Model<S> trait, OrbitalSystem / AttitudeSystem / SpacecraftDynamics, sensors, plugin host, Rerun .rrd output.
Applicationorts-cliorts run / orts serve / orts replay / orts convert. Embeds the viewer and exposes a WebSocket stream on port 9001.
Extensionorts-plugin-sdkRust SDK for writing WASM plugin guest controllers (callback-style or main-loop style).
Bridgerrd-wasmRerun RRD decoder compiled to WebAssembly for in-browser replay.

3. Core trait hierarchy

The simulation core is built on two ideas: a generic numerical-integration abstraction from utsuroi, and a capability-based model system in orts that lets the same perturbation model be reused across orbit-only, attitude- only, and coupled spacecraft systems.

classDiagram
  class OdeState {
    <<trait>>
    +zero_like()
    +axpy()
    +scale()
    +error_norm()
  }
  class DynamicalSystem {
    <<trait>>
    +type State : OdeState
    +derivatives(t, y, dy)
  }

  class HasFrame {
    <<capability>>
    +type Frame : Eci
  }
  class HasOrbit {
    <<capability>>
    +orbit() OrbitalState~Frame~
  }
  class HasAttitude {
    <<capability>>
    +attitude() AttitudeState
    +attitude_to_inertial() Rotation~Body, Frame~
  }
  class HasMass {
    <<capability>>
    +mass() f64
  }

  class Model~S~ {
    <<trait>>
    +name() str
    +eval(t, state, epoch) ExternalLoads~S::Frame~
  }

  OdeState <|.. OrbitalState
  OdeState <|.. AttitudeState
  OdeState <|.. SpacecraftState

  HasFrame <|-- HasOrbit
  HasFrame <|-- HasAttitude

  HasFrame <|.. OrbitalState
  HasFrame <|.. AttitudeState
  HasFrame <|.. SpacecraftState
  HasOrbit <|.. OrbitalState
  HasOrbit <|.. SpacecraftState
  HasAttitude <|.. AttitudeState
  HasAttitude <|.. SpacecraftState
  HasMass <|.. SpacecraftState

Key points:

  • A Model<S> declares the state capabilities it needs via trait bounds on S (e.g. impl<S: HasFrame + HasOrbit> Model<S> for atmospheric drag, impl<S: HasFrame + HasAttitude + HasOrbit> Model<S> for gravity-gradient torque). The same implementation plugs into any system whose state satisfies those bounds.
  • HasFrame::Frame is the inertial frame the state is propagated in, declared once and shared by HasOrbit and HasAttitude as their supertrait. A model returns ExternalLoads<S::Frame>: the frame it reports loads in is the frame it read the state in, so the two cannot disagree. A model carrying a frame of its own binds it to the state’s with an equality bound, which is where such a bound says something: because it needs a capability of the frame (impl<F: EarthFixedTransform, S: HasFrame<Frame = F> + HasOrbit> Model<S> for AtmosphericDrag<F>), or because it holds frame-typed data (ConstantThrust<F> stores its Δv as a Vec3<F>). A requirement that is a property of the frame’s axes is written per frame instead: ConstantThrust holds its direction fixed for a whole burn, which the of-date Cirs and Teme cannot honour, so it implements Model for SimpleEci and Gcrs rather than for every F: Eci.
  • Systems come in three flavors — OrbitalSystem, AttitudeSystem, SpacecraftDynamics — each a DynamicalSystem that bundles a state with Vec<Box<dyn Model<S>>>.

4. Plugin system

Guest controllers (attitude control laws, mode managers, etc.) run in a WebAssembly sandbox so they can be written in any language that targets WASI and the Component Model.

  • Interface: WIT world at orts/wit/v0/orts.wit.
  • World exports (guest → host): metadata(config), run(config), current-mode().
  • World imports (host → guest): host-env (geomagnetic field, logging), tick-io (wait_tick, send_command).
  • Per-tick contract: the host supplies a TickInput (truth state + per-device sensor readings + actuator telemetry); the guest replies with a Command (per-MTQ dipole, per-wheel speed or torque, per-thruster throttle).
  • Runtime: wasmtime with the Pulley interpreter for deterministic, host-independent execution.
  • Distribution: .wasm (portable).

WASM guests are driven through the PluginController trait; built-in native controllers implement the separate DiscreteController trait. Unifying the two is planned — see ROADMAP.md.

5. Data flow (simulation → viewer)

sequenceDiagram
  participant sim as orts-cli serve
  participant ws as WebSocket :9001
  participant src as Source layer
  participant trail as TrailBuffer (GPU)
  participant chart as ChartBuffer (ring)
  participant duck as DuckDB (uneri)
  participant ui as React UI

  sim->>ws: WsMessage { State, metrics }
  ws->>src: SourceEvent
  src->>trail: orbit points
  src->>chart: columnar samples
  src->>duck: ingest (history cache)
  chart->>ui: uPlot live frame
  duck->>ui: zoom / downsampled query
  • Live path (hot): ChartBuffer → uPlot directly. DuckDB is not on the live render path.
  • History path (cold): IngestBuffer → DuckDB is the cache used for zoom, downsampling, and post-hoc queries. Eventually consistent with the ring buffer.
  • Source abstraction: every input normalizes into the same SourceEvent stream, so live and replay go through one pipeline. The live WebSocket path bridges through the useWebSocket hook (useWebSocketSource); file replay (CSVFileAdapter / RrdFileAdapter) parses off the main thread in Web Workers.
  • Two views, shared primitives: the ./lib entry exposes an orbit view (OrbitViewerOrbitSceneOrbitSceneContents) and an attitude view (AttitudeViewerAttitudeSceneAttitudeSceneContents), each a batteries-included wrapper over a bring-your-own-Canvas scene graph over an internal renderer. They share the display-frame transform (displayFrame.ts) and SpacecraftVisual rather than the scene graph — see DESIGN.md for why. DirectionArrows is written to the same contract and drawn today by the attitude view; the orbit view picks it up in a follow-up.

6. Design principles

  1. Capability-based composition. States declare what they provide (HasOrbit, HasAttitude, HasMass); models declare what they need. This is the mechanism that lets one drag implementation work under OrbitalSystem and SpacecraftDynamics without duplication.
  2. Type-safe coordinate frames. Vec3<F: Frame> makes ECI / ECEF / Body distinct types so frame mix-ups are compile errors, not silent bugs.
  3. Monomorphization over dynamic dispatch on the hot path. ODE state is fixed-size (6D / 7D / 14D, plus auxiliary state via AugmentedState) so the integrator inlines tightly. Variable-N cases (constellations, flexible bodies) go through GroupState<S: OdeState>.
  4. Deterministic plugin execution. The Pulley interpreter makes guest behavior reproducible across hosts and CI environments.
  5. Source abstraction at the viewer edge. Transport (WS / CSV / RRD) is normalized to a single SourceEvent type, so adding a new source only means implementing one adapter.

7. See also