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