Skip to main content

comnum_spec/blocks/
context.rs

1//! What the builders need besides the parameters of a block: [`BuildContext`], with
2//! the fibre and Raman parameters of the specification.
3
4use super::Params;
5use super::schema::Mapping;
6use comnum_channel::optical::fiber::FiberParams;
7use comnum_channel::optical::raman::{Direction, Raman, RamanGain, RamanPump};
8use comnum_core::{Error, Result};
9use serde_json::{Map, Value as Json};
10use std::path::PathBuf;
11
12/// The meaning of the `fiber` parameter of the optical blocks.
13pub(super) const FIBER_DOC: &str = "fibre parameters, merged over the `fiber` of the specification (see FiberParams); its `raman` mapping adds Raman pumps and ISRS";
14
15/// What the builders of the blocks need besides their parameters.
16#[derive(Clone, Debug, Default)]
17pub struct BuildContext {
18    /// The `fiber` mapping of the specification (defaults of every fibre block).
19    pub fiber: Option<Json>,
20    /// The directory of the specification (relative paths of files).
21    pub base_dir: Option<PathBuf>,
22    /// The name of the campaign (the files a block writes).
23    pub name: Option<String>,
24    /// The label of the condition the link is built for (the files a block writes).
25    pub condition: Option<String>,
26}
27
28impl BuildContext {
29    /// The fibre parameters of a block: the defaults of [`FiberParams`], then the
30    /// specification's `fiber`, then the block's `fiber`.
31    ///
32    /// # Errors
33    /// [`Error::Config`] for parameters that are not a valid fibre mapping.
34    pub fn fiber(&self, block: Option<&Json>) -> Result<FiberParams> {
35        Ok(FiberSpec::parse("fiber", Json::Object(self.fiber_map(block)?))?.fiber_params())
36    }
37
38    /// The Raman amplification of a block: the `raman` mapping of its merged `fiber`
39    /// (see the optical blocks), `None` without one.
40    ///
41    /// # Errors
42    /// [`Error::Config`] for a `raman` mapping that is not valid.
43    pub fn raman(&self, block: Option<&Json>) -> Result<Option<Raman>> {
44        let f = FiberSpec::parse("fiber", Json::Object(self.fiber_map(block)?))?;
45        f.raman.map(|r| r.parse("raman")?.raman()).transpose()
46    }
47
48    fn fiber_map(&self, block: Option<&Json>) -> Result<Map<String, Json>> {
49        merge_fiber(self.fiber.as_ref(), block)
50    }
51
52    /// A path relative to the specification.
53    pub fn path(&self, p: &str) -> PathBuf {
54        let p = PathBuf::from(p);
55        match &self.base_dir {
56            Some(d) if p.is_relative() => d.join(p),
57            _ => p,
58        }
59    }
60}
61
62/// The fibre mapping of a block (or of a theory) merged over that of the
63/// specification: the keys of `block` replace those of `spec`.
64///
65/// # Errors
66/// [`Error::Config`] for a value that is not a mapping.
67pub(crate) fn merge_fiber(spec: Option<&Json>, block: Option<&Json>) -> Result<Map<String, Json>> {
68    let mut m = Map::new();
69    for src in [spec, block].into_iter().flatten() {
70        match src {
71            Json::Object(o) => m.extend(o.clone()),
72            Json::Null => {}
73            other => return Err(Error::Config(format!("fiber must be a mapping, got {other}"))),
74        }
75    }
76    Ok(m)
77}
78
79block_params! {
80    /// The `fiber` mapping of a specification or of a block: the fields of
81    /// [`FiberParams`], their defaults its own, and the Raman pumps of every span.
82    pub(crate) struct FiberSpec {
83        alpha_db_km: f64 [FiberParams::default().alpha_db_km => "0.2"] "dB/km", "attenuation α";
84        d_ps_nm_km: f64 [FiberParams::default().d_ps_nm_km => "17.0"] "ps/(nm km)",
85            "dispersion parameter D (positive for standard fibre at 1550 nm)";
86        gamma_w_km: f64 [FiberParams::default().gamma_w_km => "1.3"] "1/(W km)", "nonlinear coefficient γ";
87        span_km: f64 [FiberParams::default().span_km => "80.0"] "km", "length of one span";
88        n_spans: usize [FiberParams::default().n_spans => "1"], "number of spans";
89        fc_hz: f64 [FiberParams::default().fc_hz => "193.1e12"] "Hz", "carrier frequency";
90        nf_db: f64 [FiberParams::default().nf_db => "4.5"] "dB", "noise figure of the amplifiers";
91        slope_ps_nm2_km: f64 [FiberParams::default().slope_ps_nm2_km => "0.0"] "ps/(nm² km)",
92            "dispersion slope S (the third-order dispersion), 0 for none";
93        raman: Option<Mapping<RamanSpec>> [absent "no Raman pump"],
94            "Raman amplification of every span (see comnum_channel::optical::raman)";
95    }
96}
97
98impl FiberSpec {
99    /// The fibre parameters.
100    fn fiber_params(&self) -> FiberParams {
101        FiberParams {
102            alpha_db_km: self.alpha_db_km,
103            d_ps_nm_km: self.d_ps_nm_km,
104            gamma_w_km: self.gamma_w_km,
105            span_km: self.span_km,
106            n_spans: self.n_spans,
107            fc_hz: self.fc_hz,
108            nf_db: self.nf_db,
109            slope_ps_nm2_km: self.slope_ps_nm2_km,
110        }
111    }
112}
113
114block_params! {
115    /// The `raman` mapping of a fibre: the pumps and the settings of [`Raman`], their
116    /// defaults its own.
117    pub(crate) struct RamanSpec {
118        pumps: Vec<Mapping<PumpSpec>> [serde_json::json!([]) => "[]"], "the pumps of every span, any number, co- or counter-propagating";
119        aeff_um2: f64 [Raman::default().aeff_um2 => "80.0"] "µm²" > 0, "effective area";
120        temperature_k: f64 [Raman::default().temperature_k => "300.0"] "K" > 0,
121            "temperature, for the spontaneous Raman noise";
122        isrs: bool [Raman::default().isrs => "true"], "the channels also exchange power (inter-channel stimulated Raman scattering)";
123        step_km: f64 [Raman::default().step_km => "0.1"] "km" > 0, "grid of the power equations";
124        slice_hz: f64 [Raman::default().slice_hz => "25e9"] "Hz" > 0, "slices of the spectrum of the field";
125        gain: Option<Mapping<GainSpec>> [absent "standard single-mode fibre"], "the Raman gain curve";
126    }
127}
128
129block_params! {
130    /// A pump of the `raman` mapping.
131    pub(crate) struct PumpSpec {
132        wavelength_nm: Option<f64> [absent "frequency_thz"] "nm" > 0, "vacuum wavelength (or frequency_thz)";
133        frequency_thz: Option<f64> [absent "wavelength_nm"] "THz" > 0, "frequency (or wavelength_nm)";
134        power_mw: f64 [required] "mW" >= 0, "power launched at its end of the span";
135        direction: String ["counter"] one_of ["co", "forward", "counter", "backward"],
136            "co (forward, launched at z = 0) or counter (backward, launched at z = L)";
137        alpha_db_km: Option<f64> [absent "that of the fibre"] "dB/km", "attenuation at the pump wavelength";
138    }
139}
140
141block_params! {
142    /// A measured Raman gain curve of the `raman` mapping.
143    pub(crate) struct GainSpec {
144        offsets_thz: Vec<f64> [required] "THz", "frequency offsets of the curve";
145        gain_m_w: Vec<f64> [required] "1/(W m)", "gain coefficients at the offsets";
146        reference_thz: f64 [required] "THz", "pump frequency of the measurement";
147    }
148}
149
150impl RamanSpec {
151    /// The Raman amplification.
152    fn raman(&self) -> Result<Raman> {
153        let d = Raman::default();
154        let pumps = self.pumps.iter().map(|p| p.parse("raman pump")?.pump()).collect::<Result<_>>()?;
155        let gain = match &self.gain {
156            None => d.gain.clone(),
157            Some(g) => {
158                let g = g.parse("raman gain")?;
159                RamanGain::new(g.offsets_thz.iter().map(|f| f * 1e12).collect(), g.gain_m_w, g.reference_thz * 1e12)?
160            }
161        };
162        Ok(Raman {
163            pumps,
164            gain,
165            aeff_um2: self.aeff_um2,
166            temperature_k: self.temperature_k,
167            isrs: self.isrs,
168            step_km: self.step_km,
169            slice_hz: self.slice_hz,
170            ..d
171        })
172    }
173}
174
175impl PumpSpec {
176    /// The pump.
177    fn pump(&self) -> Result<RamanPump> {
178        let direction = match self.direction.as_str() {
179            "co" | "forward" => Direction::Co,
180            _ => Direction::Counter,
181        };
182        let frequency_hz = match (self.wavelength_nm, self.frequency_thz) {
183            (Some(w), None) => comnum_math::nm2hz(w),
184            (None, Some(f)) => f * 1e12,
185            _ => return Err(Error::Config("raman: a pump needs wavelength_nm or frequency_thz".into())),
186        };
187        Ok(RamanPump { frequency_hz, power_w: self.power_mw * 1e-3, direction, alpha_db_km: self.alpha_db_km })
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::blocks::ParamDefault;
195
196    #[test]
197    fn the_defaults_of_the_fibre_and_of_raman_are_those_of_com_channel() {
198        let f = BuildContext::default().fiber(None).unwrap();
199        assert_eq!(f, FiberParams::default());
200        let r = RamanSpec::parse("raman", serde_json::json!({})).unwrap().raman().unwrap();
201        assert_eq!(r, Raman::default());
202        // the documented text is the value
203        for p in FiberSpec::params().into_iter().chain(RamanSpec::params()) {
204            if let ParamDefault::Value(v, text) = p.default {
205                let shown: f64 = text.parse().unwrap_or(f64::NAN);
206                let ok = v
207                    .as_f64()
208                    .map_or(serde_json::from_str::<serde_json::Value>(&text).ok().as_ref() == Some(&v), |x| x == shown);
209                assert!(ok, "{}: {text} for {v}", p.name);
210            }
211        }
212        // the choices of a pump are read without case
213        let p = PumpSpec::parse("pump", serde_json::json!({"wavelength_nm": 1450, "power_mw": 100, "direction": "CO"}));
214        assert_eq!(p.unwrap().pump().unwrap().direction, Direction::Co);
215    }
216}