Skip to main content

comnum_spec/
link.rs

1//! The link of a condition ([`Link`]) and a specification as a scenario of the runner
2//! ([`SpecScenario`]).
3
4use crate::blocks::{self, Block, BuildContext, Frame};
5use crate::metrics::Metrics;
6use crate::{Spec, params};
7use comnum_core::rng::{Rng, fnv1a64, stream};
8use comnum_core::{Error, Result};
9use comnum_sim::grid::Condition;
10use comnum_sim::scenario::{Record, Scenario};
11use std::time::Instant;
12
13impl Spec {
14    /// Build the link of a condition: substitute the variables, build the blocks, train
15    /// the blocks that are trained once, parse the metrics.
16    ///
17    /// # Errors
18    /// [`Error::Config`] for an invalid block or metric; the errors of a training.
19    pub fn link_for(&self, cond: &Condition) -> Result<Link> {
20        let ctx = BuildContext {
21            fiber: self.fiber.clone(),
22            base_dir: self.base_dir.clone(),
23            name: Some(self.name.clone()),
24            condition: Some(cond.label()),
25        };
26        let mut blocks: Vec<Box<dyn Block>> = Vec::with_capacity(self.link.len());
27        let mut kinds = Vec::with_capacity(self.link.len());
28        let mut labels = Vec::with_capacity(self.link.len());
29        for (i, b) in self.link.iter().enumerate() {
30            let (kind, label, p) = params::split_block(&params::substitute(b, cond)?)?;
31            labels.push(label.clone().unwrap_or_else(|| kind.clone()));
32            let block = blocks::build_parts(kind.clone(), label, p, &ctx)
33                .map_err(|e| e.context(format!("link block {}", i + 1)))?;
34            blocks.push(block);
35            kinds.push(kind);
36        }
37        let labels = number_labels(&labels);
38        blocks::with_registry(|registry| {
39            if registry.iter().any(|b| b.starts && b.name == kinds[0]) {
40                return Ok(());
41            }
42            let starts: Vec<&str> = registry.iter().filter(|b| b.starts).map(|b| b.name).collect();
43            Err(Error::Config(format!("the first block of the link must be one of {starts:?}")))
44        })?;
45        blocks::check_link(&kinds.iter().map(String::as_str).collect::<Vec<_>>())?;
46        for i in 0..blocks.len() {
47            if blocks[i].needs_training() {
48                let mut rng = self.training_rng(cond, i);
49                let mut s = Frame::default();
50                for b in &blocks[..i] {
51                    b.apply(&mut s, &mut rng)?;
52                }
53                blocks[i].train(&s, &mut rng)?;
54            }
55        }
56        let metrics = Metrics::from_json(&params::substitute(&self.metrics, cond)?)?;
57        Ok(Link { blocks, kinds, labels, metrics, profile: self.profile })
58    }
59
60    fn training_rng(&self, cond: &Condition, block: usize) -> Rng {
61        stream(self.seed, &format!("{}/train", self.name), fnv1a64(cond.label().as_bytes()), block as u64)
62    }
63}
64
65/// The labels of the blocks of a link, as the profile names them (and the Python chains
66/// their blocks): the `label` of a block, else its type, numbered by [`number_labels`].
67///
68/// # Errors
69/// [`Error::Config`] for a block that is neither a name nor a mapping with a type.
70pub fn link_labels(link: &[serde_json::Value]) -> Result<Vec<String>> {
71    let labels = link
72        .iter()
73        .map(|b| params::split_block(b).map(|(kind, label, _)| label.unwrap_or(kind)))
74        .collect::<Result<Vec<_>>>()?;
75    Ok(number_labels(&labels))
76}
77
78/// Unique labels: a repeated label takes the position of its block, counted from 1
79/// (`[source, awgn, awgn]` gives `[source, awgn_2, awgn_3]`).
80pub fn number_labels(labels: &[String]) -> Vec<String> {
81    labels
82        .iter()
83        .enumerate()
84        .map(|(i, l)| if labels.iter().filter(|g| *g == l).count() > 1 { format!("{l}_{}", i + 1) } else { l.clone() })
85        .collect()
86}
87
88/// The link of one condition: its blocks and its metrics.
89pub struct Link {
90    /// The blocks, in order.
91    pub blocks: Vec<Box<dyn Block>>,
92    /// The type of every block (the context of its errors).
93    pub kinds: Vec<String>,
94    /// The label of every block (its `label`, else its type; a repeated label takes
95    /// the position of the block, `awgn_3`), the names of the profile.
96    pub labels: Vec<String>,
97    /// The metrics.
98    pub metrics: Metrics,
99    /// Time every block: the trial records `<label>.time_ms` for every block and
100    /// `link.time_ms` for the whole link (the metrics excluded), reported as means.
101    pub profile: bool,
102}
103
104impl Link {
105    /// Run one trial: the blocks in order, then the metrics and the recorded values.
106    ///
107    /// # Errors
108    /// The first error of a block or of the metrics.
109    pub fn trial(&self, rng: &mut Rng, rec: &mut Record) -> Result<Frame> {
110        let mut s = Frame { trial: rec.trial(), ..Frame::default() };
111        let t_link = Instant::now();
112        for (i, (b, kind)) in self.blocks.iter().zip(&self.kinds).enumerate() {
113            let t = Instant::now();
114            b.apply(&mut s, rng).map_err(|e| e.context(format!("link block {} ({kind})", i + 1)))?;
115            if self.profile {
116                rec.value(&format!("{}.time_ms", self.labels[i]), t.elapsed().as_secs_f64() * 1e3);
117            }
118        }
119        if self.profile {
120            rec.value("link.time_ms", t_link.elapsed().as_secs_f64() * 1e3);
121        }
122        self.metrics.record_frame(&s, rec)?;
123        Ok(s)
124    }
125}
126
127/// A specification as a [`Scenario`] of the runner.
128pub struct SpecScenario<'a> {
129    spec: &'a Spec,
130    profile: bool,
131}
132
133impl<'a> SpecScenario<'a> {
134    /// The scenario of a specification.
135    pub fn new(spec: &'a Spec) -> SpecScenario<'a> {
136        SpecScenario { spec, profile: spec.profile }
137    }
138
139    /// Time every block, whatever the specification says.
140    #[must_use]
141    pub fn profile(mut self, profile: bool) -> Self {
142        self.profile = profile;
143        self
144    }
145}
146
147impl Scenario for SpecScenario<'_> {
148    type State = Link;
149
150    fn name(&self) -> &str {
151        &self.spec.name
152    }
153
154    fn setup(&self, condition: &Condition) -> Result<Link> {
155        let mut link = self.spec.link_for(condition)?;
156        link.profile = self.profile;
157        Ok(link)
158    }
159
160    fn trial(&self, link: &Link, _: &Condition, rng: &mut Rng, record: &mut Record) -> Result<()> {
161        link.trial(rng, record).map(|_| ())
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn repeated_labels_take_the_position_of_the_block() {
171        let spec = Spec::from_yaml(
172            "name: twice\nlink: [{type: source, n_symbols: 64}, {type: awgn, snr_db: 10}, {type: awgn, snr_db: 10}, {type: awgn, snr_db: 10, label: last}]\nstop: {max_trials: 1}",
173        )
174        .unwrap();
175        let link = spec.link_for(&spec.conditions()[0]).unwrap();
176        assert_eq!(link.labels, ["source", "awgn_2", "awgn_3", "last"]);
177    }
178
179    #[test]
180    fn errors_name_their_place() {
181        let e = Spec::from_yaml("name: t\nlink: [source]\nseeds: 3").unwrap_err().to_string();
182        assert!(e.contains("seeds"), "{e}");
183        let spec = Spec::from_yaml("name: t\nlink: [awgn_ebn0]").unwrap();
184        let e = spec.link_for(&Condition::new()).err().unwrap().to_string();
185        assert!(e.contains("link block 1"), "{e}");
186        let spec = Spec::from_yaml("name: t\nlink: [{type: awgn_ebn0, ebn0_db: 3}, source]").unwrap();
187        assert!(spec.link_for(&Condition::new()).err().unwrap().to_string().contains("first block"));
188        let spec = Spec::from_yaml("name: t\nlink: [source]\nmetrics: {report: [fer]}").unwrap();
189        assert!(spec.link_for(&Condition::new()).err().unwrap().to_string().contains("fer"));
190    }
191}