Skip to main content

laddu_expr/
parameters.rs

1use std::{collections::HashMap, fmt, sync::Arc};
2
3pub use crate::{ParamError, ParamResult};
4use fastrand::Rng;
5use fastrand_contrib::RngExt;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use thiserror::Error;
8
9/// Stable identifier for a parameter in a [`ParamLayout`].
10#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct ParamId(u32);
12
13impl ParamId {
14    /// Returns the zero-based position in the full parameter layout.
15    pub fn index(self) -> usize {
16        self.0 as usize
17    }
18}
19
20/// Stable identifier for a free parameter in free-parameter order.
21#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct FreeParamId(u32);
23
24impl FreeParamId {
25    /// Returns the zero-based position among free parameters.
26    pub fn index(self) -> usize {
27        self.0 as usize
28    }
29}
30
31/// Rule used to choose a free parameter's initial value.
32#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
33pub enum InitialSpec {
34    /// Use the default value zero.
35    #[default]
36    Default,
37    /// Use a specific initial value.
38    Value(f64),
39    /// Sample uniformly from an inclusive range.
40    Uniform {
41        /// Range minimum.
42        min: f64,
43        /// Range maximum.
44        max: f64,
45    },
46}
47
48/// Whether a parameter is varied or held at a fixed value.
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub enum ParamState {
51    /// The parameter is supplied by the optimizer or caller.
52    Free,
53    /// The parameter is fixed at the contained value.
54    Fixed(f64),
55}
56
57/// Classified failure from validating a free-parameter value vector.
58///
59/// This separates structural input errors from invalid numeric input and
60/// values that are finite but outside the parameter support.
61#[derive(Clone, Debug, Error, PartialEq)]
62pub enum FreeValueValidationError {
63    /// Parameter-layout validation failed before individual values were classified.
64    #[error(transparent)]
65    Parameter(#[from] ParamError),
66    /// A free parameter was assigned a non-finite value.
67    #[error("non-finite value {value} for free parameter {name} ({id:?})")]
68    NonFiniteValue {
69        /// Identifier in free-parameter order.
70        id: FreeParamId,
71        /// Parameter name.
72        name: String,
73        /// Invalid value.
74        value: f64,
75    },
76    /// A finite free-parameter value was outside its declared support.
77    #[error("value {value} for free parameter {name} ({id:?}) is outside its support")]
78    OutsideSupport {
79        /// Identifier in free-parameter order.
80        id: FreeParamId,
81        /// Parameter name.
82        name: String,
83        /// Unsupported value.
84        value: f64,
85    },
86}
87
88/// Optional inclusive lower and upper bounds for a parameter.
89#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
90pub struct Bounds {
91    /// Inclusive lower bound, or no lower bound.
92    pub min: Option<f64>,
93    /// Inclusive upper bound, or no upper bound.
94    pub max: Option<f64>,
95}
96
97impl Bounds {
98    /// Creates bounds from optional lower and upper endpoints.
99    pub fn new(min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) -> Self {
100        Self {
101            min: min.into(),
102            max: max.into(),
103        }
104    }
105
106    fn validate(&self, name: &str) -> ParamResult<()> {
107        if let (Some(min), Some(max)) = (self.min, self.max)
108            && min > max
109        {
110            return Err(ParamError::InvalidBounds {
111                name: name.to_owned(),
112                min,
113                max,
114            });
115        }
116        Ok(())
117    }
118
119    /// Returns whether `value` lies within both configured bounds.
120    pub fn contains(&self, value: f64) -> bool {
121        self.min.is_none_or(|min| value >= min) && self.max.is_none_or(|max| value <= max)
122    }
123}
124
125/// Complete definition and user-facing metadata for one scalar parameter.
126#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
127pub struct Parameter {
128    name: Arc<str>,
129    state: ParamState,
130    initial: InitialSpec,
131    bounds: Bounds,
132    #[serde(default)]
133    periodic: bool,
134    #[serde(default)]
135    scale: Option<f64>,
136    unit: Option<Arc<str>>,
137    latex: Option<Arc<str>>,
138    description: Option<Arc<str>>,
139}
140
141impl Parameter {
142    /// Creates an unbounded free parameter with a default initial value.
143    pub fn free(name: impl Into<Arc<str>>) -> Self {
144        Self {
145            name: name.into(),
146            state: ParamState::Free,
147            initial: InitialSpec::Default,
148            bounds: Bounds::default(),
149            periodic: false,
150            scale: None,
151            unit: None,
152            latex: None,
153            description: None,
154        }
155    }
156
157    /// Creates an unbounded parameter fixed at `value`.
158    pub fn fixed(name: impl Into<Arc<str>>, value: f64) -> Self {
159        Self {
160            name: name.into(),
161            state: ParamState::Fixed(value),
162            initial: InitialSpec::Value(value),
163            bounds: Bounds::default(),
164            periodic: false,
165            scale: None,
166            unit: None,
167            latex: None,
168            description: None,
169        }
170    }
171
172    fn set_fixed_value(&mut self, value: f64) {
173        self.state = ParamState::Fixed(value);
174        self.initial = InitialSpec::Value(value);
175    }
176
177    /// Returns this parameter fixed at `value`.
178    pub fn with_fixed_value(mut self, value: f64) -> Self {
179        self.set_fixed_value(value);
180        self
181    }
182
183    fn set_free(&mut self) {
184        self.state = ParamState::Free;
185    }
186
187    /// Returns this parameter marked as free.
188    pub fn with_free(mut self) -> Self {
189        self.set_free();
190        self
191    }
192
193    fn set_initial(&mut self, initial: impl Into<InitialSpec>) {
194        self.initial = initial.into();
195    }
196
197    /// Returns this parameter with the specified initialization rule.
198    pub fn with_initial(mut self, initial: impl Into<InitialSpec>) -> Self {
199        self.set_initial(initial);
200        self
201    }
202
203    fn set_bounds(&mut self, min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) {
204        self.bounds = Bounds::new(min, max);
205    }
206
207    /// Returns this parameter with inclusive optional bounds.
208    pub fn with_bounds(mut self, min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) -> Self {
209        self.set_bounds(min, max);
210        self
211    }
212
213    /// Mark this parameter as periodic over its finite two-sided bounds.
214    pub fn with_periodic(mut self) -> Self {
215        self.periodic = true;
216        self
217    }
218
219    /// Sets whether the parameter is periodic.
220    pub fn with_periodicity(mut self, periodic: bool) -> Self {
221        self.periodic = periodic;
222        self
223    }
224
225    /// Set the characteristic optimizer scale for this parameter.
226    ///
227    /// The scale is metadata: fit integrations may use it to condition the
228    /// optimizer coordinate system, while direct model evaluation is unchanged.
229    pub fn with_scale(mut self, scale: f64) -> Self {
230        self.scale = Some(scale);
231        self
232    }
233
234    fn set_unit(&mut self, unit: impl Into<Arc<str>>) {
235        self.unit = Some(unit.into());
236    }
237
238    /// Attaches a human-readable unit label.
239    pub fn with_unit(mut self, unit: impl Into<Arc<str>>) -> Self {
240        self.set_unit(unit);
241        self
242    }
243
244    fn set_latex(&mut self, latex: impl Into<Arc<str>>) {
245        self.latex = Some(latex.into());
246    }
247
248    /// Attaches a LaTeX-formatted label.
249    pub fn with_latex(mut self, latex: impl Into<Arc<str>>) -> Self {
250        self.set_latex(latex);
251        self
252    }
253
254    fn set_description(&mut self, description: impl Into<Arc<str>>) {
255        self.description = Some(description.into());
256    }
257
258    /// Attaches a longer human-readable description.
259    pub fn with_description(mut self, description: impl Into<Arc<str>>) -> Self {
260        self.set_description(description);
261        self
262    }
263
264    /// Returns the unique parameter name.
265    pub fn name(&self) -> &str {
266        &self.name
267    }
268
269    /// Returns whether the parameter is free or fixed.
270    pub fn state(&self) -> &ParamState {
271        &self.state
272    }
273
274    /// Returns whether the parameter is free.
275    pub fn is_free(&self) -> bool {
276        matches!(self.state, ParamState::Free)
277    }
278
279    /// Returns whether the parameter is fixed.
280    pub fn is_fixed(&self) -> bool {
281        matches!(self.state, ParamState::Fixed(_))
282    }
283
284    /// Returns the initialization rule.
285    pub fn initial_spec(&self) -> &InitialSpec {
286        &self.initial
287    }
288
289    /// Returns the optional parameter bounds.
290    pub fn bounds_spec(&self) -> &Bounds {
291        &self.bounds
292    }
293
294    /// Returns whether the parameter is periodic over its bounds.
295    pub fn is_periodic(&self) -> bool {
296        self.periodic
297    }
298
299    /// Return the validated canonical half-open periodic interval.
300    pub fn periodic_bounds(&self) -> Option<(f64, f64)> {
301        match (self.periodic, self.bounds.min, self.bounds.max) {
302            (true, Some(min), Some(max)) if min.is_finite() && max.is_finite() && min < max => {
303                Some((min, max))
304            }
305            _ => None,
306        }
307    }
308
309    /// Returns the optional characteristic optimizer scale.
310    pub fn scale(&self) -> Option<f64> {
311        self.scale
312    }
313
314    /// Returns the optional human-readable unit label.
315    pub fn unit_label(&self) -> Option<&str> {
316        self.unit.as_deref()
317    }
318
319    /// Returns the optional LaTeX-formatted label.
320    pub fn latex_label(&self) -> Option<&str> {
321        self.latex.as_deref()
322    }
323
324    /// Returns the optional longer description.
325    pub fn description_text(&self) -> Option<&str> {
326        self.description.as_deref()
327    }
328
329    fn validate(&self) -> ParamResult<()> {
330        if self.name().is_empty() {
331            return Err(ParamError::EmptyName);
332        }
333        self.bounds.validate(self.name())?;
334        if self.periodic && self.periodic_bounds().is_none() {
335            return Err(ParamError::PeriodicRequiresFiniteBounds {
336                name: self.name().to_owned(),
337            });
338        }
339        if let Some(scale) = self.scale
340            && (!scale.is_finite() || scale <= 0.0)
341        {
342            return Err(ParamError::InvalidScale {
343                name: self.name().to_owned(),
344                scale,
345            });
346        }
347        self.validate_initial()
348    }
349
350    fn validate_initial(&self) -> ParamResult<()> {
351        match self.state {
352            ParamState::Fixed(value) => {
353                if !self.bounds.contains(value) {
354                    return Err(ParamError::FixedValueOutOfBounds {
355                        name: self.name().to_owned(),
356                        value,
357                    });
358                }
359                self.validate_periodic_value(value)
360            }
361            ParamState::Free => self.validate_free_initial(),
362        }
363    }
364
365    fn validate_free_initial(&self) -> ParamResult<()> {
366        match self.initial {
367            InitialSpec::Default | InitialSpec::Value(_) => {
368                let value = self.initial.representative_value();
369                if !self.bounds.contains(value) {
370                    return Err(ParamError::InitialOutOfBounds {
371                        name: self.name().to_owned(),
372                        value,
373                    });
374                }
375                self.validate_periodic_value(value)
376            }
377            InitialSpec::Uniform { min, max } => {
378                if min > max {
379                    return Err(ParamError::InvalidInitialRange {
380                        name: self.name().to_owned(),
381                        min,
382                        max,
383                    });
384                }
385                if !self.bounds.contains(min) || !self.bounds.contains(max) {
386                    return Err(ParamError::InitialRangeOutOfBounds {
387                        name: self.name().to_owned(),
388                        min,
389                        max,
390                    });
391                }
392                if let Some((domain_min, domain_max)) = self.periodic_bounds()
393                    && (min < domain_min || max > domain_max)
394                {
395                    let value = if min < domain_min { min } else { max };
396                    return Err(ParamError::ValueOutsidePeriodicDomain {
397                        name: self.name().to_owned(),
398                        value,
399                        min: domain_min,
400                        max: domain_max,
401                    });
402                }
403                Ok(())
404            }
405        }
406    }
407
408    fn default_value(&self) -> f64 {
409        match self.state {
410            ParamState::Fixed(value) => value,
411            ParamState::Free => self.initial.representative_value(),
412        }
413    }
414
415    fn validate_periodic_value(&self, value: f64) -> ParamResult<()> {
416        if let Some((min, max)) = self.periodic_bounds()
417            && !(value.is_finite() && value >= min && value < max)
418        {
419            return Err(ParamError::ValueOutsidePeriodicDomain {
420                name: self.name().to_owned(),
421                value,
422                min,
423                max,
424            });
425        }
426        Ok(())
427    }
428
429    fn validate_value(&self, value: f64) -> ParamResult<()> {
430        if !self.bounds.contains(value) {
431            return Err(ParamError::ValueOutOfBounds {
432                name: self.name().to_owned(),
433                value,
434            });
435        }
436        self.validate_periodic_value(value)
437    }
438
439    fn contains_in_support(&self, value: f64) -> bool {
440        self.bounds.contains(value)
441            && self
442                .periodic_bounds()
443                .is_none_or(|(min, max)| value >= min && value < max)
444    }
445}
446
447impl InitialSpec {
448    fn representative_value(&self) -> f64 {
449        match *self {
450            Self::Default => 0.0,
451            Self::Value(value) => value,
452            Self::Uniform { min, max } => 0.5 * (min + max),
453        }
454    }
455
456    fn sample_with(&self, rng: &mut Rng) -> f64 {
457        match *self {
458            Self::Default => 0.0,
459            Self::Value(value) => value,
460            Self::Uniform { min, max } => rng.f64_range(min..max),
461        }
462    }
463}
464
465impl From<f64> for InitialSpec {
466    fn from(value: f64) -> Self {
467        Self::Value(value)
468    }
469}
470
471impl From<(f64, f64)> for InitialSpec {
472    fn from((min, max): (f64, f64)) -> Self {
473        Self::Uniform { min, max }
474    }
475}
476
477/// Validated parameter ordering and mapping between full and free values.
478#[derive(Clone)]
479pub struct ParamLayout {
480    specs: Arc<[Parameter]>,
481    names: Arc<HashMap<Arc<str>, ParamId>>,
482    projection: ParamProjection,
483}
484
485impl fmt::Debug for ParamLayout {
486    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
487        formatter
488            .debug_struct("ParamLayout")
489            .field("specs", &self.specs)
490            .field("names", &self.names)
491            .field("free_params", &self.projection.free_params)
492            .field("full_to_free", &self.projection.full_to_free)
493            .field("defaults", &self.projection.defaults)
494            .finish()
495    }
496}
497
498#[derive(Clone, Debug)]
499// Owns the stable mappings and defaults that define full/free projection.
500struct ParamProjection {
501    free_params: Arc<[ParamId]>,
502    full_to_free: Arc<[Option<FreeParamId>]>,
503    defaults: Arc<[f64]>,
504}
505
506impl ParamProjection {
507    fn n_free(&self) -> usize {
508        self.free_params.len()
509    }
510
511    fn free_params(&self) -> &[ParamId] {
512        &self.free_params
513    }
514
515    fn free_id(&self, id: ParamId) -> Option<FreeParamId> {
516        self.full_to_free[id.index()]
517    }
518
519    fn full_id(&self, id: FreeParamId) -> ParamId {
520        self.free_params[id.index()]
521    }
522
523    fn validate_free_dimension<T>(&self, values: &[T]) -> ParamResult<()> {
524        if values.len() == self.n_free() {
525            Ok(())
526        } else {
527            Err(ParamError::FreeLengthMismatch {
528                expected: self.n_free(),
529                actual: values.len(),
530            })
531        }
532    }
533
534    fn initial_free_values(&self) -> Vec<f64> {
535        self.free_params
536            .iter()
537            .map(|id| self.defaults[id.index()])
538            .collect()
539    }
540
541    fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
542        self.validate_free_dimension(free)?;
543        debug_assert_eq!(full.len(), self.defaults.len());
544        full.copy_from_slice(&self.defaults);
545        for (value, id) in free.iter().zip(self.free_params.iter()) {
546            full[id.index()] = *value;
547        }
548        Ok(())
549    }
550
551    fn free_values_from_full(&self, full: &[f64]) -> Vec<f64> {
552        debug_assert_eq!(full.len(), self.defaults.len());
553        self.free_params.iter().map(|id| full[id.index()]).collect()
554    }
555}
556
557#[derive(Serialize, Deserialize)]
558// Keep ParamLayout's established flat serialized representation while its
559// projection fields are grouped behind one internal artifact.
560struct ParamLayoutSerde {
561    specs: Arc<[Parameter]>,
562    names: Arc<HashMap<Arc<str>, ParamId>>,
563    free_params: Arc<[ParamId]>,
564    full_to_free: Arc<[Option<FreeParamId>]>,
565    defaults: Arc<[f64]>,
566}
567
568impl Serialize for ParamLayout {
569    fn serialize<__S>(&self, serializer: __S) -> Result<__S::Ok, __S::Error>
570    where
571        __S: Serializer,
572    {
573        ParamLayoutSerde {
574            specs: Arc::clone(&self.specs),
575            names: Arc::clone(&self.names),
576            free_params: Arc::clone(&self.projection.free_params),
577            full_to_free: Arc::clone(&self.projection.full_to_free),
578            defaults: Arc::clone(&self.projection.defaults),
579        }
580        .serialize(serializer)
581    }
582}
583
584impl<'de> Deserialize<'de> for ParamLayout {
585    fn deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
586    where
587        __D: Deserializer<'de>,
588    {
589        let serialized = ParamLayoutSerde::deserialize(deserializer)?;
590        Ok(Self {
591            specs: serialized.specs,
592            names: serialized.names,
593            projection: ParamProjection {
594                free_params: serialized.free_params,
595                full_to_free: serialized.full_to_free,
596                defaults: serialized.defaults,
597            },
598        })
599    }
600}
601
602struct LayoutBuilder {
603    specs: Vec<Parameter>,
604    names: HashMap<Arc<str>, ParamId>,
605    free_params: Vec<ParamId>,
606    full_to_free: Vec<Option<FreeParamId>>,
607    defaults: Vec<f64>,
608}
609
610impl LayoutBuilder {
611    fn with_capacity(capacity: usize) -> Self {
612        Self {
613            specs: Vec::with_capacity(capacity),
614            names: HashMap::with_capacity(capacity),
615            free_params: Vec::new(),
616            full_to_free: Vec::with_capacity(capacity),
617            defaults: Vec::with_capacity(capacity),
618        }
619    }
620
621    fn push_validated(&mut self, spec: Parameter) -> ParamResult<()> {
622        spec.validate()?;
623        let id = ParamId(self.specs.len() as u32);
624        if self.names.insert(Arc::clone(&spec.name), id).is_some() {
625            return Err(ParamError::DuplicateName(spec.name().to_owned()));
626        }
627        self.defaults.push(spec.default_value());
628        match spec.state {
629            ParamState::Free => {
630                let free_id = FreeParamId(self.free_params.len() as u32);
631                self.free_params.push(id);
632                self.full_to_free.push(Some(free_id));
633            }
634            ParamState::Fixed(_) => self.full_to_free.push(None),
635        }
636        self.specs.push(spec);
637        Ok(())
638    }
639
640    fn finish(self) -> ParamLayout {
641        ParamLayout {
642            specs: self.specs.into(),
643            names: Arc::new(self.names),
644            projection: ParamProjection {
645                free_params: self.free_params.into(),
646                full_to_free: self.full_to_free.into(),
647                defaults: self.defaults.into(),
648            },
649        }
650    }
651}
652
653impl ParamLayout {
654    /// Validates parameter definitions and constructs a layout.
655    ///
656    /// # Errors
657    ///
658    /// Returns [`ParamError`] when a definition has an empty or duplicate
659    /// name, invalid bounds, invalid periodic metadata, an invalid scale, or
660    /// an initial or fixed value outside its permitted domain.
661    pub fn new<S>(specs: impl IntoIterator<Item = S>) -> ParamResult<Self>
662    where
663        S: Into<Parameter>,
664    {
665        let specs: Vec<_> = specs.into_iter().map(Into::into).collect();
666        let mut builder = LayoutBuilder::with_capacity(specs.len());
667        for spec in specs {
668            builder.push_validated(spec)?;
669        }
670        Ok(builder.finish())
671    }
672
673    /// Returns all parameter definitions in full-layout order.
674    pub fn specs(&self) -> &[Parameter] {
675        &self.specs
676    }
677
678    /// Returns the total number of free and fixed parameters.
679    pub fn len(&self) -> usize {
680        self.specs.len()
681    }
682
683    /// Returns whether the layout contains no parameters.
684    pub fn is_empty(&self) -> bool {
685        self.specs.is_empty()
686    }
687
688    /// Returns the number of free parameters.
689    pub fn n_free(&self) -> usize {
690        self.projection.n_free()
691    }
692
693    /// Looks up a full-layout identifier by parameter name.
694    pub fn id(&self, name: &str) -> Option<ParamId> {
695        self.names.get(name).copied()
696    }
697
698    /// Returns the name associated with a full-layout identifier.
699    ///
700    /// # Errors
701    ///
702    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
703    /// layout.
704    pub fn name(&self, id: ParamId) -> ParamResult<&str> {
705        self.check_id(id)?;
706        Ok(self.specs[id.index()].name())
707    }
708
709    /// Returns the definition associated with a full-layout identifier.
710    ///
711    /// # Errors
712    ///
713    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
714    /// layout.
715    pub fn spec(&self, id: ParamId) -> ParamResult<&Parameter> {
716        self.check_id(id)?;
717        Ok(&self.specs[id.index()])
718    }
719
720    /// Maps a full-layout identifier to free-parameter order.
721    ///
722    /// Fixed parameters return `None`.
723    ///
724    /// # Errors
725    ///
726    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
727    /// layout.
728    pub fn free_id(&self, id: ParamId) -> ParamResult<Option<FreeParamId>> {
729        self.check_id(id)?;
730        Ok(self.projection.free_id(id))
731    }
732
733    fn free_param(&self, id: FreeParamId) -> ParamResult<ParamId> {
734        self.check_free_id(id)?;
735        Ok(self.projection.full_id(id))
736    }
737
738    /// Returns full-layout identifiers in free-parameter order.
739    pub fn free_params(&self) -> &[ParamId] {
740        self.projection.free_params()
741    }
742
743    /// Iterates parameter definitions in stable free-parameter order.
744    pub fn free_parameters(
745        &self,
746    ) -> impl ExactSizeIterator<Item = &Parameter> + DoubleEndedIterator {
747        self.free_params().iter().map(|id| &self.specs[id.index()])
748    }
749
750    /// Creates a full value set using fixed and deterministic initial values.
751    pub fn default_values(&self) -> ParamValues {
752        ParamValues {
753            layout: Arc::new(self.clone()),
754            values: self.projection.defaults.to_vec(),
755        }
756    }
757
758    /// Return deterministic initial values in free-parameter order.
759    ///
760    /// Uniform initial ranges use their midpoint.
761    pub fn initial_free_values(&self) -> Vec<f64> {
762        self.projection.initial_free_values()
763    }
764
765    /// Expand a free-parameter slice while restoring fixed values from the layout.
766    ///
767    /// # Errors
768    ///
769    /// Returns [`ParamError::FreeLengthMismatch`] when `free` does not contain
770    /// exactly one value per free parameter.
771    pub fn values(&self, free: &[f64]) -> ParamResult<ParamValues> {
772        let mut values = self.projection.defaults.to_vec();
773        self.fill_full_from_free(free, &mut values)?;
774        Ok(ParamValues {
775            layout: Arc::new(self.clone()),
776            values,
777        })
778    }
779
780    /// Generate one value per free parameter in layout order.
781    pub fn free_values_with(&self, mut value: impl FnMut(&Parameter) -> f64) -> Vec<f64> {
782        self.free_parameters().map(&mut value).collect()
783    }
784
785    /// Generate initial free values, invoking `uniform` only for uniform initial ranges.
786    pub fn sample_initial(&self, seed: u64) -> Vec<f64> {
787        let mut rng = Rng::with_seed(seed);
788        self.free_values_with(|parameter| parameter.initial.sample_with(&mut rng))
789    }
790
791    /// Validate free values against ordinary bounds and canonical periodic domains.
792    ///
793    /// # Errors
794    ///
795    /// Returns [`ParamError::FreeLengthMismatch`] when `free` has the wrong
796    /// length, or a value-related [`ParamError`] when a value lies outside its
797    /// parameter's bounds or canonical periodic domain.
798    pub fn validate_free_values(&self, free: &[f64]) -> ParamResult<()> {
799        self.projection.validate_free_dimension(free)?;
800        for (value, parameter) in free.iter().zip(self.free_parameters()) {
801            parameter.validate_value(*value)?;
802        }
803        Ok(())
804    }
805
806    /// Validate free values while classifying structural, numeric, and support failures.
807    ///
808    /// Unlike [`Self::validate_free_values`], this method treats every non-finite
809    /// value as invalid input, including for an otherwise unbounded parameter.
810    ///
811    /// # Errors
812    ///
813    /// Returns [`FreeValueValidationError::Parameter`] for layout-level
814    /// validation failures such as the wrong number of values,
815    /// [`FreeValueValidationError::NonFiniteValue`] for NaN or infinity, and
816    /// [`FreeValueValidationError::OutsideSupport`] for a finite value outside
817    /// ordinary bounds or a canonical periodic domain.
818    pub fn validate_free_values_classified(
819        &self,
820        free: &[f64],
821    ) -> Result<(), FreeValueValidationError> {
822        self.projection.validate_free_dimension(free)?;
823        for (index, (value, parameter)) in free.iter().zip(self.free_parameters()).enumerate() {
824            let id = FreeParamId(index as u32);
825            if !value.is_finite() {
826                return Err(FreeValueValidationError::NonFiniteValue {
827                    id,
828                    name: parameter.name().to_owned(),
829                    value: *value,
830                });
831            }
832            if !parameter.contains_in_support(*value) {
833                return Err(FreeValueValidationError::OutsideSupport {
834                    id,
835                    name: parameter.name().to_owned(),
836                    value: *value,
837                });
838            }
839        }
840        Ok(())
841    }
842
843    /// Return free values with periodic parameters mapped into their canonical domains.
844    /// Non-periodic values are unchanged; ordinary bounds are not clamped.
845    ///
846    /// # Errors
847    ///
848    /// Returns [`ParamError::FreeLengthMismatch`] when `free` does not contain
849    /// exactly one value per free parameter.
850    pub fn wrap_periodic_free_values(&self, free: &[f64]) -> ParamResult<Vec<f64>> {
851        self.projection.validate_free_dimension(free)?;
852        Ok(free
853            .iter()
854            .zip(self.free_params().iter())
855            .map(|(value, id)| {
856                let parameter = &self.specs[id.index()];
857                parameter.periodic_bounds().map_or(*value, |(min, max)| {
858                    min + (*value - min).rem_euclid(max - min)
859                })
860            })
861            .collect())
862    }
863
864    fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
865        self.projection.fill_full_from_free(free, full)
866    }
867
868    fn check_id(&self, id: ParamId) -> ParamResult<()> {
869        if id.index() >= self.len() {
870            Err(ParamError::InvalidParamId {
871                id: id.index(),
872                len: self.len(),
873            })
874        } else {
875            Ok(())
876        }
877    }
878
879    fn check_free_id(&self, id: FreeParamId) -> ParamResult<()> {
880        if id.index() >= self.n_free() {
881            Err(ParamError::InvalidFreeParamId {
882                id: id.index(),
883                len: self.n_free(),
884            })
885        } else {
886            Ok(())
887        }
888    }
889}
890
891/// Incremental collection of uniquely named parameter definitions.
892#[derive(Clone, Debug, Default)]
893pub struct ParamRegistry {
894    specs: Vec<Parameter>,
895    names: HashMap<Arc<str>, ParamId>,
896}
897
898impl ParamRegistry {
899    /// Creates an empty registry.
900    pub fn new() -> Self {
901        Self::default()
902    }
903
904    /// Registers a parameter and returns its stable identifier.
905    ///
906    /// Re-registering an identical definition returns its existing identifier;
907    /// incompatible definitions with the same name return an error.
908    ///
909    /// # Errors
910    ///
911    /// Returns [`ParamError::EmptyName`] when the parameter name is empty, or
912    /// [`ParamError::ParameterConflict`] when the name is already associated
913    /// with a different definition.
914    pub fn register<S>(&mut self, spec: S) -> ParamResult<ParamId>
915    where
916        S: Into<Parameter>,
917    {
918        let spec = spec.into();
919        if spec.name().is_empty() {
920            return Err(ParamError::EmptyName);
921        }
922        if let Some(id) = self.names.get(spec.name()).copied() {
923            let existing = &self.specs[id.index()];
924            if existing != &spec {
925                return Err(ParamError::ParameterConflict {
926                    name: spec.name().to_owned(),
927                    reason: "duplicate parameter name has incompatible metadata".into(),
928                });
929            }
930            return Ok(id);
931        }
932
933        let id = ParamId(self.specs.len() as u32);
934        self.names.insert(Arc::clone(&spec.name), id);
935        self.specs.push(spec);
936        Ok(id)
937    }
938
939    /// Validates the registered parameters and builds their layout.
940    ///
941    /// # Errors
942    ///
943    /// Returns [`ParamError`] when any registered definition has invalid
944    /// bounds, periodic metadata, scale, or initial or fixed values.
945    pub fn layout(&self) -> ParamResult<ParamLayout> {
946        ParamLayout::new(self.specs.clone())
947    }
948}
949
950/// Concrete full-layout parameter values paired with their defining layout.
951#[derive(Clone, Debug, Serialize, Deserialize)]
952pub struct ParamValues {
953    layout: Arc<ParamLayout>,
954    values: Vec<f64>,
955}
956
957impl ParamValues {
958    /// Returns the shared layout that interprets these values.
959    pub fn layout(&self) -> &Arc<ParamLayout> {
960        &self.layout
961    }
962
963    /// Returns all values in full-layout order.
964    pub fn as_slice(&self) -> &[f64] {
965        &self.values
966    }
967
968    /// Returns the value associated with a full-layout identifier.
969    ///
970    /// # Errors
971    ///
972    /// Returns [`ParamError::InvalidParamId`] when `id` is outside the shared
973    /// layout.
974    pub fn get(&self, id: ParamId) -> ParamResult<f64> {
975        self.layout.check_id(id)?;
976        Ok(self.values[id.index()])
977    }
978
979    /// Copies the values of free parameters in free-parameter order.
980    pub fn free_values(&self) -> Vec<f64> {
981        self.layout.projection.free_values_from_full(&self.values)
982    }
983
984    /// Assigns one value by its free-parameter identifier.
985    ///
986    /// # Errors
987    ///
988    /// Returns [`ParamError::InvalidFreeParamId`] when `id` is outside the
989    /// shared layout's free-parameter ordering.
990    pub fn set_free(&mut self, id: FreeParamId, value: f64) -> ParamResult<()> {
991        let full_id = self.layout.free_param(id)?;
992        self.values[full_id.index()] = value;
993        Ok(())
994    }
995
996    /// Replaces all free values and restores fixed values from the layout.
997    ///
998    /// # Errors
999    ///
1000    /// Returns [`ParamError::FreeLengthMismatch`] when `values` does not
1001    /// contain exactly one value per free parameter.
1002    pub fn set_free_values(&mut self, values: &[f64]) -> ParamResult<()> {
1003        let layout = Arc::clone(&self.layout);
1004        layout.fill_full_from_free(values, &mut self.values)
1005    }
1006}
1007
1008/// Convenience macro for creating parameters. Usage:
1009/// `parameter!("name")` for a free parameter, or `parameter!("name", 1.0)` for a fixed one.
1010#[macro_export]
1011macro_rules! parameter {
1012    ($name:expr) => {{
1013        $crate::parameters::Parameter::free($name)
1014    }};
1015
1016    ($name:expr, $value:expr) => {{
1017        $crate::parameters::Parameter::fixed($name, $value)
1018    }};
1019
1020    ($name:expr, $($rest:tt)+) => {{
1021        let mut p = $crate::parameters::Parameter::free($name);
1022        $crate::parameter!(@parse p, [fixed = false, initial = false]; $($rest)+);
1023        p
1024    }};
1025
1026    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; ) => {};
1027
1028    (@parse $p:ident, [fixed = false, initial = false]; fixed : $value:expr $(, $($rest:tt)*)?) => {{
1029        $p = $p.with_fixed_value($value);
1030        $crate::parameter!(@parse $p, [fixed = true, initial = false]; $($($rest)*)?);
1031    }};
1032
1033    (@parse $p:ident, [fixed = false, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {{
1034        $p = $p.with_initial($value);
1035        $crate::parameter!(@parse $p, [fixed = false, initial = true]; $($($rest)*)?);
1036    }};
1037
1038    (@parse $p:ident, [fixed = true, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {
1039        compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1040    };
1041
1042    (@parse $p:ident, [fixed = false, initial = true]; fixed : $value:expr $(, $($rest:tt)*)?) => {
1043        compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1044    };
1045
1046    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; bounds : ($min:expr, $max:expr) $(, $($rest:tt)*)?) => {{
1047        $p = $p.with_bounds($min, $max);
1048        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1049    }};
1050
1051    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic : $value:expr $(, $($rest:tt)*)?) => {{
1052        $p = $p.with_periodicity($value);
1053        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1054    }};
1055
1056    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic $(, $($rest:tt)*)?) => {{
1057        $p = $p.with_periodic();
1058        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1059    }};
1060
1061    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; scale : $value:expr $(, $($rest:tt)*)?) => {{
1062        $p = $p.with_scale($value);
1063        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1064    }};
1065
1066    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; unit : $value:expr $(, $($rest:tt)*)?) => {{
1067        $p = $p.with_unit($value);
1068        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1069    }};
1070
1071    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; latex : $value:expr $(, $($rest:tt)*)?) => {{
1072        $p = $p.with_latex($value);
1073        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1074    }};
1075
1076    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; description : $value:expr $(, $($rest:tt)*)?) => {{
1077        $p = $p.with_description($value);
1078        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1079    }};
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084    use super::*;
1085
1086    #[test]
1087    fn parameter_macro_constructs_fixed_parameters() {
1088        let positional = crate::parameter!("positional", 1.25);
1089        let named = crate::parameter!("named", fixed: -0.5);
1090
1091        assert_eq!(positional.state(), &ParamState::Fixed(1.25));
1092        assert_eq!(named.state(), &ParamState::Fixed(-0.5));
1093    }
1094
1095    #[test]
1096    fn parameter_scale_is_validated_and_supported_by_the_macro() {
1097        let scaled = crate::parameter!("scaled", initial: 2.0, scale: 0.25);
1098        let layout = ParamLayout::new([scaled]).unwrap();
1099        assert_eq!(layout.specs()[0].scale(), Some(0.25));
1100
1101        let error = ParamLayout::new([Parameter::free("bad").with_scale(0.0)]).unwrap_err();
1102        assert!(matches!(error, ParamError::InvalidScale { .. }));
1103    }
1104
1105    #[test]
1106    fn layout_tracks_free_and_fixed_values() {
1107        let layout = ParamLayout::new([
1108            Parameter::free("mass")
1109                .with_initial(1.2)
1110                .with_bounds(Some(0.0), Some(2.0)),
1111            Parameter::fixed("pi", std::f64::consts::PI),
1112            Parameter::free("width").with_initial((0.0, 1.0)),
1113        ])
1114        .unwrap();
1115
1116        assert_eq!(layout.len(), 3);
1117        assert_eq!(layout.n_free(), 2);
1118        assert_eq!(layout.initial_free_values(), vec![1.2, 0.5]);
1119        assert_eq!(layout.id("mass").map(ParamId::index), Some(0));
1120        assert_eq!(layout.id("pi").map(ParamId::index), Some(1));
1121        assert_eq!(layout.id("width").map(ParamId::index), Some(2));
1122        assert_eq!(
1123            layout
1124                .free_params()
1125                .iter()
1126                .map(|id| layout.name(*id).unwrap())
1127                .collect::<Vec<_>>(),
1128            vec!["mass", "width"]
1129        );
1130
1131        let values = layout.values(&[1.4, 0.2]).unwrap();
1132        assert_eq!(values.as_slice(), &[1.4, std::f64::consts::PI, 0.2]);
1133        assert_eq!(values.free_values(), vec![1.4, 0.2]);
1134    }
1135
1136    #[test]
1137    fn free_values_can_be_generated_or_sampled_in_layout_order() {
1138        let layout = ParamLayout::new([
1139            Parameter::fixed("fixed", 8.0),
1140            Parameter::free("uniform").with_initial((-2.0, 4.0)),
1141            Parameter::free("value").with_initial(3.0),
1142            Parameter::free("default"),
1143        ])
1144        .unwrap();
1145
1146        assert_eq!(layout.initial_free_values(), vec![1.0, 3.0, 0.0]);
1147        assert_eq!(layout.sample_initial(0), vec![1.6157656431461036, 3.0, 0.0]);
1148        assert_eq!(
1149            layout.free_values_with(|parameter| parameter.name().len() as f64),
1150            vec![7.0, 5.0, 7.0]
1151        );
1152        assert_eq!(
1153            layout
1154                .free_parameters()
1155                .map(Parameter::name)
1156                .collect::<Vec<_>>(),
1157            vec!["uniform", "value", "default"]
1158        );
1159    }
1160
1161    #[test]
1162    fn deterministic_and_sampled_initial_values_share_initial_spec_semantics() {
1163        let layout = ParamLayout::new([
1164            Parameter::free("default"),
1165            Parameter::free("value").with_initial(2.5),
1166            Parameter::free("uniform").with_initial((-4.0, 6.0)),
1167        ])
1168        .unwrap();
1169
1170        assert_eq!(layout.initial_free_values(), vec![0.0, 2.5, 1.0]);
1171        for seed in 0..32 {
1172            let sampled = layout.sample_initial(seed);
1173            assert_eq!(sampled[0], 0.0);
1174            assert_eq!(sampled[1], 2.5);
1175            assert!((-4.0..6.0).contains(&sampled[2]));
1176        }
1177    }
1178
1179    #[test]
1180    fn classified_free_value_validation_separates_failure_kinds() {
1181        let layout = ParamLayout::new([
1182            Parameter::free("bounded").with_bounds(-1.0, 1.0),
1183            Parameter::free("phase")
1184                .with_bounds(0.0, std::f64::consts::TAU)
1185                .with_periodic(),
1186        ])
1187        .unwrap();
1188
1189        assert_eq!(
1190            layout.validate_free_values_classified(&[0.0]),
1191            Err(FreeValueValidationError::Parameter(
1192                ParamError::FreeLengthMismatch {
1193                    expected: 2,
1194                    actual: 1,
1195                }
1196            ))
1197        );
1198        assert!(matches!(
1199            layout.validate_free_values_classified(&[f64::NAN, 0.0]),
1200            Err(FreeValueValidationError::NonFiniteValue { id, name, value })
1201                if id.index() == 0 && name == "bounded" && value.is_nan()
1202        ));
1203        assert_eq!(
1204            layout.validate_free_values_classified(&[2.0, 0.0]),
1205            Err(FreeValueValidationError::OutsideSupport {
1206                id: FreeParamId(0),
1207                name: "bounded".into(),
1208                value: 2.0,
1209            })
1210        );
1211        assert_eq!(
1212            layout.validate_free_values_classified(&[0.0, std::f64::consts::TAU]),
1213            Err(FreeValueValidationError::OutsideSupport {
1214                id: FreeParamId(1),
1215                name: "phase".into(),
1216                value: std::f64::consts::TAU,
1217            })
1218        );
1219        assert!(layout.validate_free_values_classified(&[1.0, 0.0]).is_ok());
1220    }
1221
1222    #[test]
1223    fn periodic_domains_wrap_and_validate_without_changing_bounds() {
1224        let tau = std::f64::consts::TAU;
1225        let phase = Parameter::free("phase")
1226            .with_initial(0.25)
1227            .with_bounds(0.0, tau)
1228            .with_periodic();
1229        assert_eq!(phase.periodic_bounds(), Some((0.0, tau)));
1230
1231        let layout = ParamLayout::new([phase]).unwrap();
1232        assert_eq!(
1233            layout.wrap_periodic_free_values(&[-0.25]).unwrap(),
1234            vec![tau - 0.25]
1235        );
1236        assert!(layout.validate_free_values(&[tau - 0.25]).is_ok());
1237        assert!(matches!(
1238            layout.validate_free_values(&[tau]),
1239            Err(ParamError::ValueOutsidePeriodicDomain { .. })
1240        ));
1241    }
1242
1243    #[test]
1244    fn invalid_periodic_metadata_and_initial_values_are_rejected() {
1245        assert!(matches!(
1246            ParamLayout::new([Parameter::free("phase").with_periodic()]),
1247            Err(ParamError::PeriodicRequiresFiniteBounds { .. })
1248        ));
1249        assert!(matches!(
1250            ParamLayout::new([Parameter::free("phase")
1251                .with_initial(std::f64::consts::TAU)
1252                .with_bounds(0.0, std::f64::consts::TAU)
1253                .with_periodic(),]),
1254            Err(ParamError::ValueOutsidePeriodicDomain { .. })
1255        ));
1256    }
1257
1258    #[test]
1259    fn duplicate_names_are_rejected() {
1260        let err = ParamLayout::new([Parameter::free("x"), Parameter::fixed("x", 1.0)]).unwrap_err();
1261        assert_eq!(err, ParamError::DuplicateName("x".into()));
1262    }
1263
1264    #[test]
1265    fn free_length_is_checked() {
1266        let layout = ParamLayout::new([Parameter::free("x"), Parameter::free("y")]).unwrap();
1267        let err = layout.values(&[1.0]).unwrap_err();
1268        assert_eq!(
1269            err,
1270            ParamError::FreeLengthMismatch {
1271                expected: 2,
1272                actual: 1
1273            }
1274        );
1275    }
1276
1277    #[test]
1278    fn full_and_free_vectors_round_trip_in_stable_order() {
1279        let layout = ParamLayout::new([
1280            Parameter::fixed("offset", -1.0),
1281            Parameter::free("mass").with_initial(1.2),
1282            Parameter::fixed("scale", 2.0),
1283            Parameter::free("width").with_initial(0.1),
1284        ])
1285        .unwrap();
1286
1287        let full = layout.values(&[1.4, 0.2]).unwrap();
1288        assert_eq!(full.as_slice(), &[-1.0, 1.4, 2.0, 0.2]);
1289        let mut rewritten = vec![0.0; layout.len()];
1290        layout
1291            .fill_full_from_free(&[1.5, 0.3], &mut rewritten)
1292            .unwrap();
1293        assert_eq!(rewritten, vec![-1.0, 1.5, 2.0, 0.3]);
1294    }
1295
1296    #[test]
1297    fn values_only_mutate_free_parameters() {
1298        let layout = ParamLayout::new([
1299            Parameter::fixed("fixed", 1.0),
1300            Parameter::free("x"),
1301            Parameter::free("y"),
1302        ])
1303        .unwrap();
1304        let x_id = layout.id("x").unwrap();
1305        let y_id = layout.id("y").unwrap();
1306        let x_free = layout.free_id(x_id).unwrap().unwrap();
1307        let y_free = layout.free_id(y_id).unwrap().unwrap();
1308
1309        let mut values = layout.default_values();
1310        values.set_free(x_free, 3.0).unwrap();
1311        values.set_free(y_free, 4.0).unwrap();
1312
1313        assert_eq!(values.as_slice(), &[1.0, 3.0, 4.0]);
1314        assert_eq!(values.free_values(), vec![3.0, 4.0]);
1315    }
1316
1317    #[test]
1318    fn invalid_specs_are_rejected() {
1319        assert_eq!(
1320            ParamLayout::new([Parameter::free("")]).unwrap_err(),
1321            ParamError::EmptyName
1322        );
1323
1324        assert_eq!(
1325            ParamLayout::new([Parameter::free("x").with_bounds(Some(2.0), Some(1.0))]).unwrap_err(),
1326            ParamError::InvalidBounds {
1327                name: "x".into(),
1328                min: 2.0,
1329                max: 1.0
1330            }
1331        );
1332
1333        assert_eq!(
1334            ParamLayout::new([Parameter::free("x").with_initial((2.0, 1.0))]).unwrap_err(),
1335            ParamError::InvalidInitialRange {
1336                name: "x".into(),
1337                min: 2.0,
1338                max: 1.0
1339            }
1340        );
1341
1342        assert_eq!(
1343            ParamLayout::new([Parameter::free("x")
1344                .with_initial(3.0)
1345                .with_bounds(Some(0.0), Some(2.0))])
1346            .unwrap_err(),
1347            ParamError::InitialOutOfBounds {
1348                name: "x".into(),
1349                value: 3.0
1350            }
1351        );
1352
1353        assert_eq!(
1354            ParamLayout::new([Parameter::free("x")
1355                .with_initial((-1.0, 1.0))
1356                .with_bounds(Some(0.0), Some(2.0))])
1357            .unwrap_err(),
1358            ParamError::InitialRangeOutOfBounds {
1359                name: "x".into(),
1360                min: -1.0,
1361                max: 1.0
1362            }
1363        );
1364
1365        assert_eq!(
1366            ParamLayout::new([Parameter::fixed("x", 3.0).with_bounds(Some(0.0), Some(2.0))])
1367                .unwrap_err(),
1368            ParamError::FixedValueOutOfBounds {
1369                name: "x".into(),
1370                value: 3.0
1371            }
1372        );
1373    }
1374
1375    #[test]
1376    fn direct_and_registry_layouts_report_the_same_invalid_spec_errors() {
1377        let invalid = [
1378            Parameter::fixed("fixed", 2.0).with_bounds(0.0, 1.0),
1379            Parameter::free("default").with_bounds(1.0, 2.0),
1380            Parameter::free("value")
1381                .with_initial(2.0)
1382                .with_bounds(0.0, 1.0),
1383            Parameter::free("range")
1384                .with_initial((-1.0, 0.5))
1385                .with_bounds(0.0, 1.0),
1386            Parameter::free("periodic-value")
1387                .with_initial(std::f64::consts::TAU)
1388                .with_bounds(0.0, std::f64::consts::TAU)
1389                .with_periodic(),
1390        ];
1391
1392        for parameter in invalid {
1393            let direct = ParamLayout::new([parameter.clone()]).unwrap_err();
1394            let mut registry = ParamRegistry::new();
1395            registry.register(parameter).unwrap();
1396            assert_eq!(registry.layout().unwrap_err(), direct);
1397        }
1398    }
1399
1400    #[test]
1401    fn free_vector_lengths_are_checked() {
1402        let layout = ParamLayout::new([
1403            Parameter::fixed("a", 0.0),
1404            Parameter::free("x"),
1405            Parameter::free("y"),
1406        ])
1407        .unwrap();
1408
1409        assert_eq!(
1410            layout
1411                .fill_full_from_free(&[1.0], &mut [0.0, 0.0, 0.0])
1412                .unwrap_err(),
1413            ParamError::FreeLengthMismatch {
1414                expected: 2,
1415                actual: 1
1416            }
1417        );
1418    }
1419
1420    #[test]
1421    fn free_dimension_contract_is_shared_by_projection_operations() {
1422        let layout = ParamLayout::new([
1423            Parameter::fixed("fixed", 4.0),
1424            Parameter::free("x"),
1425            Parameter::free("y"),
1426        ])
1427        .unwrap();
1428        let expected = ParamError::FreeLengthMismatch {
1429            expected: 2,
1430            actual: 1,
1431        };
1432
1433        assert_eq!(layout.values(&[1.0]).unwrap_err(), expected);
1434        assert_eq!(layout.validate_free_values(&[1.0]).unwrap_err(), expected);
1435        assert_eq!(
1436            layout.wrap_periodic_free_values(&[1.0]).unwrap_err(),
1437            expected
1438        );
1439
1440        let mut values = layout.default_values();
1441        assert_eq!(values.set_free_values(&[1.0]).unwrap_err(), expected);
1442        assert_eq!(values.as_slice(), &[4.0, 0.0, 0.0]);
1443        assert_eq!(
1444            layout.validate_free_values_classified(&[1.0]),
1445            Err(FreeValueValidationError::Parameter(
1446                ParamError::FreeLengthMismatch {
1447                    expected: 2,
1448                    actual: 1,
1449                }
1450            ))
1451        );
1452    }
1453
1454    #[test]
1455    fn registry_merges_identical_parameters_in_first_seen_order() {
1456        let mut registry = ParamRegistry::new();
1457        let y = registry
1458            .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1459            .unwrap();
1460        let x = registry.register(Parameter::free("x")).unwrap();
1461        let y_again = registry
1462            .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1463            .unwrap();
1464
1465        assert_eq!(y.index(), 0);
1466        assert_eq!(x.index(), 1);
1467        assert_eq!(y_again, y);
1468
1469        let layout = registry.layout().unwrap();
1470        assert_eq!(
1471            layout
1472                .specs()
1473                .iter()
1474                .map(Parameter::name)
1475                .collect::<Vec<_>>(),
1476            vec!["y", "x"]
1477        );
1478    }
1479
1480    #[test]
1481    fn registry_rejects_incompatible_parameter_reuse() {
1482        let mut registry = ParamRegistry::new();
1483        registry
1484            .register(Parameter::free("x").with_initial(1.0))
1485            .unwrap();
1486
1487        assert!(matches!(
1488            registry.register(Parameter::free("x").with_initial(2.0)),
1489            Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1490        ));
1491    }
1492}