Skip to main content

sim_lib_interference_runtime/
records.rs

1//! Validated medium, emitter, problem, and physical-plane records.
2
3use sim_kernel::{Cx, Result, Symbol, Value};
4use sim_lib_interference_core::{
5    Emitter, FieldAmplitude, Hertz, InterferenceProblem, MetresPerSecond, NepersPerMetre, Point3M,
6    PositiveMetres, Radians, SamplingPlane, ScalarMedium, SourceSet, UnitVector3,
7};
8
9use crate::citizen::{
10    RecordCitizenSpec, decode_record, decode_records, encode_field, encode_record, encode_records,
11    invalid, next_field,
12};
13
14/// Citizen descriptor for a homogeneous scalar propagation medium.
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct MediumDescriptor {
17    /// Propagation speed in metres per second.
18    pub speed_m_s: f64,
19    /// Attenuation in nepers per metre.
20    pub attenuation_np_m: f64,
21}
22
23impl MediumDescriptor {
24    /// Checks physical units and builds a medium descriptor.
25    pub fn new(speed_m_s: f64, attenuation_np_m: f64) -> Result<Self> {
26        let value = Self {
27            speed_m_s,
28            attenuation_np_m,
29        };
30        value.validate()?;
31        Ok(value)
32    }
33
34    /// Projects an admitted domain medium.
35    pub fn from_medium(medium: ScalarMedium) -> Self {
36        Self {
37            speed_m_s: medium.speed().get(),
38            attenuation_np_m: medium.attenuation().get(),
39        }
40    }
41
42    /// Reconstructs the dependency-free domain medium.
43    pub fn to_medium(self) -> Result<ScalarMedium> {
44        Ok(ScalarMedium::new(
45            MetresPerSecond::new(self.speed_m_s)
46                .map_err(|error| invalid("Medium", format!("{error:?}")))?,
47            NepersPerMetre::new(self.attenuation_np_m)
48                .map_err(|error| invalid("Medium", format!("{error:?}")))?,
49        ))
50    }
51}
52
53impl RecordCitizenSpec for MediumDescriptor {
54    const FIELDS: &'static [&'static str] = &["speed-m-s", "attenuation-np-m"];
55
56    fn encode_fields(&self, _cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
57        Ok(vec![
58            encode_field(&self.speed_m_s),
59            encode_field(&self.attenuation_np_m),
60        ])
61    }
62
63    fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
64        let mut fields = fields.into_iter();
65        Self::new(
66            next_field(cx, &mut fields, "speed-m-s")?,
67            next_field(cx, &mut fields, "attenuation-np-m")?,
68        )
69    }
70
71    fn example() -> Self {
72        Self::new(343.0, 0.0).expect("example medium")
73    }
74
75    fn validate(&self) -> Result<()> {
76        self.to_medium().map(|_| ())
77    }
78}
79
80impl_record_citizen!(MediumDescriptor, "interference/Medium", 2);
81
82/// Citizen descriptor for a point or forward-plane coherent emitter.
83#[derive(Clone, Debug, PartialEq)]
84pub struct EmitterDescriptor {
85    /// `interference/point` or `interference/forward-plane`.
86    pub kind: Symbol,
87    /// Stable source identity.
88    pub id: String,
89    /// Point position or a point on the zero-phase plane, in metres.
90    pub anchor_m: Vec<f64>,
91    /// Forward direction; absent for a point source.
92    pub direction: Option<Vec<f64>>,
93    /// Field amplitude at the point reference distance or zero-phase plane.
94    pub amplitude: f64,
95    /// Source phase in radians, canonically stored in `[-pi, pi)`.
96    pub phase_rad: f64,
97}
98
99impl EmitterDescriptor {
100    /// Projects a checked domain emitter.
101    pub fn from_emitter(emitter: &Emitter) -> Self {
102        match emitter {
103            Emitter::Point {
104                id,
105                position,
106                amplitude_at_reference,
107                phase,
108            } => Self {
109                kind: point_kind(),
110                id: id.clone(),
111                anchor_m: position.coordinates_metres().to_vec(),
112                direction: None,
113                amplitude: amplitude_at_reference.get(),
114                phase_rad: phase.get(),
115            },
116            Emitter::ForwardPlane {
117                id,
118                through,
119                direction,
120                amplitude,
121                phase,
122            } => Self {
123                kind: forward_plane_kind(),
124                id: id.clone(),
125                anchor_m: through.coordinates_metres().to_vec(),
126                direction: Some(direction.components().to_vec()),
127                amplitude: amplitude.get(),
128                phase_rad: phase.get(),
129            },
130        }
131    }
132
133    /// Reconstructs the checked domain emitter.
134    pub fn to_emitter(&self) -> Result<Emitter> {
135        if self.id.is_empty() {
136            return Err(invalid("Emitter", "source id cannot be empty"));
137        }
138        let [x, y, z] = vector3(&self.anchor_m, "emitter anchor")?;
139        let anchor = Point3M::from_metres(x, y, z)
140            .map_err(|error| invalid("Emitter", format!("{error:?}")))?;
141        let amplitude = FieldAmplitude::new(self.amplitude)
142            .map_err(|error| invalid("Emitter", format!("{error:?}")))?;
143        let phase = Radians::new(self.phase_rad)
144            .map_err(|error| invalid("Emitter", format!("{error:?}")))?;
145        if phase.get().to_bits() != self.phase_rad.to_bits() {
146            return Err(invalid(
147                "Emitter",
148                "phase-rad must already be canonical in [-pi, pi)",
149            ));
150        }
151        if self.kind == point_kind() {
152            if self.direction.is_some() {
153                return Err(invalid(
154                    "Emitter",
155                    "point emitters cannot carry a direction",
156                ));
157            }
158            Ok(Emitter::Point {
159                id: self.id.clone(),
160                position: anchor,
161                amplitude_at_reference: amplitude,
162                phase,
163            })
164        } else if self.kind == forward_plane_kind() {
165            let [dx, dy, dz] = vector3(
166                self.direction
167                    .as_deref()
168                    .ok_or_else(|| invalid("Emitter", "forward-plane direction is required"))?,
169                "emitter direction",
170            )?;
171            let direction = UnitVector3::new(dx, dy, dz)
172                .map_err(|error| invalid("Emitter", format!("{error:?}")))?;
173            let normalized = direction.components();
174            if normalized
175                .iter()
176                .zip([dx, dy, dz])
177                .any(|(actual, supplied)| actual.to_bits() != supplied.to_bits())
178            {
179                return Err(invalid(
180                    "Emitter",
181                    "forward-plane direction must already be normalized",
182                ));
183            }
184            Ok(Emitter::ForwardPlane {
185                id: self.id.clone(),
186                through: anchor,
187                direction,
188                amplitude,
189                phase,
190            })
191        } else {
192            Err(invalid(
193                "Emitter",
194                format!("unknown emitter kind {}", self.kind),
195            ))
196        }
197    }
198}
199
200impl RecordCitizenSpec for EmitterDescriptor {
201    const FIELDS: &'static [&'static str] = &[
202        "kind",
203        "id",
204        "anchor-m",
205        "direction",
206        "amplitude",
207        "phase-rad",
208    ];
209
210    fn encode_fields(&self, _cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
211        Ok(vec![
212            encode_field(&self.kind),
213            encode_field(&self.id),
214            encode_field(&self.anchor_m),
215            encode_field(&self.direction),
216            encode_field(&self.amplitude),
217            encode_field(&self.phase_rad),
218        ])
219    }
220
221    fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
222        let mut fields = fields.into_iter();
223        let value = Self {
224            kind: next_field(cx, &mut fields, "kind")?,
225            id: next_field(cx, &mut fields, "id")?,
226            anchor_m: next_field(cx, &mut fields, "anchor-m")?,
227            direction: next_field(cx, &mut fields, "direction")?,
228            amplitude: next_field(cx, &mut fields, "amplitude")?,
229            phase_rad: next_field(cx, &mut fields, "phase-rad")?,
230        };
231        value.validate()?;
232        Ok(value)
233    }
234
235    fn example() -> Self {
236        Self::from_emitter(&Emitter::Point {
237            id: "source".to_owned(),
238            position: Point3M::from_metres(0.0, 0.0, 0.0).expect("example point"),
239            amplitude_at_reference: FieldAmplitude::new(1.0).expect("example amplitude"),
240            phase: Radians::new(0.0).expect("example phase"),
241        })
242    }
243
244    fn validate(&self) -> Result<()> {
245        self.to_emitter().map(|_| ())
246    }
247}
248
249impl_record_citizen!(EmitterDescriptor, "interference/Emitter", 6);
250
251/// Citizen descriptor for one complete coherent single-frequency problem.
252#[derive(Clone, Debug, PartialEq)]
253pub struct ProblemDescriptor {
254    /// Shared coherent frequency in hertz.
255    pub frequency_hz: f64,
256    /// Homogeneous scalar medium.
257    pub medium: MediumDescriptor,
258    /// Non-empty sources in canonical stable-id order.
259    pub emitters: Vec<EmitterDescriptor>,
260    /// Point-source exclusion radius in metres.
261    pub singularity_radius_m: f64,
262}
263
264impl ProblemDescriptor {
265    /// Projects a checked domain problem.
266    pub fn from_problem(problem: &InterferenceProblem) -> Self {
267        Self {
268            frequency_hz: problem.frequency.get(),
269            medium: MediumDescriptor::from_medium(problem.medium),
270            emitters: problem
271                .sources
272                .iter()
273                .map(EmitterDescriptor::from_emitter)
274                .collect(),
275            singularity_radius_m: problem.singularity_radius.get(),
276        }
277    }
278
279    /// Reconstructs the checked dependency-free problem.
280    pub fn to_problem(&self) -> Result<InterferenceProblem> {
281        let frequency = Hertz::new(self.frequency_hz)
282            .map_err(|error| invalid("Problem", format!("{error:?}")))?;
283        let sources = self
284            .emitters
285            .iter()
286            .map(EmitterDescriptor::to_emitter)
287            .collect::<Result<Vec<_>>>()?;
288        let source_set =
289            SourceSet::new(sources).map_err(|error| invalid("Problem", format!("{error:?}")))?;
290        let canonical_ids = source_set.iter().map(Emitter::id);
291        if !canonical_ids.eq(self.emitters.iter().map(|emitter| emitter.id.as_str())) {
292            return Err(invalid(
293                "Problem",
294                "emitters must be in canonical stable-id order",
295            ));
296        }
297        let radius = PositiveMetres::new(self.singularity_radius_m)
298            .map_err(|error| invalid("Problem", format!("{error:?}")))?;
299        Ok(InterferenceProblem::new(
300            frequency,
301            self.medium.to_medium()?,
302            source_set,
303            radius,
304        ))
305    }
306}
307
308impl RecordCitizenSpec for ProblemDescriptor {
309    const FIELDS: &'static [&'static str] =
310        &["frequency-hz", "medium", "emitters", "singularity-radius-m"];
311
312    fn encode_fields(&self, cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
313        Ok(vec![
314            encode_field(&self.frequency_hz),
315            encode_record(cx, &self.medium)?,
316            encode_records(cx, &self.emitters)?,
317            encode_field(&self.singularity_radius_m),
318        ])
319    }
320
321    fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
322        let mut fields = fields.into_iter();
323        let frequency_hz = next_field(cx, &mut fields, "frequency-hz")?;
324        let medium = decode_record(
325            cx,
326            fields
327                .next()
328                .ok_or_else(|| invalid("Problem", "missing medium"))?,
329            "medium",
330        )?;
331        let emitters = decode_records(
332            cx,
333            fields
334                .next()
335                .ok_or_else(|| invalid("Problem", "missing emitters"))?,
336            "emitters",
337        )?;
338        let singularity_radius_m = next_field(cx, &mut fields, "singularity-radius-m")?;
339        let value = Self {
340            frequency_hz,
341            medium,
342            emitters,
343            singularity_radius_m,
344        };
345        value.validate()?;
346        Ok(value)
347    }
348
349    fn example() -> Self {
350        sample_problem()
351    }
352
353    fn validate(&self) -> Result<()> {
354        self.to_problem().map(|_| ())
355    }
356}
357
358impl_record_citizen!(ProblemDescriptor, "interference/Problem", 4);
359
360/// Citizen descriptor for an orthonormal finite physical sampling plane.
361#[derive(Clone, Debug, PartialEq)]
362pub struct PlaneDescriptor {
363    /// Corner before the first cell, in metres.
364    pub origin_m: Vec<f64>,
365    /// Unit direction along columns.
366    pub u_axis: Vec<f64>,
367    /// Unit direction along rows.
368    pub v_axis: Vec<f64>,
369    /// Physical column-axis extent in metres.
370    pub extent_u_m: f64,
371    /// Physical row-axis extent in metres.
372    pub extent_v_m: f64,
373    /// Row count.
374    pub rows: usize,
375    /// Column count.
376    pub columns: usize,
377}
378
379impl PlaneDescriptor {
380    /// Projects a checked sampling plane.
381    pub fn from_plane(plane: SamplingPlane) -> Self {
382        Self {
383            origin_m: plane.origin().coordinates_metres().to_vec(),
384            u_axis: plane.u_axis().components().to_vec(),
385            v_axis: plane.v_axis().components().to_vec(),
386            extent_u_m: plane.extent_u().get(),
387            extent_v_m: plane.extent_v().get(),
388            rows: plane.rows(),
389            columns: plane.columns(),
390        }
391    }
392
393    /// Reconstructs and revalidates the physical plane.
394    pub fn to_plane(&self) -> Result<SamplingPlane> {
395        let [x, y, z] = vector3(&self.origin_m, "plane origin")?;
396        let [ux, uy, uz] = vector3(&self.u_axis, "plane u-axis")?;
397        let [vx, vy, vz] = vector3(&self.v_axis, "plane v-axis")?;
398        let u_axis = canonical_unit_vector(ux, uy, uz, "u-axis")?;
399        let v_axis = canonical_unit_vector(vx, vy, vz, "v-axis")?;
400        SamplingPlane::new(
401            Point3M::from_metres(x, y, z)
402                .map_err(|error| invalid("Plane", format!("{error:?}")))?,
403            u_axis,
404            v_axis,
405            PositiveMetres::new(self.extent_u_m)
406                .map_err(|error| invalid("Plane", format!("{error:?}")))?,
407            PositiveMetres::new(self.extent_v_m)
408                .map_err(|error| invalid("Plane", format!("{error:?}")))?,
409            self.rows,
410            self.columns,
411        )
412        .map_err(|error| invalid("Plane", format!("{error:?}")))
413    }
414}
415
416impl RecordCitizenSpec for PlaneDescriptor {
417    const FIELDS: &'static [&'static str] = &[
418        "origin-m",
419        "u-axis",
420        "v-axis",
421        "extent-u-m",
422        "extent-v-m",
423        "rows",
424        "columns",
425    ];
426
427    fn encode_fields(&self, _cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
428        Ok(vec![
429            encode_field(&self.origin_m),
430            encode_field(&self.u_axis),
431            encode_field(&self.v_axis),
432            encode_field(&self.extent_u_m),
433            encode_field(&self.extent_v_m),
434            encode_field(&self.rows),
435            encode_field(&self.columns),
436        ])
437    }
438
439    fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
440        let mut fields = fields.into_iter();
441        let value = Self {
442            origin_m: next_field(cx, &mut fields, "origin-m")?,
443            u_axis: next_field(cx, &mut fields, "u-axis")?,
444            v_axis: next_field(cx, &mut fields, "v-axis")?,
445            extent_u_m: next_field(cx, &mut fields, "extent-u-m")?,
446            extent_v_m: next_field(cx, &mut fields, "extent-v-m")?,
447            rows: next_field(cx, &mut fields, "rows")?,
448            columns: next_field(cx, &mut fields, "columns")?,
449        };
450        value.validate()?;
451        Ok(value)
452    }
453
454    fn example() -> Self {
455        sample_plane()
456    }
457
458    fn validate(&self) -> Result<()> {
459        self.to_plane().map(|_| ())
460    }
461}
462
463impl_record_citizen!(PlaneDescriptor, "interference/Plane", 7);
464
465pub(crate) fn sample_problem() -> ProblemDescriptor {
466    let medium = MediumDescriptor::new(343.0, 0.0).expect("example medium");
467    let emitter = <EmitterDescriptor as RecordCitizenSpec>::example();
468    let value = ProblemDescriptor {
469        frequency_hz: 1_000.0,
470        medium,
471        emitters: vec![emitter],
472        singularity_radius_m: 0.01,
473    };
474    value.validate().expect("example problem");
475    value
476}
477
478pub(crate) fn sample_plane() -> PlaneDescriptor {
479    let value = PlaneDescriptor {
480        origin_m: vec![0.0, 0.0, 1.0],
481        u_axis: vec![1.0, 0.0, 0.0],
482        v_axis: vec![0.0, 1.0, 0.0],
483        extent_u_m: 0.1,
484        extent_v_m: 0.1,
485        rows: 2,
486        columns: 2,
487    };
488    value.validate().expect("example plane");
489    value
490}
491
492fn vector3(values: &[f64], field: &str) -> Result<[f64; 3]> {
493    let [x, y, z] = values else {
494        return Err(invalid(field, "expected exactly three components"));
495    };
496    if [*x, *y, *z].iter().any(|value| !value.is_finite()) {
497        return Err(invalid(field, "components must be finite"));
498    }
499    Ok([*x, *y, *z])
500}
501
502fn canonical_unit_vector(x: f64, y: f64, z: f64, field: &str) -> Result<UnitVector3> {
503    let vector = UnitVector3::new(x, y, z)
504        .map_err(|error| invalid("Plane", format!("{field}: {error:?}")))?;
505    if vector
506        .components()
507        .iter()
508        .zip([x, y, z])
509        .any(|(actual, supplied)| actual.to_bits() != supplied.to_bits())
510    {
511        return Err(invalid(
512            "Plane",
513            format!("{field} must already be normalized"),
514        ));
515    }
516    Ok(vector)
517}
518
519fn point_kind() -> Symbol {
520    Symbol::qualified("interference", "point")
521}
522
523fn forward_plane_kind() -> Symbol {
524    Symbol::qualified("interference", "forward-plane")
525}