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
| Layer | Crate | Responsibility |
|---|---|---|
| Foundation | utsuroi | Generic ODE solvers (RK4, DOP853, Dormand-Prince, Störmer-Verlet, Yoshida). Exposes OdeState, DynamicalSystem. |
| Foundation | arika | Typed coordinate frames (ECI / ECEF / IAU), time scales (UTC / TT / TDB / TAI), Meeus analytic ephemerides, JPL Horizons fetcher, WGS-84, EOP. |
| Environment | tobari | Atmosphere models (Exponential, Harris-Priester, NRLMSISE-00), geomagnetic field (IGRF-14, tilted-dipole), space-weather providers (CSSI, GFZ). |
| Simulation | orts | OrbitalState / AttitudeState / SpacecraftState, unified Model<S> trait, OrbitalSystem / AttitudeSystem / SpacecraftDynamics, sensors, plugin host, Rerun .rrd output. |
| Application | orts-cli | orts run / orts serve / orts replay / orts convert. Embeds the viewer and exposes a WebSocket stream on port 9001. |
| Extension | orts-plugin-sdk | Rust SDK for writing WASM plugin guest controllers (callback-style or main-loop style). |
| Bridge | rrd-wasm | Rerun 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 onS(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::Frameis the inertial frame the state is propagated in, declared once and shared byHasOrbitandHasAttitudeas their supertrait. A model returnsExternalLoads<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 aVec3<F>). A requirement that is a property of the frame’s axes is written per frame instead:ConstantThrustholds its direction fixed for a whole burn, which the of-dateCirsandTemecannot honour, so it implementsModelforSimpleEciandGcrsrather than for everyF: Eci.- Systems come in three flavors —
OrbitalSystem,AttitudeSystem,SpacecraftDynamics— each aDynamicalSystemthat bundles a state withVec<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 aCommand(per-MTQ dipole, per-wheel speed or torque, per-thruster throttle). - Runtime:
wasmtimewith 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
SourceEventstream, so live and replay go through one pipeline. The live WebSocket path bridges through theuseWebSockethook (useWebSocketSource); file replay (CSVFileAdapter/RrdFileAdapter) parses off the main thread in Web Workers. - Two views, shared primitives: the
./libentry exposes an orbit view (OrbitViewer→OrbitScene→OrbitSceneContents) and an attitude view (AttitudeViewer→AttitudeScene→AttitudeSceneContents), 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) andSpacecraftVisualrather than the scene graph — see DESIGN.md for why.DirectionArrowsis 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
- 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 underOrbitalSystemandSpacecraftDynamicswithout duplication. - Type-safe coordinate frames.
Vec3<F: Frame>makes ECI / ECEF / Body distinct types so frame mix-ups are compile errors, not silent bugs. - 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 throughGroupState<S: OdeState>. - Deterministic plugin execution. The Pulley interpreter makes guest behavior reproducible across hosts and CI environments.
- Source abstraction at the viewer edge. Transport (WS / CSV / RRD) is
normalized to a single
SourceEventtype, so adding a new source only means implementing one adapter.
7. See also
- DESIGN.md — extended design intent (Japanese)
- ROADMAP.md — planned but unimplemented work (Japanese)
- README.md — installation, quick start, feature list
- CLAUDE.md — guide for Claude Code working on this repo
- Docs site: https://sksat.github.io/orts/
- Per-crate
README.mdunderorts/,arika/,utsuroi/,tobari/,uneri/,viewer/,plugin-sdk/