Skip to main content

phasesmith_workflows/
parameters.rs

1//! Stable scalar parameter identities, bounds, and ordered sets.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7/// Stable structured identity for one refinable scalar.
8#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct ParameterKey {
10    module: String,
11    owner_id: String,
12    name: String,
13}
14
15impl ParameterKey {
16    /// Validate and own a three-segment parameter identity.
17    ///
18    /// # Errors
19    ///
20    /// Returns [`ParameterError::InvalidKeySegment`] for empty, untrimmed, or
21    /// delimiter-ambiguous segments.
22    pub fn new(
23        module: impl Into<String>,
24        owner_id: impl Into<String>,
25        name: impl Into<String>,
26    ) -> Result<Self, ParameterError> {
27        let key = Self {
28            module: module.into(),
29            owner_id: owner_id.into(),
30            name: name.into(),
31        };
32        validate_key_segment("module", &key.module)?;
33        validate_key_segment("owner_id", &key.owner_id)?;
34        validate_key_segment("name", &key.name)?;
35        Ok(key)
36    }
37
38    /// Return the owning module segment.
39    #[must_use]
40    pub fn module(&self) -> &str {
41        &self.module
42    }
43
44    /// Return the stable owner segment.
45    #[must_use]
46    pub fn owner_id(&self) -> &str {
47        &self.owner_id
48    }
49
50    /// Return the local parameter name.
51    #[must_use]
52    pub fn name(&self) -> &str {
53        &self.name
54    }
55
56    /// Return `module[owner_id].name` for diagnostics and reports.
57    #[must_use]
58    pub fn label(&self) -> String {
59        format!("{}[{}].{}", self.module, self.owner_id, self.name)
60    }
61}
62
63impl Display for ParameterKey {
64    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
65        formatter.write_str(&self.label())
66    }
67}
68
69/// Closed lower and upper scalar bounds.
70#[derive(Clone, Copy, Debug, PartialEq)]
71pub struct ParameterBounds {
72    /// Inclusive lower bound; negative infinity is allowed.
73    lower: f64,
74    /// Inclusive upper bound; positive infinity is allowed.
75    upper: f64,
76}
77
78impl ParameterBounds {
79    /// Validate ordered, non-NaN bounds.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`ParameterError::InvalidBounds`] for NaN or reversed bounds.
84    pub fn new(lower: f64, upper: f64) -> Result<Self, ParameterError> {
85        if lower.is_nan() || upper.is_nan() || lower > upper {
86            return Err(ParameterError::InvalidBounds);
87        }
88        Ok(Self { lower, upper })
89    }
90
91    /// Return whether `value` is inside the closed interval.
92    #[must_use]
93    pub fn contains(self, value: f64) -> bool {
94        self.lower <= value && value <= self.upper
95    }
96
97    /// Project one scalar into the closed interval.
98    #[must_use]
99    pub fn clip(self, value: f64) -> f64 {
100        value.clamp(self.lower, self.upper)
101    }
102
103    /// Return the inclusive lower bound.
104    #[must_use]
105    pub const fn lower(self) -> f64 {
106        self.lower
107    }
108
109    /// Return the inclusive upper bound.
110    #[must_use]
111    pub const fn upper(self) -> f64 {
112        self.upper
113    }
114}
115
116impl Default for ParameterBounds {
117    fn default() -> Self {
118        Self {
119            lower: f64::NEG_INFINITY,
120            upper: f64::INFINITY,
121        }
122    }
123}
124
125/// Value, unit, scale, bounds, and selection for one scalar.
126#[derive(Clone, Debug, PartialEq)]
127pub struct ParameterSpec {
128    /// Stable structured identity.
129    key: ParameterKey,
130    /// Current physical value.
131    value: f64,
132    /// Explicit physical unit label.
133    unit: String,
134    /// Closed physical bounds.
135    bounds: ParameterBounds,
136    /// Positive scale mapping physical values to solver coordinates.
137    scale: f64,
138    /// Whether this unconstrained parameter is selected for refinement.
139    refine: bool,
140}
141
142impl ParameterSpec {
143    /// Validate and construct one scalar specification.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`ParameterError`] for non-finite values, empty units,
148    /// out-of-bounds values, or non-positive/non-finite scales.
149    pub fn new(
150        key: ParameterKey,
151        value: f64,
152        unit: impl Into<String>,
153        bounds: ParameterBounds,
154        scale: f64,
155        refine: bool,
156    ) -> Result<Self, ParameterError> {
157        let unit = unit.into();
158        if !value.is_finite() {
159            return Err(ParameterError::NonFiniteValue { key });
160        }
161        if unit.is_empty() {
162            return Err(ParameterError::InvalidUnit { key });
163        }
164        if !bounds.contains(value) {
165            return Err(ParameterError::ValueOutsideBounds { key, value });
166        }
167        if !scale.is_finite() || scale <= 0.0 {
168            return Err(ParameterError::InvalidScale { key });
169        }
170        Ok(Self {
171            key,
172            value,
173            unit,
174            bounds,
175            scale,
176            refine,
177        })
178    }
179
180    /// Borrow the stable parameter identity.
181    #[must_use]
182    pub const fn key(&self) -> &ParameterKey {
183        &self.key
184    }
185
186    /// Return the current physical value.
187    #[must_use]
188    pub const fn value(&self) -> f64 {
189        self.value
190    }
191
192    /// Borrow the explicit unit label.
193    #[must_use]
194    pub fn unit(&self) -> &str {
195        &self.unit
196    }
197
198    /// Return the physical bounds.
199    #[must_use]
200    pub const fn bounds(&self) -> ParameterBounds {
201        self.bounds
202    }
203
204    /// Return the positive solver scale.
205    #[must_use]
206    pub const fn scale(&self) -> f64 {
207        self.scale
208    }
209
210    /// Return whether this unconstrained parameter is selected.
211    #[must_use]
212    pub const fn refine(&self) -> bool {
213        self.refine
214    }
215}
216
217/// Deterministically ordered immutable scalar specifications.
218#[derive(Clone, Debug, PartialEq)]
219pub struct ParameterSet {
220    specs: Vec<ParameterSpec>,
221    index_by_key: BTreeMap<ParameterKey, usize>,
222}
223
224impl ParameterSet {
225    /// Preserve input order while validating unique stable keys.
226    ///
227    /// # Errors
228    ///
229    /// Returns [`ParameterError::DuplicateKey`] for repeated identities.
230    pub fn new(specs: Vec<ParameterSpec>) -> Result<Self, ParameterError> {
231        let mut index_by_key = BTreeMap::new();
232        for (index, spec) in specs.iter().enumerate() {
233            if index_by_key.insert(spec.key.clone(), index).is_some() {
234                return Err(ParameterError::DuplicateKey {
235                    key: spec.key.clone(),
236                });
237            }
238        }
239        Ok(Self {
240            specs,
241            index_by_key,
242        })
243    }
244
245    /// Borrow specifications in stable packing order.
246    #[must_use]
247    pub fn specs(&self) -> &[ParameterSpec] {
248        &self.specs
249    }
250
251    /// Return one specification by stable key.
252    #[must_use]
253    pub fn spec(&self, key: &ParameterKey) -> Option<&ParameterSpec> {
254        self.index_by_key.get(key).map(|index| &self.specs[*index])
255    }
256
257    /// Return the stable row index of one parameter.
258    #[must_use]
259    pub fn index_of(&self, key: &ParameterKey) -> Option<usize> {
260        self.index_by_key.get(key).copied()
261    }
262
263    /// Copy physical values into a key-addressed map.
264    #[must_use]
265    pub fn values(&self) -> BTreeMap<ParameterKey, f64> {
266        self.specs
267            .iter()
268            .map(|spec| (spec.key.clone(), spec.value))
269            .collect()
270    }
271
272    /// Return a new ordered set with selected physical values replaced.
273    ///
274    /// # Errors
275    ///
276    /// Returns [`ParameterError`] for an unknown key or a replacement that is
277    /// non-finite or outside its existing bounds.
278    pub fn replace_values(
279        &self,
280        values: &BTreeMap<ParameterKey, f64>,
281    ) -> Result<Self, ParameterError> {
282        if let Some(key) = values
283            .keys()
284            .find(|key| !self.index_by_key.contains_key(*key))
285        {
286            return Err(ParameterError::UnknownReplacementKey { key: key.clone() });
287        }
288        Self::new(
289            self.specs
290                .iter()
291                .map(|spec| {
292                    ParameterSpec::new(
293                        spec.key.clone(),
294                        values.get(&spec.key).copied().unwrap_or(spec.value),
295                        spec.unit.clone(),
296                        spec.bounds,
297                        spec.scale,
298                        spec.refine,
299                    )
300                })
301                .collect::<Result<Vec<_>, _>>()?,
302        )
303    }
304
305    /// Return all stable keys as a set for dependency validation.
306    pub(crate) fn key_set(&self) -> BTreeSet<ParameterKey> {
307        self.index_by_key.keys().cloned().collect()
308    }
309}
310
311/// Invalid native parameter identity or scalar specification.
312#[derive(Clone, Debug, PartialEq)]
313pub enum ParameterError {
314    /// One key segment is empty, untrimmed, or contains a reserved delimiter.
315    InvalidKeySegment {
316        /// Stable segment name.
317        segment: &'static str,
318    },
319    /// Bounds contain NaN or are reversed.
320    InvalidBounds,
321    /// A parameter value is non-finite.
322    NonFiniteValue {
323        /// Parameter identity.
324        key: ParameterKey,
325    },
326    /// A unit label is empty.
327    InvalidUnit {
328        /// Parameter identity.
329        key: ParameterKey,
330    },
331    /// A value is outside its declared bounds.
332    ValueOutsideBounds {
333        /// Parameter identity.
334        key: ParameterKey,
335        /// Rejected value.
336        value: f64,
337    },
338    /// Solver scale is non-finite or non-positive.
339    InvalidScale {
340        /// Parameter identity.
341        key: ParameterKey,
342    },
343    /// A set repeats one key.
344    DuplicateKey {
345        /// Repeated identity.
346        key: ParameterKey,
347    },
348    /// A value replacement names a key outside the set.
349    UnknownReplacementKey {
350        /// Unknown identity.
351        key: ParameterKey,
352    },
353}
354
355impl Display for ParameterError {
356    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
357        match self {
358            Self::InvalidKeySegment { segment } => write!(
359                formatter,
360                "parameter {segment} must be trimmed, non-empty, and delimiter-safe"
361            ),
362            Self::InvalidBounds => {
363                formatter.write_str("parameter bounds must be ordered and must not contain NaN")
364            }
365            Self::NonFiniteValue { key } => {
366                write!(formatter, "parameter {key} value must be finite")
367            }
368            Self::InvalidUnit { key } => {
369                write!(formatter, "parameter {key} unit must be non-empty")
370            }
371            Self::ValueOutsideBounds { key, value } => {
372                write!(
373                    formatter,
374                    "parameter {key} value {value} lies outside its bounds"
375                )
376            }
377            Self::InvalidScale { key } => {
378                write!(
379                    formatter,
380                    "parameter {key} scale must be positive and finite"
381                )
382            }
383            Self::DuplicateKey { key } => write!(formatter, "duplicate parameter key {key}"),
384            Self::UnknownReplacementKey { key } => {
385                write!(formatter, "cannot replace unknown parameter key {key}")
386            }
387        }
388    }
389}
390
391impl Error for ParameterError {}
392
393fn validate_key_segment(segment: &'static str, value: &str) -> Result<(), ParameterError> {
394    if value.is_empty()
395        || value.trim() != value
396        || value
397            .chars()
398            .any(|character| matches!(character, '[' | ']' | '\n' | '\r' | '\t'))
399    {
400        return Err(ParameterError::InvalidKeySegment { segment });
401    }
402    Ok(())
403}