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/// A partial, atomic update to a parameter definition.
58///
59/// An outer `None` preserves the current field. For metadata fields and the
60/// scale, `Some(None)` clears the current value.
61///
62/// ```
63/// use laddu_expr::{Expr, parameters::{ParamState, ParameterUpdate}};
64///
65/// let graph = Expr::from(laddu_expr::parameter!("mass")).to_graph();
66/// let update = ParameterUpdate {
67///     state: Some(ParamState::Fixed(1.0)),
68///     ..Default::default()
69/// };
70/// let updated = graph.with_parameters([("mass", update)])?;
71/// # let _ = updated;
72/// # Ok::<(), laddu_expr::ParamError>(())
73/// ```
74#[derive(Clone, Debug, Default, PartialEq)]
75pub struct ParameterUpdate {
76    /// Replacement state.
77    pub state: Option<ParamState>,
78    /// Replacement initialization rule.
79    pub initial: Option<InitialSpec>,
80    /// Replacement bounds; [`Bounds::default`] removes all bounds.
81    pub bounds: Option<Bounds>,
82    /// Replacement periodicity flag.
83    pub periodic: Option<bool>,
84    /// Replacement optimizer scale, or `Some(None)` to clear it.
85    pub scale: Option<Option<f64>>,
86    /// Replacement unit label, or `Some(None)` to clear it.
87    pub unit: Option<Option<String>>,
88    /// Replacement LaTeX label, or `Some(None)` to clear it.
89    pub latex: Option<Option<String>>,
90    /// Replacement description, or `Some(None)` to clear it.
91    pub description: Option<Option<String>>,
92}
93
94impl ParameterUpdate {
95    /// Validates values intrinsic to this patch.
96    ///
97    /// Checks that fixed and scalar initial values are finite, uniform initial
98    /// ranges are finite and ordered, and replacement scales are finite and
99    /// positive. Relationships between fields are validated after applying
100    /// the update to a complete [`Parameter`].
101    ///
102    /// # Errors
103    ///
104    /// Returns a field-specific [`ParamError`] when a fixed value, initial
105    /// value or range, bounds, or scale is invalid.
106    pub fn validate(&self) -> ParamResult<()> {
107        if let Some(ParamState::Fixed(value)) = &self.state
108            && !value.is_finite()
109        {
110            return Err(ParamError::InvalidUpdateFixedValue { value: *value });
111        }
112        if let Some(initial) = &self.initial {
113            match initial {
114                InitialSpec::Default => {}
115                InitialSpec::Value(value) if value.is_finite() => {}
116                InitialSpec::Value(value) => {
117                    return Err(ParamError::InvalidUpdateInitialValue { value: *value });
118                }
119                InitialSpec::Uniform { min, max }
120                    if min.is_finite() && max.is_finite() && min <= max => {}
121                InitialSpec::Uniform { min, max } => {
122                    return Err(ParamError::InvalidUpdateInitialRange {
123                        min: *min,
124                        max: *max,
125                    });
126                }
127            }
128        }
129        if let Some(bounds) = &self.bounds {
130            let has_nan =
131                bounds.min.is_some_and(f64::is_nan) || bounds.max.is_some_and(f64::is_nan);
132            let unordered = matches!((bounds.min, bounds.max), (Some(min), Some(max)) if min > max);
133            if has_nan || unordered {
134                return Err(ParamError::InvalidUpdateBounds {
135                    min: bounds.min,
136                    max: bounds.max,
137                });
138            }
139        }
140        if let Some(Some(scale)) = self.scale
141            && (!scale.is_finite() || scale <= 0.0)
142        {
143            return Err(ParamError::InvalidUpdateScale { scale });
144        }
145        Ok(())
146    }
147}
148
149/// Classified failure from validating a free-parameter value vector.
150///
151/// This separates structural input errors from invalid numeric input and
152/// values that are finite but outside the parameter support.
153#[derive(Clone, Debug, Error, PartialEq)]
154pub enum FreeValueValidationError {
155    /// Parameter-layout validation failed before individual values were classified.
156    #[error(transparent)]
157    Parameter(#[from] ParamError),
158    /// A free parameter was assigned a non-finite value.
159    #[error("non-finite value {value} for free parameter {name} ({id:?})")]
160    NonFiniteValue {
161        /// Identifier in free-parameter order.
162        id: FreeParamId,
163        /// Parameter name.
164        name: String,
165        /// Invalid value.
166        value: f64,
167    },
168    /// A finite free-parameter value was outside its declared support.
169    #[error("value {value} for free parameter {name} ({id:?}) is outside its support")]
170    OutsideSupport {
171        /// Identifier in free-parameter order.
172        id: FreeParamId,
173        /// Parameter name.
174        name: String,
175        /// Unsupported value.
176        value: f64,
177    },
178}
179
180/// Optional inclusive lower and upper bounds for a parameter.
181#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
182pub struct Bounds {
183    /// Inclusive lower bound, or no lower bound.
184    pub min: Option<f64>,
185    /// Inclusive upper bound, or no upper bound.
186    pub max: Option<f64>,
187}
188
189impl Bounds {
190    /// Creates bounds from optional lower and upper endpoints.
191    pub fn new(min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) -> Self {
192        Self {
193            min: min.into(),
194            max: max.into(),
195        }
196    }
197
198    fn validate(&self, name: &str) -> ParamResult<()> {
199        if let Some(value) = self.min.filter(|value| value.is_nan()) {
200            return Err(ParamError::InvalidBoundValue {
201                name: name.to_owned(),
202                value,
203            });
204        }
205        if let Some(value) = self.max.filter(|value| value.is_nan()) {
206            return Err(ParamError::InvalidBoundValue {
207                name: name.to_owned(),
208                value,
209            });
210        }
211        if let (Some(min), Some(max)) = (self.min, self.max)
212            && min > max
213        {
214            return Err(ParamError::InvalidBounds {
215                name: name.to_owned(),
216                min,
217                max,
218            });
219        }
220        Ok(())
221    }
222
223    /// Returns whether `value` lies within both configured bounds.
224    pub fn contains(&self, value: f64) -> bool {
225        self.min.is_none_or(|min| value >= min) && self.max.is_none_or(|max| value <= max)
226    }
227}
228
229/// Complete definition and user-facing metadata for one scalar parameter.
230#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
231pub struct Parameter {
232    name: Arc<str>,
233    state: ParamState,
234    initial: InitialSpec,
235    bounds: Bounds,
236    #[serde(default)]
237    periodic: bool,
238    #[serde(default)]
239    scale: Option<f64>,
240    unit: Option<Arc<str>>,
241    latex: Option<Arc<str>>,
242    description: Option<Arc<str>>,
243}
244
245impl Parameter {
246    /// Creates an unbounded free parameter with a default initial value.
247    pub fn free(name: impl Into<Arc<str>>) -> Self {
248        Self {
249            name: name.into(),
250            state: ParamState::Free,
251            initial: InitialSpec::Default,
252            bounds: Bounds::default(),
253            periodic: false,
254            scale: None,
255            unit: None,
256            latex: None,
257            description: None,
258        }
259    }
260
261    /// Creates an unbounded parameter fixed at `value`.
262    pub fn fixed(name: impl Into<Arc<str>>, value: f64) -> Self {
263        Self {
264            name: name.into(),
265            state: ParamState::Fixed(value),
266            initial: InitialSpec::Value(value),
267            bounds: Bounds::default(),
268            periodic: false,
269            scale: None,
270            unit: None,
271            latex: None,
272            description: None,
273        }
274    }
275
276    fn set_fixed_value(&mut self, value: f64) {
277        self.state = ParamState::Fixed(value);
278        self.initial = InitialSpec::Value(value);
279    }
280
281    /// Returns this parameter fixed at `value`.
282    pub fn with_fixed_value(mut self, value: f64) -> Self {
283        self.set_fixed_value(value);
284        self
285    }
286
287    /// Returns this parameter after applying a validated partial update.
288    ///
289    /// A fixed state also updates the initial value to that fixed value unless
290    /// the patch explicitly supplies an initial rule. All fields are applied
291    /// before the complete parameter is validated, so a patch can replace a
292    /// bound and initial value together.
293    ///
294    /// # Errors
295    ///
296    /// Returns [`ParamError`] when the patch or resulting parameter definition
297    /// contains invalid numeric values, bounds, periodic metadata, or support.
298    pub fn with_update(&self, update: &ParameterUpdate) -> ParamResult<Self> {
299        update.validate()?;
300        let mut parameter = self.clone();
301        if let Some(state) = &update.state {
302            parameter.state = state.clone();
303            if let ParamState::Fixed(value) = state
304                && update.initial.is_none()
305            {
306                parameter.initial = InitialSpec::Value(*value);
307            }
308        }
309        if let Some(initial) = &update.initial {
310            parameter.initial = initial.clone();
311        }
312        if let Some(bounds) = &update.bounds {
313            parameter.bounds = bounds.clone();
314        }
315        if let Some(periodic) = update.periodic {
316            parameter.periodic = periodic;
317        }
318        if let Some(scale) = update.scale {
319            parameter.scale = scale;
320        }
321        if let Some(unit) = &update.unit {
322            parameter.unit = unit.as_deref().map(Arc::<str>::from);
323        }
324        if let Some(latex) = &update.latex {
325            parameter.latex = latex.as_deref().map(Arc::<str>::from);
326        }
327        if let Some(description) = &update.description {
328            parameter.description = description.as_deref().map(Arc::<str>::from);
329        }
330        parameter.validate()?;
331        if parameter.is_fixed() {
332            parameter.validate_free_initial()?;
333        }
334        Ok(parameter)
335    }
336
337    fn set_initial(&mut self, initial: impl Into<InitialSpec>) {
338        self.initial = initial.into();
339    }
340
341    /// Returns this parameter with the specified initialization rule.
342    pub fn with_initial(mut self, initial: impl Into<InitialSpec>) -> Self {
343        self.set_initial(initial);
344        self
345    }
346
347    fn set_bounds(&mut self, min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) {
348        self.bounds = Bounds::new(min, max);
349    }
350
351    /// Returns this parameter with inclusive optional bounds.
352    pub fn with_bounds(mut self, min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) -> Self {
353        self.set_bounds(min, max);
354        self
355    }
356
357    /// Mark this parameter as periodic over its finite two-sided bounds.
358    pub fn with_periodic(mut self) -> Self {
359        self.periodic = true;
360        self
361    }
362
363    /// Sets whether the parameter is periodic.
364    pub fn with_periodicity(mut self, periodic: bool) -> Self {
365        self.periodic = periodic;
366        self
367    }
368
369    /// Set the characteristic optimizer scale for this parameter.
370    ///
371    /// The scale is metadata: fit integrations may use it to condition the
372    /// optimizer coordinate system, while direct model evaluation is unchanged.
373    pub fn with_scale(mut self, scale: f64) -> Self {
374        self.scale = Some(scale);
375        self
376    }
377
378    fn set_unit(&mut self, unit: impl Into<Arc<str>>) {
379        self.unit = Some(unit.into());
380    }
381
382    /// Attaches a human-readable unit label.
383    pub fn with_unit(mut self, unit: impl Into<Arc<str>>) -> Self {
384        self.set_unit(unit);
385        self
386    }
387
388    fn set_latex(&mut self, latex: impl Into<Arc<str>>) {
389        self.latex = Some(latex.into());
390    }
391
392    /// Attaches a LaTeX-formatted label.
393    pub fn with_latex(mut self, latex: impl Into<Arc<str>>) -> Self {
394        self.set_latex(latex);
395        self
396    }
397
398    fn set_description(&mut self, description: impl Into<Arc<str>>) {
399        self.description = Some(description.into());
400    }
401
402    /// Attaches a longer human-readable description.
403    pub fn with_description(mut self, description: impl Into<Arc<str>>) -> Self {
404        self.set_description(description);
405        self
406    }
407
408    /// Returns the unique parameter name.
409    pub fn name(&self) -> &str {
410        &self.name
411    }
412
413    /// Returns whether the parameter is free or fixed.
414    pub fn state(&self) -> &ParamState {
415        &self.state
416    }
417
418    /// Returns whether the parameter is free.
419    pub fn is_free(&self) -> bool {
420        matches!(self.state, ParamState::Free)
421    }
422
423    /// Returns whether the parameter is fixed.
424    pub fn is_fixed(&self) -> bool {
425        matches!(self.state, ParamState::Fixed(_))
426    }
427
428    /// Returns the initialization rule.
429    pub fn initial_spec(&self) -> &InitialSpec {
430        &self.initial
431    }
432
433    /// Returns the optional parameter bounds.
434    pub fn bounds_spec(&self) -> &Bounds {
435        &self.bounds
436    }
437
438    /// Returns whether the parameter is periodic over its bounds.
439    pub fn is_periodic(&self) -> bool {
440        self.periodic
441    }
442
443    /// Return the validated canonical half-open periodic interval.
444    pub fn periodic_bounds(&self) -> Option<(f64, f64)> {
445        match (self.periodic, self.bounds.min, self.bounds.max) {
446            (true, Some(min), Some(max)) if min.is_finite() && max.is_finite() && min < max => {
447                Some((min, max))
448            }
449            _ => None,
450        }
451    }
452
453    /// Returns the optional characteristic optimizer scale.
454    pub fn scale(&self) -> Option<f64> {
455        self.scale
456    }
457
458    /// Returns the optional human-readable unit label.
459    pub fn unit_label(&self) -> Option<&str> {
460        self.unit.as_deref()
461    }
462
463    /// Returns the optional LaTeX-formatted label.
464    pub fn latex_label(&self) -> Option<&str> {
465        self.latex.as_deref()
466    }
467
468    /// Returns the optional longer description.
469    pub fn description_text(&self) -> Option<&str> {
470        self.description.as_deref()
471    }
472
473    fn validate(&self) -> ParamResult<()> {
474        if self.name().is_empty() {
475            return Err(ParamError::EmptyName);
476        }
477        self.bounds.validate(self.name())?;
478        if self.periodic && self.periodic_bounds().is_none() {
479            return Err(ParamError::PeriodicRequiresFiniteBounds {
480                name: self.name().to_owned(),
481            });
482        }
483        if let Some(scale) = self.scale
484            && (!scale.is_finite() || scale <= 0.0)
485        {
486            return Err(ParamError::InvalidScale {
487                name: self.name().to_owned(),
488                scale,
489            });
490        }
491        self.validate_initial()
492    }
493
494    fn validate_initial(&self) -> ParamResult<()> {
495        match self.state {
496            ParamState::Fixed(value) => {
497                if !value.is_finite() {
498                    return Err(ParamError::NonFiniteFixedValue {
499                        name: self.name().to_owned(),
500                        value,
501                    });
502                }
503                if !self.bounds.contains(value) {
504                    return Err(ParamError::FixedValueOutOfBounds {
505                        name: self.name().to_owned(),
506                        value,
507                    });
508                }
509                self.validate_periodic_value(value)
510            }
511            ParamState::Free => self.validate_free_initial(),
512        }
513    }
514
515    fn validate_free_initial(&self) -> ParamResult<()> {
516        match self.initial {
517            InitialSpec::Default => {
518                let value = self.initial.representative_value();
519                if !self.bounds.contains(value) {
520                    return Err(ParamError::InitialOutOfBounds {
521                        name: self.name().to_owned(),
522                        value,
523                    });
524                }
525                self.validate_periodic_value(value)
526            }
527            InitialSpec::Value(value) => {
528                if !value.is_finite() {
529                    return Err(ParamError::NonFiniteInitialValue {
530                        name: self.name().to_owned(),
531                        value,
532                    });
533                }
534                if !self.bounds.contains(value) {
535                    return Err(ParamError::InitialOutOfBounds {
536                        name: self.name().to_owned(),
537                        value,
538                    });
539                }
540                self.validate_periodic_value(value)
541            }
542            InitialSpec::Uniform { min, max } => {
543                if !min.is_finite() || !max.is_finite() {
544                    return Err(ParamError::NonFiniteInitialRange {
545                        name: self.name().to_owned(),
546                        min,
547                        max,
548                    });
549                }
550                if min > max {
551                    return Err(ParamError::InvalidInitialRange {
552                        name: self.name().to_owned(),
553                        min,
554                        max,
555                    });
556                }
557                if !self.bounds.contains(min) || !self.bounds.contains(max) {
558                    return Err(ParamError::InitialRangeOutOfBounds {
559                        name: self.name().to_owned(),
560                        min,
561                        max,
562                    });
563                }
564                if let Some((domain_min, domain_max)) = self.periodic_bounds()
565                    && (min < domain_min || max > domain_max)
566                {
567                    let value = if min < domain_min { min } else { max };
568                    return Err(ParamError::ValueOutsidePeriodicDomain {
569                        name: self.name().to_owned(),
570                        value,
571                        min: domain_min,
572                        max: domain_max,
573                    });
574                }
575                Ok(())
576            }
577        }
578    }
579
580    fn default_value(&self) -> f64 {
581        match self.state {
582            ParamState::Fixed(value) => value,
583            ParamState::Free => self.initial.representative_value(),
584        }
585    }
586
587    fn validate_periodic_value(&self, value: f64) -> ParamResult<()> {
588        if let Some((min, max)) = self.periodic_bounds()
589            && !(value.is_finite() && value >= min && value < max)
590        {
591            return Err(ParamError::ValueOutsidePeriodicDomain {
592                name: self.name().to_owned(),
593                value,
594                min,
595                max,
596            });
597        }
598        Ok(())
599    }
600
601    fn validate_value(&self, value: f64) -> ParamResult<()> {
602        if !self.bounds.contains(value) {
603            return Err(ParamError::ValueOutOfBounds {
604                name: self.name().to_owned(),
605                value,
606            });
607        }
608        self.validate_periodic_value(value)
609    }
610
611    fn contains_in_support(&self, value: f64) -> bool {
612        self.bounds.contains(value)
613            && self
614                .periodic_bounds()
615                .is_none_or(|(min, max)| value >= min && value < max)
616    }
617}
618
619impl InitialSpec {
620    fn representative_value(&self) -> f64 {
621        match *self {
622            Self::Default => 0.0,
623            Self::Value(value) => value,
624            Self::Uniform { min, max } => 0.5 * (min + max),
625        }
626    }
627
628    fn sample_with(&self, rng: &mut Rng) -> f64 {
629        match *self {
630            Self::Default => 0.0,
631            Self::Value(value) => value,
632            Self::Uniform { min, max } => rng.f64_range(min..max),
633        }
634    }
635}
636
637impl From<f64> for InitialSpec {
638    fn from(value: f64) -> Self {
639        Self::Value(value)
640    }
641}
642
643impl From<(f64, f64)> for InitialSpec {
644    fn from((min, max): (f64, f64)) -> Self {
645        Self::Uniform { min, max }
646    }
647}
648
649/// Validated parameter ordering and mapping between full and free values.
650#[derive(Clone)]
651pub struct ParamLayout {
652    specs: Arc<[Parameter]>,
653    names: Arc<HashMap<Arc<str>, ParamId>>,
654    projection: ParamLayoutProjection,
655}
656
657impl fmt::Debug for ParamLayout {
658    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
659        formatter
660            .debug_struct("ParamLayout")
661            .field("specs", &self.specs)
662            .field("names", &self.names)
663            .field("free_params", &self.projection.free_params)
664            .field("full_to_free", &self.projection.full_to_free)
665            .field("defaults", &self.projection.defaults)
666            .finish()
667    }
668}
669
670#[derive(Clone, Debug)]
671// Owns the stable mappings and defaults that define full/free projection.
672struct ParamLayoutProjection {
673    free_params: Arc<[ParamId]>,
674    full_to_free: Arc<[Option<FreeParamId>]>,
675    defaults: Arc<[f64]>,
676}
677
678impl ParamLayoutProjection {
679    fn n_free(&self) -> usize {
680        self.free_params.len()
681    }
682
683    fn free_params(&self) -> &[ParamId] {
684        &self.free_params
685    }
686
687    fn free_id(&self, id: ParamId) -> Option<FreeParamId> {
688        self.full_to_free[id.index()]
689    }
690
691    fn full_id(&self, id: FreeParamId) -> ParamId {
692        self.free_params[id.index()]
693    }
694
695    fn validate_free_dimension<T>(&self, values: &[T]) -> ParamResult<()> {
696        if values.len() == self.n_free() {
697            Ok(())
698        } else {
699            Err(ParamError::FreeLengthMismatch {
700                expected: self.n_free(),
701                actual: values.len(),
702            })
703        }
704    }
705
706    fn initial_free_values(&self) -> Vec<f64> {
707        self.free_params
708            .iter()
709            .map(|id| self.defaults[id.index()])
710            .collect()
711    }
712
713    fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
714        self.validate_free_dimension(free)?;
715        debug_assert_eq!(full.len(), self.defaults.len());
716        full.copy_from_slice(&self.defaults);
717        for (value, id) in free.iter().zip(self.free_params.iter()) {
718            full[id.index()] = *value;
719        }
720        Ok(())
721    }
722
723    fn free_values_from_full(&self, full: &[f64]) -> Vec<f64> {
724        debug_assert_eq!(full.len(), self.defaults.len());
725        self.free_params.iter().map(|id| full[id.index()]).collect()
726    }
727}
728
729/// Validated projection from one parameter layout into another.
730///
731/// The target layout owns the values produced by [`Self::project`], while the
732/// source layout owns the values accepted by it.  Only free parameters are
733/// projected: fixed target parameters retain their configured values, and a
734/// target free parameter must also be free in the source layout.
735#[derive(Clone, Debug)]
736pub struct ParamProjection {
737    source: Arc<ParamLayout>,
738    target: Arc<ParamLayout>,
739    source_free_ids: Arc<[FreeParamId]>,
740}
741
742#[derive(Serialize, Deserialize)]
743// Keep ParamLayout's established flat serialized representation while its
744// projection fields are grouped behind one internal artifact.
745struct ParamLayoutSerde {
746    specs: Arc<[Parameter]>,
747    names: Arc<HashMap<Arc<str>, ParamId>>,
748    free_params: Arc<[ParamId]>,
749    full_to_free: Arc<[Option<FreeParamId>]>,
750    defaults: Arc<[f64]>,
751}
752
753impl Serialize for ParamLayout {
754    fn serialize<__S>(&self, serializer: __S) -> Result<__S::Ok, __S::Error>
755    where
756        __S: Serializer,
757    {
758        ParamLayoutSerde {
759            specs: Arc::clone(&self.specs),
760            names: Arc::clone(&self.names),
761            free_params: Arc::clone(&self.projection.free_params),
762            full_to_free: Arc::clone(&self.projection.full_to_free),
763            defaults: Arc::clone(&self.projection.defaults),
764        }
765        .serialize(serializer)
766    }
767}
768
769impl<'de> Deserialize<'de> for ParamLayout {
770    fn deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
771    where
772        __D: Deserializer<'de>,
773    {
774        let serialized = ParamLayoutSerde::deserialize(deserializer)?;
775        Ok(Self {
776            specs: serialized.specs,
777            names: serialized.names,
778            projection: ParamLayoutProjection {
779                free_params: serialized.free_params,
780                full_to_free: serialized.full_to_free,
781                defaults: serialized.defaults,
782            },
783        })
784    }
785}
786
787impl ParamProjection {
788    /// Projects values from the source layout into the target layout.
789    ///
790    /// # Errors
791    ///
792    /// Returns [`ParamError::UnknownName`] when the supplied values do not
793    /// contain a required target parameter.
794    pub fn project(&self, source: &ParamValues) -> ParamResult<ParamValues> {
795        let free = if source.layout().specs() == self.source.specs() {
796            self.source_free_ids
797                .iter()
798                .map(|id| source.get(self.source.projection.full_id(*id)))
799                .collect::<ParamResult<Vec<_>>>()?
800        } else {
801            self.target
802                .free_params()
803                .iter()
804                .map(|target_id| {
805                    let name = self.target.name(*target_id)?;
806                    let source_id = source
807                        .layout()
808                        .id(name)
809                        .ok_or_else(|| ParamError::UnknownName(name.to_owned()))?;
810                    source.get(source_id)
811                })
812                .collect::<ParamResult<Vec<_>>>()?
813        };
814        self.target.values(&free)
815    }
816
817    /// Scatters a target-layout free gradient into a source-layout gradient.
818    ///
819    /// Values are added to `source`, allowing several projections to
820    /// contribute to one gradient.  Both dimensions are validated against the
821    /// layouts captured by this artifact.
822    ///
823    /// # Errors
824    ///
825    /// Returns [`ParamError::FreeLengthMismatch`] when either slice has an
826    /// incompatible free-parameter dimension.
827    pub fn scatter_add(&self, target: &[f64], source: &mut [f64]) -> ParamResult<()> {
828        self.target.projection.validate_free_dimension(target)?;
829        self.source.projection.validate_free_dimension(source)?;
830        for (value, id) in target.iter().zip(self.source_free_ids.iter()) {
831            source[id.index()] += value;
832        }
833        Ok(())
834    }
835}
836
837struct LayoutBuilder {
838    specs: Vec<Parameter>,
839    names: HashMap<Arc<str>, ParamId>,
840    free_params: Vec<ParamId>,
841    full_to_free: Vec<Option<FreeParamId>>,
842    defaults: Vec<f64>,
843}
844
845impl LayoutBuilder {
846    fn with_capacity(capacity: usize) -> Self {
847        Self {
848            specs: Vec::with_capacity(capacity),
849            names: HashMap::with_capacity(capacity),
850            free_params: Vec::new(),
851            full_to_free: Vec::with_capacity(capacity),
852            defaults: Vec::with_capacity(capacity),
853        }
854    }
855
856    fn push_validated(&mut self, spec: Parameter) -> ParamResult<()> {
857        spec.validate()?;
858        let id = ParamId(self.specs.len() as u32);
859        if self.names.insert(Arc::clone(&spec.name), id).is_some() {
860            return Err(ParamError::DuplicateName(spec.name().to_owned()));
861        }
862        self.defaults.push(spec.default_value());
863        match spec.state {
864            ParamState::Free => {
865                let free_id = FreeParamId(self.free_params.len() as u32);
866                self.free_params.push(id);
867                self.full_to_free.push(Some(free_id));
868            }
869            ParamState::Fixed(_) => self.full_to_free.push(None),
870        }
871        self.specs.push(spec);
872        Ok(())
873    }
874
875    fn finish(self) -> ParamLayout {
876        ParamLayout {
877            specs: self.specs.into(),
878            names: Arc::new(self.names),
879            projection: ParamLayoutProjection {
880                free_params: self.free_params.into(),
881                full_to_free: self.full_to_free.into(),
882                defaults: self.defaults.into(),
883            },
884        }
885    }
886}
887
888impl ParamLayout {
889    /// Validates parameter definitions and constructs a layout.
890    ///
891    /// # Errors
892    ///
893    /// Returns [`ParamError`] when a definition has an empty or duplicate
894    /// name, invalid bounds, invalid periodic metadata, an invalid scale, or
895    /// an initial or fixed value outside its permitted domain.
896    pub fn new<S>(specs: impl IntoIterator<Item = S>) -> ParamResult<Self>
897    where
898        S: Into<Parameter>,
899    {
900        let specs: Vec<_> = specs.into_iter().map(Into::into).collect();
901        let mut builder = LayoutBuilder::with_capacity(specs.len());
902        for spec in specs {
903            builder.push_validated(spec)?;
904        }
905        Ok(builder.finish())
906    }
907
908    /// Returns all parameter definitions in full-layout order.
909    pub fn specs(&self) -> &[Parameter] {
910        &self.specs
911    }
912
913    /// Returns the total number of free and fixed parameters.
914    pub fn len(&self) -> usize {
915        self.specs.len()
916    }
917
918    /// Returns whether the layout contains no parameters.
919    pub fn is_empty(&self) -> bool {
920        self.specs.is_empty()
921    }
922
923    /// Returns the number of free parameters.
924    pub fn n_free(&self) -> usize {
925        self.projection.n_free()
926    }
927
928    /// Looks up a full-layout identifier by parameter name.
929    pub fn id(&self, name: &str) -> Option<ParamId> {
930        self.names.get(name).copied()
931    }
932
933    /// Returns the name associated with a full-layout identifier.
934    ///
935    /// # Errors
936    ///
937    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
938    /// layout.
939    pub fn name(&self, id: ParamId) -> ParamResult<&str> {
940        self.check_id(id)?;
941        Ok(self.specs[id.index()].name())
942    }
943
944    /// Returns the definition associated with a full-layout identifier.
945    ///
946    /// # Errors
947    ///
948    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
949    /// layout.
950    pub fn spec(&self, id: ParamId) -> ParamResult<&Parameter> {
951        self.check_id(id)?;
952        Ok(&self.specs[id.index()])
953    }
954
955    /// Maps a full-layout identifier to free-parameter order.
956    ///
957    /// Fixed parameters return `None`.
958    ///
959    /// # Errors
960    ///
961    /// Returns [`ParamError::InvalidParamId`] when `id` is outside this
962    /// layout.
963    pub fn free_id(&self, id: ParamId) -> ParamResult<Option<FreeParamId>> {
964        self.check_id(id)?;
965        Ok(self.projection.free_id(id))
966    }
967
968    fn free_param(&self, id: FreeParamId) -> ParamResult<ParamId> {
969        self.check_free_id(id)?;
970        Ok(self.projection.full_id(id))
971    }
972
973    /// Returns full-layout identifiers in free-parameter order.
974    pub fn free_params(&self) -> &[ParamId] {
975        self.projection.free_params()
976    }
977
978    /// Builds a validated projection from `source` into this layout.
979    ///
980    /// Free parameters are matched by name and must be free in both layouts.
981    /// Parameters present only in the source are permitted; fixed parameters
982    /// in this layout use their own configured values.
983    ///
984    /// # Errors
985    ///
986    /// Returns [`ParamError::UnknownName`] when a free target parameter is
987    /// absent from `source`, or [`ParamError::ParameterConflict`] when it is
988    /// fixed there.
989    pub fn projection_from(&self, source: &ParamLayout) -> ParamResult<ParamProjection> {
990        let source_free_ids = self
991            .free_params()
992            .iter()
993            .map(|target_id| {
994                let name = self.name(*target_id)?;
995                let source_id = source
996                    .id(name)
997                    .ok_or_else(|| ParamError::UnknownName(name.to_owned()))?;
998                source
999                    .free_id(source_id)?
1000                    .ok_or_else(|| ParamError::ParameterConflict {
1001                        name: name.to_owned(),
1002                        reason: "target free parameter is fixed in the source layout".to_owned(),
1003                    })
1004            })
1005            .collect::<ParamResult<Arc<[_]>>>()?;
1006        Ok(ParamProjection {
1007            source: Arc::new(source.clone()),
1008            target: Arc::new(self.clone()),
1009            source_free_ids,
1010        })
1011    }
1012
1013    /// Iterates parameter definitions in stable free-parameter order.
1014    pub fn free_parameters(
1015        &self,
1016    ) -> impl ExactSizeIterator<Item = &Parameter> + DoubleEndedIterator {
1017        self.free_params().iter().map(|id| &self.specs[id.index()])
1018    }
1019
1020    /// Creates a full value set using fixed and deterministic initial values.
1021    pub fn default_values(&self) -> ParamValues {
1022        ParamValues {
1023            layout: Arc::new(self.clone()),
1024            values: self.projection.defaults.to_vec(),
1025        }
1026    }
1027
1028    /// Return deterministic initial values in free-parameter order.
1029    ///
1030    /// Uniform initial ranges use their midpoint.
1031    pub fn initial_free_values(&self) -> Vec<f64> {
1032        self.projection.initial_free_values()
1033    }
1034
1035    /// Expand a free-parameter slice while restoring fixed values from the layout.
1036    ///
1037    /// # Errors
1038    ///
1039    /// Returns [`ParamError::FreeLengthMismatch`] when `free` does not contain
1040    /// exactly one value per free parameter.
1041    pub fn values(&self, free: &[f64]) -> ParamResult<ParamValues> {
1042        let mut values = self.projection.defaults.to_vec();
1043        self.fill_full_from_free(free, &mut values)?;
1044        Ok(ParamValues {
1045            layout: Arc::new(self.clone()),
1046            values,
1047        })
1048    }
1049
1050    /// Generate one value per free parameter in layout order.
1051    pub fn free_values_with(&self, mut value: impl FnMut(&Parameter) -> f64) -> Vec<f64> {
1052        self.free_parameters().map(&mut value).collect()
1053    }
1054
1055    /// Generate initial free values, invoking `uniform` only for uniform initial ranges.
1056    pub fn sample_initial(&self, seed: u64) -> Vec<f64> {
1057        let mut rng = Rng::with_seed(seed);
1058        self.free_values_with(|parameter| parameter.initial.sample_with(&mut rng))
1059    }
1060
1061    /// Validate free values against ordinary bounds and canonical periodic domains.
1062    ///
1063    /// # Errors
1064    ///
1065    /// Returns [`ParamError::FreeLengthMismatch`] when `free` has the wrong
1066    /// length, or a value-related [`ParamError`] when a value lies outside its
1067    /// parameter's bounds or canonical periodic domain.
1068    pub fn validate_free_values(&self, free: &[f64]) -> ParamResult<()> {
1069        self.projection.validate_free_dimension(free)?;
1070        for (value, parameter) in free.iter().zip(self.free_parameters()) {
1071            parameter.validate_value(*value)?;
1072        }
1073        Ok(())
1074    }
1075
1076    /// Validate free values while classifying structural, numeric, and support failures.
1077    ///
1078    /// Unlike [`Self::validate_free_values`], this method treats every non-finite
1079    /// value as invalid input, including for an otherwise unbounded parameter.
1080    ///
1081    /// # Errors
1082    ///
1083    /// Returns [`FreeValueValidationError::Parameter`] for layout-level
1084    /// validation failures such as the wrong number of values,
1085    /// [`FreeValueValidationError::NonFiniteValue`] for NaN or infinity, and
1086    /// [`FreeValueValidationError::OutsideSupport`] for a finite value outside
1087    /// ordinary bounds or a canonical periodic domain.
1088    pub fn validate_free_values_classified(
1089        &self,
1090        free: &[f64],
1091    ) -> Result<(), FreeValueValidationError> {
1092        self.projection.validate_free_dimension(free)?;
1093        for (index, (value, parameter)) in free.iter().zip(self.free_parameters()).enumerate() {
1094            let id = FreeParamId(index as u32);
1095            if !value.is_finite() {
1096                return Err(FreeValueValidationError::NonFiniteValue {
1097                    id,
1098                    name: parameter.name().to_owned(),
1099                    value: *value,
1100                });
1101            }
1102            if !parameter.contains_in_support(*value) {
1103                return Err(FreeValueValidationError::OutsideSupport {
1104                    id,
1105                    name: parameter.name().to_owned(),
1106                    value: *value,
1107                });
1108            }
1109        }
1110        Ok(())
1111    }
1112
1113    /// Return free values with periodic parameters mapped into their canonical domains.
1114    /// Non-periodic values are unchanged; ordinary bounds are not clamped.
1115    ///
1116    /// # Errors
1117    ///
1118    /// Returns [`ParamError::FreeLengthMismatch`] when `free` does not contain
1119    /// exactly one value per free parameter.
1120    pub fn wrap_periodic_free_values(&self, free: &[f64]) -> ParamResult<Vec<f64>> {
1121        self.projection.validate_free_dimension(free)?;
1122        Ok(free
1123            .iter()
1124            .zip(self.free_params().iter())
1125            .map(|(value, id)| {
1126                let parameter = &self.specs[id.index()];
1127                parameter.periodic_bounds().map_or(*value, |(min, max)| {
1128                    min + (*value - min).rem_euclid(max - min)
1129                })
1130            })
1131            .collect())
1132    }
1133
1134    fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
1135        self.projection.fill_full_from_free(free, full)
1136    }
1137
1138    fn check_id(&self, id: ParamId) -> ParamResult<()> {
1139        if id.index() >= self.len() {
1140            Err(ParamError::InvalidParamId {
1141                id: id.index(),
1142                len: self.len(),
1143            })
1144        } else {
1145            Ok(())
1146        }
1147    }
1148
1149    fn check_free_id(&self, id: FreeParamId) -> ParamResult<()> {
1150        if id.index() >= self.n_free() {
1151            Err(ParamError::InvalidFreeParamId {
1152                id: id.index(),
1153                len: self.n_free(),
1154            })
1155        } else {
1156            Ok(())
1157        }
1158    }
1159}
1160
1161/// Incremental collection of uniquely named parameter definitions.
1162#[derive(Clone, Debug, Default)]
1163pub struct ParamRegistry {
1164    specs: Vec<Parameter>,
1165    names: HashMap<Arc<str>, ParamId>,
1166}
1167
1168impl ParamRegistry {
1169    /// Creates an empty registry.
1170    pub fn new() -> Self {
1171        Self::default()
1172    }
1173
1174    /// Registers a parameter and returns its stable identifier.
1175    ///
1176    /// Re-registering an identical definition returns its existing identifier;
1177    /// incompatible definitions with the same name return an error.
1178    ///
1179    /// # Errors
1180    ///
1181    /// Returns [`ParamError::EmptyName`] when the parameter name is empty, or
1182    /// [`ParamError::ParameterConflict`] when the name is already associated
1183    /// with a different definition.
1184    pub fn register<S>(&mut self, spec: S) -> ParamResult<ParamId>
1185    where
1186        S: Into<Parameter>,
1187    {
1188        let spec = spec.into();
1189        if spec.name().is_empty() {
1190            return Err(ParamError::EmptyName);
1191        }
1192        if let Some(id) = self.names.get(spec.name()).copied() {
1193            let existing = &self.specs[id.index()];
1194            if existing != &spec {
1195                return Err(ParamError::ParameterConflict {
1196                    name: spec.name().to_owned(),
1197                    reason: "duplicate parameter name has incompatible metadata".into(),
1198                });
1199            }
1200            return Ok(id);
1201        }
1202
1203        let id = ParamId(self.specs.len() as u32);
1204        self.names.insert(Arc::clone(&spec.name), id);
1205        self.specs.push(spec);
1206        Ok(id)
1207    }
1208
1209    /// Validates the registered parameters and builds their layout.
1210    ///
1211    /// # Errors
1212    ///
1213    /// Returns [`ParamError`] when any registered definition has invalid
1214    /// bounds, periodic metadata, scale, or initial or fixed values.
1215    pub fn layout(&self) -> ParamResult<ParamLayout> {
1216        ParamLayout::new(self.specs.clone())
1217    }
1218}
1219
1220/// Concrete full-layout parameter values paired with their defining layout.
1221#[derive(Clone, Debug, Serialize, Deserialize)]
1222pub struct ParamValues {
1223    layout: Arc<ParamLayout>,
1224    values: Vec<f64>,
1225}
1226
1227impl ParamValues {
1228    /// Returns the shared layout that interprets these values.
1229    pub fn layout(&self) -> &Arc<ParamLayout> {
1230        &self.layout
1231    }
1232
1233    /// Returns all values in full-layout order.
1234    pub fn as_slice(&self) -> &[f64] {
1235        &self.values
1236    }
1237
1238    /// Returns the value associated with a full-layout identifier.
1239    ///
1240    /// # Errors
1241    ///
1242    /// Returns [`ParamError::InvalidParamId`] when `id` is outside the shared
1243    /// layout.
1244    pub fn get(&self, id: ParamId) -> ParamResult<f64> {
1245        self.layout.check_id(id)?;
1246        Ok(self.values[id.index()])
1247    }
1248
1249    /// Copies the values of free parameters in free-parameter order.
1250    pub fn free_values(&self) -> Vec<f64> {
1251        self.layout.projection.free_values_from_full(&self.values)
1252    }
1253
1254    /// Assigns one value by its free-parameter identifier.
1255    ///
1256    /// # Errors
1257    ///
1258    /// Returns [`ParamError::InvalidFreeParamId`] when `id` is outside the
1259    /// shared layout's free-parameter ordering.
1260    pub fn set_free(&mut self, id: FreeParamId, value: f64) -> ParamResult<()> {
1261        let full_id = self.layout.free_param(id)?;
1262        self.values[full_id.index()] = value;
1263        Ok(())
1264    }
1265
1266    /// Replaces all free values and restores fixed values from the layout.
1267    ///
1268    /// # Errors
1269    ///
1270    /// Returns [`ParamError::FreeLengthMismatch`] when `values` does not
1271    /// contain exactly one value per free parameter.
1272    pub fn set_free_values(&mut self, values: &[f64]) -> ParamResult<()> {
1273        let layout = Arc::clone(&self.layout);
1274        layout.fill_full_from_free(values, &mut self.values)
1275    }
1276}
1277
1278/// Convenience macro for creating parameters. Usage:
1279/// `parameter!("name")` for a free parameter, or `parameter!("name", 1.0)` for a fixed one.
1280#[macro_export]
1281macro_rules! parameter {
1282    ($name:expr) => {{
1283        $crate::parameters::Parameter::free($name)
1284    }};
1285
1286    ($name:expr, $value:expr) => {{
1287        $crate::parameters::Parameter::fixed($name, $value)
1288    }};
1289
1290    ($name:expr, $($rest:tt)+) => {{
1291        let mut p = $crate::parameters::Parameter::free($name);
1292        $crate::parameter!(@parse p, [fixed = false, initial = false]; $($rest)+);
1293        p
1294    }};
1295
1296    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; ) => {};
1297
1298    (@parse $p:ident, [fixed = false, initial = false]; fixed : $value:expr $(, $($rest:tt)*)?) => {{
1299        $p = $p.with_fixed_value($value);
1300        $crate::parameter!(@parse $p, [fixed = true, initial = false]; $($($rest)*)?);
1301    }};
1302
1303    (@parse $p:ident, [fixed = false, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {{
1304        $p = $p.with_initial($value);
1305        $crate::parameter!(@parse $p, [fixed = false, initial = true]; $($($rest)*)?);
1306    }};
1307
1308    (@parse $p:ident, [fixed = true, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {
1309        compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1310    };
1311
1312    (@parse $p:ident, [fixed = false, initial = true]; fixed : $value:expr $(, $($rest:tt)*)?) => {
1313        compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1314    };
1315
1316    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; bounds : ($min:expr, $max:expr) $(, $($rest:tt)*)?) => {{
1317        $p = $p.with_bounds($min, $max);
1318        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1319    }};
1320
1321    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic : $value:expr $(, $($rest:tt)*)?) => {{
1322        $p = $p.with_periodicity($value);
1323        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1324    }};
1325
1326    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic $(, $($rest:tt)*)?) => {{
1327        $p = $p.with_periodic();
1328        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1329    }};
1330
1331    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; scale : $value:expr $(, $($rest:tt)*)?) => {{
1332        $p = $p.with_scale($value);
1333        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1334    }};
1335
1336    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; unit : $value:expr $(, $($rest:tt)*)?) => {{
1337        $p = $p.with_unit($value);
1338        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1339    }};
1340
1341    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; latex : $value:expr $(, $($rest:tt)*)?) => {{
1342        $p = $p.with_latex($value);
1343        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1344    }};
1345
1346    (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; description : $value:expr $(, $($rest:tt)*)?) => {{
1347        $p = $p.with_description($value);
1348        $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1349    }};
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354    use super::*;
1355
1356    #[test]
1357    fn parameter_macro_constructs_fixed_parameters() {
1358        let positional = crate::parameter!("positional", 1.25);
1359        let named = crate::parameter!("named", fixed: -0.5);
1360        let reordered = crate::parameter!(
1361            "reordered",
1362            bounds: (0.0, 2.0),
1363            unit: "GeV",
1364            scale: 0.5,
1365            fixed: 1.0
1366        );
1367
1368        assert_eq!(positional.state(), &ParamState::Fixed(1.25));
1369        assert_eq!(named.state(), &ParamState::Fixed(-0.5));
1370        assert_eq!(reordered.state(), &ParamState::Fixed(1.0));
1371        assert_eq!(reordered.bounds_spec(), &Bounds::new(0.0, 2.0));
1372        assert_eq!(reordered.unit_label(), Some("GeV"));
1373        assert_eq!(reordered.scale(), Some(0.5));
1374    }
1375
1376    #[test]
1377    fn parameter_updates_apply_state_initial_and_metadata_atomically() {
1378        let parameter = Parameter::free("mass")
1379            .with_initial(1.0)
1380            .with_bounds(0.0, 2.0)
1381            .with_unit("GeV")
1382            .with_latex("m")
1383            .with_description("mass")
1384            .with_scale(0.5);
1385        let updated = parameter
1386            .with_update(&ParameterUpdate {
1387                state: Some(ParamState::Fixed(3.0)),
1388                bounds: Some(Bounds::new(0.0, 4.0)),
1389                unit: Some(None),
1390                latex: Some(Some("m_0".into())),
1391                description: Some(None),
1392                ..Default::default()
1393            })
1394            .unwrap();
1395
1396        assert_eq!(updated.state(), &ParamState::Fixed(3.0));
1397        assert_eq!(updated.initial_spec(), &InitialSpec::Value(3.0));
1398        assert_eq!(updated.bounds_spec(), &Bounds::new(0.0, 4.0));
1399        assert_eq!(updated.unit_label(), None);
1400        assert_eq!(updated.latex_label(), Some("m_0"));
1401        assert_eq!(updated.description_text(), None);
1402        assert_eq!(updated.scale(), Some(0.5));
1403    }
1404
1405    #[test]
1406    fn parameter_updates_allow_related_fields_to_change_together() {
1407        let parameter = Parameter::free("x").with_initial(1.0).with_bounds(0.0, 2.0);
1408        let updated = parameter
1409            .with_update(&ParameterUpdate {
1410                initial: Some(InitialSpec::Value(4.0)),
1411                bounds: Some(Bounds::new(0.0, 5.0)),
1412                ..Default::default()
1413            })
1414            .unwrap();
1415        assert_eq!(updated.initial_spec(), &InitialSpec::Value(4.0));
1416        assert_eq!(updated.bounds_spec(), &Bounds::new(0.0, 5.0));
1417    }
1418
1419    #[test]
1420    fn fixed_update_validates_explicit_dormant_initial_rule() {
1421        let parameter = Parameter::free("x").with_initial(1.0).with_bounds(0.0, 2.0);
1422        let error = parameter
1423            .with_update(&ParameterUpdate {
1424                state: Some(ParamState::Fixed(1.0)),
1425                initial: Some(InitialSpec::Value(3.0)),
1426                ..Default::default()
1427            })
1428            .unwrap_err();
1429        assert!(
1430            matches!(error, ParamError::InitialOutOfBounds { name, value } if name == "x" && value == 3.0)
1431        );
1432    }
1433
1434    #[test]
1435    fn parameter_update_intrinsic_errors_are_field_specific() {
1436        assert!(matches!(
1437            ParameterUpdate {
1438                state: Some(ParamState::Fixed(f64::NAN)),
1439                ..Default::default()
1440            }
1441            .validate(),
1442            Err(ParamError::InvalidUpdateFixedValue { .. })
1443        ));
1444        assert!(matches!(
1445            ParameterUpdate {
1446                bounds: Some(Bounds::new(f64::NAN, None)),
1447                ..Default::default()
1448            }
1449            .validate(),
1450            Err(ParamError::InvalidUpdateBounds { .. })
1451        ));
1452    }
1453
1454    #[test]
1455    fn parameter_scale_is_validated_and_supported_by_the_macro() {
1456        let scaled = crate::parameter!("scaled", initial: 2.0, scale: 0.25);
1457        let layout = ParamLayout::new([scaled]).unwrap();
1458        assert_eq!(layout.specs()[0].scale(), Some(0.25));
1459
1460        let error = ParamLayout::new([Parameter::free("bad").with_scale(0.0)]).unwrap_err();
1461        assert!(matches!(error, ParamError::InvalidScale { .. }));
1462    }
1463
1464    #[test]
1465    fn layout_tracks_free_and_fixed_values() {
1466        let layout = ParamLayout::new([
1467            Parameter::free("mass")
1468                .with_initial(1.2)
1469                .with_bounds(Some(0.0), Some(2.0)),
1470            Parameter::fixed("pi", std::f64::consts::PI),
1471            Parameter::free("width").with_initial((0.0, 1.0)),
1472        ])
1473        .unwrap();
1474
1475        assert_eq!(layout.len(), 3);
1476        assert_eq!(layout.n_free(), 2);
1477        assert_eq!(layout.initial_free_values(), vec![1.2, 0.5]);
1478        assert_eq!(layout.id("mass").map(ParamId::index), Some(0));
1479        assert_eq!(layout.id("pi").map(ParamId::index), Some(1));
1480        assert_eq!(layout.id("width").map(ParamId::index), Some(2));
1481        assert_eq!(
1482            layout
1483                .free_params()
1484                .iter()
1485                .map(|id| layout.name(*id).unwrap())
1486                .collect::<Vec<_>>(),
1487            vec!["mass", "width"]
1488        );
1489
1490        let values = layout.values(&[1.4, 0.2]).unwrap();
1491        assert_eq!(values.as_slice(), &[1.4, std::f64::consts::PI, 0.2]);
1492        assert_eq!(values.free_values(), vec![1.4, 0.2]);
1493    }
1494
1495    #[test]
1496    fn free_values_can_be_generated_or_sampled_in_layout_order() {
1497        let layout = ParamLayout::new([
1498            Parameter::fixed("fixed", 8.0),
1499            Parameter::free("uniform").with_initial((-2.0, 4.0)),
1500            Parameter::free("value").with_initial(3.0),
1501            Parameter::free("default"),
1502        ])
1503        .unwrap();
1504
1505        assert_eq!(layout.initial_free_values(), vec![1.0, 3.0, 0.0]);
1506        assert_eq!(layout.sample_initial(0), vec![1.6157656431461036, 3.0, 0.0]);
1507        assert_eq!(
1508            layout.free_values_with(|parameter| parameter.name().len() as f64),
1509            vec![7.0, 5.0, 7.0]
1510        );
1511        assert_eq!(
1512            layout
1513                .free_parameters()
1514                .map(Parameter::name)
1515                .collect::<Vec<_>>(),
1516            vec!["uniform", "value", "default"]
1517        );
1518    }
1519
1520    #[test]
1521    fn deterministic_and_sampled_initial_values_share_initial_spec_semantics() {
1522        let layout = ParamLayout::new([
1523            Parameter::free("default"),
1524            Parameter::free("value").with_initial(2.5),
1525            Parameter::free("uniform").with_initial((-4.0, 6.0)),
1526        ])
1527        .unwrap();
1528
1529        assert_eq!(layout.initial_free_values(), vec![0.0, 2.5, 1.0]);
1530        for seed in 0..32 {
1531            let sampled = layout.sample_initial(seed);
1532            assert_eq!(sampled[0], 0.0);
1533            assert_eq!(sampled[1], 2.5);
1534            assert!((-4.0..6.0).contains(&sampled[2]));
1535        }
1536    }
1537
1538    #[test]
1539    fn classified_free_value_validation_separates_failure_kinds() {
1540        let layout = ParamLayout::new([
1541            Parameter::free("bounded").with_bounds(-1.0, 1.0),
1542            Parameter::free("phase")
1543                .with_bounds(0.0, std::f64::consts::TAU)
1544                .with_periodic(),
1545        ])
1546        .unwrap();
1547
1548        assert_eq!(
1549            layout.validate_free_values_classified(&[0.0]),
1550            Err(FreeValueValidationError::Parameter(
1551                ParamError::FreeLengthMismatch {
1552                    expected: 2,
1553                    actual: 1,
1554                }
1555            ))
1556        );
1557        assert!(matches!(
1558            layout.validate_free_values_classified(&[f64::NAN, 0.0]),
1559            Err(FreeValueValidationError::NonFiniteValue { id, name, value })
1560                if id.index() == 0 && name == "bounded" && value.is_nan()
1561        ));
1562        assert_eq!(
1563            layout.validate_free_values_classified(&[2.0, 0.0]),
1564            Err(FreeValueValidationError::OutsideSupport {
1565                id: FreeParamId(0),
1566                name: "bounded".into(),
1567                value: 2.0,
1568            })
1569        );
1570        assert_eq!(
1571            layout.validate_free_values_classified(&[0.0, std::f64::consts::TAU]),
1572            Err(FreeValueValidationError::OutsideSupport {
1573                id: FreeParamId(1),
1574                name: "phase".into(),
1575                value: std::f64::consts::TAU,
1576            })
1577        );
1578        assert!(layout.validate_free_values_classified(&[1.0, 0.0]).is_ok());
1579    }
1580
1581    #[test]
1582    fn periodic_domains_wrap_and_validate_without_changing_bounds() {
1583        let tau = std::f64::consts::TAU;
1584        let phase = Parameter::free("phase")
1585            .with_initial(0.25)
1586            .with_bounds(0.0, tau)
1587            .with_periodic();
1588        assert_eq!(phase.periodic_bounds(), Some((0.0, tau)));
1589
1590        let layout = ParamLayout::new([phase]).unwrap();
1591        assert_eq!(
1592            layout.wrap_periodic_free_values(&[-0.25]).unwrap(),
1593            vec![tau - 0.25]
1594        );
1595        assert!(layout.validate_free_values(&[tau - 0.25]).is_ok());
1596        assert!(matches!(
1597            layout.validate_free_values(&[tau]),
1598            Err(ParamError::ValueOutsidePeriodicDomain { .. })
1599        ));
1600    }
1601
1602    #[test]
1603    fn invalid_periodic_metadata_and_initial_values_are_rejected() {
1604        assert!(matches!(
1605            ParamLayout::new([Parameter::free("phase").with_periodic()]),
1606            Err(ParamError::PeriodicRequiresFiniteBounds { .. })
1607        ));
1608        assert!(matches!(
1609            ParamLayout::new([Parameter::free("phase")
1610                .with_initial(std::f64::consts::TAU)
1611                .with_bounds(0.0, std::f64::consts::TAU)
1612                .with_periodic(),]),
1613            Err(ParamError::ValueOutsidePeriodicDomain { .. })
1614        ));
1615    }
1616
1617    #[test]
1618    fn duplicate_names_are_rejected() {
1619        let err = ParamLayout::new([Parameter::free("x"), Parameter::fixed("x", 1.0)]).unwrap_err();
1620        assert_eq!(err, ParamError::DuplicateName("x".into()));
1621    }
1622
1623    #[test]
1624    fn free_length_is_checked() {
1625        let layout = ParamLayout::new([Parameter::free("x"), Parameter::free("y")]).unwrap();
1626        let err = layout.values(&[1.0]).unwrap_err();
1627        assert_eq!(
1628            err,
1629            ParamError::FreeLengthMismatch {
1630                expected: 2,
1631                actual: 1
1632            }
1633        );
1634    }
1635
1636    #[test]
1637    fn full_and_free_vectors_round_trip_in_stable_order() {
1638        let layout = ParamLayout::new([
1639            Parameter::fixed("offset", -1.0),
1640            Parameter::free("mass").with_initial(1.2),
1641            Parameter::fixed("scale", 2.0),
1642            Parameter::free("width").with_initial(0.1),
1643        ])
1644        .unwrap();
1645
1646        let full = layout.values(&[1.4, 0.2]).unwrap();
1647        assert_eq!(full.as_slice(), &[-1.0, 1.4, 2.0, 0.2]);
1648        let mut rewritten = vec![0.0; layout.len()];
1649        layout
1650            .fill_full_from_free(&[1.5, 0.3], &mut rewritten)
1651            .unwrap();
1652        assert_eq!(rewritten, vec![-1.0, 1.5, 2.0, 0.3]);
1653    }
1654
1655    #[test]
1656    fn values_only_mutate_free_parameters() {
1657        let layout = ParamLayout::new([
1658            Parameter::fixed("fixed", 1.0),
1659            Parameter::free("x"),
1660            Parameter::free("y"),
1661        ])
1662        .unwrap();
1663        let x_id = layout.id("x").unwrap();
1664        let y_id = layout.id("y").unwrap();
1665        let x_free = layout.free_id(x_id).unwrap().unwrap();
1666        let y_free = layout.free_id(y_id).unwrap().unwrap();
1667
1668        let mut values = layout.default_values();
1669        values.set_free(x_free, 3.0).unwrap();
1670        values.set_free(y_free, 4.0).unwrap();
1671
1672        assert_eq!(values.as_slice(), &[1.0, 3.0, 4.0]);
1673        assert_eq!(values.free_values(), vec![3.0, 4.0]);
1674    }
1675
1676    #[test]
1677    fn invalid_specs_are_rejected() {
1678        assert_eq!(
1679            ParamLayout::new([Parameter::free("")]).unwrap_err(),
1680            ParamError::EmptyName
1681        );
1682
1683        assert_eq!(
1684            ParamLayout::new([Parameter::free("x").with_bounds(Some(2.0), Some(1.0))]).unwrap_err(),
1685            ParamError::InvalidBounds {
1686                name: "x".into(),
1687                min: 2.0,
1688                max: 1.0
1689            }
1690        );
1691
1692        assert_eq!(
1693            ParamLayout::new([Parameter::free("x").with_initial((2.0, 1.0))]).unwrap_err(),
1694            ParamError::InvalidInitialRange {
1695                name: "x".into(),
1696                min: 2.0,
1697                max: 1.0
1698            }
1699        );
1700
1701        assert_eq!(
1702            ParamLayout::new([Parameter::free("x")
1703                .with_initial(3.0)
1704                .with_bounds(Some(0.0), Some(2.0))])
1705            .unwrap_err(),
1706            ParamError::InitialOutOfBounds {
1707                name: "x".into(),
1708                value: 3.0
1709            }
1710        );
1711
1712        assert_eq!(
1713            ParamLayout::new([Parameter::free("x")
1714                .with_initial((-1.0, 1.0))
1715                .with_bounds(Some(0.0), Some(2.0))])
1716            .unwrap_err(),
1717            ParamError::InitialRangeOutOfBounds {
1718                name: "x".into(),
1719                min: -1.0,
1720                max: 1.0
1721            }
1722        );
1723
1724        assert_eq!(
1725            ParamLayout::new([Parameter::fixed("x", 3.0).with_bounds(Some(0.0), Some(2.0))])
1726                .unwrap_err(),
1727            ParamError::FixedValueOutOfBounds {
1728                name: "x".into(),
1729                value: 3.0
1730            }
1731        );
1732    }
1733
1734    #[test]
1735    fn direct_and_registry_layouts_report_the_same_invalid_spec_errors() {
1736        let invalid = [
1737            Parameter::fixed("fixed", 2.0).with_bounds(0.0, 1.0),
1738            Parameter::free("default").with_bounds(1.0, 2.0),
1739            Parameter::free("value")
1740                .with_initial(2.0)
1741                .with_bounds(0.0, 1.0),
1742            Parameter::free("range")
1743                .with_initial((-1.0, 0.5))
1744                .with_bounds(0.0, 1.0),
1745            Parameter::free("periodic-value")
1746                .with_initial(std::f64::consts::TAU)
1747                .with_bounds(0.0, std::f64::consts::TAU)
1748                .with_periodic(),
1749        ];
1750
1751        for parameter in invalid {
1752            let direct = ParamLayout::new([parameter.clone()]).unwrap_err();
1753            let mut registry = ParamRegistry::new();
1754            registry.register(parameter).unwrap();
1755            assert_eq!(registry.layout().unwrap_err(), direct);
1756        }
1757    }
1758
1759    #[test]
1760    fn free_vector_lengths_are_checked() {
1761        let layout = ParamLayout::new([
1762            Parameter::fixed("a", 0.0),
1763            Parameter::free("x"),
1764            Parameter::free("y"),
1765        ])
1766        .unwrap();
1767
1768        assert_eq!(
1769            layout
1770                .fill_full_from_free(&[1.0], &mut [0.0, 0.0, 0.0])
1771                .unwrap_err(),
1772            ParamError::FreeLengthMismatch {
1773                expected: 2,
1774                actual: 1
1775            }
1776        );
1777    }
1778
1779    #[test]
1780    fn free_dimension_contract_is_shared_by_projection_operations() {
1781        let layout = ParamLayout::new([
1782            Parameter::fixed("fixed", 4.0),
1783            Parameter::free("x"),
1784            Parameter::free("y"),
1785        ])
1786        .unwrap();
1787        let expected = ParamError::FreeLengthMismatch {
1788            expected: 2,
1789            actual: 1,
1790        };
1791
1792        assert_eq!(layout.values(&[1.0]).unwrap_err(), expected);
1793        assert_eq!(layout.validate_free_values(&[1.0]).unwrap_err(), expected);
1794        assert_eq!(
1795            layout.wrap_periodic_free_values(&[1.0]).unwrap_err(),
1796            expected
1797        );
1798
1799        let mut values = layout.default_values();
1800        assert_eq!(values.set_free_values(&[1.0]).unwrap_err(), expected);
1801        assert_eq!(values.as_slice(), &[4.0, 0.0, 0.0]);
1802        assert_eq!(
1803            layout.validate_free_values_classified(&[1.0]),
1804            Err(FreeValueValidationError::Parameter(
1805                ParamError::FreeLengthMismatch {
1806                    expected: 2,
1807                    actual: 1,
1808                }
1809            ))
1810        );
1811    }
1812
1813    #[test]
1814    fn cross_layout_projection_reorders_values_and_scatters_gradients() {
1815        let source = ParamLayout::new([
1816            Parameter::fixed("offset", -1.0),
1817            Parameter::free("x"),
1818            Parameter::free("y"),
1819        ])
1820        .unwrap();
1821        let target = ParamLayout::new([
1822            Parameter::free("y"),
1823            Parameter::fixed("scale", 2.0),
1824            Parameter::free("x"),
1825        ])
1826        .unwrap();
1827        let projection = target.projection_from(&source).unwrap();
1828        let source_values = source.values(&[1.0, 2.0]).unwrap();
1829
1830        assert_eq!(
1831            projection.project(&source_values).unwrap().as_slice(),
1832            &[2.0, 2.0, 1.0]
1833        );
1834
1835        let mut source_gradient = vec![10.0, 20.0];
1836        projection
1837            .scatter_add(&[0.5, 1.5], &mut source_gradient)
1838            .unwrap();
1839        assert_eq!(source_gradient, vec![11.5, 20.5]);
1840    }
1841
1842    #[test]
1843    fn cross_layout_projection_rejects_missing_and_fixed_source_parameters() {
1844        let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1845        let missing = ParamLayout::new([Parameter::free("y")]).unwrap();
1846        assert_eq!(
1847            target.projection_from(&missing).unwrap_err(),
1848            ParamError::UnknownName("x".into())
1849        );
1850
1851        let fixed = ParamLayout::new([Parameter::fixed("x", 1.0)]).unwrap();
1852        assert!(matches!(
1853            target.projection_from(&fixed),
1854            Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1855        ));
1856    }
1857
1858    #[test]
1859    fn cross_layout_projection_supports_zero_free_layouts() {
1860        let source = ParamLayout::new([Parameter::fixed("source", 3.0)]).unwrap();
1861        let target = ParamLayout::new([Parameter::fixed("target", 4.0)]).unwrap();
1862        let projection = target.projection_from(&source).unwrap();
1863        let values = source.default_values();
1864
1865        assert_eq!(projection.project(&values).unwrap().as_slice(), &[4.0]);
1866        let mut gradient = Vec::new();
1867        projection.scatter_add(&[], &mut gradient).unwrap();
1868    }
1869
1870    #[test]
1871    fn cross_layout_projection_accepts_reordered_compatible_source_layouts() {
1872        let source = ParamLayout::new([Parameter::free("x")]).unwrap();
1873        let other =
1874            ParamLayout::new([Parameter::fixed("extra", 9.0), Parameter::fixed("x", 3.0)]).unwrap();
1875        let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1876        let projection = target.projection_from(&source).unwrap();
1877        let values = other.default_values();
1878
1879        assert_eq!(projection.project(&values).unwrap().as_slice(), &[3.0]);
1880        assert_eq!(values.as_slice(), &[9.0, 3.0]);
1881    }
1882
1883    #[test]
1884    fn cross_layout_projection_rejects_missing_alternate_source_name() {
1885        let source = ParamLayout::new([Parameter::free("x")]).unwrap();
1886        let other = ParamLayout::new([Parameter::free("y")]).unwrap();
1887        let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1888        let projection = target.projection_from(&source).unwrap();
1889        let values = other.values(&[7.0]).unwrap();
1890
1891        assert_eq!(
1892            projection.project(&values).unwrap_err(),
1893            ParamError::UnknownName("x".into())
1894        );
1895        assert_eq!(values.as_slice(), &[7.0]);
1896    }
1897
1898    #[test]
1899    fn registry_merges_identical_parameters_in_first_seen_order() {
1900        let mut registry = ParamRegistry::new();
1901        let y = registry
1902            .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1903            .unwrap();
1904        let x = registry.register(Parameter::free("x")).unwrap();
1905        let y_again = registry
1906            .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1907            .unwrap();
1908
1909        assert_eq!(y.index(), 0);
1910        assert_eq!(x.index(), 1);
1911        assert_eq!(y_again, y);
1912
1913        let layout = registry.layout().unwrap();
1914        assert_eq!(
1915            layout
1916                .specs()
1917                .iter()
1918                .map(Parameter::name)
1919                .collect::<Vec<_>>(),
1920            vec!["y", "x"]
1921        );
1922    }
1923
1924    #[test]
1925    fn registry_rejects_incompatible_parameter_reuse() {
1926        let mut registry = ParamRegistry::new();
1927        registry
1928            .register(Parameter::free("x").with_initial(1.0))
1929            .unwrap();
1930
1931        assert!(matches!(
1932            registry.register(Parameter::free("x").with_initial(2.0)),
1933            Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1934        ));
1935    }
1936}