Skip to main content

phoxal_model/
simulation.rs

1//! Canonical simulation facts used after authored documents are loaded.
2//!
3//! A simulation describes how one component type behaves in a simulated world.
4//! It never introduces capabilities of its own: every entry here must match a
5//! capability the component type already declares, of the same
6//! [`CapabilityKind`].
7
8use std::collections::BTreeMap;
9
10use crate::component::capability::CapabilityKind;
11use crate::identity::{CapabilityId, LinkId};
12
13/// The simulated behaviour of one component type.
14#[derive(
15    phoxal_macros::DescribeWire, serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq,
16)]
17#[serde(deny_unknown_fields)]
18pub struct Simulation {
19    capabilities: BTreeMap<CapabilityId, Capability>,
20    links: BTreeMap<LinkId, Link>,
21}
22
23/// The simulated properties of one component-local link.
24#[derive(
25    phoxal_macros::DescribeWire, serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq,
26)]
27#[serde(deny_unknown_fields)]
28pub struct Link {
29    contact_material: Option<String>,
30}
31
32impl Simulation {
33    pub(crate) fn new(
34        capabilities: BTreeMap<CapabilityId, Capability>,
35        links: BTreeMap<LinkId, Option<String>>,
36    ) -> Self {
37        Self {
38            capabilities,
39            links: links
40                .into_iter()
41                .map(|(id, contact_material)| (id, Link { contact_material }))
42                .collect(),
43        }
44    }
45
46    /// Every simulated capability, ordered by capability id.
47    pub fn capabilities(&self) -> impl ExactSizeIterator<Item = (&CapabilityId, &Capability)> {
48        self.capabilities.iter()
49    }
50
51    /// The named simulated capability, if this simulation models it.
52    #[must_use]
53    pub fn capability(&self, id: &str) -> Option<&Capability> {
54        self.capabilities.get(id)
55    }
56
57    /// Every simulated link, ordered by link id.
58    pub fn links(&self) -> impl ExactSizeIterator<Item = (&LinkId, &Link)> {
59        self.links.iter()
60    }
61}
62
63impl Link {
64    /// The named contact material, when the world defines one for this link.
65    #[must_use]
66    pub fn contact_material(&self) -> Option<&str> {
67        self.contact_material.as_deref()
68    }
69}
70
71/// Canonical simulation parameters normalized from a versioned `simulation.yaml`.
72#[derive(
73    phoxal_macros::DescribeWire, serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq,
74)]
75#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
76pub enum Capability {
77    Motor(Motor),
78    Encoder(Encoder),
79    Accelerometer(Accelerometer),
80    Gyroscope(Gyroscope),
81    Magnetometer(Magnetometer),
82    Imu(Imu),
83    Gnss(Gnss),
84    Camera(Camera),
85    Depth(Depth),
86    Range(Range),
87    Lidar(Lidar),
88    Mmwave(Mmwave),
89    Microphone(Microphone),
90    Speaker,
91    Battery,
92    Led,
93    EmergencyStop,
94}
95
96impl Capability {
97    /// The device kind this simulation models.
98    #[must_use]
99    pub const fn kind(&self) -> CapabilityKind {
100        match self {
101            Self::Motor(_) => CapabilityKind::Motor,
102            Self::Encoder(_) => CapabilityKind::Encoder,
103            Self::Accelerometer(_) => CapabilityKind::Accelerometer,
104            Self::Gyroscope(_) => CapabilityKind::Gyroscope,
105            Self::Magnetometer(_) => CapabilityKind::Magnetometer,
106            Self::Imu(_) => CapabilityKind::Imu,
107            Self::Gnss(_) => CapabilityKind::Gnss,
108            Self::Camera(_) => CapabilityKind::Camera,
109            Self::Depth(_) => CapabilityKind::Depth,
110            Self::Range(_) => CapabilityKind::Range,
111            Self::Lidar(_) => CapabilityKind::Lidar,
112            Self::Mmwave(_) => CapabilityKind::Mmwave,
113            Self::Microphone(_) => CapabilityKind::Microphone,
114            Self::Speaker => CapabilityKind::Speaker,
115            Self::Battery => CapabilityKind::Battery,
116            Self::Led => CapabilityKind::Led,
117            Self::EmergencyStop => CapabilityKind::EmergencyStop,
118        }
119    }
120}
121
122#[derive(
123    phoxal_macros::DescribeWire,
124    serde::Serialize,
125    serde::Deserialize,
126    Debug,
127    Clone,
128    Copy,
129    PartialEq,
130    Eq,
131    Default,
132)]
133#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
134#[serde(rename_all = "snake_case")]
135pub enum ActuatorType {
136    #[default]
137    Velocity,
138    Position,
139    Torque,
140}
141
142#[derive(
143    phoxal_macros::DescribeWire,
144    serde::Serialize,
145    serde::Deserialize,
146    Debug,
147    Clone,
148    Copy,
149    PartialEq,
150    Eq,
151)]
152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
153#[serde(rename_all = "snake_case")]
154pub enum CameraProjection {
155    Planar,
156    Cylindrical,
157    Spherical,
158}
159
160#[derive(
161    phoxal_macros::DescribeWire,
162    serde::Serialize,
163    serde::Deserialize,
164    Debug,
165    Clone,
166    PartialEq,
167    Default,
168)]
169#[serde(deny_unknown_fields)]
170pub struct Motor {
171    pub actuator_type: ActuatorType,
172    pub acceleration_radps2: Option<f64>,
173    pub control_pid: Option<Vec<f64>>,
174    pub sampling_period_torque_hz: Option<f64>,
175}
176
177#[derive(
178    phoxal_macros::DescribeWire,
179    serde::Serialize,
180    serde::Deserialize,
181    Debug,
182    Clone,
183    Copy,
184    PartialEq,
185    Default,
186)]
187#[serde(deny_unknown_fields)]
188pub struct Encoder {
189    pub sampling_period_hz: f64,
190    pub resolution: Option<f64>,
191    pub noise: Option<f64>,
192}
193
194#[derive(
195    phoxal_macros::DescribeWire,
196    serde::Serialize,
197    serde::Deserialize,
198    Debug,
199    Clone,
200    PartialEq,
201    Default,
202)]
203#[serde(deny_unknown_fields)]
204pub struct Accelerometer {
205    pub sampling_period_hz: f64,
206    pub resolution: Option<f64>,
207    pub lookup_table: Option<Vec<Vec<f64>>>,
208}
209
210#[derive(
211    phoxal_macros::DescribeWire,
212    serde::Serialize,
213    serde::Deserialize,
214    Debug,
215    Clone,
216    PartialEq,
217    Default,
218)]
219#[serde(deny_unknown_fields)]
220pub struct Gyroscope {
221    pub sampling_period_hz: f64,
222    pub resolution: Option<f64>,
223    pub lookup_table: Option<Vec<Vec<f64>>>,
224}
225
226#[derive(
227    phoxal_macros::DescribeWire,
228    serde::Serialize,
229    serde::Deserialize,
230    Debug,
231    Clone,
232    PartialEq,
233    Default,
234)]
235#[serde(deny_unknown_fields)]
236pub struct Magnetometer {
237    pub sampling_period_hz: f64,
238    pub resolution: Option<f64>,
239    pub lookup_table: Option<Vec<Vec<f64>>>,
240}
241
242#[derive(
243    phoxal_macros::DescribeWire,
244    serde::Serialize,
245    serde::Deserialize,
246    Debug,
247    Clone,
248    Copy,
249    PartialEq,
250    Default,
251)]
252#[serde(deny_unknown_fields)]
253pub struct Imu {
254    pub sampling_period_hz: f64,
255    pub resolution: Option<f64>,
256    pub noise: Option<f64>,
257}
258
259#[derive(
260    phoxal_macros::DescribeWire,
261    serde::Serialize,
262    serde::Deserialize,
263    Debug,
264    Clone,
265    Copy,
266    PartialEq,
267    Default,
268)]
269#[serde(deny_unknown_fields)]
270pub struct Gnss {
271    pub sampling_period_hz: f64,
272    pub resolution: Option<f64>,
273    pub accuracy: Option<f64>,
274    pub noise_correlation: Option<f64>,
275    pub speed_resolution: Option<f64>,
276    pub speed_noise: Option<f64>,
277}
278
279#[derive(
280    phoxal_macros::DescribeWire,
281    serde::Serialize,
282    serde::Deserialize,
283    Debug,
284    Clone,
285    PartialEq,
286    Default,
287)]
288#[serde(deny_unknown_fields)]
289pub struct Camera {
290    pub sampling_period_hz: f64,
291    pub projection: Option<CameraProjection>,
292    pub near: Option<f64>,
293    pub far: Option<f64>,
294    pub exposure: Option<f64>,
295    pub anti_aliasing: Option<bool>,
296    pub ambient_occlusion_radius: Option<f64>,
297    pub bloom_threshold: Option<f64>,
298    pub noise: Option<f64>,
299    pub motion_blur: Option<f64>,
300    pub noise_mask_url: Option<String>,
301}
302
303#[derive(
304    phoxal_macros::DescribeWire,
305    serde::Serialize,
306    serde::Deserialize,
307    Debug,
308    Clone,
309    Copy,
310    PartialEq,
311    Default,
312)]
313#[serde(deny_unknown_fields)]
314pub struct Depth {
315    pub sampling_period_hz: f64,
316    pub noise: Option<f64>,
317    pub resolution: Option<f64>,
318    pub motion_blur: Option<f64>,
319}
320
321#[derive(
322    phoxal_macros::DescribeWire,
323    serde::Serialize,
324    serde::Deserialize,
325    Debug,
326    Clone,
327    Copy,
328    PartialEq,
329    Default,
330)]
331#[serde(deny_unknown_fields)]
332pub struct Range {
333    pub sampling_period_hz: f64,
334    pub noise: Option<f64>,
335    pub resolution: Option<f64>,
336}
337
338#[derive(
339    phoxal_macros::DescribeWire,
340    serde::Serialize,
341    serde::Deserialize,
342    Debug,
343    Clone,
344    Copy,
345    PartialEq,
346    Default,
347)]
348#[serde(deny_unknown_fields)]
349pub struct Lidar {
350    pub sampling_period_hz: f64,
351    pub noise: Option<f64>,
352    pub resolution: Option<f64>,
353}
354
355#[derive(
356    phoxal_macros::DescribeWire,
357    serde::Serialize,
358    serde::Deserialize,
359    Debug,
360    Clone,
361    PartialEq,
362    Default,
363)]
364#[serde(deny_unknown_fields)]
365pub struct Mmwave {
366    pub sampling_period_hz: f64,
367    pub noise: Option<f64>,
368    pub resolution: Option<f64>,
369    pub lookup_table: Option<Vec<Vec<f64>>>,
370}
371
372#[derive(
373    phoxal_macros::DescribeWire,
374    serde::Serialize,
375    serde::Deserialize,
376    Debug,
377    Clone,
378    Copy,
379    PartialEq,
380    Default,
381)]
382#[serde(deny_unknown_fields)]
383pub struct Microphone {
384    pub sampling_period_hz: f64,
385    pub aperture: Option<f64>,
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn a_simulation_keeps_its_bare_string_map_keys_on_the_wire() {
394        // Typing the map keys must not turn them into objects: the wire form
395        // is still a plain id-to-record map.
396        let simulation = Simulation::new(
397            [(
398                CapabilityId::new("spin").expect("a normalized capability id"),
399                Capability::Speaker,
400            )]
401            .into_iter()
402            .collect(),
403            [(LinkId::new("body"), Some("rubber".to_string()))]
404                .into_iter()
405                .collect(),
406        );
407        let json = serde_json::to_string(&simulation).expect("the simulation serializes");
408        assert_eq!(
409            json,
410            r#"{"capabilities":{"spin":{"kind":"speaker"}},"links":{"body":{"contact_material":"rubber"}}}"#
411        );
412        assert_eq!(
413            serde_json::from_str::<Simulation>(&json).expect("the simulation round-trips"),
414            simulation
415        );
416    }
417
418    #[test]
419    fn a_simulated_capability_reports_the_kind_it_models() {
420        assert_eq!(Capability::Led.kind(), CapabilityKind::Led);
421        assert_eq!(
422            Capability::Encoder(Encoder::default()).kind(),
423            CapabilityKind::Encoder
424        );
425    }
426}