Skip to main content

comnum_sim/
grid.rs

1//! Grids of simulation conditions.
2//!
3//! A [`Condition`] is an ordered set of named [`Value`]s (numbers, integers, strings,
4//! booleans) read by the scenario with typed getters ([`Condition::f64`],
5//! [`Condition::usize`], [`Condition::str`], …). A [`Grid`] produces the conditions of a
6//! campaign:
7//!
8//! - [`Grid::axis`] adds an axis; the conditions are the **Cartesian product** of the
9//!   axes, enumerated like nested loops in declaration order (the **last axis varies
10//!   fastest**: an SNR declared last gives one sweep per value of the other axes);
11//! - [`Grid::fixed`] adds a constant to every condition;
12//! - [`Grid::zip`] adds several axes that vary together (one value of each per step);
13//! - [`Grid::from_conditions`] takes an explicit list.
14//!
15//! Every key is given once: [`Grid::check`] (which the runner calls) refuses a key that
16//! two axes, zips or fixed values give, or that they give on top of an explicit
17//! condition, since the second value would replace the first and repeat conditions.
18//!
19//! # Examples
20//! ```
21//! use comnum_sim::grid::{Grid, Value};
22//! let g = Grid::new()
23//!     .axis("modulation", ["qpsk", "16qam"])
24//!     .axis("snr_db", [10.0, 15.0, 20.0])
25//!     .fixed("n_symbols", 4096usize);
26//! let c = g.conditions();
27//! assert_eq!(c.len(), 6);
28//! assert_eq!(c[1].str("modulation").unwrap(), "qpsk");
29//! assert_eq!(c[1].f64("snr_db").unwrap(), 15.0);           // the last axis varies fastest
30//! assert_eq!(c[5].usize("n_symbols").unwrap(), 4096);
31//! assert_eq!(c[0].label(), "modulation=qpsk, snr_db=10, n_symbols=4096");
32//! assert_eq!(Value::from(3usize).as_f64(), Some(3.0));
33//! ```
34
35use comnum_core::{Error, Result};
36use std::fmt;
37
38/// The value of a condition entry.
39#[derive(Clone, Debug, PartialEq)]
40pub enum Value {
41    /// A real number.
42    F64(f64),
43    /// An integer.
44    I64(i64),
45    /// A string.
46    Str(String),
47    /// A boolean.
48    Bool(bool),
49}
50
51impl Value {
52    /// The value as a real number (integers are converted).
53    pub fn as_f64(&self) -> Option<f64> {
54        match self {
55            Value::F64(v) => Some(*v),
56            Value::I64(v) => Some(*v as f64),
57            _ => None,
58        }
59    }
60}
61
62impl fmt::Display for Value {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Value::F64(v) => write!(f, "{v}"),
66            Value::I64(v) => write!(f, "{v}"),
67            Value::Str(v) => write!(f, "{v}"),
68            Value::Bool(v) => write!(f, "{v}"),
69        }
70    }
71}
72
73impl From<f64> for Value {
74    fn from(v: f64) -> Self {
75        Value::F64(v)
76    }
77}
78impl From<f32> for Value {
79    fn from(v: f32) -> Self {
80        Value::F64(v as f64)
81    }
82}
83impl From<i64> for Value {
84    fn from(v: i64) -> Self {
85        Value::I64(v)
86    }
87}
88impl From<i32> for Value {
89    fn from(v: i32) -> Self {
90        Value::I64(v as i64)
91    }
92}
93impl From<usize> for Value {
94    fn from(v: usize) -> Self {
95        Value::I64(v as i64)
96    }
97}
98impl From<u32> for Value {
99    fn from(v: u32) -> Self {
100        Value::I64(v as i64)
101    }
102}
103impl From<&str> for Value {
104    fn from(v: &str) -> Self {
105        Value::Str(v.to_string())
106    }
107}
108impl From<String> for Value {
109    fn from(v: String) -> Self {
110        Value::Str(v)
111    }
112}
113impl From<bool> for Value {
114    fn from(v: bool) -> Self {
115        Value::Bool(v)
116    }
117}
118
119/// One simulation condition: named values in a fixed order.
120#[derive(Clone, Debug, Default, PartialEq)]
121pub struct Condition {
122    entries: Vec<(String, Value)>,
123}
124
125impl Condition {
126    /// An empty condition.
127    pub fn new() -> Condition {
128        Condition::default()
129    }
130
131    /// The same condition with `key` set to `value` (replaced if present).
132    pub fn with(mut self, key: &str, value: impl Into<Value>) -> Condition {
133        self.set(key, value);
134        self
135    }
136
137    /// Set `key` to `value` (replaced if present, appended otherwise).
138    pub fn set(&mut self, key: &str, value: impl Into<Value>) {
139        let v = value.into();
140        match self.entries.iter_mut().find(|(k, _)| k == key) {
141            Some(e) => e.1 = v,
142            None => self.entries.push((key.to_string(), v)),
143        }
144    }
145
146    /// The entries in order.
147    pub fn entries(&self) -> &[(String, Value)] {
148        &self.entries
149    }
150
151    /// The keys in order.
152    pub fn keys(&self) -> impl Iterator<Item = &str> {
153        self.entries.iter().map(|(k, _)| k.as_str())
154    }
155
156    /// The value of `key`, if present.
157    pub fn get(&self, key: &str) -> Option<&Value> {
158        self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
159    }
160
161    fn need(&self, key: &str) -> Result<&Value> {
162        self.get(key).ok_or_else(|| Error::Condition(format!("no key {key:?} in ({})", self.label())))
163    }
164
165    /// The real number at `key` (integers are converted).
166    ///
167    /// # Errors
168    /// [`Error::Condition`] if the key is missing or is not a number.
169    pub fn f64(&self, key: &str) -> Result<f64> {
170        self.need(key)?.as_f64().ok_or_else(|| Error::Condition(format!("{key:?} is not a number")))
171    }
172
173    /// The integer at `key`.
174    ///
175    /// # Errors
176    /// [`Error::Condition`] if the key is missing or is not an integer.
177    pub fn i64(&self, key: &str) -> Result<i64> {
178        match self.need(key)? {
179            Value::I64(v) => Ok(*v),
180            Value::F64(v) if v.fract() == 0.0 => Ok(*v as i64),
181            _ => Err(Error::Condition(format!("{key:?} is not an integer"))),
182        }
183    }
184
185    /// The non-negative integer at `key`.
186    ///
187    /// # Errors
188    /// [`Error::Condition`] if the key is missing or is not a non-negative integer.
189    pub fn usize(&self, key: &str) -> Result<usize> {
190        let v = self.i64(key)?;
191        usize::try_from(v).map_err(|_| Error::Condition(format!("{key:?} = {v} is negative")))
192    }
193
194    /// The string at `key`.
195    ///
196    /// # Errors
197    /// [`Error::Condition`] if the key is missing or is not a string.
198    pub fn str(&self, key: &str) -> Result<&str> {
199        match self.need(key)? {
200            Value::Str(s) => Ok(s),
201            _ => Err(Error::Condition(format!("{key:?} is not a string"))),
202        }
203    }
204
205    /// The boolean at `key`.
206    ///
207    /// # Errors
208    /// [`Error::Condition`] if the key is missing or is not a boolean.
209    pub fn bool(&self, key: &str) -> Result<bool> {
210        match self.need(key)? {
211            Value::Bool(b) => Ok(*b),
212            _ => Err(Error::Condition(format!("{key:?} is not a boolean"))),
213        }
214    }
215
216    /// `key=value, …` in order.
217    pub fn label(&self) -> String {
218        self.entries.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(", ")
219    }
220}
221
222#[derive(Clone, Debug, PartialEq)]
223enum Part {
224    Axis(String, Vec<Value>),
225    Zip(Vec<String>, Vec<Vec<Value>>),
226    Fixed(String, Value),
227}
228
229/// A grid of conditions (see the module documentation).
230#[derive(Clone, Debug, Default, PartialEq)]
231pub struct Grid {
232    parts: Vec<Part>,
233    explicit: Option<Vec<Condition>>,
234}
235
236impl Grid {
237    /// An empty grid (one empty condition).
238    pub fn new() -> Grid {
239        Grid::default()
240    }
241
242    /// An explicit list of conditions.
243    pub fn from_conditions(conditions: Vec<Condition>) -> Grid {
244        Grid { parts: Vec::new(), explicit: Some(conditions) }
245    }
246
247    /// Add an axis of values.
248    pub fn axis<V: Into<Value>>(mut self, name: &str, values: impl IntoIterator<Item = V>) -> Grid {
249        self.parts.push(Part::Axis(name.to_string(), values.into_iter().map(Into::into).collect()));
250        self
251    }
252
253    /// Add axes that vary together: `rows[i]` gives one value per name.
254    ///
255    /// # Errors
256    /// [`Error::Shape`] if a row does not have one value per name.
257    ///
258    /// # Examples
259    /// ```
260    /// use comnum_sim::grid::Grid;
261    /// let g = Grid::new().zip(&["m", "kind"], vec![vec![4usize.into(), "qam".into()], vec![8usize.into(), "psk".into()]])?;
262    /// assert_eq!(g.conditions()[1].label(), "m=8, kind=psk");
263    /// assert!(Grid::new().zip(&["m", "kind"], vec![vec![4usize.into()]]).is_err());
264    /// # Ok::<(), comnum_core::Error>(())
265    /// ```
266    pub fn zip(mut self, names: &[&str], rows: Vec<Vec<Value>>) -> Result<Grid> {
267        if let Some((i, r)) = rows.iter().enumerate().find(|(_, r)| r.len() != names.len()) {
268            return Err(Error::Shape(format!(
269                "grid: row {i} of the zip of {names:?} has {} values for {} names",
270                r.len(),
271                names.len()
272            )));
273        }
274        self.parts.push(Part::Zip(names.iter().map(std::string::ToString::to_string).collect(), rows));
275        Ok(self)
276    }
277
278    /// Add a constant to every condition.
279    pub fn fixed(mut self, name: &str, value: impl Into<Value>) -> Grid {
280        self.parts.push(Part::Fixed(name.to_string(), value.into()));
281        self
282    }
283
284    /// Check that every key is given once: by one axis, zip or fixed value, and not by
285    /// one of them on top of an explicit condition.
286    ///
287    /// # Errors
288    /// [`Error::Config`] naming the first key given twice.
289    ///
290    /// # Examples
291    /// ```
292    /// use comnum_sim::grid::{Condition, Grid};
293    /// assert!(Grid::new().axis("snr_db", [0.0, 1.0]).fixed("m", 4usize).check().is_ok());
294    /// let e = Grid::new().axis("snr_db", [0.0]).axis("snr_db", [1.0]).check().unwrap_err();
295    /// assert!(e.to_string().contains("\"snr_db\" twice"));
296    /// let cases = Grid::from_conditions(vec![Condition::new().with("m", 4usize)]);
297    /// assert!(cases.axis("m", [16usize]).check().is_err());
298    /// ```
299    pub fn check(&self) -> Result<()> {
300        let mut seen: Vec<&str> = Vec::new();
301        for p in &self.parts {
302            let names: Vec<&str> = match p {
303                Part::Axis(n, _) | Part::Fixed(n, _) => vec![n.as_str()],
304                Part::Zip(names, _) => names.iter().map(String::as_str).collect(),
305            };
306            for n in names {
307                let explicit = self.explicit.iter().flatten().any(|c| c.get(n).is_some());
308                if seen.contains(&n) || explicit {
309                    return Err(Error::Config(format!(
310                        "grid: the key {n:?} twice{}: the second value would replace the first and repeat conditions",
311                        if explicit { " (in the explicit conditions and an axis)" } else { "" }
312                    )));
313                }
314                seen.push(n);
315            }
316        }
317        Ok(())
318    }
319
320    /// The conditions, in order.
321    pub fn conditions(&self) -> Vec<Condition> {
322        let mut out = vec![Condition::new()];
323        for p in &self.parts {
324            let mut next = Vec::new();
325            for c in &out {
326                match p {
327                    Part::Axis(name, values) => {
328                        for v in values {
329                            next.push(c.clone().with(name, v.clone()));
330                        }
331                    }
332                    Part::Zip(names, rows) => {
333                        for row in rows {
334                            let mut c2 = c.clone();
335                            for (n, v) in names.iter().zip(row) {
336                                c2.set(n, v.clone());
337                            }
338                            next.push(c2);
339                        }
340                    }
341                    Part::Fixed(name, v) => next.push(c.clone().with(name, v.clone())),
342                }
343            }
344            out = next;
345        }
346        match &self.explicit {
347            Some(list) => list
348                .iter()
349                .flat_map(|e| {
350                    out.iter().map(move |c| {
351                        let mut m = e.clone();
352                        for (k, v) in c.entries() {
353                            m.set(k, v.clone());
354                        }
355                        m
356                    })
357                })
358                .collect(),
359            None => out,
360        }
361    }
362
363    /// Number of conditions.
364    pub fn len(&self) -> usize {
365        self.conditions().len()
366    }
367
368    /// Whether the grid has no condition.
369    pub fn is_empty(&self) -> bool {
370        self.len() == 0
371    }
372}