A high-performance actor-model framework built in Rust for assembling, simulating, and running the GMT control system. Compose multi-rate concurrent actors with a type-safe domain-specific language.
From structural dynamics to optics, all wired together through a safe, composable actor graph.
Each subsystem runs as an independent async Tokio actor. Actors communicate via typed channels—no shared mutable state, no data races.
Declare actor graphs with a concise, readable macro. Wiring, sampling-rate negotiation, and feedback loops are resolved at compile time.
Different subsystems run at different rates—FEM at 8 kHz, actuators at 100 Hz, optics at 1 kHz. Samplers are inserted automatically.
First-class clients for every telescope subsystem: M1/M2 mirrors, mount control, ASM voice coils, wind loads, and wavefront sensors.
Every signal carries a Rust type. Mismatched connections are compile errors. Physics units are encoded in types—arcseconds, nanometers, and more.
Annotate any output with ~ to stream it live to a scope display, or with $ to record it to Apache Arrow / Parquet.
The structural dynamics solver (gmt_dos-clients_fem with the cuda feature) and the full optical simulation (gmt_dos-clients_crseo) are CUDA-accelerated, enabling high-fidelity real-time models.
Split a model across multiple scripts — on the same machine or across a network — using the transceiver client. Scripts exchange actor data over the QUIC protocol with minimal latency.
Wind load forces and dome-seeing wavefront errors are sourced from the GMT CFD simulations database at cfd.gmto.im, directly consumed by the windloads and domeseeing clients.
Integrated models can run self-hosted on local hardware or on GMTO IM-owned cloud machines, enabling collaborative simulation campaigns without local data or compute constraints.
Bring your own subsystem by implementing the Read, Update, and Write traits from gmt_dos-actors-clients_interface. Any Rust struct that satisfies the interface becomes a first-class actor in the graph.
actorscript!A procedural macro that turns a concise dataflow description into a fully wired, asynchronous actor graph.
Each line in actorscript! is a flow: a sampling-rate multiplier followed by a chain of actors connected by arrows. The macro generates all the channel scaffolding, spawns Tokio tasks, and resolves rate mismatches for you.
| Token | Meaning |
|---|---|
| 1: | Sampling-rate multiplier (1× nominal, 8×, 80×, …) |
| actor[Type] | Actor with its output data type |
| -> | Data flow — connect output to next actor's input |
| ! | Send immediately — breaks feedback-loop deadlocks |
| ~ | Scope — stream the signal live to a real-time display |
| $ | Enable data logging to Apache Arrow / Parquet |
| {sys::Sub} | Scoped access to a sub-actor inside a system |
use gmt_dos_actors::actorscript;
use gmt_dos_clients::signals::{Signal, Signals};
use gmt_dos_clients_io::{
gmt_m1::M1RigidBodyMotions,
gmt_m2::M2RigidBodyMotions,
mount::{MountEncoders, MountSetPoint, MountTorques},
optics::TipTilt,
};
use gmt_dos_clients_lom::LinearOpticalModel;
use gmt_dos_clients_mount::Mount;
use interface::units::Arcsec;
use skyangle::Conversion;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 1 arcsec step on the elevation axis
let setpoint = Signals::new(3, n_step)
.channel(1, Signal::Constant(1f64.from_arcsec()));
let fem = state_space; // Finite-element structural model
let mount = Mount::new(); // Mount PID feedback controller
let lom = LinearOpticalModel::new()?; // Linear optical sensitivity
actorscript! {
#[labels(fem = "GMT Structural Model",
mount = "Mount\nControl",
lom = "Linear Optical\nModel")]
// Feedback loop: set-point → controller → FEM → encoders → controller
1: setpoint[MountSetPoint]
-> mount[MountTorques]
-> fem[MountEncoders]! -> mount
// Structural motion → optical model
1: fem[M1RigidBodyMotions] -> lom
1: fem[M2RigidBodyMotions] -> lom[Arcsec<TipTilt>]~
}
Ok(())
}
use gmt_dos_actors::actorscript;
use gmt_dos_clients::signals::{OneSignal, Signal, Signals};
use gmt_dos_clients::smooth::{Smooth, Weight};
use gmt_dos_clients_io::{
cfd_wind_loads::{CFDM1WindLoads, CFDM2WindLoads, CFDMountWindLoads},
gmt_m1::M1RigidBodyMotions,
gmt_m2::{M2RigidBodyMotions, asm::M2ASMReferenceBodyNodes},
mount::{MountEncoders, MountSetPoint, MountTorques},
optics::WfeRms,
};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Sigmoid envelope ramps wind loads on smoothly
let sigmoid = OneSignal::try_from(Signals::new(1, n_step).channel(0,
Signal::Sigmoid { amplitude: 1.0,
sampling_frequency_hz: 8000.0 }))?;
let cfd_loads = CfdLoads::foh(".", 8000).build()?;
let fem = state_space;
let mount = Mount::new();
let lom = LinearOpticalModel::new()?; // M1 + M2
let m1_lom = LinearOpticalModel::new()?; // M1 only
let asm_shell_lom = LinearOpticalModel::new()?; // ASM shell
let asm_rb_lom = LinearOpticalModel::new()?; // ASM ref-body
actorscript! {
#[labels(fem = "GMT FEM", mount = "Mount\nControl")]
// Mount feedback
1: setpoint[MountSetPoint]
-> mount[MountTorques]
-> fem[MountEncoders]! -> mount
// Wind loads tapered in via sigmoid × smoother
1: cfd_loads[CFDM1WindLoads] -> m1_smoother
1: sigmoid[Weight] -> m1_smoother[CFDM1WindLoads] -> fem
1: cfd_loads[CFDM2WindLoads] -> m2_smoother
1: sigmoid[Weight] -> m2_smoother[CFDM2WindLoads] -> fem
1: cfd_loads[CFDMountWindLoads] -> mount_smoother
1: sigmoid[Weight] -> mount_smoother[CFDMountWindLoads] -> fem
// Stream WFE RMS to scope every 8 samples (1 kHz)
8: lom[WfeRms<-6>]~
1: fem[M1RigidBodyMotions] -> lom
1: fem[M2RigidBodyMotions] -> lom
8: m1_lom[M1RbmWfeRms]~
1: fem[M1RigidBodyMotions] -> m1_lom
8: asm_shell_lom[AsmShellWfeRms]~
1: fem[M2RigidBodyMotions] -> asm_shell_lom
8: asm_rb_lom[AsmRefBodyWfeRms]~
1: fem[M2ASMReferenceBodyNodes] -> asm_rb_lom
}
Ok(())
}
use gmt_dos_actors::{actorscript, system::Sys};
use gmt_dos_clients_io::{
cfd_wind_loads::{CFDM1WindLoads, CFDM2WindLoads, CFDMountWindLoads},
gmt_m1::M1RigidBodyMotions,
gmt_m2::{M2RigidBodyMotions, asm::M2ASMReferenceBodyNodes},
optics::WfeRms,
};
use gmt_dos_clients_lom::LinearOpticalModel;
use gmt_dos_clients_servos::{
AsmsServo, GmtServoMechanisms, WindLoads, asms_servo::ReferenceBody,
};
use gmt_dos_clients_windloads::{CfdLoads, system::{M1, M2, Mount, SigmoidCfdLoads}};
const ACTUATOR_RATE: usize = 80; // actuators run at 8000/80 = 100 Hz
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Load (or build) pre-serialised system state
let cfd_loads = Sys::<SigmoidCfdLoads>::from_data_repo_or_else("windloads.bin", || {
CfdLoads::foh(".", 8000).duration(5.0).windloads(&fem, Default::default())
})?;
let gmt_servos = Sys::<GmtServoMechanisms<ACTUATOR_RATE, 1>>::from_data_repo_or_else(
"servos.bin",
|| GmtServoMechanisms::<ACTUATOR_RATE, 1>::new(8000.0, fem)
.wind_loads(WindLoads::default())
.asms_servo(AsmsServo::new().reference_body(ReferenceBody::new())),
)?;
// Four optical models for independent WFE contributions
let lom = LinearOpticalModel::new()?; // M1 + M2 combined
let m1_lom = LinearOpticalModel::new()?; // M1 rigid-body only
let asm_shell_lom = LinearOpticalModel::new()?; // ASM shell
let asm_rb_lom = LinearOpticalModel::new()?; // ASM reference body
actorscript! {
// Wind loads into the servo-mechanism system (FEM + controllers)
1: {cfd_loads::M1}[CFDM1WindLoads] -> {gmt_servos::GmtFem}
1: {cfd_loads::M2}[CFDM2WindLoads] -> {gmt_servos::GmtFem}
1: {cfd_loads::Mount}[CFDMountWindLoads] -> {gmt_servos::GmtFem}
// Stream combined WFE to scope every 8 samples (1 kHz)
8: lom[WfeRms<-6>]~
1: {gmt_servos::GmtFem}[M1RigidBodyMotions] -> lom
1: {gmt_servos::GmtFem}[M2RigidBodyMotions] -> lom
// M1 rigid-body contribution
8: m1_lom[M1RbmWfeRms]~
1: {gmt_servos::GmtFem}[M1RigidBodyMotions] -> m1_lom
// ASM shell contribution
8: asm_shell_lom[AsmShellWfeRms]~
1: {gmt_servos::GmtFem}[M2RigidBodyMotions] -> asm_shell_lom
// ASM reference-body contribution
8: asm_rb_lom[AsmRefBodyWfeRms]~
1: {gmt_servos::GmtFem}[M2ASMReferenceBodyNodes] -> asm_rb_lom
}
Ok(())
}
Each demo in the demos/ crate is a self-contained model of the GMT operating under realistic conditions.
Commands a 1-arcsecond step on the elevation axis and records the tip-tilt optical response through the linear optical model.
Same step test but with the full GMT servo-mechanism system instead of the bare mount controller, including all actuator dynamics.
CFD wind-load time series applied to the telescope structure with a sigmoid ramp-on. Streams wavefront-error RMS from four independent optical models to scope.
Adds the M1 primary-mirror hardpoint and actuator control loops. Demonstrates multi-rate operation at 8 kHz (FEM) and 100 Hz (M1 actuators).
The most complete demo: Mount + M1 mirror + M2 positioners + ASM voice coils + optical model logging segment piston, tip-tilt, and WFE.
Uses the high-level GmtServoMechanisms system crate — loads precomputed model state and streams four WFE metrics in parallel to scope.
Adds the Fast-Steering Mirror (FSM) subsystem to the servo model for image-stabilization loop closure at the focal plane.
Demonstrates model bootstrapping: first a short bootstrap run with #[model(state = ready)], then a full simulation using steady-state initial conditions.
A simplified view of the step-mount model. Each box is an actor or display client; arrows are typed channels.
Every subsystem lives in its own crate. Mix and match only what your model needs.
Actor runtime, channel scaffolding, and the actorscript! procedural macro.
Parser and code-generator for the actorscript domain-specific language.
Shared I/O type identifiers for all GMT subsystem inputs and outputs.
Discrete-time state-space solver wrapping the GMT finite-element model. Enable the cuda feature for GPU-accelerated solving via fem-cuda-solver.
CFD-derived wind force and torque time series with first-order-hold interpolation, sourced from the GMT CFD simulations database at cfd.gmto.im.
PID-based mount controller for azimuth, elevation, and GIR axes.
M1 primary mirror hardpoint feedback and segment actuator force distribution.
M2 positioner controller, Adaptive Secondary Mirror (ASM) voice-coil driver, and Fast Steering Mirror piezo-stack actuator driver.
Linear Optical Model: maps rigid-body motions to wavefront error, tip-tilt, and segment piston via sensitivity matrices.
Full optical simulation client wrapping the crseo CUDA-accelerated library for GPU-accelerated end-to-end ray tracing and Fourier optics simulation.
Dome-seeing wavefront-error maps sourced from the GMT CFD simulations database at cfd.gmto.im.
Composite M1 system: hardpoints, actuators, and control modes pre-wired.
Composite M2 ASMS or FSMS system with reference-body dynamics included.
Live signal display using a client-server architecture. Actors annotated with ~ stream data to a scope client for real-time visualisation.
Apache Arrow / Parquet logger — attach $ to any output for zero-copy serialisation.