1use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7#[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 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 #[must_use]
40 pub fn module(&self) -> &str {
41 &self.module
42 }
43
44 #[must_use]
46 pub fn owner_id(&self) -> &str {
47 &self.owner_id
48 }
49
50 #[must_use]
52 pub fn name(&self) -> &str {
53 &self.name
54 }
55
56 #[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#[derive(Clone, Copy, Debug, PartialEq)]
71pub struct ParameterBounds {
72 lower: f64,
74 upper: f64,
76}
77
78impl ParameterBounds {
79 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 #[must_use]
93 pub fn contains(self, value: f64) -> bool {
94 self.lower <= value && value <= self.upper
95 }
96
97 #[must_use]
99 pub fn clip(self, value: f64) -> f64 {
100 value.clamp(self.lower, self.upper)
101 }
102
103 #[must_use]
105 pub const fn lower(self) -> f64 {
106 self.lower
107 }
108
109 #[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#[derive(Clone, Debug, PartialEq)]
127pub struct ParameterSpec {
128 key: ParameterKey,
130 value: f64,
132 unit: String,
134 bounds: ParameterBounds,
136 scale: f64,
138 refine: bool,
140}
141
142impl ParameterSpec {
143 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 #[must_use]
182 pub const fn key(&self) -> &ParameterKey {
183 &self.key
184 }
185
186 #[must_use]
188 pub const fn value(&self) -> f64 {
189 self.value
190 }
191
192 #[must_use]
194 pub fn unit(&self) -> &str {
195 &self.unit
196 }
197
198 #[must_use]
200 pub const fn bounds(&self) -> ParameterBounds {
201 self.bounds
202 }
203
204 #[must_use]
206 pub const fn scale(&self) -> f64 {
207 self.scale
208 }
209
210 #[must_use]
212 pub const fn refine(&self) -> bool {
213 self.refine
214 }
215}
216
217#[derive(Clone, Debug, PartialEq)]
219pub struct ParameterSet {
220 specs: Vec<ParameterSpec>,
221 index_by_key: BTreeMap<ParameterKey, usize>,
222}
223
224impl ParameterSet {
225 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 #[must_use]
247 pub fn specs(&self) -> &[ParameterSpec] {
248 &self.specs
249 }
250
251 #[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 #[must_use]
259 pub fn index_of(&self, key: &ParameterKey) -> Option<usize> {
260 self.index_by_key.get(key).copied()
261 }
262
263 #[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 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 pub(crate) fn key_set(&self) -> BTreeSet<ParameterKey> {
307 self.index_by_key.keys().cloned().collect()
308 }
309}
310
311#[derive(Clone, Debug, PartialEq)]
313pub enum ParameterError {
314 InvalidKeySegment {
316 segment: &'static str,
318 },
319 InvalidBounds,
321 NonFiniteValue {
323 key: ParameterKey,
325 },
326 InvalidUnit {
328 key: ParameterKey,
330 },
331 ValueOutsideBounds {
333 key: ParameterKey,
335 value: f64,
337 },
338 InvalidScale {
340 key: ParameterKey,
342 },
343 DuplicateKey {
345 key: ParameterKey,
347 },
348 UnknownReplacementKey {
350 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}