Skip to main content

laddu_expr/
parameters.rs

1use std::{collections::HashMap, sync::Arc};
2
3pub use crate::{ParamError, ParamResult};
4use fastrand::Rng;
5use fastrand_contrib::RngExt;
6use serde::{Deserialize, Serialize};
7
8/// Stable identifier for a parameter in a [`ParamLayout`].
9#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct ParamId(u32);
11
12impl ParamId {
13    /// Returns the zero-based position in the full parameter layout.
14    pub fn index(self) -> usize {
15        self.0 as usize
16    }
17}
18
19/// Stable identifier for a free parameter in free-parameter order.
20#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct FreeParamId(u32);
22
23impl FreeParamId {
24    /// Returns the zero-based position among free parameters.
25    pub fn index(self) -> usize {
26        self.0 as usize
27    }
28}
29
30/// Rule used to choose a free parameter's initial value.
31#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
32pub enum InitialSpec {
33    /// Use the default value zero.
34    #[default]
35    Default,
36    /// Use a specific initial value.
37    Value(f64),
38    /// Sample uniformly from an inclusive range.
39    Uniform {
40        /// Range minimum.
41        min: f64,
42        /// Range maximum.
43        max: f64,
44    },
45}
46
47/// Whether a parameter is varied or held at a fixed value.
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub enum ParamState {
50    /// The parameter is supplied by the optimizer or caller.
51    Free,
52    /// The parameter is fixed at the contained value.
53    Fixed(f64),
54}
55
56/// Optional inclusive lower and upper bounds for a parameter.
57#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
58pub struct Bounds {
59    /// Inclusive lower bound, or no lower bound.
60    pub min: Option<f64>,
61    /// Inclusive upper bound, or no upper bound.
62    pub max: Option<f64>,
63}
64
65impl Bounds {
66    /// Creates bounds from optional lower and upper endpoints.
67    pub fn new(min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) -> Self {
68        Self {
69            min: min.into(),
70            max: max.into(),
71        }
72    }
73
74    fn validate(&self, name: &str) -> ParamResult<()> {
75        if let (Some(min), Some(max)) = (self.min, self.max)
76            && min > max
77        {
78            return Err(ParamError::InvalidBounds {
79                name: name.to_owned(),
80                min,
81                max,
82            });
83        }
84        Ok(())
85    }
86
87    /// Returns whether `value` lies within both configured bounds.
88    pub fn contains(&self, value: f64) -> bool {
89        self.min.is_none_or(|min| value >= min) && self.max.is_none_or(|max| value <= max)
90    }
91}
92
93/// Complete definition and user-facing metadata for one scalar parameter.
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
95pub struct Parameter {
96    name: Arc<str>,
97    state: ParamState,
98    initial: InitialSpec,
99    bounds: Bounds,
100    #[serde(default)]
101    periodic: bool,
102    #[serde(default)]
103    scale: Option<f64>,
104    unit: Option<Arc<str>>,
105    latex: Option<Arc<str>>,
106    description: Option<Arc<str>>,
107}
108
109impl Parameter {
110    /// Creates an unbounded free parameter with a default initial value.
111    pub fn free(name: impl Into<Arc<str>>) -> Self {
112        Self {
113            name: name.into(),
114            state: ParamState::Free,
115            initial: InitialSpec::Default,
116            bounds: Bounds::default(),
117            periodic: false,
118            scale: None,
119            unit: None,
120            latex: None,
121            description: None,
122        }
123    }
124
125    /// Creates an unbounded parameter fixed at `value`.
126    pub fn fixed(name: impl Into<Arc<str>>, value: f64) -> Self {
127        Self {
128            name: name.into(),
129            state: ParamState::Fixed(value),
130            initial: InitialSpec::Value(value),
131            bounds: Bounds::default(),
132            periodic: false,
133            scale: None,
134            unit: None,
135            latex: None,
136            description: None,
137        }
138    }
139
140    fn set_fixed_value(&mut self, value: f64) {
141        self.state = ParamState::Fixed(value);
142        self.initial = InitialSpec::Value(value);
143    }
144
145    /// Returns this parameter fixed at `value`.
146    pub fn with_fixed_value(mut self, value: f64) -> Self {
147        self.set_fixed_value(value);
148        self
149    }
150
151    fn set_free(&mut self) {
152        self.state = ParamState::Free;
153    }
154
155    /// Returns this parameter marked as free.
156    pub fn with_free(mut self) -> Self {
157        self.set_free();
158        self
159    }
160
161    fn set_initial(&mut self, initial: impl Into<InitialSpec>) {
162        self.initial = initial.into();
163    }
164
165    /// Returns this parameter with the specified initialization rule.
166    pub fn with_initial(mut self, initial: impl Into<InitialSpec>) -> Self {
167        self.set_initial(initial);
168        self
169    }
170
171    fn set_bounds(&mut self, min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) {
172        self.bounds = Bounds::new(min, max);
173    }
174
175    /// Returns this parameter with inclusive optional bounds.
176    pub fn with_bounds(mut self, min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) -> Self {
177        self.set_bounds(min, max);
178        self
179    }
180
181    /// Mark this parameter as periodic over its finite two-sided bounds.
182    pub fn with_periodic(mut self) -> Self {
183        self.periodic = true;
184        self
185    }
186
187    /// Sets whether the parameter is periodic.
188    pub fn with_periodicity(mut self, periodic: bool) -> Self {
189        self.periodic = periodic;
190        self
191    }
192
193    /// Set the characteristic optimizer scale for this parameter.
194    ///
195    /// The scale is metadata: fit integrations may use it to condition the
196    /// optimizer coordinate system, while direct model evaluation is unchanged.
197    pub fn with_scale(mut self, scale: f64) -> Self {
198        self.scale = Some(scale);
199        self
200    }
201
202    fn set_unit(&mut self, unit: impl Into<Arc<str>>) {
203        self.unit = Some(unit.into());
204    }
205
206    /// Attaches a human-readable unit label.
207    pub fn with_unit(mut self, unit: impl Into<Arc<str>>) -> Self {
208        self.set_unit(unit);
209        self
210    }
211
212    fn set_latex(&mut self, latex: impl Into<Arc<str>>) {
213        self.latex = Some(latex.into());
214    }
215
216    /// Attaches a LaTeX-formatted label.
217    pub fn with_latex(mut self, latex: impl Into<Arc<str>>) -> Self {
218        self.set_latex(latex);
219        self
220    }
221
222    fn set_description(&mut self, description: impl Into<Arc<str>>) {
223        self.description = Some(description.into());
224    }
225
226    /// Attaches a longer human-readable description.
227    pub fn with_description(mut self, description: impl Into<Arc<str>>) -> Self {
228        self.set_description(description);
229        self
230    }
231
232    /// Returns the unique parameter name.
233    pub fn name(&self) -> &str {
234        &self.name
235    }
236
237    /// Returns whether the parameter is free or fixed.
238    pub fn state(&self) -> &ParamState {
239        &self.state
240    }
241
242    /// Returns whether the parameter is free.
243    pub fn is_free(&self) -> bool {
244        matches!(self.state, ParamState::Free)
245    }
246
247    /// Returns whether the parameter is fixed.
248    pub fn is_fixed(&self) -> bool {
249        matches!(self.state, ParamState::Fixed(_))
250    }
251
252    /// Returns the initialization rule.
253    pub fn initial_spec(&self) -> &InitialSpec {
254        &self.initial
255    }
256
257    /// Returns the optional parameter bounds.
258    pub fn bounds_spec(&self) -> &Bounds {
259        &self.bounds
260    }
261
262    /// Returns whether the parameter is periodic over its bounds.
263    pub fn is_periodic(&self) -> bool {
264        self.periodic
265    }
266
267    /// Return the validated canonical half-open periodic interval.
268    pub fn periodic_bounds(&self) -> Option<(f64, f64)> {
269        match (self.periodic, self.bounds.min, self.bounds.max) {
270            (true, Some(min), Some(max)) if min.is_finite() && max.is_finite() && min < max => {
271                Some((min, max))
272            }
273            _ => None,
274        }
275    }
276
277    /// Returns the optional characteristic optimizer scale.
278    pub fn scale(&self) -> Option<f64> {
279        self.scale
280    }
281
282    /// Returns the optional human-readable unit label.
283    pub fn unit_label(&self) -> Option<&str> {
284        self.unit.as_deref()
285    }
286
287    /// Returns the optional LaTeX-formatted label.
288    pub fn latex_label(&self) -> Option<&str> {
289        self.latex.as_deref()
290    }
291
292    /// Returns the optional longer description.
293    pub fn description_text(&self) -> Option<&str> {
294        self.description.as_deref()
295    }
296}
297
298impl From<f64> for InitialSpec {
299    fn from(value: f64) -> Self {
300        Self::Value(value)
301    }
302}
303
304impl From<(f64, f64)> for InitialSpec {
305    fn from((min, max): (f64, f64)) -> Self {
306        Self::Uniform { min, max }
307    }
308}
309
310/// Validated parameter ordering and mapping between full and free values.
311#[derive(Clone, Debug, Serialize, Deserialize)]
312pub struct ParamLayout {
313    specs: Arc<[Parameter]>,
314    names: Arc<HashMap<Arc<str>, ParamId>>,
315    free_params: Arc<[ParamId]>,
316    full_to_free: Arc<[Option<FreeParamId>]>,
317    defaults: Arc<[f64]>,
318}
319
320impl ParamLayout {
321    /// Validates parameter definitions and constructs a layout.
322    ///
323    /// # Errors
324    ///
325    /// Returns [`ParamError`] when a definition has an empty or duplicate
326    /// name, invalid bounds, invalid periodic metadata, an invalid scale, or
327    /// an initial or fixed value outside its permitted domain.
328    pub fn new<S>(specs: impl IntoIterator<Item = S>) -> ParamResult<Self>
329    where
330        S: Into<Parameter>,
331    {
332        let specs: Vec<_> = specs.into_iter().map(Into::into).collect();
333        let mut names = HashMap::with_capacity(specs.len());
334        let mut free_params = Vec::new();
335        let mut full_to_free = Vec::with_capacity(specs.len());
336        let mut defaults = Vec::with_capacity(specs.len());
337
338        for (index, spec) in specs.iter().enumerate() {
339            if spec.name().is_empty() {
340                return Err(ParamError::EmptyName);
341            }
342            spec.bounds.validate(spec.name())?;
343            if spec.periodic
344                && !matches!(
345                    (spec.bounds.min, spec.bounds.max),
346                    (Some(min), Some(max)) if min.is_finite() && max.is_finite() && min < max
347                )
348            {
349                return Err(ParamError::PeriodicRequiresFiniteBounds {
350                    name: spec.name().to_owned(),
351                });
352            }
353            if let Some(scale) = spec.scale
354                && (!scale.is_finite() || scale <= 0.0)
355            {
356                return Err(ParamError::InvalidScale {
357                    name: spec.name().to_owned(),
358                    scale,
359                });
360            }
361            validate_initial(spec)?;
362            let id = ParamId(index as u32);
363            if names.insert(Arc::clone(&spec.name), id).is_some() {
364                return Err(ParamError::DuplicateName(spec.name().to_owned()));
365            }
366            defaults.push(default_value(spec));
367            match spec.state {
368                ParamState::Free => {
369                    let free_id = FreeParamId(free_params.len() as u32);
370                    free_params.push(id);
371                    full_to_free.push(Some(free_id));
372                }
373                ParamState::Fixed(_) => full_to_free.push(None),
374            }
375        }
376
377        Ok(Self {
378            specs: specs.into(),
379            names: Arc::new(names),
380            free_params: free_params.into(),
381            full_to_free: full_to_free.into(),
382            defaults: defaults.into(),
383        })
384    }
385
386    /// Returns all parameter definitions in full-layout order.
387    pub fn specs(&self) -> &[Parameter] {
388        &self.specs
389    }
390
391    /// Returns the total number of free and fixed parameters.
392    pub fn len(&self) -> usize {
393        self.specs.len()
394    }
395
396    /// Returns whether the layout contains no parameters.
397    pub fn is_empty(&self) -> bool {
398        self.specs.is_empty()
399    }
400
401    /// Returns the number of free parameters.
402    pub fn n_free(&self) -> usize {
403        self.free_params.len()
404    }
405
406    /// Looks up a full-layout identifier by parameter name.
407    pub fn id(&self, name: &str) -> Option<ParamId> {
408        self.names.get(name).copied()
409    }
410
411    /// Returns the name associated with a full-layout identifier.
412    ///
413    /// # Errors
414    ///
415    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
416    /// layout.
417    pub fn name(&self, id: ParamId) -> ParamResult<&str> {
418        self.check_id(id)?;
419        Ok(self.specs[id.index()].name())
420    }
421
422    /// Returns the definition associated with a full-layout identifier.
423    ///
424    /// # Errors
425    ///
426    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
427    /// layout.
428    pub fn spec(&self, id: ParamId) -> ParamResult<&Parameter> {
429        self.check_id(id)?;
430        Ok(&self.specs[id.index()])
431    }
432
433    /// Maps a full-layout identifier to free-parameter order.
434    ///
435    /// Fixed parameters return `None`.
436    ///
437    /// # Errors
438    ///
439    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
440    /// layout.
441    pub fn free_id(&self, id: ParamId) -> ParamResult<Option<FreeParamId>> {
442        self.check_id(id)?;
443        Ok(self.full_to_free[id.index()])
444    }
445
446    fn free_param(&self, id: FreeParamId) -> ParamResult<ParamId> {
447        self.check_free_id(id)?;
448        Ok(self.free_params[id.index()])
449    }
450
451    /// Returns full-layout identifiers in free-parameter order.
452    pub fn free_params(&self) -> &[ParamId] {
453        &self.free_params
454    }
455
456    /// Creates a full value set using fixed and deterministic initial values.
457    pub fn default_values(&self) -> ParamValues {
458        ParamValues {
459            layout: Arc::new(self.clone()),
460            values: self.defaults.to_vec(),
461        }
462    }
463
464    /// Return deterministic initial values in free-parameter order.
465    ///
466    /// Uniform initial ranges use their midpoint.
467    pub fn initial_free_values(&self) -> Vec<f64> {
468        self.free_params
469            .iter()
470            .map(|id| self.defaults[id.index()])
471            .collect()
472    }
473
474    /// Expand a free-parameter slice while restoring fixed values from the layout.
475    ///
476    /// # Errors
477    ///
478    /// Returns [`ParamError::FreeLengthMismatch`] when `free` does not contain
479    /// exactly one value per free parameter.
480    pub fn values(&self, free: &[f64]) -> ParamResult<ParamValues> {
481        let mut values = self.defaults.to_vec();
482        self.fill_full_from_free(free, &mut values)?;
483        Ok(ParamValues {
484            layout: Arc::new(self.clone()),
485            values,
486        })
487    }
488
489    /// Generate one value per free parameter in layout order.
490    pub fn free_values_with(&self, mut value: impl FnMut(&Parameter) -> f64) -> Vec<f64> {
491        self.free_params
492            .iter()
493            .map(|id| value(&self.specs[id.index()]))
494            .collect()
495    }
496
497    /// Generate initial free values, invoking `uniform` only for uniform initial ranges.
498    pub fn sample_initial(&self, seed: u64) -> Vec<f64> {
499        let mut rng = Rng::with_seed(seed);
500        self.free_values_with(|parameter| match parameter.initial {
501            InitialSpec::Default => 0.0,
502            InitialSpec::Value(value) => value,
503            InitialSpec::Uniform { min, max } => rng.f64_range(min..max),
504        })
505    }
506
507    /// Validate free values against ordinary bounds and canonical periodic domains.
508    ///
509    /// # Errors
510    ///
511    /// Returns [`ParamError::FreeLengthMismatch`] when `free` has the wrong
512    /// length, or a value-related [`ParamError`] when a value lies outside its
513    /// parameter's bounds or canonical periodic domain.
514    pub fn validate_free_values(&self, free: &[f64]) -> ParamResult<()> {
515        if free.len() != self.n_free() {
516            return Err(ParamError::FreeLengthMismatch {
517                expected: self.n_free(),
518                actual: free.len(),
519            });
520        }
521        for (value, id) in free.iter().zip(self.free_params.iter()) {
522            validate_value(&self.specs[id.index()], *value)?;
523        }
524        Ok(())
525    }
526
527    /// Return free values with periodic parameters mapped into their canonical domains.
528    /// Non-periodic values are unchanged; ordinary bounds are not clamped.
529    ///
530    /// # Errors
531    ///
532    /// Returns [`ParamError::FreeLengthMismatch`] when `free` does not contain
533    /// exactly one value per free parameter.
534    pub fn wrap_periodic_free_values(&self, free: &[f64]) -> ParamResult<Vec<f64>> {
535        if free.len() != self.n_free() {
536            return Err(ParamError::FreeLengthMismatch {
537                expected: self.n_free(),
538                actual: free.len(),
539            });
540        }
541        Ok(free
542            .iter()
543            .zip(self.free_params.iter())
544            .map(|(value, id)| {
545                let parameter = &self.specs[id.index()];
546                parameter.periodic_bounds().map_or(*value, |(min, max)| {
547                    min + (*value - min).rem_euclid(max - min)
548                })
549            })
550            .collect())
551    }
552
553    fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
554        if free.len() != self.n_free() {
555            return Err(ParamError::FreeLengthMismatch {
556                expected: self.n_free(),
557                actual: free.len(),
558            });
559        }
560        debug_assert_eq!(full.len(), self.len());
561        full.copy_from_slice(&self.defaults);
562        for (free_index, id) in self.free_params.iter().enumerate() {
563            full[id.index()] = free[free_index];
564        }
565        Ok(())
566    }
567
568    fn check_id(&self, id: ParamId) -> ParamResult<()> {
569        if id.index() >= self.len() {
570            Err(ParamError::InvalidParamId {
571                id: id.index(),
572                len: self.len(),
573            })
574        } else {
575            Ok(())
576        }
577    }
578
579    fn check_free_id(&self, id: FreeParamId) -> ParamResult<()> {
580        if id.index() >= self.n_free() {
581            Err(ParamError::InvalidFreeParamId {
582                id: id.index(),
583                len: self.n_free(),
584            })
585        } else {
586            Ok(())
587        }
588    }
589}
590
591/// Incremental collection of uniquely named parameter definitions.
592#[derive(Clone, Debug, Default)]
593pub struct ParamRegistry {
594    specs: Vec<Parameter>,
595    names: HashMap<Arc<str>, ParamId>,
596}
597
598impl ParamRegistry {
599    /// Creates an empty registry.
600    pub fn new() -> Self {
601        Self::default()
602    }
603
604    /// Registers a parameter and returns its stable identifier.
605    ///
606    /// Re-registering an identical definition returns its existing identifier;
607    /// incompatible definitions with the same name return an error.
608    ///
609    /// # Errors
610    ///
611    /// Returns [`ParamError::EmptyName`] when the parameter name is empty, or
612    /// [`ParamError::ParameterConflict`] when the name is already associated
613    /// with a different definition.
614    pub fn register<S>(&mut self, spec: S) -> ParamResult<ParamId>
615    where
616        S: Into<Parameter>,
617    {
618        let spec = spec.into();
619        if spec.name().is_empty() {
620            return Err(ParamError::EmptyName);
621        }
622        if let Some(id) = self.names.get(spec.name()).copied() {
623            let existing = &self.specs[id.index()];
624            if existing != &spec {
625                return Err(ParamError::ParameterConflict {
626                    name: spec.name().to_owned(),
627                    reason: "duplicate parameter name has incompatible metadata".into(),
628                });
629            }
630            return Ok(id);
631        }
632
633        let id = ParamId(self.specs.len() as u32);
634        self.names.insert(Arc::clone(&spec.name), id);
635        self.specs.push(spec);
636        Ok(id)
637    }
638
639    /// Validates the registered parameters and builds their layout.
640    ///
641    /// # Errors
642    ///
643    /// Returns [`ParamError`] when any registered definition has invalid
644    /// bounds, periodic metadata, scale, or initial or fixed values.
645    pub fn layout(&self) -> ParamResult<ParamLayout> {
646        ParamLayout::new(self.specs.clone())
647    }
648}
649
650/// Concrete full-layout parameter values paired with their defining layout.
651#[derive(Clone, Debug, Serialize, Deserialize)]
652pub struct ParamValues {
653    layout: Arc<ParamLayout>,
654    values: Vec<f64>,
655}
656
657impl ParamValues {
658    /// Returns the shared layout that interprets these values.
659    pub fn layout(&self) -> &Arc<ParamLayout> {
660        &self.layout
661    }
662
663    /// Returns all values in full-layout order.
664    pub fn as_slice(&self) -> &[f64] {
665        &self.values
666    }
667
668    /// Returns the value associated with a full-layout identifier.
669    ///
670    /// # Errors
671    ///
672    /// Returns [`ParamError::InvalidParamId`] when `id` is outside the shared
673    /// layout.
674    pub fn get(&self, id: ParamId) -> ParamResult<f64> {
675        self.layout.check_id(id)?;
676        Ok(self.values[id.index()])
677    }
678
679    /// Copies the values of free parameters in free-parameter order.
680    pub fn free_values(&self) -> Vec<f64> {
681        self.layout
682            .free_params()
683            .iter()
684            .map(|id| self.values[id.index()])
685            .collect()
686    }
687
688    /// Assigns one value by its free-parameter identifier.
689    ///
690    /// # Errors
691    ///
692    /// Returns [`ParamError::InvalidFreeParamId`] when `id` is outside the
693    /// shared layout's free-parameter ordering.
694    pub fn set_free(&mut self, id: FreeParamId, value: f64) -> ParamResult<()> {
695        let full_id = self.layout.free_param(id)?;
696        self.values[full_id.index()] = value;
697        Ok(())
698    }
699
700    /// Replaces all free values and restores fixed values from the layout.
701    ///
702    /// # Errors
703    ///
704    /// Returns [`ParamError::FreeLengthMismatch`] when `values` does not
705    /// contain exactly one value per free parameter.
706    pub fn set_free_values(&mut self, values: &[f64]) -> ParamResult<()> {
707        let layout = Arc::clone(&self.layout);
708        layout.fill_full_from_free(values, &mut self.values)
709    }
710}
711
712fn default_value(spec: &Parameter) -> f64 {
713    match spec.state {
714        ParamState::Fixed(value) => value,
715        ParamState::Free => match spec.initial {
716            InitialSpec::Default => 0.0,
717            InitialSpec::Value(value) => value,
718            InitialSpec::Uniform { min, max } => 0.5 * (min + max),
719        },
720    }
721}
722
723fn validate_initial(spec: &Parameter) -> ParamResult<()> {
724    match spec.state {
725        ParamState::Fixed(value) => {
726            if !spec.bounds.contains(value) {
727                return Err(ParamError::FixedValueOutOfBounds {
728                    name: spec.name().to_owned(),
729                    value,
730                });
731            }
732            validate_periodic_value(spec, value)?;
733        }
734        ParamState::Free => match spec.initial {
735            InitialSpec::Default => {
736                if !spec.bounds.contains(0.0) {
737                    return Err(ParamError::InitialOutOfBounds {
738                        name: spec.name().to_owned(),
739                        value: 0.0,
740                    });
741                }
742                validate_periodic_value(spec, 0.0)?;
743            }
744            InitialSpec::Value(value) => {
745                if !spec.bounds.contains(value) {
746                    return Err(ParamError::InitialOutOfBounds {
747                        name: spec.name().to_owned(),
748                        value,
749                    });
750                }
751                validate_periodic_value(spec, value)?;
752            }
753            InitialSpec::Uniform { min, max } => {
754                if min > max {
755                    return Err(ParamError::InvalidInitialRange {
756                        name: spec.name().to_owned(),
757                        min,
758                        max,
759                    });
760                }
761                if !spec.bounds.contains(min) || !spec.bounds.contains(max) {
762                    return Err(ParamError::InitialRangeOutOfBounds {
763                        name: spec.name().to_owned(),
764                        min,
765                        max,
766                    });
767                }
768                if let Some((domain_min, domain_max)) = spec.periodic_bounds()
769                    && (min < domain_min || max > domain_max)
770                {
771                    let value = if min < domain_min { min } else { max };
772                    return Err(ParamError::ValueOutsidePeriodicDomain {
773                        name: spec.name().to_owned(),
774                        value,
775                        min: domain_min,
776                        max: domain_max,
777                    });
778                }
779            }
780        },
781    }
782    Ok(())
783}
784
785fn validate_periodic_value(spec: &Parameter, value: f64) -> ParamResult<()> {
786    if let Some((min, max)) = spec.periodic_bounds()
787        && !(value.is_finite() && value >= min && value < max)
788    {
789        return Err(ParamError::ValueOutsidePeriodicDomain {
790            name: spec.name().to_owned(),
791            value,
792            min,
793            max,
794        });
795    }
796    Ok(())
797}
798
799fn validate_value(spec: &Parameter, value: f64) -> ParamResult<()> {
800    if !spec.bounds.contains(value) {
801        return Err(ParamError::ValueOutOfBounds {
802            name: spec.name().to_owned(),
803            value,
804        });
805    }
806    if let Some((min, max)) = spec.periodic_bounds()
807        && !(value.is_finite() && value >= min && value < max)
808    {
809        return Err(ParamError::ValueOutsidePeriodicDomain {
810            name: spec.name().to_owned(),
811            value,
812            min,
813            max,
814        });
815    }
816    Ok(())
817}
818
819/// Convenience macro for creating parameters. Usage:
820/// `parameter!("name")` for a free parameter, or `parameter!("name", 1.0)` for a fixed one.
821#[macro_export]
822macro_rules! parameter {
823    ($name:expr) => {{
824        $crate::parameters::Parameter::free($name)
825    }};
826
827    ($name:expr, $value:expr) => {{
828        $crate::parameters::Parameter::fixed($name, $value)
829    }};
830
831    ($name:expr, $($rest:tt)+) => {{
832        let mut p = $crate::parameters::Parameter::free($name);
833        $crate::parameter!(@parse p, [fixed = false, initial = false]; $($rest)+);
834        p
835    }};
836
837    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; ) => {};
838
839    (@parse $p:ident, [fixed = false, initial = false]; fixed : $value:expr $(, $($rest:tt)*)?) => {{
840        $p = $p.with_fixed_value($value);
841        $crate::parameter!(@parse $p, [fixed = true, initial = false]; $($($rest)*)?);
842    }};
843
844    (@parse $p:ident, [fixed = false, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {{
845        $p = $p.with_initial($value);
846        $crate::parameter!(@parse $p, [fixed = false, initial = true]; $($($rest)*)?);
847    }};
848
849    (@parse $p:ident, [fixed = true, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {
850        compile_error!("parameter!: cannot specify both `fixed` and `initial`");
851    };
852
853    (@parse $p:ident, [fixed = false, initial = true]; fixed : $value:expr $(, $($rest:tt)*)?) => {
854        compile_error!("parameter!: cannot specify both `fixed` and `initial`");
855    };
856
857    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; bounds : ($min:expr, $max:expr) $(, $($rest:tt)*)?) => {{
858        $p = $p.with_bounds($min, $max);
859        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
860    }};
861
862    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic : $value:expr $(, $($rest:tt)*)?) => {{
863        $p = $p.with_periodicity($value);
864        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
865    }};
866
867    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic $(, $($rest:tt)*)?) => {{
868        $p = $p.with_periodic();
869        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
870    }};
871
872    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; scale : $value:expr $(, $($rest:tt)*)?) => {{
873        $p = $p.with_scale($value);
874        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
875    }};
876
877    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; unit : $value:expr $(, $($rest:tt)*)?) => {{
878        $p = $p.with_unit($value);
879        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
880    }};
881
882    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; latex : $value:expr $(, $($rest:tt)*)?) => {{
883        $p = $p.with_latex($value);
884        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
885    }};
886
887    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; description : $value:expr $(, $($rest:tt)*)?) => {{
888        $p = $p.with_description($value);
889        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
890    }};
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896
897    #[test]
898    fn parameter_macro_constructs_fixed_parameters() {
899        let positional = crate::parameter!("positional", 1.25);
900        let named = crate::parameter!("named", fixed: -0.5);
901
902        assert_eq!(positional.state(), &ParamState::Fixed(1.25));
903        assert_eq!(named.state(), &ParamState::Fixed(-0.5));
904    }
905
906    #[test]
907    fn parameter_scale_is_validated_and_supported_by_the_macro() {
908        let scaled = crate::parameter!("scaled", initial: 2.0, scale: 0.25);
909        let layout = ParamLayout::new([scaled]).unwrap();
910        assert_eq!(layout.specs()[0].scale(), Some(0.25));
911
912        let error = ParamLayout::new([Parameter::free("bad").with_scale(0.0)]).unwrap_err();
913        assert!(matches!(error, ParamError::InvalidScale { .. }));
914    }
915
916    #[test]
917    fn layout_tracks_free_and_fixed_values() {
918        let layout = ParamLayout::new([
919            Parameter::free("mass")
920                .with_initial(1.2)
921                .with_bounds(Some(0.0), Some(2.0)),
922            Parameter::fixed("pi", std::f64::consts::PI),
923            Parameter::free("width").with_initial((0.0, 1.0)),
924        ])
925        .unwrap();
926
927        assert_eq!(layout.len(), 3);
928        assert_eq!(layout.n_free(), 2);
929        assert_eq!(layout.initial_free_values(), vec![1.2, 0.5]);
930        assert_eq!(layout.id("mass").map(ParamId::index), Some(0));
931        assert_eq!(layout.id("pi").map(ParamId::index), Some(1));
932        assert_eq!(layout.id("width").map(ParamId::index), Some(2));
933        assert_eq!(
934            layout
935                .free_params()
936                .iter()
937                .map(|id| layout.name(*id).unwrap())
938                .collect::<Vec<_>>(),
939            vec!["mass", "width"]
940        );
941
942        let values = layout.values(&[1.4, 0.2]).unwrap();
943        assert_eq!(values.as_slice(), &[1.4, std::f64::consts::PI, 0.2]);
944        assert_eq!(values.free_values(), vec![1.4, 0.2]);
945    }
946
947    #[test]
948    fn free_values_can_be_generated_or_sampled_in_layout_order() {
949        let layout = ParamLayout::new([
950            Parameter::fixed("fixed", 8.0),
951            Parameter::free("uniform").with_initial((-2.0, 4.0)),
952            Parameter::free("value").with_initial(3.0),
953            Parameter::free("default"),
954        ])
955        .unwrap();
956
957        assert_eq!(layout.initial_free_values(), vec![1.0, 3.0, 0.0]);
958        assert_eq!(layout.sample_initial(0), vec![1.6157656431461036, 3.0, 0.0]);
959        assert_eq!(
960            layout.free_values_with(|parameter| parameter.name().len() as f64),
961            vec![7.0, 5.0, 7.0]
962        );
963    }
964
965    #[test]
966    fn periodic_domains_wrap_and_validate_without_changing_bounds() {
967        let tau = std::f64::consts::TAU;
968        let phase = Parameter::free("phase")
969            .with_initial(0.25)
970            .with_bounds(0.0, tau)
971            .with_periodic();
972        assert_eq!(phase.periodic_bounds(), Some((0.0, tau)));
973
974        let layout = ParamLayout::new([phase]).unwrap();
975        assert_eq!(
976            layout.wrap_periodic_free_values(&[-0.25]).unwrap(),
977            vec![tau - 0.25]
978        );
979        assert!(layout.validate_free_values(&[tau - 0.25]).is_ok());
980        assert!(matches!(
981            layout.validate_free_values(&[tau]),
982            Err(ParamError::ValueOutsidePeriodicDomain { .. })
983        ));
984    }
985
986    #[test]
987    fn invalid_periodic_metadata_and_initial_values_are_rejected() {
988        assert!(matches!(
989            ParamLayout::new([Parameter::free("phase").with_periodic()]),
990            Err(ParamError::PeriodicRequiresFiniteBounds { .. })
991        ));
992        assert!(matches!(
993            ParamLayout::new([Parameter::free("phase")
994                .with_initial(std::f64::consts::TAU)
995                .with_bounds(0.0, std::f64::consts::TAU)
996                .with_periodic(),]),
997            Err(ParamError::ValueOutsidePeriodicDomain { .. })
998        ));
999    }
1000
1001    #[test]
1002    fn duplicate_names_are_rejected() {
1003        let err = ParamLayout::new([Parameter::free("x"), Parameter::fixed("x", 1.0)]).unwrap_err();
1004        assert_eq!(err, ParamError::DuplicateName("x".into()));
1005    }
1006
1007    #[test]
1008    fn free_length_is_checked() {
1009        let layout = ParamLayout::new([Parameter::free("x"), Parameter::free("y")]).unwrap();
1010        let err = layout.values(&[1.0]).unwrap_err();
1011        assert_eq!(
1012            err,
1013            ParamError::FreeLengthMismatch {
1014                expected: 2,
1015                actual: 1
1016            }
1017        );
1018    }
1019
1020    #[test]
1021    fn full_and_free_vectors_round_trip_in_stable_order() {
1022        let layout = ParamLayout::new([
1023            Parameter::fixed("offset", -1.0),
1024            Parameter::free("mass").with_initial(1.2),
1025            Parameter::fixed("scale", 2.0),
1026            Parameter::free("width").with_initial(0.1),
1027        ])
1028        .unwrap();
1029
1030        let full = layout.values(&[1.4, 0.2]).unwrap();
1031        assert_eq!(full.as_slice(), &[-1.0, 1.4, 2.0, 0.2]);
1032        let mut rewritten = vec![0.0; layout.len()];
1033        layout
1034            .fill_full_from_free(&[1.5, 0.3], &mut rewritten)
1035            .unwrap();
1036        assert_eq!(rewritten, vec![-1.0, 1.5, 2.0, 0.3]);
1037    }
1038
1039    #[test]
1040    fn values_only_mutate_free_parameters() {
1041        let layout = ParamLayout::new([
1042            Parameter::fixed("fixed", 1.0),
1043            Parameter::free("x"),
1044            Parameter::free("y"),
1045        ])
1046        .unwrap();
1047        let x_id = layout.id("x").unwrap();
1048        let y_id = layout.id("y").unwrap();
1049        let x_free = layout.free_id(x_id).unwrap().unwrap();
1050        let y_free = layout.free_id(y_id).unwrap().unwrap();
1051
1052        let mut values = layout.default_values();
1053        values.set_free(x_free, 3.0).unwrap();
1054        values.set_free(y_free, 4.0).unwrap();
1055
1056        assert_eq!(values.as_slice(), &[1.0, 3.0, 4.0]);
1057        assert_eq!(values.free_values(), vec![3.0, 4.0]);
1058    }
1059
1060    #[test]
1061    fn invalid_specs_are_rejected() {
1062        assert_eq!(
1063            ParamLayout::new([Parameter::free("")]).unwrap_err(),
1064            ParamError::EmptyName
1065        );
1066
1067        assert_eq!(
1068            ParamLayout::new([Parameter::free("x").with_bounds(Some(2.0), Some(1.0))]).unwrap_err(),
1069            ParamError::InvalidBounds {
1070                name: "x".into(),
1071                min: 2.0,
1072                max: 1.0
1073            }
1074        );
1075
1076        assert_eq!(
1077            ParamLayout::new([Parameter::free("x").with_initial((2.0, 1.0))]).unwrap_err(),
1078            ParamError::InvalidInitialRange {
1079                name: "x".into(),
1080                min: 2.0,
1081                max: 1.0
1082            }
1083        );
1084
1085        assert_eq!(
1086            ParamLayout::new([Parameter::free("x")
1087                .with_initial(3.0)
1088                .with_bounds(Some(0.0), Some(2.0))])
1089            .unwrap_err(),
1090            ParamError::InitialOutOfBounds {
1091                name: "x".into(),
1092                value: 3.0
1093            }
1094        );
1095
1096        assert_eq!(
1097            ParamLayout::new([Parameter::free("x")
1098                .with_initial((-1.0, 1.0))
1099                .with_bounds(Some(0.0), Some(2.0))])
1100            .unwrap_err(),
1101            ParamError::InitialRangeOutOfBounds {
1102                name: "x".into(),
1103                min: -1.0,
1104                max: 1.0
1105            }
1106        );
1107
1108        assert_eq!(
1109            ParamLayout::new([Parameter::fixed("x", 3.0).with_bounds(Some(0.0), Some(2.0))])
1110                .unwrap_err(),
1111            ParamError::FixedValueOutOfBounds {
1112                name: "x".into(),
1113                value: 3.0
1114            }
1115        );
1116    }
1117
1118    #[test]
1119    fn free_vector_lengths_are_checked() {
1120        let layout = ParamLayout::new([
1121            Parameter::fixed("a", 0.0),
1122            Parameter::free("x"),
1123            Parameter::free("y"),
1124        ])
1125        .unwrap();
1126
1127        assert_eq!(
1128            layout
1129                .fill_full_from_free(&[1.0], &mut [0.0, 0.0, 0.0])
1130                .unwrap_err(),
1131            ParamError::FreeLengthMismatch {
1132                expected: 2,
1133                actual: 1
1134            }
1135        );
1136    }
1137
1138    #[test]
1139    fn registry_merges_identical_parameters_in_first_seen_order() {
1140        let mut registry = ParamRegistry::new();
1141        let y = registry
1142            .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1143            .unwrap();
1144        let x = registry.register(Parameter::free("x")).unwrap();
1145        let y_again = registry
1146            .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1147            .unwrap();
1148
1149        assert_eq!(y.index(), 0);
1150        assert_eq!(x.index(), 1);
1151        assert_eq!(y_again, y);
1152
1153        let layout = registry.layout().unwrap();
1154        assert_eq!(
1155            layout
1156                .specs()
1157                .iter()
1158                .map(Parameter::name)
1159                .collect::<Vec<_>>(),
1160            vec!["y", "x"]
1161        );
1162    }
1163
1164    #[test]
1165    fn registry_rejects_incompatible_parameter_reuse() {
1166        let mut registry = ParamRegistry::new();
1167        registry
1168            .register(Parameter::free("x").with_initial(1.0))
1169            .unwrap();
1170
1171        assert!(matches!(
1172            registry.register(Parameter::free("x").with_initial(2.0)),
1173            Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1174        ));
1175    }
1176}