1use 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
12pub(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#[derive(Clone, Debug, Default)]
17pub struct BuildContext {
18 pub fiber: Option<Json>,
20 pub base_dir: Option<PathBuf>,
22 pub name: Option<String>,
24 pub condition: Option<String>,
26}
27
28impl BuildContext {
29 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 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 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
62pub(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 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 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 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 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 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 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 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 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 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}