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, Default, PartialEq)]
75pub struct ParameterUpdate {
76 pub state: Option<ParamState>,
78 pub initial: Option<InitialSpec>,
80 pub bounds: Option<Bounds>,
82 pub periodic: Option<bool>,
84 pub scale: Option<Option<f64>>,
86 pub unit: Option<Option<String>>,
88 pub latex: Option<Option<String>>,
90 pub description: Option<Option<String>>,
92}
93
94impl ParameterUpdate {
95 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#[derive(Clone, Debug, Error, PartialEq)]
154pub enum FreeValueValidationError {
155 #[error(transparent)]
157 Parameter(#[from] ParamError),
158 #[error("non-finite value {value} for free parameter {name} ({id:?})")]
160 NonFiniteValue {
161 id: FreeParamId,
163 name: String,
165 value: f64,
167 },
168 #[error("value {value} for free parameter {name} ({id:?}) is outside its support")]
170 OutsideSupport {
171 id: FreeParamId,
173 name: String,
175 value: f64,
177 },
178}
179
180#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
182pub struct Bounds {
183 pub min: Option<f64>,
185 pub max: Option<f64>,
187}
188
189impl Bounds {
190 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 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#[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 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 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 pub fn with_fixed_value(mut self, value: f64) -> Self {
283 self.set_fixed_value(value);
284 self
285 }
286
287 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 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 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 pub fn with_periodic(mut self) -> Self {
359 self.periodic = true;
360 self
361 }
362
363 pub fn with_periodicity(mut self, periodic: bool) -> Self {
365 self.periodic = periodic;
366 self
367 }
368
369 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 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 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 pub fn with_description(mut self, description: impl Into<Arc<str>>) -> Self {
404 self.set_description(description);
405 self
406 }
407
408 pub fn name(&self) -> &str {
410 &self.name
411 }
412
413 pub fn state(&self) -> &ParamState {
415 &self.state
416 }
417
418 pub fn is_free(&self) -> bool {
420 matches!(self.state, ParamState::Free)
421 }
422
423 pub fn is_fixed(&self) -> bool {
425 matches!(self.state, ParamState::Fixed(_))
426 }
427
428 pub fn initial_spec(&self) -> &InitialSpec {
430 &self.initial
431 }
432
433 pub fn bounds_spec(&self) -> &Bounds {
435 &self.bounds
436 }
437
438 pub fn is_periodic(&self) -> bool {
440 self.periodic
441 }
442
443 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 pub fn scale(&self) -> Option<f64> {
455 self.scale
456 }
457
458 pub fn unit_label(&self) -> Option<&str> {
460 self.unit.as_deref()
461 }
462
463 pub fn latex_label(&self) -> Option<&str> {
465 self.latex.as_deref()
466 }
467
468 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#[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)]
671struct 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#[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)]
743struct 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 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 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 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 pub fn specs(&self) -> &[Parameter] {
910 &self.specs
911 }
912
913 pub fn len(&self) -> usize {
915 self.specs.len()
916 }
917
918 pub fn is_empty(&self) -> bool {
920 self.specs.is_empty()
921 }
922
923 pub fn n_free(&self) -> usize {
925 self.projection.n_free()
926 }
927
928 pub fn id(&self, name: &str) -> Option<ParamId> {
930 self.names.get(name).copied()
931 }
932
933 pub fn name(&self, id: ParamId) -> ParamResult<&str> {
940 self.check_id(id)?;
941 Ok(self.specs[id.index()].name())
942 }
943
944 pub fn spec(&self, id: ParamId) -> ParamResult<&Parameter> {
951 self.check_id(id)?;
952 Ok(&self.specs[id.index()])
953 }
954
955 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 pub fn free_params(&self) -> &[ParamId] {
975 self.projection.free_params()
976 }
977
978 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 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 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 pub fn initial_free_values(&self) -> Vec<f64> {
1032 self.projection.initial_free_values()
1033 }
1034
1035 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 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 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 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 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 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#[derive(Clone, Debug, Default)]
1163pub struct ParamRegistry {
1164 specs: Vec<Parameter>,
1165 names: HashMap<Arc<str>, ParamId>,
1166}
1167
1168impl ParamRegistry {
1169 pub fn new() -> Self {
1171 Self::default()
1172 }
1173
1174 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 pub fn layout(&self) -> ParamResult<ParamLayout> {
1216 ParamLayout::new(self.specs.clone())
1217 }
1218}
1219
1220#[derive(Clone, Debug, Serialize, Deserialize)]
1222pub struct ParamValues {
1223 layout: Arc<ParamLayout>,
1224 values: Vec<f64>,
1225}
1226
1227impl ParamValues {
1228 pub fn layout(&self) -> &Arc<ParamLayout> {
1230 &self.layout
1231 }
1232
1233 pub fn as_slice(&self) -> &[f64] {
1235 &self.values
1236 }
1237
1238 pub fn get(&self, id: ParamId) -> ParamResult<f64> {
1245 self.layout.check_id(id)?;
1246 Ok(self.values[id.index()])
1247 }
1248
1249 pub fn free_values(&self) -> Vec<f64> {
1251 self.layout.projection.free_values_from_full(&self.values)
1252 }
1253
1254 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 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#[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}