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: ParamLayoutProjection,
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 ParamLayoutProjection {
501    free_params: Arc<[ParamId]>,
502    full_to_free: Arc<[Option<FreeParamId>]>,
503    defaults: Arc<[f64]>,
504}
505
506impl ParamLayoutProjection {
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/// Validated projection from one parameter layout into another.
558///
559/// The target layout owns the values produced by [`Self::project`], while the
560/// source layout owns the values accepted by it.  Only free parameters are
561/// projected: fixed target parameters retain their configured values, and a
562/// target free parameter must also be free in the source layout.
563#[derive(Clone, Debug)]
564pub struct ParamProjection {
565    source: Arc<ParamLayout>,
566    target: Arc<ParamLayout>,
567    source_free_ids: Arc<[FreeParamId]>,
568}
569
570#[derive(Serialize, Deserialize)]
571// Keep ParamLayout's established flat serialized representation while its
572// projection fields are grouped behind one internal artifact.
573struct ParamLayoutSerde {
574    specs: Arc<[Parameter]>,
575    names: Arc<HashMap<Arc<str>, ParamId>>,
576    free_params: Arc<[ParamId]>,
577    full_to_free: Arc<[Option<FreeParamId>]>,
578    defaults: Arc<[f64]>,
579}
580
581impl Serialize for ParamLayout {
582    fn serialize<__S>(&self, serializer: __S) -> Result<__S::Ok, __S::Error>
583    where
584        __S: Serializer,
585    {
586        ParamLayoutSerde {
587            specs: Arc::clone(&self.specs),
588            names: Arc::clone(&self.names),
589            free_params: Arc::clone(&self.projection.free_params),
590            full_to_free: Arc::clone(&self.projection.full_to_free),
591            defaults: Arc::clone(&self.projection.defaults),
592        }
593        .serialize(serializer)
594    }
595}
596
597impl<'de> Deserialize<'de> for ParamLayout {
598    fn deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
599    where
600        __D: Deserializer<'de>,
601    {
602        let serialized = ParamLayoutSerde::deserialize(deserializer)?;
603        Ok(Self {
604            specs: serialized.specs,
605            names: serialized.names,
606            projection: ParamLayoutProjection {
607                free_params: serialized.free_params,
608                full_to_free: serialized.full_to_free,
609                defaults: serialized.defaults,
610            },
611        })
612    }
613}
614
615impl ParamProjection {
616    /// Projects values from the source layout into the target layout.
617    ///
618    /// # Errors
619    ///
620    /// Returns [`ParamError::UnknownName`] when the supplied values do not
621    /// contain a required target parameter.
622    pub fn project(&self, source: &ParamValues) -> ParamResult<ParamValues> {
623        let free = if source.layout().specs() == self.source.specs() {
624            self.source_free_ids
625                .iter()
626                .map(|id| source.get(self.source.projection.full_id(*id)))
627                .collect::<ParamResult<Vec<_>>>()?
628        } else {
629            self.target
630                .free_params()
631                .iter()
632                .map(|target_id| {
633                    let name = self.target.name(*target_id)?;
634                    let source_id = source
635                        .layout()
636                        .id(name)
637                        .ok_or_else(|| ParamError::UnknownName(name.to_owned()))?;
638                    source.get(source_id)
639                })
640                .collect::<ParamResult<Vec<_>>>()?
641        };
642        self.target.values(&free)
643    }
644
645    /// Scatters a target-layout free gradient into a source-layout gradient.
646    ///
647    /// Values are added to `source`, allowing several projections to
648    /// contribute to one gradient.  Both dimensions are validated against the
649    /// layouts captured by this artifact.
650    ///
651    /// # Errors
652    ///
653    /// Returns [`ParamError::FreeLengthMismatch`] when either slice has an
654    /// incompatible free-parameter dimension.
655    pub fn scatter_add(&self, target: &[f64], source: &mut [f64]) -> ParamResult<()> {
656        self.target.projection.validate_free_dimension(target)?;
657        self.source.projection.validate_free_dimension(source)?;
658        for (value, id) in target.iter().zip(self.source_free_ids.iter()) {
659            source[id.index()] += value;
660        }
661        Ok(())
662    }
663}
664
665struct LayoutBuilder {
666    specs: Vec<Parameter>,
667    names: HashMap<Arc<str>, ParamId>,
668    free_params: Vec<ParamId>,
669    full_to_free: Vec<Option<FreeParamId>>,
670    defaults: Vec<f64>,
671}
672
673impl LayoutBuilder {
674    fn with_capacity(capacity: usize) -> Self {
675        Self {
676            specs: Vec::with_capacity(capacity),
677            names: HashMap::with_capacity(capacity),
678            free_params: Vec::new(),
679            full_to_free: Vec::with_capacity(capacity),
680            defaults: Vec::with_capacity(capacity),
681        }
682    }
683
684    fn push_validated(&mut self, spec: Parameter) -> ParamResult<()> {
685        spec.validate()?;
686        let id = ParamId(self.specs.len() as u32);
687        if self.names.insert(Arc::clone(&spec.name), id).is_some() {
688            return Err(ParamError::DuplicateName(spec.name().to_owned()));
689        }
690        self.defaults.push(spec.default_value());
691        match spec.state {
692            ParamState::Free => {
693                let free_id = FreeParamId(self.free_params.len() as u32);
694                self.free_params.push(id);
695                self.full_to_free.push(Some(free_id));
696            }
697            ParamState::Fixed(_) => self.full_to_free.push(None),
698        }
699        self.specs.push(spec);
700        Ok(())
701    }
702
703    fn finish(self) -> ParamLayout {
704        ParamLayout {
705            specs: self.specs.into(),
706            names: Arc::new(self.names),
707            projection: ParamLayoutProjection {
708                free_params: self.free_params.into(),
709                full_to_free: self.full_to_free.into(),
710                defaults: self.defaults.into(),
711            },
712        }
713    }
714}
715
716impl ParamLayout {
717    /// Validates parameter definitions and constructs a layout.
718    ///
719    /// # Errors
720    ///
721    /// Returns [`ParamError`] when a definition has an empty or duplicate
722    /// name, invalid bounds, invalid periodic metadata, an invalid scale, or
723    /// an initial or fixed value outside its permitted domain.
724    pub fn new<S>(specs: impl IntoIterator<Item = S>) -> ParamResult<Self>
725    where
726        S: Into<Parameter>,
727    {
728        let specs: Vec<_> = specs.into_iter().map(Into::into).collect();
729        let mut builder = LayoutBuilder::with_capacity(specs.len());
730        for spec in specs {
731            builder.push_validated(spec)?;
732        }
733        Ok(builder.finish())
734    }
735
736    /// Returns all parameter definitions in full-layout order.
737    pub fn specs(&self) -> &[Parameter] {
738        &self.specs
739    }
740
741    /// Returns the total number of free and fixed parameters.
742    pub fn len(&self) -> usize {
743        self.specs.len()
744    }
745
746    /// Returns whether the layout contains no parameters.
747    pub fn is_empty(&self) -> bool {
748        self.specs.is_empty()
749    }
750
751    /// Returns the number of free parameters.
752    pub fn n_free(&self) -> usize {
753        self.projection.n_free()
754    }
755
756    /// Looks up a full-layout identifier by parameter name.
757    pub fn id(&self, name: &str) -> Option<ParamId> {
758        self.names.get(name).copied()
759    }
760
761    /// Returns the name associated with a full-layout identifier.
762    ///
763    /// # Errors
764    ///
765    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
766    /// layout.
767    pub fn name(&self, id: ParamId) -> ParamResult<&str> {
768        self.check_id(id)?;
769        Ok(self.specs[id.index()].name())
770    }
771
772    /// Returns the definition associated with a full-layout identifier.
773    ///
774    /// # Errors
775    ///
776    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
777    /// layout.
778    pub fn spec(&self, id: ParamId) -> ParamResult<&Parameter> {
779        self.check_id(id)?;
780        Ok(&self.specs[id.index()])
781    }
782
783    /// Maps a full-layout identifier to free-parameter order.
784    ///
785    /// Fixed parameters return `None`.
786    ///
787    /// # Errors
788    ///
789    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
790    /// layout.
791    pub fn free_id(&self, id: ParamId) -> ParamResult<Option<FreeParamId>> {
792        self.check_id(id)?;
793        Ok(self.projection.free_id(id))
794    }
795
796    fn free_param(&self, id: FreeParamId) -> ParamResult<ParamId> {
797        self.check_free_id(id)?;
798        Ok(self.projection.full_id(id))
799    }
800
801    /// Returns full-layout identifiers in free-parameter order.
802    pub fn free_params(&self) -> &[ParamId] {
803        self.projection.free_params()
804    }
805
806    /// Builds a validated projection from `source` into this layout.
807    ///
808    /// Free parameters are matched by name and must be free in both layouts.
809    /// Parameters present only in the source are permitted; fixed parameters
810    /// in this layout use their own configured values.
811    ///
812    /// # Errors
813    ///
814    /// Returns [`ParamError::UnknownName`] when a free target parameter is
815    /// absent from `source`, or [`ParamError::ParameterConflict`] when it is
816    /// fixed there.
817    pub fn projection_from(&self, source: &ParamLayout) -> ParamResult<ParamProjection> {
818        let source_free_ids = self
819            .free_params()
820            .iter()
821            .map(|target_id| {
822                let name = self.name(*target_id)?;
823                let source_id = source
824                    .id(name)
825                    .ok_or_else(|| ParamError::UnknownName(name.to_owned()))?;
826                source
827                    .free_id(source_id)?
828                    .ok_or_else(|| ParamError::ParameterConflict {
829                        name: name.to_owned(),
830                        reason: "target free parameter is fixed in the source layout".to_owned(),
831                    })
832            })
833            .collect::<ParamResult<Arc<[_]>>>()?;
834        Ok(ParamProjection {
835            source: Arc::new(source.clone()),
836            target: Arc::new(self.clone()),
837            source_free_ids,
838        })
839    }
840
841    /// Iterates parameter definitions in stable free-parameter order.
842    pub fn free_parameters(
843        &self,
844    ) -> impl ExactSizeIterator<Item = &Parameter> + DoubleEndedIterator {
845        self.free_params().iter().map(|id| &self.specs[id.index()])
846    }
847
848    /// Creates a full value set using fixed and deterministic initial values.
849    pub fn default_values(&self) -> ParamValues {
850        ParamValues {
851            layout: Arc::new(self.clone()),
852            values: self.projection.defaults.to_vec(),
853        }
854    }
855
856    /// Return deterministic initial values in free-parameter order.
857    ///
858    /// Uniform initial ranges use their midpoint.
859    pub fn initial_free_values(&self) -> Vec<f64> {
860        self.projection.initial_free_values()
861    }
862
863    /// Expand a free-parameter slice while restoring fixed values from the layout.
864    ///
865    /// # Errors
866    ///
867    /// Returns [`ParamError::FreeLengthMismatch`] when `free` does not contain
868    /// exactly one value per free parameter.
869    pub fn values(&self, free: &[f64]) -> ParamResult<ParamValues> {
870        let mut values = self.projection.defaults.to_vec();
871        self.fill_full_from_free(free, &mut values)?;
872        Ok(ParamValues {
873            layout: Arc::new(self.clone()),
874            values,
875        })
876    }
877
878    /// Generate one value per free parameter in layout order.
879    pub fn free_values_with(&self, mut value: impl FnMut(&Parameter) -> f64) -> Vec<f64> {
880        self.free_parameters().map(&mut value).collect()
881    }
882
883    /// Generate initial free values, invoking `uniform` only for uniform initial ranges.
884    pub fn sample_initial(&self, seed: u64) -> Vec<f64> {
885        let mut rng = Rng::with_seed(seed);
886        self.free_values_with(|parameter| parameter.initial.sample_with(&mut rng))
887    }
888
889    /// Validate free values against ordinary bounds and canonical periodic domains.
890    ///
891    /// # Errors
892    ///
893    /// Returns [`ParamError::FreeLengthMismatch`] when `free` has the wrong
894    /// length, or a value-related [`ParamError`] when a value lies outside its
895    /// parameter's bounds or canonical periodic domain.
896    pub fn validate_free_values(&self, free: &[f64]) -> ParamResult<()> {
897        self.projection.validate_free_dimension(free)?;
898        for (value, parameter) in free.iter().zip(self.free_parameters()) {
899            parameter.validate_value(*value)?;
900        }
901        Ok(())
902    }
903
904    /// Validate free values while classifying structural, numeric, and support failures.
905    ///
906    /// Unlike [`Self::validate_free_values`], this method treats every non-finite
907    /// value as invalid input, including for an otherwise unbounded parameter.
908    ///
909    /// # Errors
910    ///
911    /// Returns [`FreeValueValidationError::Parameter`] for layout-level
912    /// validation failures such as the wrong number of values,
913    /// [`FreeValueValidationError::NonFiniteValue`] for NaN or infinity, and
914    /// [`FreeValueValidationError::OutsideSupport`] for a finite value outside
915    /// ordinary bounds or a canonical periodic domain.
916    pub fn validate_free_values_classified(
917        &self,
918        free: &[f64],
919    ) -> Result<(), FreeValueValidationError> {
920        self.projection.validate_free_dimension(free)?;
921        for (index, (value, parameter)) in free.iter().zip(self.free_parameters()).enumerate() {
922            let id = FreeParamId(index as u32);
923            if !value.is_finite() {
924                return Err(FreeValueValidationError::NonFiniteValue {
925                    id,
926                    name: parameter.name().to_owned(),
927                    value: *value,
928                });
929            }
930            if !parameter.contains_in_support(*value) {
931                return Err(FreeValueValidationError::OutsideSupport {
932                    id,
933                    name: parameter.name().to_owned(),
934                    value: *value,
935                });
936            }
937        }
938        Ok(())
939    }
940
941    /// Return free values with periodic parameters mapped into their canonical domains.
942    /// Non-periodic values are unchanged; ordinary bounds are not clamped.
943    ///
944    /// # Errors
945    ///
946    /// Returns [`ParamError::FreeLengthMismatch`] when `free` does not contain
947    /// exactly one value per free parameter.
948    pub fn wrap_periodic_free_values(&self, free: &[f64]) -> ParamResult<Vec<f64>> {
949        self.projection.validate_free_dimension(free)?;
950        Ok(free
951            .iter()
952            .zip(self.free_params().iter())
953            .map(|(value, id)| {
954                let parameter = &self.specs[id.index()];
955                parameter.periodic_bounds().map_or(*value, |(min, max)| {
956                    min + (*value - min).rem_euclid(max - min)
957                })
958            })
959            .collect())
960    }
961
962    fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
963        self.projection.fill_full_from_free(free, full)
964    }
965
966    fn check_id(&self, id: ParamId) -> ParamResult<()> {
967        if id.index() >= self.len() {
968            Err(ParamError::InvalidParamId {
969                id: id.index(),
970                len: self.len(),
971            })
972        } else {
973            Ok(())
974        }
975    }
976
977    fn check_free_id(&self, id: FreeParamId) -> ParamResult<()> {
978        if id.index() >= self.n_free() {
979            Err(ParamError::InvalidFreeParamId {
980                id: id.index(),
981                len: self.n_free(),
982            })
983        } else {
984            Ok(())
985        }
986    }
987}
988
989/// Incremental collection of uniquely named parameter definitions.
990#[derive(Clone, Debug, Default)]
991pub struct ParamRegistry {
992    specs: Vec<Parameter>,
993    names: HashMap<Arc<str>, ParamId>,
994}
995
996impl ParamRegistry {
997    /// Creates an empty registry.
998    pub fn new() -> Self {
999        Self::default()
1000    }
1001
1002    /// Registers a parameter and returns its stable identifier.
1003    ///
1004    /// Re-registering an identical definition returns its existing identifier;
1005    /// incompatible definitions with the same name return an error.
1006    ///
1007    /// # Errors
1008    ///
1009    /// Returns [`ParamError::EmptyName`] when the parameter name is empty, or
1010    /// [`ParamError::ParameterConflict`] when the name is already associated
1011    /// with a different definition.
1012    pub fn register<S>(&mut self, spec: S) -> ParamResult<ParamId>
1013    where
1014        S: Into<Parameter>,
1015    {
1016        let spec = spec.into();
1017        if spec.name().is_empty() {
1018            return Err(ParamError::EmptyName);
1019        }
1020        if let Some(id) = self.names.get(spec.name()).copied() {
1021            let existing = &self.specs[id.index()];
1022            if existing != &spec {
1023                return Err(ParamError::ParameterConflict {
1024                    name: spec.name().to_owned(),
1025                    reason: "duplicate parameter name has incompatible metadata".into(),
1026                });
1027            }
1028            return Ok(id);
1029        }
1030
1031        let id = ParamId(self.specs.len() as u32);
1032        self.names.insert(Arc::clone(&spec.name), id);
1033        self.specs.push(spec);
1034        Ok(id)
1035    }
1036
1037    /// Validates the registered parameters and builds their layout.
1038    ///
1039    /// # Errors
1040    ///
1041    /// Returns [`ParamError`] when any registered definition has invalid
1042    /// bounds, periodic metadata, scale, or initial or fixed values.
1043    pub fn layout(&self) -> ParamResult<ParamLayout> {
1044        ParamLayout::new(self.specs.clone())
1045    }
1046}
1047
1048/// Concrete full-layout parameter values paired with their defining layout.
1049#[derive(Clone, Debug, Serialize, Deserialize)]
1050pub struct ParamValues {
1051    layout: Arc<ParamLayout>,
1052    values: Vec<f64>,
1053}
1054
1055impl ParamValues {
1056    /// Returns the shared layout that interprets these values.
1057    pub fn layout(&self) -> &Arc<ParamLayout> {
1058        &self.layout
1059    }
1060
1061    /// Returns all values in full-layout order.
1062    pub fn as_slice(&self) -> &[f64] {
1063        &self.values
1064    }
1065
1066    /// Returns the value associated with a full-layout identifier.
1067    ///
1068    /// # Errors
1069    ///
1070    /// Returns [`ParamError::InvalidParamId`] when `id` is outside the shared
1071    /// layout.
1072    pub fn get(&self, id: ParamId) -> ParamResult<f64> {
1073        self.layout.check_id(id)?;
1074        Ok(self.values[id.index()])
1075    }
1076
1077    /// Copies the values of free parameters in free-parameter order.
1078    pub fn free_values(&self) -> Vec<f64> {
1079        self.layout.projection.free_values_from_full(&self.values)
1080    }
1081
1082    /// Assigns one value by its free-parameter identifier.
1083    ///
1084    /// # Errors
1085    ///
1086    /// Returns [`ParamError::InvalidFreeParamId`] when `id` is outside the
1087    /// shared layout's free-parameter ordering.
1088    pub fn set_free(&mut self, id: FreeParamId, value: f64) -> ParamResult<()> {
1089        let full_id = self.layout.free_param(id)?;
1090        self.values[full_id.index()] = value;
1091        Ok(())
1092    }
1093
1094    /// Replaces all free values and restores fixed values from the layout.
1095    ///
1096    /// # Errors
1097    ///
1098    /// Returns [`ParamError::FreeLengthMismatch`] when `values` does not
1099    /// contain exactly one value per free parameter.
1100    pub fn set_free_values(&mut self, values: &[f64]) -> ParamResult<()> {
1101        let layout = Arc::clone(&self.layout);
1102        layout.fill_full_from_free(values, &mut self.values)
1103    }
1104}
1105
1106/// Convenience macro for creating parameters. Usage:
1107/// `parameter!("name")` for a free parameter, or `parameter!("name", 1.0)` for a fixed one.
1108#[macro_export]
1109macro_rules! parameter {
1110    ($name:expr) => {{
1111        $crate::parameters::Parameter::free($name)
1112    }};
1113
1114    ($name:expr, $value:expr) => {{
1115        $crate::parameters::Parameter::fixed($name, $value)
1116    }};
1117
1118    ($name:expr, $($rest:tt)+) => {{
1119        let mut p = $crate::parameters::Parameter::free($name);
1120        $crate::parameter!(@parse p, [fixed = false, initial = false]; $($rest)+);
1121        p
1122    }};
1123
1124    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; ) => {};
1125
1126    (@parse $p:ident, [fixed = false, initial = false]; fixed : $value:expr $(, $($rest:tt)*)?) => {{
1127        $p = $p.with_fixed_value($value);
1128        $crate::parameter!(@parse $p, [fixed = true, initial = false]; $($($rest)*)?);
1129    }};
1130
1131    (@parse $p:ident, [fixed = false, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {{
1132        $p = $p.with_initial($value);
1133        $crate::parameter!(@parse $p, [fixed = false, initial = true]; $($($rest)*)?);
1134    }};
1135
1136    (@parse $p:ident, [fixed = true, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {
1137        compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1138    };
1139
1140    (@parse $p:ident, [fixed = false, initial = true]; fixed : $value:expr $(, $($rest:tt)*)?) => {
1141        compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1142    };
1143
1144    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; bounds : ($min:expr, $max:expr) $(, $($rest:tt)*)?) => {{
1145        $p = $p.with_bounds($min, $max);
1146        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1147    }};
1148
1149    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic : $value:expr $(, $($rest:tt)*)?) => {{
1150        $p = $p.with_periodicity($value);
1151        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1152    }};
1153
1154    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic $(, $($rest:tt)*)?) => {{
1155        $p = $p.with_periodic();
1156        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1157    }};
1158
1159    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; scale : $value:expr $(, $($rest:tt)*)?) => {{
1160        $p = $p.with_scale($value);
1161        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1162    }};
1163
1164    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; unit : $value:expr $(, $($rest:tt)*)?) => {{
1165        $p = $p.with_unit($value);
1166        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1167    }};
1168
1169    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; latex : $value:expr $(, $($rest:tt)*)?) => {{
1170        $p = $p.with_latex($value);
1171        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1172    }};
1173
1174    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; description : $value:expr $(, $($rest:tt)*)?) => {{
1175        $p = $p.with_description($value);
1176        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1177    }};
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182    use super::*;
1183
1184    #[test]
1185    fn parameter_macro_constructs_fixed_parameters() {
1186        let positional = crate::parameter!("positional", 1.25);
1187        let named = crate::parameter!("named", fixed: -0.5);
1188
1189        assert_eq!(positional.state(), &ParamState::Fixed(1.25));
1190        assert_eq!(named.state(), &ParamState::Fixed(-0.5));
1191    }
1192
1193    #[test]
1194    fn parameter_scale_is_validated_and_supported_by_the_macro() {
1195        let scaled = crate::parameter!("scaled", initial: 2.0, scale: 0.25);
1196        let layout = ParamLayout::new([scaled]).unwrap();
1197        assert_eq!(layout.specs()[0].scale(), Some(0.25));
1198
1199        let error = ParamLayout::new([Parameter::free("bad").with_scale(0.0)]).unwrap_err();
1200        assert!(matches!(error, ParamError::InvalidScale { .. }));
1201    }
1202
1203    #[test]
1204    fn layout_tracks_free_and_fixed_values() {
1205        let layout = ParamLayout::new([
1206            Parameter::free("mass")
1207                .with_initial(1.2)
1208                .with_bounds(Some(0.0), Some(2.0)),
1209            Parameter::fixed("pi", std::f64::consts::PI),
1210            Parameter::free("width").with_initial((0.0, 1.0)),
1211        ])
1212        .unwrap();
1213
1214        assert_eq!(layout.len(), 3);
1215        assert_eq!(layout.n_free(), 2);
1216        assert_eq!(layout.initial_free_values(), vec![1.2, 0.5]);
1217        assert_eq!(layout.id("mass").map(ParamId::index), Some(0));
1218        assert_eq!(layout.id("pi").map(ParamId::index), Some(1));
1219        assert_eq!(layout.id("width").map(ParamId::index), Some(2));
1220        assert_eq!(
1221            layout
1222                .free_params()
1223                .iter()
1224                .map(|id| layout.name(*id).unwrap())
1225                .collect::<Vec<_>>(),
1226            vec!["mass", "width"]
1227        );
1228
1229        let values = layout.values(&[1.4, 0.2]).unwrap();
1230        assert_eq!(values.as_slice(), &[1.4, std::f64::consts::PI, 0.2]);
1231        assert_eq!(values.free_values(), vec![1.4, 0.2]);
1232    }
1233
1234    #[test]
1235    fn free_values_can_be_generated_or_sampled_in_layout_order() {
1236        let layout = ParamLayout::new([
1237            Parameter::fixed("fixed", 8.0),
1238            Parameter::free("uniform").with_initial((-2.0, 4.0)),
1239            Parameter::free("value").with_initial(3.0),
1240            Parameter::free("default"),
1241        ])
1242        .unwrap();
1243
1244        assert_eq!(layout.initial_free_values(), vec![1.0, 3.0, 0.0]);
1245        assert_eq!(layout.sample_initial(0), vec![1.6157656431461036, 3.0, 0.0]);
1246        assert_eq!(
1247            layout.free_values_with(|parameter| parameter.name().len() as f64),
1248            vec![7.0, 5.0, 7.0]
1249        );
1250        assert_eq!(
1251            layout
1252                .free_parameters()
1253                .map(Parameter::name)
1254                .collect::<Vec<_>>(),
1255            vec!["uniform", "value", "default"]
1256        );
1257    }
1258
1259    #[test]
1260    fn deterministic_and_sampled_initial_values_share_initial_spec_semantics() {
1261        let layout = ParamLayout::new([
1262            Parameter::free("default"),
1263            Parameter::free("value").with_initial(2.5),
1264            Parameter::free("uniform").with_initial((-4.0, 6.0)),
1265        ])
1266        .unwrap();
1267
1268        assert_eq!(layout.initial_free_values(), vec![0.0, 2.5, 1.0]);
1269        for seed in 0..32 {
1270            let sampled = layout.sample_initial(seed);
1271            assert_eq!(sampled[0], 0.0);
1272            assert_eq!(sampled[1], 2.5);
1273            assert!((-4.0..6.0).contains(&sampled[2]));
1274        }
1275    }
1276
1277    #[test]
1278    fn classified_free_value_validation_separates_failure_kinds() {
1279        let layout = ParamLayout::new([
1280            Parameter::free("bounded").with_bounds(-1.0, 1.0),
1281            Parameter::free("phase")
1282                .with_bounds(0.0, std::f64::consts::TAU)
1283                .with_periodic(),
1284        ])
1285        .unwrap();
1286
1287        assert_eq!(
1288            layout.validate_free_values_classified(&[0.0]),
1289            Err(FreeValueValidationError::Parameter(
1290                ParamError::FreeLengthMismatch {
1291                    expected: 2,
1292                    actual: 1,
1293                }
1294            ))
1295        );
1296        assert!(matches!(
1297            layout.validate_free_values_classified(&[f64::NAN, 0.0]),
1298            Err(FreeValueValidationError::NonFiniteValue { id, name, value })
1299                if id.index() == 0 && name == "bounded" && value.is_nan()
1300        ));
1301        assert_eq!(
1302            layout.validate_free_values_classified(&[2.0, 0.0]),
1303            Err(FreeValueValidationError::OutsideSupport {
1304                id: FreeParamId(0),
1305                name: "bounded".into(),
1306                value: 2.0,
1307            })
1308        );
1309        assert_eq!(
1310            layout.validate_free_values_classified(&[0.0, std::f64::consts::TAU]),
1311            Err(FreeValueValidationError::OutsideSupport {
1312                id: FreeParamId(1),
1313                name: "phase".into(),
1314                value: std::f64::consts::TAU,
1315            })
1316        );
1317        assert!(layout.validate_free_values_classified(&[1.0, 0.0]).is_ok());
1318    }
1319
1320    #[test]
1321    fn periodic_domains_wrap_and_validate_without_changing_bounds() {
1322        let tau = std::f64::consts::TAU;
1323        let phase = Parameter::free("phase")
1324            .with_initial(0.25)
1325            .with_bounds(0.0, tau)
1326            .with_periodic();
1327        assert_eq!(phase.periodic_bounds(), Some((0.0, tau)));
1328
1329        let layout = ParamLayout::new([phase]).unwrap();
1330        assert_eq!(
1331            layout.wrap_periodic_free_values(&[-0.25]).unwrap(),
1332            vec![tau - 0.25]
1333        );
1334        assert!(layout.validate_free_values(&[tau - 0.25]).is_ok());
1335        assert!(matches!(
1336            layout.validate_free_values(&[tau]),
1337            Err(ParamError::ValueOutsidePeriodicDomain { .. })
1338        ));
1339    }
1340
1341    #[test]
1342    fn invalid_periodic_metadata_and_initial_values_are_rejected() {
1343        assert!(matches!(
1344            ParamLayout::new([Parameter::free("phase").with_periodic()]),
1345            Err(ParamError::PeriodicRequiresFiniteBounds { .. })
1346        ));
1347        assert!(matches!(
1348            ParamLayout::new([Parameter::free("phase")
1349                .with_initial(std::f64::consts::TAU)
1350                .with_bounds(0.0, std::f64::consts::TAU)
1351                .with_periodic(),]),
1352            Err(ParamError::ValueOutsidePeriodicDomain { .. })
1353        ));
1354    }
1355
1356    #[test]
1357    fn duplicate_names_are_rejected() {
1358        let err = ParamLayout::new([Parameter::free("x"), Parameter::fixed("x", 1.0)]).unwrap_err();
1359        assert_eq!(err, ParamError::DuplicateName("x".into()));
1360    }
1361
1362    #[test]
1363    fn free_length_is_checked() {
1364        let layout = ParamLayout::new([Parameter::free("x"), Parameter::free("y")]).unwrap();
1365        let err = layout.values(&[1.0]).unwrap_err();
1366        assert_eq!(
1367            err,
1368            ParamError::FreeLengthMismatch {
1369                expected: 2,
1370                actual: 1
1371            }
1372        );
1373    }
1374
1375    #[test]
1376    fn full_and_free_vectors_round_trip_in_stable_order() {
1377        let layout = ParamLayout::new([
1378            Parameter::fixed("offset", -1.0),
1379            Parameter::free("mass").with_initial(1.2),
1380            Parameter::fixed("scale", 2.0),
1381            Parameter::free("width").with_initial(0.1),
1382        ])
1383        .unwrap();
1384
1385        let full = layout.values(&[1.4, 0.2]).unwrap();
1386        assert_eq!(full.as_slice(), &[-1.0, 1.4, 2.0, 0.2]);
1387        let mut rewritten = vec![0.0; layout.len()];
1388        layout
1389            .fill_full_from_free(&[1.5, 0.3], &mut rewritten)
1390            .unwrap();
1391        assert_eq!(rewritten, vec![-1.0, 1.5, 2.0, 0.3]);
1392    }
1393
1394    #[test]
1395    fn values_only_mutate_free_parameters() {
1396        let layout = ParamLayout::new([
1397            Parameter::fixed("fixed", 1.0),
1398            Parameter::free("x"),
1399            Parameter::free("y"),
1400        ])
1401        .unwrap();
1402        let x_id = layout.id("x").unwrap();
1403        let y_id = layout.id("y").unwrap();
1404        let x_free = layout.free_id(x_id).unwrap().unwrap();
1405        let y_free = layout.free_id(y_id).unwrap().unwrap();
1406
1407        let mut values = layout.default_values();
1408        values.set_free(x_free, 3.0).unwrap();
1409        values.set_free(y_free, 4.0).unwrap();
1410
1411        assert_eq!(values.as_slice(), &[1.0, 3.0, 4.0]);
1412        assert_eq!(values.free_values(), vec![3.0, 4.0]);
1413    }
1414
1415    #[test]
1416    fn invalid_specs_are_rejected() {
1417        assert_eq!(
1418            ParamLayout::new([Parameter::free("")]).unwrap_err(),
1419            ParamError::EmptyName
1420        );
1421
1422        assert_eq!(
1423            ParamLayout::new([Parameter::free("x").with_bounds(Some(2.0), Some(1.0))]).unwrap_err(),
1424            ParamError::InvalidBounds {
1425                name: "x".into(),
1426                min: 2.0,
1427                max: 1.0
1428            }
1429        );
1430
1431        assert_eq!(
1432            ParamLayout::new([Parameter::free("x").with_initial((2.0, 1.0))]).unwrap_err(),
1433            ParamError::InvalidInitialRange {
1434                name: "x".into(),
1435                min: 2.0,
1436                max: 1.0
1437            }
1438        );
1439
1440        assert_eq!(
1441            ParamLayout::new([Parameter::free("x")
1442                .with_initial(3.0)
1443                .with_bounds(Some(0.0), Some(2.0))])
1444            .unwrap_err(),
1445            ParamError::InitialOutOfBounds {
1446                name: "x".into(),
1447                value: 3.0
1448            }
1449        );
1450
1451        assert_eq!(
1452            ParamLayout::new([Parameter::free("x")
1453                .with_initial((-1.0, 1.0))
1454                .with_bounds(Some(0.0), Some(2.0))])
1455            .unwrap_err(),
1456            ParamError::InitialRangeOutOfBounds {
1457                name: "x".into(),
1458                min: -1.0,
1459                max: 1.0
1460            }
1461        );
1462
1463        assert_eq!(
1464            ParamLayout::new([Parameter::fixed("x", 3.0).with_bounds(Some(0.0), Some(2.0))])
1465                .unwrap_err(),
1466            ParamError::FixedValueOutOfBounds {
1467                name: "x".into(),
1468                value: 3.0
1469            }
1470        );
1471    }
1472
1473    #[test]
1474    fn direct_and_registry_layouts_report_the_same_invalid_spec_errors() {
1475        let invalid = [
1476            Parameter::fixed("fixed", 2.0).with_bounds(0.0, 1.0),
1477            Parameter::free("default").with_bounds(1.0, 2.0),
1478            Parameter::free("value")
1479                .with_initial(2.0)
1480                .with_bounds(0.0, 1.0),
1481            Parameter::free("range")
1482                .with_initial((-1.0, 0.5))
1483                .with_bounds(0.0, 1.0),
1484            Parameter::free("periodic-value")
1485                .with_initial(std::f64::consts::TAU)
1486                .with_bounds(0.0, std::f64::consts::TAU)
1487                .with_periodic(),
1488        ];
1489
1490        for parameter in invalid {
1491            let direct = ParamLayout::new([parameter.clone()]).unwrap_err();
1492            let mut registry = ParamRegistry::new();
1493            registry.register(parameter).unwrap();
1494            assert_eq!(registry.layout().unwrap_err(), direct);
1495        }
1496    }
1497
1498    #[test]
1499    fn free_vector_lengths_are_checked() {
1500        let layout = ParamLayout::new([
1501            Parameter::fixed("a", 0.0),
1502            Parameter::free("x"),
1503            Parameter::free("y"),
1504        ])
1505        .unwrap();
1506
1507        assert_eq!(
1508            layout
1509                .fill_full_from_free(&[1.0], &mut [0.0, 0.0, 0.0])
1510                .unwrap_err(),
1511            ParamError::FreeLengthMismatch {
1512                expected: 2,
1513                actual: 1
1514            }
1515        );
1516    }
1517
1518    #[test]
1519    fn free_dimension_contract_is_shared_by_projection_operations() {
1520        let layout = ParamLayout::new([
1521            Parameter::fixed("fixed", 4.0),
1522            Parameter::free("x"),
1523            Parameter::free("y"),
1524        ])
1525        .unwrap();
1526        let expected = ParamError::FreeLengthMismatch {
1527            expected: 2,
1528            actual: 1,
1529        };
1530
1531        assert_eq!(layout.values(&[1.0]).unwrap_err(), expected);
1532        assert_eq!(layout.validate_free_values(&[1.0]).unwrap_err(), expected);
1533        assert_eq!(
1534            layout.wrap_periodic_free_values(&[1.0]).unwrap_err(),
1535            expected
1536        );
1537
1538        let mut values = layout.default_values();
1539        assert_eq!(values.set_free_values(&[1.0]).unwrap_err(), expected);
1540        assert_eq!(values.as_slice(), &[4.0, 0.0, 0.0]);
1541        assert_eq!(
1542            layout.validate_free_values_classified(&[1.0]),
1543            Err(FreeValueValidationError::Parameter(
1544                ParamError::FreeLengthMismatch {
1545                    expected: 2,
1546                    actual: 1,
1547                }
1548            ))
1549        );
1550    }
1551
1552    #[test]
1553    fn cross_layout_projection_reorders_values_and_scatters_gradients() {
1554        let source = ParamLayout::new([
1555            Parameter::fixed("offset", -1.0),
1556            Parameter::free("x"),
1557            Parameter::free("y"),
1558        ])
1559        .unwrap();
1560        let target = ParamLayout::new([
1561            Parameter::free("y"),
1562            Parameter::fixed("scale", 2.0),
1563            Parameter::free("x"),
1564        ])
1565        .unwrap();
1566        let projection = target.projection_from(&source).unwrap();
1567        let source_values = source.values(&[1.0, 2.0]).unwrap();
1568
1569        assert_eq!(
1570            projection.project(&source_values).unwrap().as_slice(),
1571            &[2.0, 2.0, 1.0]
1572        );
1573
1574        let mut source_gradient = vec![10.0, 20.0];
1575        projection
1576            .scatter_add(&[0.5, 1.5], &mut source_gradient)
1577            .unwrap();
1578        assert_eq!(source_gradient, vec![11.5, 20.5]);
1579    }
1580
1581    #[test]
1582    fn cross_layout_projection_rejects_missing_and_fixed_source_parameters() {
1583        let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1584        let missing = ParamLayout::new([Parameter::free("y")]).unwrap();
1585        assert_eq!(
1586            target.projection_from(&missing).unwrap_err(),
1587            ParamError::UnknownName("x".into())
1588        );
1589
1590        let fixed = ParamLayout::new([Parameter::fixed("x", 1.0)]).unwrap();
1591        assert!(matches!(
1592            target.projection_from(&fixed),
1593            Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1594        ));
1595    }
1596
1597    #[test]
1598    fn cross_layout_projection_supports_zero_free_layouts() {
1599        let source = ParamLayout::new([Parameter::fixed("source", 3.0)]).unwrap();
1600        let target = ParamLayout::new([Parameter::fixed("target", 4.0)]).unwrap();
1601        let projection = target.projection_from(&source).unwrap();
1602        let values = source.default_values();
1603
1604        assert_eq!(projection.project(&values).unwrap().as_slice(), &[4.0]);
1605        let mut gradient = Vec::new();
1606        projection.scatter_add(&[], &mut gradient).unwrap();
1607    }
1608
1609    #[test]
1610    fn cross_layout_projection_accepts_reordered_compatible_source_layouts() {
1611        let source = ParamLayout::new([Parameter::free("x")]).unwrap();
1612        let other =
1613            ParamLayout::new([Parameter::fixed("extra", 9.0), Parameter::fixed("x", 3.0)]).unwrap();
1614        let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1615        let projection = target.projection_from(&source).unwrap();
1616        let values = other.default_values();
1617
1618        assert_eq!(projection.project(&values).unwrap().as_slice(), &[3.0]);
1619        assert_eq!(values.as_slice(), &[9.0, 3.0]);
1620    }
1621
1622    #[test]
1623    fn cross_layout_projection_rejects_missing_alternate_source_name() {
1624        let source = ParamLayout::new([Parameter::free("x")]).unwrap();
1625        let other = ParamLayout::new([Parameter::free("y")]).unwrap();
1626        let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1627        let projection = target.projection_from(&source).unwrap();
1628        let values = other.values(&[7.0]).unwrap();
1629
1630        assert_eq!(
1631            projection.project(&values).unwrap_err(),
1632            ParamError::UnknownName("x".into())
1633        );
1634        assert_eq!(values.as_slice(), &[7.0]);
1635    }
1636
1637    #[test]
1638    fn registry_merges_identical_parameters_in_first_seen_order() {
1639        let mut registry = ParamRegistry::new();
1640        let y = registry
1641            .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1642            .unwrap();
1643        let x = registry.register(Parameter::free("x")).unwrap();
1644        let y_again = registry
1645            .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1646            .unwrap();
1647
1648        assert_eq!(y.index(), 0);
1649        assert_eq!(x.index(), 1);
1650        assert_eq!(y_again, y);
1651
1652        let layout = registry.layout().unwrap();
1653        assert_eq!(
1654            layout
1655                .specs()
1656                .iter()
1657                .map(Parameter::name)
1658                .collect::<Vec<_>>(),
1659            vec!["y", "x"]
1660        );
1661    }
1662
1663    #[test]
1664    fn registry_rejects_incompatible_parameter_reuse() {
1665        let mut registry = ParamRegistry::new();
1666        registry
1667            .register(Parameter::free("x").with_initial(1.0))
1668            .unwrap();
1669
1670        assert!(matches!(
1671            registry.register(Parameter::free("x").with_initial(2.0)),
1672            Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1673        ));
1674    }
1675}