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#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct ParamId(u32);
12
13impl ParamId {
14 pub fn index(self) -> usize {
16 self.0 as usize
17 }
18}
19
20#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct FreeParamId(u32);
23
24impl FreeParamId {
25 pub fn index(self) -> usize {
27 self.0 as usize
28 }
29}
30
31#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
33pub enum InitialSpec {
34 #[default]
36 Default,
37 Value(f64),
39 Uniform {
41 min: f64,
43 max: f64,
45 },
46}
47
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50pub enum ParamState {
51 Free,
53 Fixed(f64),
55}
56
57#[derive(Clone, Debug, Error, PartialEq)]
62pub enum FreeValueValidationError {
63 #[error(transparent)]
65 Parameter(#[from] ParamError),
66 #[error("non-finite value {value} for free parameter {name} ({id:?})")]
68 NonFiniteValue {
69 id: FreeParamId,
71 name: String,
73 value: f64,
75 },
76 #[error("value {value} for free parameter {name} ({id:?}) is outside its support")]
78 OutsideSupport {
79 id: FreeParamId,
81 name: String,
83 value: f64,
85 },
86}
87
88#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
90pub struct Bounds {
91 pub min: Option<f64>,
93 pub max: Option<f64>,
95}
96
97impl Bounds {
98 pub fn new(min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) -> Self {
100 Self {
101 min: min.into(),
102 max: max.into(),
103 }
104 }
105
106 fn validate(&self, name: &str) -> ParamResult<()> {
107 if let (Some(min), Some(max)) = (self.min, self.max)
108 && min > max
109 {
110 return Err(ParamError::InvalidBounds {
111 name: name.to_owned(),
112 min,
113 max,
114 });
115 }
116 Ok(())
117 }
118
119 pub fn contains(&self, value: f64) -> bool {
121 self.min.is_none_or(|min| value >= min) && self.max.is_none_or(|max| value <= max)
122 }
123}
124
125#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
127pub struct Parameter {
128 name: Arc<str>,
129 state: ParamState,
130 initial: InitialSpec,
131 bounds: Bounds,
132 #[serde(default)]
133 periodic: bool,
134 #[serde(default)]
135 scale: Option<f64>,
136 unit: Option<Arc<str>>,
137 latex: Option<Arc<str>>,
138 description: Option<Arc<str>>,
139}
140
141impl Parameter {
142 pub fn free(name: impl Into<Arc<str>>) -> Self {
144 Self {
145 name: name.into(),
146 state: ParamState::Free,
147 initial: InitialSpec::Default,
148 bounds: Bounds::default(),
149 periodic: false,
150 scale: None,
151 unit: None,
152 latex: None,
153 description: None,
154 }
155 }
156
157 pub fn fixed(name: impl Into<Arc<str>>, value: f64) -> Self {
159 Self {
160 name: name.into(),
161 state: ParamState::Fixed(value),
162 initial: InitialSpec::Value(value),
163 bounds: Bounds::default(),
164 periodic: false,
165 scale: None,
166 unit: None,
167 latex: None,
168 description: None,
169 }
170 }
171
172 fn set_fixed_value(&mut self, value: f64) {
173 self.state = ParamState::Fixed(value);
174 self.initial = InitialSpec::Value(value);
175 }
176
177 pub fn with_fixed_value(mut self, value: f64) -> Self {
179 self.set_fixed_value(value);
180 self
181 }
182
183 fn set_free(&mut self) {
184 self.state = ParamState::Free;
185 }
186
187 pub fn with_free(mut self) -> Self {
189 self.set_free();
190 self
191 }
192
193 fn set_initial(&mut self, initial: impl Into<InitialSpec>) {
194 self.initial = initial.into();
195 }
196
197 pub fn with_initial(mut self, initial: impl Into<InitialSpec>) -> Self {
199 self.set_initial(initial);
200 self
201 }
202
203 fn set_bounds(&mut self, min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) {
204 self.bounds = Bounds::new(min, max);
205 }
206
207 pub fn with_bounds(mut self, min: impl Into<Option<f64>>, max: impl Into<Option<f64>>) -> Self {
209 self.set_bounds(min, max);
210 self
211 }
212
213 pub fn with_periodic(mut self) -> Self {
215 self.periodic = true;
216 self
217 }
218
219 pub fn with_periodicity(mut self, periodic: bool) -> Self {
221 self.periodic = periodic;
222 self
223 }
224
225 pub fn with_scale(mut self, scale: f64) -> Self {
230 self.scale = Some(scale);
231 self
232 }
233
234 fn set_unit(&mut self, unit: impl Into<Arc<str>>) {
235 self.unit = Some(unit.into());
236 }
237
238 pub fn with_unit(mut self, unit: impl Into<Arc<str>>) -> Self {
240 self.set_unit(unit);
241 self
242 }
243
244 fn set_latex(&mut self, latex: impl Into<Arc<str>>) {
245 self.latex = Some(latex.into());
246 }
247
248 pub fn with_latex(mut self, latex: impl Into<Arc<str>>) -> Self {
250 self.set_latex(latex);
251 self
252 }
253
254 fn set_description(&mut self, description: impl Into<Arc<str>>) {
255 self.description = Some(description.into());
256 }
257
258 pub fn with_description(mut self, description: impl Into<Arc<str>>) -> Self {
260 self.set_description(description);
261 self
262 }
263
264 pub fn name(&self) -> &str {
266 &self.name
267 }
268
269 pub fn state(&self) -> &ParamState {
271 &self.state
272 }
273
274 pub fn is_free(&self) -> bool {
276 matches!(self.state, ParamState::Free)
277 }
278
279 pub fn is_fixed(&self) -> bool {
281 matches!(self.state, ParamState::Fixed(_))
282 }
283
284 pub fn initial_spec(&self) -> &InitialSpec {
286 &self.initial
287 }
288
289 pub fn bounds_spec(&self) -> &Bounds {
291 &self.bounds
292 }
293
294 pub fn is_periodic(&self) -> bool {
296 self.periodic
297 }
298
299 pub fn periodic_bounds(&self) -> Option<(f64, f64)> {
301 match (self.periodic, self.bounds.min, self.bounds.max) {
302 (true, Some(min), Some(max)) if min.is_finite() && max.is_finite() && min < max => {
303 Some((min, max))
304 }
305 _ => None,
306 }
307 }
308
309 pub fn scale(&self) -> Option<f64> {
311 self.scale
312 }
313
314 pub fn unit_label(&self) -> Option<&str> {
316 self.unit.as_deref()
317 }
318
319 pub fn latex_label(&self) -> Option<&str> {
321 self.latex.as_deref()
322 }
323
324 pub fn description_text(&self) -> Option<&str> {
326 self.description.as_deref()
327 }
328
329 fn validate(&self) -> ParamResult<()> {
330 if self.name().is_empty() {
331 return Err(ParamError::EmptyName);
332 }
333 self.bounds.validate(self.name())?;
334 if self.periodic && self.periodic_bounds().is_none() {
335 return Err(ParamError::PeriodicRequiresFiniteBounds {
336 name: self.name().to_owned(),
337 });
338 }
339 if let Some(scale) = self.scale
340 && (!scale.is_finite() || scale <= 0.0)
341 {
342 return Err(ParamError::InvalidScale {
343 name: self.name().to_owned(),
344 scale,
345 });
346 }
347 self.validate_initial()
348 }
349
350 fn validate_initial(&self) -> ParamResult<()> {
351 match self.state {
352 ParamState::Fixed(value) => {
353 if !self.bounds.contains(value) {
354 return Err(ParamError::FixedValueOutOfBounds {
355 name: self.name().to_owned(),
356 value,
357 });
358 }
359 self.validate_periodic_value(value)
360 }
361 ParamState::Free => self.validate_free_initial(),
362 }
363 }
364
365 fn validate_free_initial(&self) -> ParamResult<()> {
366 match self.initial {
367 InitialSpec::Default | InitialSpec::Value(_) => {
368 let value = self.initial.representative_value();
369 if !self.bounds.contains(value) {
370 return Err(ParamError::InitialOutOfBounds {
371 name: self.name().to_owned(),
372 value,
373 });
374 }
375 self.validate_periodic_value(value)
376 }
377 InitialSpec::Uniform { min, max } => {
378 if min > max {
379 return Err(ParamError::InvalidInitialRange {
380 name: self.name().to_owned(),
381 min,
382 max,
383 });
384 }
385 if !self.bounds.contains(min) || !self.bounds.contains(max) {
386 return Err(ParamError::InitialRangeOutOfBounds {
387 name: self.name().to_owned(),
388 min,
389 max,
390 });
391 }
392 if let Some((domain_min, domain_max)) = self.periodic_bounds()
393 && (min < domain_min || max > domain_max)
394 {
395 let value = if min < domain_min { min } else { max };
396 return Err(ParamError::ValueOutsidePeriodicDomain {
397 name: self.name().to_owned(),
398 value,
399 min: domain_min,
400 max: domain_max,
401 });
402 }
403 Ok(())
404 }
405 }
406 }
407
408 fn default_value(&self) -> f64 {
409 match self.state {
410 ParamState::Fixed(value) => value,
411 ParamState::Free => self.initial.representative_value(),
412 }
413 }
414
415 fn validate_periodic_value(&self, value: f64) -> ParamResult<()> {
416 if let Some((min, max)) = self.periodic_bounds()
417 && !(value.is_finite() && value >= min && value < max)
418 {
419 return Err(ParamError::ValueOutsidePeriodicDomain {
420 name: self.name().to_owned(),
421 value,
422 min,
423 max,
424 });
425 }
426 Ok(())
427 }
428
429 fn validate_value(&self, value: f64) -> ParamResult<()> {
430 if !self.bounds.contains(value) {
431 return Err(ParamError::ValueOutOfBounds {
432 name: self.name().to_owned(),
433 value,
434 });
435 }
436 self.validate_periodic_value(value)
437 }
438
439 fn contains_in_support(&self, value: f64) -> bool {
440 self.bounds.contains(value)
441 && self
442 .periodic_bounds()
443 .is_none_or(|(min, max)| value >= min && value < max)
444 }
445}
446
447impl InitialSpec {
448 fn representative_value(&self) -> f64 {
449 match *self {
450 Self::Default => 0.0,
451 Self::Value(value) => value,
452 Self::Uniform { min, max } => 0.5 * (min + max),
453 }
454 }
455
456 fn sample_with(&self, rng: &mut Rng) -> f64 {
457 match *self {
458 Self::Default => 0.0,
459 Self::Value(value) => value,
460 Self::Uniform { min, max } => rng.f64_range(min..max),
461 }
462 }
463}
464
465impl From<f64> for InitialSpec {
466 fn from(value: f64) -> Self {
467 Self::Value(value)
468 }
469}
470
471impl From<(f64, f64)> for InitialSpec {
472 fn from((min, max): (f64, f64)) -> Self {
473 Self::Uniform { min, max }
474 }
475}
476
477#[derive(Clone)]
479pub struct ParamLayout {
480 specs: Arc<[Parameter]>,
481 names: Arc<HashMap<Arc<str>, ParamId>>,
482 projection: ParamLayoutProjection,
483}
484
485impl fmt::Debug for ParamLayout {
486 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
487 formatter
488 .debug_struct("ParamLayout")
489 .field("specs", &self.specs)
490 .field("names", &self.names)
491 .field("free_params", &self.projection.free_params)
492 .field("full_to_free", &self.projection.full_to_free)
493 .field("defaults", &self.projection.defaults)
494 .finish()
495 }
496}
497
498#[derive(Clone, Debug)]
499struct ParamLayoutProjection {
501 free_params: Arc<[ParamId]>,
502 full_to_free: Arc<[Option<FreeParamId>]>,
503 defaults: Arc<[f64]>,
504}
505
506impl ParamLayoutProjection {
507 fn n_free(&self) -> usize {
508 self.free_params.len()
509 }
510
511 fn free_params(&self) -> &[ParamId] {
512 &self.free_params
513 }
514
515 fn free_id(&self, id: ParamId) -> Option<FreeParamId> {
516 self.full_to_free[id.index()]
517 }
518
519 fn full_id(&self, id: FreeParamId) -> ParamId {
520 self.free_params[id.index()]
521 }
522
523 fn validate_free_dimension<T>(&self, values: &[T]) -> ParamResult<()> {
524 if values.len() == self.n_free() {
525 Ok(())
526 } else {
527 Err(ParamError::FreeLengthMismatch {
528 expected: self.n_free(),
529 actual: values.len(),
530 })
531 }
532 }
533
534 fn initial_free_values(&self) -> Vec<f64> {
535 self.free_params
536 .iter()
537 .map(|id| self.defaults[id.index()])
538 .collect()
539 }
540
541 fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
542 self.validate_free_dimension(free)?;
543 debug_assert_eq!(full.len(), self.defaults.len());
544 full.copy_from_slice(&self.defaults);
545 for (value, id) in free.iter().zip(self.free_params.iter()) {
546 full[id.index()] = *value;
547 }
548 Ok(())
549 }
550
551 fn free_values_from_full(&self, full: &[f64]) -> Vec<f64> {
552 debug_assert_eq!(full.len(), self.defaults.len());
553 self.free_params.iter().map(|id| full[id.index()]).collect()
554 }
555}
556
557#[derive(Clone, Debug)]
564pub struct ParamProjection {
565 source: Arc<ParamLayout>,
566 target: Arc<ParamLayout>,
567 source_free_ids: Arc<[FreeParamId]>,
568}
569
570#[derive(Serialize, Deserialize)]
571struct ParamLayoutSerde {
574 specs: Arc<[Parameter]>,
575 names: Arc<HashMap<Arc<str>, ParamId>>,
576 free_params: Arc<[ParamId]>,
577 full_to_free: Arc<[Option<FreeParamId>]>,
578 defaults: Arc<[f64]>,
579}
580
581impl Serialize for ParamLayout {
582 fn serialize<__S>(&self, serializer: __S) -> Result<__S::Ok, __S::Error>
583 where
584 __S: Serializer,
585 {
586 ParamLayoutSerde {
587 specs: Arc::clone(&self.specs),
588 names: Arc::clone(&self.names),
589 free_params: Arc::clone(&self.projection.free_params),
590 full_to_free: Arc::clone(&self.projection.full_to_free),
591 defaults: Arc::clone(&self.projection.defaults),
592 }
593 .serialize(serializer)
594 }
595}
596
597impl<'de> Deserialize<'de> for ParamLayout {
598 fn deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
599 where
600 __D: Deserializer<'de>,
601 {
602 let serialized = ParamLayoutSerde::deserialize(deserializer)?;
603 Ok(Self {
604 specs: serialized.specs,
605 names: serialized.names,
606 projection: ParamLayoutProjection {
607 free_params: serialized.free_params,
608 full_to_free: serialized.full_to_free,
609 defaults: serialized.defaults,
610 },
611 })
612 }
613}
614
615impl ParamProjection {
616 pub fn project(&self, source: &ParamValues) -> ParamResult<ParamValues> {
623 let free = if source.layout().specs() == self.source.specs() {
624 self.source_free_ids
625 .iter()
626 .map(|id| source.get(self.source.projection.full_id(*id)))
627 .collect::<ParamResult<Vec<_>>>()?
628 } else {
629 self.target
630 .free_params()
631 .iter()
632 .map(|target_id| {
633 let name = self.target.name(*target_id)?;
634 let source_id = source
635 .layout()
636 .id(name)
637 .ok_or_else(|| ParamError::UnknownName(name.to_owned()))?;
638 source.get(source_id)
639 })
640 .collect::<ParamResult<Vec<_>>>()?
641 };
642 self.target.values(&free)
643 }
644
645 pub fn scatter_add(&self, target: &[f64], source: &mut [f64]) -> ParamResult<()> {
656 self.target.projection.validate_free_dimension(target)?;
657 self.source.projection.validate_free_dimension(source)?;
658 for (value, id) in target.iter().zip(self.source_free_ids.iter()) {
659 source[id.index()] += value;
660 }
661 Ok(())
662 }
663}
664
665struct LayoutBuilder {
666 specs: Vec<Parameter>,
667 names: HashMap<Arc<str>, ParamId>,
668 free_params: Vec<ParamId>,
669 full_to_free: Vec<Option<FreeParamId>>,
670 defaults: Vec<f64>,
671}
672
673impl LayoutBuilder {
674 fn with_capacity(capacity: usize) -> Self {
675 Self {
676 specs: Vec::with_capacity(capacity),
677 names: HashMap::with_capacity(capacity),
678 free_params: Vec::new(),
679 full_to_free: Vec::with_capacity(capacity),
680 defaults: Vec::with_capacity(capacity),
681 }
682 }
683
684 fn push_validated(&mut self, spec: Parameter) -> ParamResult<()> {
685 spec.validate()?;
686 let id = ParamId(self.specs.len() as u32);
687 if self.names.insert(Arc::clone(&spec.name), id).is_some() {
688 return Err(ParamError::DuplicateName(spec.name().to_owned()));
689 }
690 self.defaults.push(spec.default_value());
691 match spec.state {
692 ParamState::Free => {
693 let free_id = FreeParamId(self.free_params.len() as u32);
694 self.free_params.push(id);
695 self.full_to_free.push(Some(free_id));
696 }
697 ParamState::Fixed(_) => self.full_to_free.push(None),
698 }
699 self.specs.push(spec);
700 Ok(())
701 }
702
703 fn finish(self) -> ParamLayout {
704 ParamLayout {
705 specs: self.specs.into(),
706 names: Arc::new(self.names),
707 projection: ParamLayoutProjection {
708 free_params: self.free_params.into(),
709 full_to_free: self.full_to_free.into(),
710 defaults: self.defaults.into(),
711 },
712 }
713 }
714}
715
716impl ParamLayout {
717 pub fn new<S>(specs: impl IntoIterator<Item = S>) -> ParamResult<Self>
725 where
726 S: Into<Parameter>,
727 {
728 let specs: Vec<_> = specs.into_iter().map(Into::into).collect();
729 let mut builder = LayoutBuilder::with_capacity(specs.len());
730 for spec in specs {
731 builder.push_validated(spec)?;
732 }
733 Ok(builder.finish())
734 }
735
736 pub fn specs(&self) -> &[Parameter] {
738 &self.specs
739 }
740
741 pub fn len(&self) -> usize {
743 self.specs.len()
744 }
745
746 pub fn is_empty(&self) -> bool {
748 self.specs.is_empty()
749 }
750
751 pub fn n_free(&self) -> usize {
753 self.projection.n_free()
754 }
755
756 pub fn id(&self, name: &str) -> Option<ParamId> {
758 self.names.get(name).copied()
759 }
760
761 pub fn name(&self, id: ParamId) -> ParamResult<&str> {
768 self.check_id(id)?;
769 Ok(self.specs[id.index()].name())
770 }
771
772 pub fn spec(&self, id: ParamId) -> ParamResult<&Parameter> {
779 self.check_id(id)?;
780 Ok(&self.specs[id.index()])
781 }
782
783 pub fn free_id(&self, id: ParamId) -> ParamResult<Option<FreeParamId>> {
792 self.check_id(id)?;
793 Ok(self.projection.free_id(id))
794 }
795
796 fn free_param(&self, id: FreeParamId) -> ParamResult<ParamId> {
797 self.check_free_id(id)?;
798 Ok(self.projection.full_id(id))
799 }
800
801 pub fn free_params(&self) -> &[ParamId] {
803 self.projection.free_params()
804 }
805
806 pub fn projection_from(&self, source: &ParamLayout) -> ParamResult<ParamProjection> {
818 let source_free_ids = self
819 .free_params()
820 .iter()
821 .map(|target_id| {
822 let name = self.name(*target_id)?;
823 let source_id = source
824 .id(name)
825 .ok_or_else(|| ParamError::UnknownName(name.to_owned()))?;
826 source
827 .free_id(source_id)?
828 .ok_or_else(|| ParamError::ParameterConflict {
829 name: name.to_owned(),
830 reason: "target free parameter is fixed in the source layout".to_owned(),
831 })
832 })
833 .collect::<ParamResult<Arc<[_]>>>()?;
834 Ok(ParamProjection {
835 source: Arc::new(source.clone()),
836 target: Arc::new(self.clone()),
837 source_free_ids,
838 })
839 }
840
841 pub fn free_parameters(
843 &self,
844 ) -> impl ExactSizeIterator<Item = &Parameter> + DoubleEndedIterator {
845 self.free_params().iter().map(|id| &self.specs[id.index()])
846 }
847
848 pub fn default_values(&self) -> ParamValues {
850 ParamValues {
851 layout: Arc::new(self.clone()),
852 values: self.projection.defaults.to_vec(),
853 }
854 }
855
856 pub fn initial_free_values(&self) -> Vec<f64> {
860 self.projection.initial_free_values()
861 }
862
863 pub fn values(&self, free: &[f64]) -> ParamResult<ParamValues> {
870 let mut values = self.projection.defaults.to_vec();
871 self.fill_full_from_free(free, &mut values)?;
872 Ok(ParamValues {
873 layout: Arc::new(self.clone()),
874 values,
875 })
876 }
877
878 pub fn free_values_with(&self, mut value: impl FnMut(&Parameter) -> f64) -> Vec<f64> {
880 self.free_parameters().map(&mut value).collect()
881 }
882
883 pub fn sample_initial(&self, seed: u64) -> Vec<f64> {
885 let mut rng = Rng::with_seed(seed);
886 self.free_values_with(|parameter| parameter.initial.sample_with(&mut rng))
887 }
888
889 pub fn validate_free_values(&self, free: &[f64]) -> ParamResult<()> {
897 self.projection.validate_free_dimension(free)?;
898 for (value, parameter) in free.iter().zip(self.free_parameters()) {
899 parameter.validate_value(*value)?;
900 }
901 Ok(())
902 }
903
904 pub fn validate_free_values_classified(
917 &self,
918 free: &[f64],
919 ) -> Result<(), FreeValueValidationError> {
920 self.projection.validate_free_dimension(free)?;
921 for (index, (value, parameter)) in free.iter().zip(self.free_parameters()).enumerate() {
922 let id = FreeParamId(index as u32);
923 if !value.is_finite() {
924 return Err(FreeValueValidationError::NonFiniteValue {
925 id,
926 name: parameter.name().to_owned(),
927 value: *value,
928 });
929 }
930 if !parameter.contains_in_support(*value) {
931 return Err(FreeValueValidationError::OutsideSupport {
932 id,
933 name: parameter.name().to_owned(),
934 value: *value,
935 });
936 }
937 }
938 Ok(())
939 }
940
941 pub fn wrap_periodic_free_values(&self, free: &[f64]) -> ParamResult<Vec<f64>> {
949 self.projection.validate_free_dimension(free)?;
950 Ok(free
951 .iter()
952 .zip(self.free_params().iter())
953 .map(|(value, id)| {
954 let parameter = &self.specs[id.index()];
955 parameter.periodic_bounds().map_or(*value, |(min, max)| {
956 min + (*value - min).rem_euclid(max - min)
957 })
958 })
959 .collect())
960 }
961
962 fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
963 self.projection.fill_full_from_free(free, full)
964 }
965
966 fn check_id(&self, id: ParamId) -> ParamResult<()> {
967 if id.index() >= self.len() {
968 Err(ParamError::InvalidParamId {
969 id: id.index(),
970 len: self.len(),
971 })
972 } else {
973 Ok(())
974 }
975 }
976
977 fn check_free_id(&self, id: FreeParamId) -> ParamResult<()> {
978 if id.index() >= self.n_free() {
979 Err(ParamError::InvalidFreeParamId {
980 id: id.index(),
981 len: self.n_free(),
982 })
983 } else {
984 Ok(())
985 }
986 }
987}
988
989#[derive(Clone, Debug, Default)]
991pub struct ParamRegistry {
992 specs: Vec<Parameter>,
993 names: HashMap<Arc<str>, ParamId>,
994}
995
996impl ParamRegistry {
997 pub fn new() -> Self {
999 Self::default()
1000 }
1001
1002 pub fn register<S>(&mut self, spec: S) -> ParamResult<ParamId>
1013 where
1014 S: Into<Parameter>,
1015 {
1016 let spec = spec.into();
1017 if spec.name().is_empty() {
1018 return Err(ParamError::EmptyName);
1019 }
1020 if let Some(id) = self.names.get(spec.name()).copied() {
1021 let existing = &self.specs[id.index()];
1022 if existing != &spec {
1023 return Err(ParamError::ParameterConflict {
1024 name: spec.name().to_owned(),
1025 reason: "duplicate parameter name has incompatible metadata".into(),
1026 });
1027 }
1028 return Ok(id);
1029 }
1030
1031 let id = ParamId(self.specs.len() as u32);
1032 self.names.insert(Arc::clone(&spec.name), id);
1033 self.specs.push(spec);
1034 Ok(id)
1035 }
1036
1037 pub fn layout(&self) -> ParamResult<ParamLayout> {
1044 ParamLayout::new(self.specs.clone())
1045 }
1046}
1047
1048#[derive(Clone, Debug, Serialize, Deserialize)]
1050pub struct ParamValues {
1051 layout: Arc<ParamLayout>,
1052 values: Vec<f64>,
1053}
1054
1055impl ParamValues {
1056 pub fn layout(&self) -> &Arc<ParamLayout> {
1058 &self.layout
1059 }
1060
1061 pub fn as_slice(&self) -> &[f64] {
1063 &self.values
1064 }
1065
1066 pub fn get(&self, id: ParamId) -> ParamResult<f64> {
1073 self.layout.check_id(id)?;
1074 Ok(self.values[id.index()])
1075 }
1076
1077 pub fn free_values(&self) -> Vec<f64> {
1079 self.layout.projection.free_values_from_full(&self.values)
1080 }
1081
1082 pub fn set_free(&mut self, id: FreeParamId, value: f64) -> ParamResult<()> {
1089 let full_id = self.layout.free_param(id)?;
1090 self.values[full_id.index()] = value;
1091 Ok(())
1092 }
1093
1094 pub fn set_free_values(&mut self, values: &[f64]) -> ParamResult<()> {
1101 let layout = Arc::clone(&self.layout);
1102 layout.fill_full_from_free(values, &mut self.values)
1103 }
1104}
1105
1106#[macro_export]
1109macro_rules! parameter {
1110 ($name:expr) => {{
1111 $crate::parameters::Parameter::free($name)
1112 }};
1113
1114 ($name:expr, $value:expr) => {{
1115 $crate::parameters::Parameter::fixed($name, $value)
1116 }};
1117
1118 ($name:expr, $($rest:tt)+) => {{
1119 let mut p = $crate::parameters::Parameter::free($name);
1120 $crate::parameter!(@parse p, [fixed = false, initial = false]; $($rest)+);
1121 p
1122 }};
1123
1124 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; ) => {};
1125
1126 (@parse $p:ident, [fixed = false, initial = false]; fixed : $value:expr $(, $($rest:tt)*)?) => {{
1127 $p = $p.with_fixed_value($value);
1128 $crate::parameter!(@parse $p, [fixed = true, initial = false]; $($($rest)*)?);
1129 }};
1130
1131 (@parse $p:ident, [fixed = false, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {{
1132 $p = $p.with_initial($value);
1133 $crate::parameter!(@parse $p, [fixed = false, initial = true]; $($($rest)*)?);
1134 }};
1135
1136 (@parse $p:ident, [fixed = true, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {
1137 compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1138 };
1139
1140 (@parse $p:ident, [fixed = false, initial = true]; fixed : $value:expr $(, $($rest:tt)*)?) => {
1141 compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1142 };
1143
1144 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; bounds : ($min:expr, $max:expr) $(, $($rest:tt)*)?) => {{
1145 $p = $p.with_bounds($min, $max);
1146 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1147 }};
1148
1149 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic : $value:expr $(, $($rest:tt)*)?) => {{
1150 $p = $p.with_periodicity($value);
1151 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1152 }};
1153
1154 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic $(, $($rest:tt)*)?) => {{
1155 $p = $p.with_periodic();
1156 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1157 }};
1158
1159 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; scale : $value:expr $(, $($rest:tt)*)?) => {{
1160 $p = $p.with_scale($value);
1161 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1162 }};
1163
1164 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; unit : $value:expr $(, $($rest:tt)*)?) => {{
1165 $p = $p.with_unit($value);
1166 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1167 }};
1168
1169 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; latex : $value:expr $(, $($rest:tt)*)?) => {{
1170 $p = $p.with_latex($value);
1171 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1172 }};
1173
1174 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; description : $value:expr $(, $($rest:tt)*)?) => {{
1175 $p = $p.with_description($value);
1176 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1177 }};
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183
1184 #[test]
1185 fn parameter_macro_constructs_fixed_parameters() {
1186 let positional = crate::parameter!("positional", 1.25);
1187 let named = crate::parameter!("named", fixed: -0.5);
1188
1189 assert_eq!(positional.state(), &ParamState::Fixed(1.25));
1190 assert_eq!(named.state(), &ParamState::Fixed(-0.5));
1191 }
1192
1193 #[test]
1194 fn parameter_scale_is_validated_and_supported_by_the_macro() {
1195 let scaled = crate::parameter!("scaled", initial: 2.0, scale: 0.25);
1196 let layout = ParamLayout::new([scaled]).unwrap();
1197 assert_eq!(layout.specs()[0].scale(), Some(0.25));
1198
1199 let error = ParamLayout::new([Parameter::free("bad").with_scale(0.0)]).unwrap_err();
1200 assert!(matches!(error, ParamError::InvalidScale { .. }));
1201 }
1202
1203 #[test]
1204 fn layout_tracks_free_and_fixed_values() {
1205 let layout = ParamLayout::new([
1206 Parameter::free("mass")
1207 .with_initial(1.2)
1208 .with_bounds(Some(0.0), Some(2.0)),
1209 Parameter::fixed("pi", std::f64::consts::PI),
1210 Parameter::free("width").with_initial((0.0, 1.0)),
1211 ])
1212 .unwrap();
1213
1214 assert_eq!(layout.len(), 3);
1215 assert_eq!(layout.n_free(), 2);
1216 assert_eq!(layout.initial_free_values(), vec![1.2, 0.5]);
1217 assert_eq!(layout.id("mass").map(ParamId::index), Some(0));
1218 assert_eq!(layout.id("pi").map(ParamId::index), Some(1));
1219 assert_eq!(layout.id("width").map(ParamId::index), Some(2));
1220 assert_eq!(
1221 layout
1222 .free_params()
1223 .iter()
1224 .map(|id| layout.name(*id).unwrap())
1225 .collect::<Vec<_>>(),
1226 vec!["mass", "width"]
1227 );
1228
1229 let values = layout.values(&[1.4, 0.2]).unwrap();
1230 assert_eq!(values.as_slice(), &[1.4, std::f64::consts::PI, 0.2]);
1231 assert_eq!(values.free_values(), vec![1.4, 0.2]);
1232 }
1233
1234 #[test]
1235 fn free_values_can_be_generated_or_sampled_in_layout_order() {
1236 let layout = ParamLayout::new([
1237 Parameter::fixed("fixed", 8.0),
1238 Parameter::free("uniform").with_initial((-2.0, 4.0)),
1239 Parameter::free("value").with_initial(3.0),
1240 Parameter::free("default"),
1241 ])
1242 .unwrap();
1243
1244 assert_eq!(layout.initial_free_values(), vec![1.0, 3.0, 0.0]);
1245 assert_eq!(layout.sample_initial(0), vec![1.6157656431461036, 3.0, 0.0]);
1246 assert_eq!(
1247 layout.free_values_with(|parameter| parameter.name().len() as f64),
1248 vec![7.0, 5.0, 7.0]
1249 );
1250 assert_eq!(
1251 layout
1252 .free_parameters()
1253 .map(Parameter::name)
1254 .collect::<Vec<_>>(),
1255 vec!["uniform", "value", "default"]
1256 );
1257 }
1258
1259 #[test]
1260 fn deterministic_and_sampled_initial_values_share_initial_spec_semantics() {
1261 let layout = ParamLayout::new([
1262 Parameter::free("default"),
1263 Parameter::free("value").with_initial(2.5),
1264 Parameter::free("uniform").with_initial((-4.0, 6.0)),
1265 ])
1266 .unwrap();
1267
1268 assert_eq!(layout.initial_free_values(), vec![0.0, 2.5, 1.0]);
1269 for seed in 0..32 {
1270 let sampled = layout.sample_initial(seed);
1271 assert_eq!(sampled[0], 0.0);
1272 assert_eq!(sampled[1], 2.5);
1273 assert!((-4.0..6.0).contains(&sampled[2]));
1274 }
1275 }
1276
1277 #[test]
1278 fn classified_free_value_validation_separates_failure_kinds() {
1279 let layout = ParamLayout::new([
1280 Parameter::free("bounded").with_bounds(-1.0, 1.0),
1281 Parameter::free("phase")
1282 .with_bounds(0.0, std::f64::consts::TAU)
1283 .with_periodic(),
1284 ])
1285 .unwrap();
1286
1287 assert_eq!(
1288 layout.validate_free_values_classified(&[0.0]),
1289 Err(FreeValueValidationError::Parameter(
1290 ParamError::FreeLengthMismatch {
1291 expected: 2,
1292 actual: 1,
1293 }
1294 ))
1295 );
1296 assert!(matches!(
1297 layout.validate_free_values_classified(&[f64::NAN, 0.0]),
1298 Err(FreeValueValidationError::NonFiniteValue { id, name, value })
1299 if id.index() == 0 && name == "bounded" && value.is_nan()
1300 ));
1301 assert_eq!(
1302 layout.validate_free_values_classified(&[2.0, 0.0]),
1303 Err(FreeValueValidationError::OutsideSupport {
1304 id: FreeParamId(0),
1305 name: "bounded".into(),
1306 value: 2.0,
1307 })
1308 );
1309 assert_eq!(
1310 layout.validate_free_values_classified(&[0.0, std::f64::consts::TAU]),
1311 Err(FreeValueValidationError::OutsideSupport {
1312 id: FreeParamId(1),
1313 name: "phase".into(),
1314 value: std::f64::consts::TAU,
1315 })
1316 );
1317 assert!(layout.validate_free_values_classified(&[1.0, 0.0]).is_ok());
1318 }
1319
1320 #[test]
1321 fn periodic_domains_wrap_and_validate_without_changing_bounds() {
1322 let tau = std::f64::consts::TAU;
1323 let phase = Parameter::free("phase")
1324 .with_initial(0.25)
1325 .with_bounds(0.0, tau)
1326 .with_periodic();
1327 assert_eq!(phase.periodic_bounds(), Some((0.0, tau)));
1328
1329 let layout = ParamLayout::new([phase]).unwrap();
1330 assert_eq!(
1331 layout.wrap_periodic_free_values(&[-0.25]).unwrap(),
1332 vec![tau - 0.25]
1333 );
1334 assert!(layout.validate_free_values(&[tau - 0.25]).is_ok());
1335 assert!(matches!(
1336 layout.validate_free_values(&[tau]),
1337 Err(ParamError::ValueOutsidePeriodicDomain { .. })
1338 ));
1339 }
1340
1341 #[test]
1342 fn invalid_periodic_metadata_and_initial_values_are_rejected() {
1343 assert!(matches!(
1344 ParamLayout::new([Parameter::free("phase").with_periodic()]),
1345 Err(ParamError::PeriodicRequiresFiniteBounds { .. })
1346 ));
1347 assert!(matches!(
1348 ParamLayout::new([Parameter::free("phase")
1349 .with_initial(std::f64::consts::TAU)
1350 .with_bounds(0.0, std::f64::consts::TAU)
1351 .with_periodic(),]),
1352 Err(ParamError::ValueOutsidePeriodicDomain { .. })
1353 ));
1354 }
1355
1356 #[test]
1357 fn duplicate_names_are_rejected() {
1358 let err = ParamLayout::new([Parameter::free("x"), Parameter::fixed("x", 1.0)]).unwrap_err();
1359 assert_eq!(err, ParamError::DuplicateName("x".into()));
1360 }
1361
1362 #[test]
1363 fn free_length_is_checked() {
1364 let layout = ParamLayout::new([Parameter::free("x"), Parameter::free("y")]).unwrap();
1365 let err = layout.values(&[1.0]).unwrap_err();
1366 assert_eq!(
1367 err,
1368 ParamError::FreeLengthMismatch {
1369 expected: 2,
1370 actual: 1
1371 }
1372 );
1373 }
1374
1375 #[test]
1376 fn full_and_free_vectors_round_trip_in_stable_order() {
1377 let layout = ParamLayout::new([
1378 Parameter::fixed("offset", -1.0),
1379 Parameter::free("mass").with_initial(1.2),
1380 Parameter::fixed("scale", 2.0),
1381 Parameter::free("width").with_initial(0.1),
1382 ])
1383 .unwrap();
1384
1385 let full = layout.values(&[1.4, 0.2]).unwrap();
1386 assert_eq!(full.as_slice(), &[-1.0, 1.4, 2.0, 0.2]);
1387 let mut rewritten = vec![0.0; layout.len()];
1388 layout
1389 .fill_full_from_free(&[1.5, 0.3], &mut rewritten)
1390 .unwrap();
1391 assert_eq!(rewritten, vec![-1.0, 1.5, 2.0, 0.3]);
1392 }
1393
1394 #[test]
1395 fn values_only_mutate_free_parameters() {
1396 let layout = ParamLayout::new([
1397 Parameter::fixed("fixed", 1.0),
1398 Parameter::free("x"),
1399 Parameter::free("y"),
1400 ])
1401 .unwrap();
1402 let x_id = layout.id("x").unwrap();
1403 let y_id = layout.id("y").unwrap();
1404 let x_free = layout.free_id(x_id).unwrap().unwrap();
1405 let y_free = layout.free_id(y_id).unwrap().unwrap();
1406
1407 let mut values = layout.default_values();
1408 values.set_free(x_free, 3.0).unwrap();
1409 values.set_free(y_free, 4.0).unwrap();
1410
1411 assert_eq!(values.as_slice(), &[1.0, 3.0, 4.0]);
1412 assert_eq!(values.free_values(), vec![3.0, 4.0]);
1413 }
1414
1415 #[test]
1416 fn invalid_specs_are_rejected() {
1417 assert_eq!(
1418 ParamLayout::new([Parameter::free("")]).unwrap_err(),
1419 ParamError::EmptyName
1420 );
1421
1422 assert_eq!(
1423 ParamLayout::new([Parameter::free("x").with_bounds(Some(2.0), Some(1.0))]).unwrap_err(),
1424 ParamError::InvalidBounds {
1425 name: "x".into(),
1426 min: 2.0,
1427 max: 1.0
1428 }
1429 );
1430
1431 assert_eq!(
1432 ParamLayout::new([Parameter::free("x").with_initial((2.0, 1.0))]).unwrap_err(),
1433 ParamError::InvalidInitialRange {
1434 name: "x".into(),
1435 min: 2.0,
1436 max: 1.0
1437 }
1438 );
1439
1440 assert_eq!(
1441 ParamLayout::new([Parameter::free("x")
1442 .with_initial(3.0)
1443 .with_bounds(Some(0.0), Some(2.0))])
1444 .unwrap_err(),
1445 ParamError::InitialOutOfBounds {
1446 name: "x".into(),
1447 value: 3.0
1448 }
1449 );
1450
1451 assert_eq!(
1452 ParamLayout::new([Parameter::free("x")
1453 .with_initial((-1.0, 1.0))
1454 .with_bounds(Some(0.0), Some(2.0))])
1455 .unwrap_err(),
1456 ParamError::InitialRangeOutOfBounds {
1457 name: "x".into(),
1458 min: -1.0,
1459 max: 1.0
1460 }
1461 );
1462
1463 assert_eq!(
1464 ParamLayout::new([Parameter::fixed("x", 3.0).with_bounds(Some(0.0), Some(2.0))])
1465 .unwrap_err(),
1466 ParamError::FixedValueOutOfBounds {
1467 name: "x".into(),
1468 value: 3.0
1469 }
1470 );
1471 }
1472
1473 #[test]
1474 fn direct_and_registry_layouts_report_the_same_invalid_spec_errors() {
1475 let invalid = [
1476 Parameter::fixed("fixed", 2.0).with_bounds(0.0, 1.0),
1477 Parameter::free("default").with_bounds(1.0, 2.0),
1478 Parameter::free("value")
1479 .with_initial(2.0)
1480 .with_bounds(0.0, 1.0),
1481 Parameter::free("range")
1482 .with_initial((-1.0, 0.5))
1483 .with_bounds(0.0, 1.0),
1484 Parameter::free("periodic-value")
1485 .with_initial(std::f64::consts::TAU)
1486 .with_bounds(0.0, std::f64::consts::TAU)
1487 .with_periodic(),
1488 ];
1489
1490 for parameter in invalid {
1491 let direct = ParamLayout::new([parameter.clone()]).unwrap_err();
1492 let mut registry = ParamRegistry::new();
1493 registry.register(parameter).unwrap();
1494 assert_eq!(registry.layout().unwrap_err(), direct);
1495 }
1496 }
1497
1498 #[test]
1499 fn free_vector_lengths_are_checked() {
1500 let layout = ParamLayout::new([
1501 Parameter::fixed("a", 0.0),
1502 Parameter::free("x"),
1503 Parameter::free("y"),
1504 ])
1505 .unwrap();
1506
1507 assert_eq!(
1508 layout
1509 .fill_full_from_free(&[1.0], &mut [0.0, 0.0, 0.0])
1510 .unwrap_err(),
1511 ParamError::FreeLengthMismatch {
1512 expected: 2,
1513 actual: 1
1514 }
1515 );
1516 }
1517
1518 #[test]
1519 fn free_dimension_contract_is_shared_by_projection_operations() {
1520 let layout = ParamLayout::new([
1521 Parameter::fixed("fixed", 4.0),
1522 Parameter::free("x"),
1523 Parameter::free("y"),
1524 ])
1525 .unwrap();
1526 let expected = ParamError::FreeLengthMismatch {
1527 expected: 2,
1528 actual: 1,
1529 };
1530
1531 assert_eq!(layout.values(&[1.0]).unwrap_err(), expected);
1532 assert_eq!(layout.validate_free_values(&[1.0]).unwrap_err(), expected);
1533 assert_eq!(
1534 layout.wrap_periodic_free_values(&[1.0]).unwrap_err(),
1535 expected
1536 );
1537
1538 let mut values = layout.default_values();
1539 assert_eq!(values.set_free_values(&[1.0]).unwrap_err(), expected);
1540 assert_eq!(values.as_slice(), &[4.0, 0.0, 0.0]);
1541 assert_eq!(
1542 layout.validate_free_values_classified(&[1.0]),
1543 Err(FreeValueValidationError::Parameter(
1544 ParamError::FreeLengthMismatch {
1545 expected: 2,
1546 actual: 1,
1547 }
1548 ))
1549 );
1550 }
1551
1552 #[test]
1553 fn cross_layout_projection_reorders_values_and_scatters_gradients() {
1554 let source = ParamLayout::new([
1555 Parameter::fixed("offset", -1.0),
1556 Parameter::free("x"),
1557 Parameter::free("y"),
1558 ])
1559 .unwrap();
1560 let target = ParamLayout::new([
1561 Parameter::free("y"),
1562 Parameter::fixed("scale", 2.0),
1563 Parameter::free("x"),
1564 ])
1565 .unwrap();
1566 let projection = target.projection_from(&source).unwrap();
1567 let source_values = source.values(&[1.0, 2.0]).unwrap();
1568
1569 assert_eq!(
1570 projection.project(&source_values).unwrap().as_slice(),
1571 &[2.0, 2.0, 1.0]
1572 );
1573
1574 let mut source_gradient = vec![10.0, 20.0];
1575 projection
1576 .scatter_add(&[0.5, 1.5], &mut source_gradient)
1577 .unwrap();
1578 assert_eq!(source_gradient, vec![11.5, 20.5]);
1579 }
1580
1581 #[test]
1582 fn cross_layout_projection_rejects_missing_and_fixed_source_parameters() {
1583 let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1584 let missing = ParamLayout::new([Parameter::free("y")]).unwrap();
1585 assert_eq!(
1586 target.projection_from(&missing).unwrap_err(),
1587 ParamError::UnknownName("x".into())
1588 );
1589
1590 let fixed = ParamLayout::new([Parameter::fixed("x", 1.0)]).unwrap();
1591 assert!(matches!(
1592 target.projection_from(&fixed),
1593 Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1594 ));
1595 }
1596
1597 #[test]
1598 fn cross_layout_projection_supports_zero_free_layouts() {
1599 let source = ParamLayout::new([Parameter::fixed("source", 3.0)]).unwrap();
1600 let target = ParamLayout::new([Parameter::fixed("target", 4.0)]).unwrap();
1601 let projection = target.projection_from(&source).unwrap();
1602 let values = source.default_values();
1603
1604 assert_eq!(projection.project(&values).unwrap().as_slice(), &[4.0]);
1605 let mut gradient = Vec::new();
1606 projection.scatter_add(&[], &mut gradient).unwrap();
1607 }
1608
1609 #[test]
1610 fn cross_layout_projection_accepts_reordered_compatible_source_layouts() {
1611 let source = ParamLayout::new([Parameter::free("x")]).unwrap();
1612 let other =
1613 ParamLayout::new([Parameter::fixed("extra", 9.0), Parameter::fixed("x", 3.0)]).unwrap();
1614 let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1615 let projection = target.projection_from(&source).unwrap();
1616 let values = other.default_values();
1617
1618 assert_eq!(projection.project(&values).unwrap().as_slice(), &[3.0]);
1619 assert_eq!(values.as_slice(), &[9.0, 3.0]);
1620 }
1621
1622 #[test]
1623 fn cross_layout_projection_rejects_missing_alternate_source_name() {
1624 let source = ParamLayout::new([Parameter::free("x")]).unwrap();
1625 let other = ParamLayout::new([Parameter::free("y")]).unwrap();
1626 let target = ParamLayout::new([Parameter::free("x")]).unwrap();
1627 let projection = target.projection_from(&source).unwrap();
1628 let values = other.values(&[7.0]).unwrap();
1629
1630 assert_eq!(
1631 projection.project(&values).unwrap_err(),
1632 ParamError::UnknownName("x".into())
1633 );
1634 assert_eq!(values.as_slice(), &[7.0]);
1635 }
1636
1637 #[test]
1638 fn registry_merges_identical_parameters_in_first_seen_order() {
1639 let mut registry = ParamRegistry::new();
1640 let y = registry
1641 .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1642 .unwrap();
1643 let x = registry.register(Parameter::free("x")).unwrap();
1644 let y_again = registry
1645 .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1646 .unwrap();
1647
1648 assert_eq!(y.index(), 0);
1649 assert_eq!(x.index(), 1);
1650 assert_eq!(y_again, y);
1651
1652 let layout = registry.layout().unwrap();
1653 assert_eq!(
1654 layout
1655 .specs()
1656 .iter()
1657 .map(Parameter::name)
1658 .collect::<Vec<_>>(),
1659 vec!["y", "x"]
1660 );
1661 }
1662
1663 #[test]
1664 fn registry_rejects_incompatible_parameter_reuse() {
1665 let mut registry = ParamRegistry::new();
1666 registry
1667 .register(Parameter::free("x").with_initial(1.0))
1668 .unwrap();
1669
1670 assert!(matches!(
1671 registry.register(Parameter::free("x").with_initial(2.0)),
1672 Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1673 ));
1674 }
1675}