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: ParamProjection,
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 ParamProjection {
501 free_params: Arc<[ParamId]>,
502 full_to_free: Arc<[Option<FreeParamId>]>,
503 defaults: Arc<[f64]>,
504}
505
506impl ParamProjection {
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(Serialize, Deserialize)]
558struct ParamLayoutSerde {
561 specs: Arc<[Parameter]>,
562 names: Arc<HashMap<Arc<str>, ParamId>>,
563 free_params: Arc<[ParamId]>,
564 full_to_free: Arc<[Option<FreeParamId>]>,
565 defaults: Arc<[f64]>,
566}
567
568impl Serialize for ParamLayout {
569 fn serialize<__S>(&self, serializer: __S) -> Result<__S::Ok, __S::Error>
570 where
571 __S: Serializer,
572 {
573 ParamLayoutSerde {
574 specs: Arc::clone(&self.specs),
575 names: Arc::clone(&self.names),
576 free_params: Arc::clone(&self.projection.free_params),
577 full_to_free: Arc::clone(&self.projection.full_to_free),
578 defaults: Arc::clone(&self.projection.defaults),
579 }
580 .serialize(serializer)
581 }
582}
583
584impl<'de> Deserialize<'de> for ParamLayout {
585 fn deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
586 where
587 __D: Deserializer<'de>,
588 {
589 let serialized = ParamLayoutSerde::deserialize(deserializer)?;
590 Ok(Self {
591 specs: serialized.specs,
592 names: serialized.names,
593 projection: ParamProjection {
594 free_params: serialized.free_params,
595 full_to_free: serialized.full_to_free,
596 defaults: serialized.defaults,
597 },
598 })
599 }
600}
601
602struct LayoutBuilder {
603 specs: Vec<Parameter>,
604 names: HashMap<Arc<str>, ParamId>,
605 free_params: Vec<ParamId>,
606 full_to_free: Vec<Option<FreeParamId>>,
607 defaults: Vec<f64>,
608}
609
610impl LayoutBuilder {
611 fn with_capacity(capacity: usize) -> Self {
612 Self {
613 specs: Vec::with_capacity(capacity),
614 names: HashMap::with_capacity(capacity),
615 free_params: Vec::new(),
616 full_to_free: Vec::with_capacity(capacity),
617 defaults: Vec::with_capacity(capacity),
618 }
619 }
620
621 fn push_validated(&mut self, spec: Parameter) -> ParamResult<()> {
622 spec.validate()?;
623 let id = ParamId(self.specs.len() as u32);
624 if self.names.insert(Arc::clone(&spec.name), id).is_some() {
625 return Err(ParamError::DuplicateName(spec.name().to_owned()));
626 }
627 self.defaults.push(spec.default_value());
628 match spec.state {
629 ParamState::Free => {
630 let free_id = FreeParamId(self.free_params.len() as u32);
631 self.free_params.push(id);
632 self.full_to_free.push(Some(free_id));
633 }
634 ParamState::Fixed(_) => self.full_to_free.push(None),
635 }
636 self.specs.push(spec);
637 Ok(())
638 }
639
640 fn finish(self) -> ParamLayout {
641 ParamLayout {
642 specs: self.specs.into(),
643 names: Arc::new(self.names),
644 projection: ParamProjection {
645 free_params: self.free_params.into(),
646 full_to_free: self.full_to_free.into(),
647 defaults: self.defaults.into(),
648 },
649 }
650 }
651}
652
653impl ParamLayout {
654 pub fn new<S>(specs: impl IntoIterator<Item = S>) -> ParamResult<Self>
662 where
663 S: Into<Parameter>,
664 {
665 let specs: Vec<_> = specs.into_iter().map(Into::into).collect();
666 let mut builder = LayoutBuilder::with_capacity(specs.len());
667 for spec in specs {
668 builder.push_validated(spec)?;
669 }
670 Ok(builder.finish())
671 }
672
673 pub fn specs(&self) -> &[Parameter] {
675 &self.specs
676 }
677
678 pub fn len(&self) -> usize {
680 self.specs.len()
681 }
682
683 pub fn is_empty(&self) -> bool {
685 self.specs.is_empty()
686 }
687
688 pub fn n_free(&self) -> usize {
690 self.projection.n_free()
691 }
692
693 pub fn id(&self, name: &str) -> Option<ParamId> {
695 self.names.get(name).copied()
696 }
697
698 pub fn name(&self, id: ParamId) -> ParamResult<&str> {
705 self.check_id(id)?;
706 Ok(self.specs[id.index()].name())
707 }
708
709 pub fn spec(&self, id: ParamId) -> ParamResult<&Parameter> {
716 self.check_id(id)?;
717 Ok(&self.specs[id.index()])
718 }
719
720 pub fn free_id(&self, id: ParamId) -> ParamResult<Option<FreeParamId>> {
729 self.check_id(id)?;
730 Ok(self.projection.free_id(id))
731 }
732
733 fn free_param(&self, id: FreeParamId) -> ParamResult<ParamId> {
734 self.check_free_id(id)?;
735 Ok(self.projection.full_id(id))
736 }
737
738 pub fn free_params(&self) -> &[ParamId] {
740 self.projection.free_params()
741 }
742
743 pub fn free_parameters(
745 &self,
746 ) -> impl ExactSizeIterator<Item = &Parameter> + DoubleEndedIterator {
747 self.free_params().iter().map(|id| &self.specs[id.index()])
748 }
749
750 pub fn default_values(&self) -> ParamValues {
752 ParamValues {
753 layout: Arc::new(self.clone()),
754 values: self.projection.defaults.to_vec(),
755 }
756 }
757
758 pub fn initial_free_values(&self) -> Vec<f64> {
762 self.projection.initial_free_values()
763 }
764
765 pub fn values(&self, free: &[f64]) -> ParamResult<ParamValues> {
772 let mut values = self.projection.defaults.to_vec();
773 self.fill_full_from_free(free, &mut values)?;
774 Ok(ParamValues {
775 layout: Arc::new(self.clone()),
776 values,
777 })
778 }
779
780 pub fn free_values_with(&self, mut value: impl FnMut(&Parameter) -> f64) -> Vec<f64> {
782 self.free_parameters().map(&mut value).collect()
783 }
784
785 pub fn sample_initial(&self, seed: u64) -> Vec<f64> {
787 let mut rng = Rng::with_seed(seed);
788 self.free_values_with(|parameter| parameter.initial.sample_with(&mut rng))
789 }
790
791 pub fn validate_free_values(&self, free: &[f64]) -> ParamResult<()> {
799 self.projection.validate_free_dimension(free)?;
800 for (value, parameter) in free.iter().zip(self.free_parameters()) {
801 parameter.validate_value(*value)?;
802 }
803 Ok(())
804 }
805
806 pub fn validate_free_values_classified(
819 &self,
820 free: &[f64],
821 ) -> Result<(), FreeValueValidationError> {
822 self.projection.validate_free_dimension(free)?;
823 for (index, (value, parameter)) in free.iter().zip(self.free_parameters()).enumerate() {
824 let id = FreeParamId(index as u32);
825 if !value.is_finite() {
826 return Err(FreeValueValidationError::NonFiniteValue {
827 id,
828 name: parameter.name().to_owned(),
829 value: *value,
830 });
831 }
832 if !parameter.contains_in_support(*value) {
833 return Err(FreeValueValidationError::OutsideSupport {
834 id,
835 name: parameter.name().to_owned(),
836 value: *value,
837 });
838 }
839 }
840 Ok(())
841 }
842
843 pub fn wrap_periodic_free_values(&self, free: &[f64]) -> ParamResult<Vec<f64>> {
851 self.projection.validate_free_dimension(free)?;
852 Ok(free
853 .iter()
854 .zip(self.free_params().iter())
855 .map(|(value, id)| {
856 let parameter = &self.specs[id.index()];
857 parameter.periodic_bounds().map_or(*value, |(min, max)| {
858 min + (*value - min).rem_euclid(max - min)
859 })
860 })
861 .collect())
862 }
863
864 fn fill_full_from_free(&self, free: &[f64], full: &mut [f64]) -> ParamResult<()> {
865 self.projection.fill_full_from_free(free, full)
866 }
867
868 fn check_id(&self, id: ParamId) -> ParamResult<()> {
869 if id.index() >= self.len() {
870 Err(ParamError::InvalidParamId {
871 id: id.index(),
872 len: self.len(),
873 })
874 } else {
875 Ok(())
876 }
877 }
878
879 fn check_free_id(&self, id: FreeParamId) -> ParamResult<()> {
880 if id.index() >= self.n_free() {
881 Err(ParamError::InvalidFreeParamId {
882 id: id.index(),
883 len: self.n_free(),
884 })
885 } else {
886 Ok(())
887 }
888 }
889}
890
891#[derive(Clone, Debug, Default)]
893pub struct ParamRegistry {
894 specs: Vec<Parameter>,
895 names: HashMap<Arc<str>, ParamId>,
896}
897
898impl ParamRegistry {
899 pub fn new() -> Self {
901 Self::default()
902 }
903
904 pub fn register<S>(&mut self, spec: S) -> ParamResult<ParamId>
915 where
916 S: Into<Parameter>,
917 {
918 let spec = spec.into();
919 if spec.name().is_empty() {
920 return Err(ParamError::EmptyName);
921 }
922 if let Some(id) = self.names.get(spec.name()).copied() {
923 let existing = &self.specs[id.index()];
924 if existing != &spec {
925 return Err(ParamError::ParameterConflict {
926 name: spec.name().to_owned(),
927 reason: "duplicate parameter name has incompatible metadata".into(),
928 });
929 }
930 return Ok(id);
931 }
932
933 let id = ParamId(self.specs.len() as u32);
934 self.names.insert(Arc::clone(&spec.name), id);
935 self.specs.push(spec);
936 Ok(id)
937 }
938
939 pub fn layout(&self) -> ParamResult<ParamLayout> {
946 ParamLayout::new(self.specs.clone())
947 }
948}
949
950#[derive(Clone, Debug, Serialize, Deserialize)]
952pub struct ParamValues {
953 layout: Arc<ParamLayout>,
954 values: Vec<f64>,
955}
956
957impl ParamValues {
958 pub fn layout(&self) -> &Arc<ParamLayout> {
960 &self.layout
961 }
962
963 pub fn as_slice(&self) -> &[f64] {
965 &self.values
966 }
967
968 pub fn get(&self, id: ParamId) -> ParamResult<f64> {
975 self.layout.check_id(id)?;
976 Ok(self.values[id.index()])
977 }
978
979 pub fn free_values(&self) -> Vec<f64> {
981 self.layout.projection.free_values_from_full(&self.values)
982 }
983
984 pub fn set_free(&mut self, id: FreeParamId, value: f64) -> ParamResult<()> {
991 let full_id = self.layout.free_param(id)?;
992 self.values[full_id.index()] = value;
993 Ok(())
994 }
995
996 pub fn set_free_values(&mut self, values: &[f64]) -> ParamResult<()> {
1003 let layout = Arc::clone(&self.layout);
1004 layout.fill_full_from_free(values, &mut self.values)
1005 }
1006}
1007
1008#[macro_export]
1011macro_rules! parameter {
1012 ($name:expr) => {{
1013 $crate::parameters::Parameter::free($name)
1014 }};
1015
1016 ($name:expr, $value:expr) => {{
1017 $crate::parameters::Parameter::fixed($name, $value)
1018 }};
1019
1020 ($name:expr, $($rest:tt)+) => {{
1021 let mut p = $crate::parameters::Parameter::free($name);
1022 $crate::parameter!(@parse p, [fixed = false, initial = false]; $($rest)+);
1023 p
1024 }};
1025
1026 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; ) => {};
1027
1028 (@parse $p:ident, [fixed = false, initial = false]; fixed : $value:expr $(, $($rest:tt)*)?) => {{
1029 $p = $p.with_fixed_value($value);
1030 $crate::parameter!(@parse $p, [fixed = true, initial = false]; $($($rest)*)?);
1031 }};
1032
1033 (@parse $p:ident, [fixed = false, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {{
1034 $p = $p.with_initial($value);
1035 $crate::parameter!(@parse $p, [fixed = false, initial = true]; $($($rest)*)?);
1036 }};
1037
1038 (@parse $p:ident, [fixed = true, initial = false]; initial : $value:expr $(, $($rest:tt)*)?) => {
1039 compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1040 };
1041
1042 (@parse $p:ident, [fixed = false, initial = true]; fixed : $value:expr $(, $($rest:tt)*)?) => {
1043 compile_error!("parameter!: cannot specify both `fixed` and `initial`");
1044 };
1045
1046 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; bounds : ($min:expr, $max:expr) $(, $($rest:tt)*)?) => {{
1047 $p = $p.with_bounds($min, $max);
1048 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1049 }};
1050
1051 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic : $value:expr $(, $($rest:tt)*)?) => {{
1052 $p = $p.with_periodicity($value);
1053 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1054 }};
1055
1056 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; periodic $(, $($rest:tt)*)?) => {{
1057 $p = $p.with_periodic();
1058 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1059 }};
1060
1061 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; scale : $value:expr $(, $($rest:tt)*)?) => {{
1062 $p = $p.with_scale($value);
1063 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1064 }};
1065
1066 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; unit : $value:expr $(, $($rest:tt)*)?) => {{
1067 $p = $p.with_unit($value);
1068 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1069 }};
1070
1071 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; latex : $value:expr $(, $($rest:tt)*)?) => {{
1072 $p = $p.with_latex($value);
1073 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1074 }};
1075
1076 (@parse $p:ident, [fixed = $f:tt, initial = $i:tt]; description : $value:expr $(, $($rest:tt)*)?) => {{
1077 $p = $p.with_description($value);
1078 $crate::parameter!(@parse $p, [fixed = $f, initial = $i]; $($($rest)*)?);
1079 }};
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085
1086 #[test]
1087 fn parameter_macro_constructs_fixed_parameters() {
1088 let positional = crate::parameter!("positional", 1.25);
1089 let named = crate::parameter!("named", fixed: -0.5);
1090
1091 assert_eq!(positional.state(), &ParamState::Fixed(1.25));
1092 assert_eq!(named.state(), &ParamState::Fixed(-0.5));
1093 }
1094
1095 #[test]
1096 fn parameter_scale_is_validated_and_supported_by_the_macro() {
1097 let scaled = crate::parameter!("scaled", initial: 2.0, scale: 0.25);
1098 let layout = ParamLayout::new([scaled]).unwrap();
1099 assert_eq!(layout.specs()[0].scale(), Some(0.25));
1100
1101 let error = ParamLayout::new([Parameter::free("bad").with_scale(0.0)]).unwrap_err();
1102 assert!(matches!(error, ParamError::InvalidScale { .. }));
1103 }
1104
1105 #[test]
1106 fn layout_tracks_free_and_fixed_values() {
1107 let layout = ParamLayout::new([
1108 Parameter::free("mass")
1109 .with_initial(1.2)
1110 .with_bounds(Some(0.0), Some(2.0)),
1111 Parameter::fixed("pi", std::f64::consts::PI),
1112 Parameter::free("width").with_initial((0.0, 1.0)),
1113 ])
1114 .unwrap();
1115
1116 assert_eq!(layout.len(), 3);
1117 assert_eq!(layout.n_free(), 2);
1118 assert_eq!(layout.initial_free_values(), vec![1.2, 0.5]);
1119 assert_eq!(layout.id("mass").map(ParamId::index), Some(0));
1120 assert_eq!(layout.id("pi").map(ParamId::index), Some(1));
1121 assert_eq!(layout.id("width").map(ParamId::index), Some(2));
1122 assert_eq!(
1123 layout
1124 .free_params()
1125 .iter()
1126 .map(|id| layout.name(*id).unwrap())
1127 .collect::<Vec<_>>(),
1128 vec!["mass", "width"]
1129 );
1130
1131 let values = layout.values(&[1.4, 0.2]).unwrap();
1132 assert_eq!(values.as_slice(), &[1.4, std::f64::consts::PI, 0.2]);
1133 assert_eq!(values.free_values(), vec![1.4, 0.2]);
1134 }
1135
1136 #[test]
1137 fn free_values_can_be_generated_or_sampled_in_layout_order() {
1138 let layout = ParamLayout::new([
1139 Parameter::fixed("fixed", 8.0),
1140 Parameter::free("uniform").with_initial((-2.0, 4.0)),
1141 Parameter::free("value").with_initial(3.0),
1142 Parameter::free("default"),
1143 ])
1144 .unwrap();
1145
1146 assert_eq!(layout.initial_free_values(), vec![1.0, 3.0, 0.0]);
1147 assert_eq!(layout.sample_initial(0), vec![1.6157656431461036, 3.0, 0.0]);
1148 assert_eq!(
1149 layout.free_values_with(|parameter| parameter.name().len() as f64),
1150 vec![7.0, 5.0, 7.0]
1151 );
1152 assert_eq!(
1153 layout
1154 .free_parameters()
1155 .map(Parameter::name)
1156 .collect::<Vec<_>>(),
1157 vec!["uniform", "value", "default"]
1158 );
1159 }
1160
1161 #[test]
1162 fn deterministic_and_sampled_initial_values_share_initial_spec_semantics() {
1163 let layout = ParamLayout::new([
1164 Parameter::free("default"),
1165 Parameter::free("value").with_initial(2.5),
1166 Parameter::free("uniform").with_initial((-4.0, 6.0)),
1167 ])
1168 .unwrap();
1169
1170 assert_eq!(layout.initial_free_values(), vec![0.0, 2.5, 1.0]);
1171 for seed in 0..32 {
1172 let sampled = layout.sample_initial(seed);
1173 assert_eq!(sampled[0], 0.0);
1174 assert_eq!(sampled[1], 2.5);
1175 assert!((-4.0..6.0).contains(&sampled[2]));
1176 }
1177 }
1178
1179 #[test]
1180 fn classified_free_value_validation_separates_failure_kinds() {
1181 let layout = ParamLayout::new([
1182 Parameter::free("bounded").with_bounds(-1.0, 1.0),
1183 Parameter::free("phase")
1184 .with_bounds(0.0, std::f64::consts::TAU)
1185 .with_periodic(),
1186 ])
1187 .unwrap();
1188
1189 assert_eq!(
1190 layout.validate_free_values_classified(&[0.0]),
1191 Err(FreeValueValidationError::Parameter(
1192 ParamError::FreeLengthMismatch {
1193 expected: 2,
1194 actual: 1,
1195 }
1196 ))
1197 );
1198 assert!(matches!(
1199 layout.validate_free_values_classified(&[f64::NAN, 0.0]),
1200 Err(FreeValueValidationError::NonFiniteValue { id, name, value })
1201 if id.index() == 0 && name == "bounded" && value.is_nan()
1202 ));
1203 assert_eq!(
1204 layout.validate_free_values_classified(&[2.0, 0.0]),
1205 Err(FreeValueValidationError::OutsideSupport {
1206 id: FreeParamId(0),
1207 name: "bounded".into(),
1208 value: 2.0,
1209 })
1210 );
1211 assert_eq!(
1212 layout.validate_free_values_classified(&[0.0, std::f64::consts::TAU]),
1213 Err(FreeValueValidationError::OutsideSupport {
1214 id: FreeParamId(1),
1215 name: "phase".into(),
1216 value: std::f64::consts::TAU,
1217 })
1218 );
1219 assert!(layout.validate_free_values_classified(&[1.0, 0.0]).is_ok());
1220 }
1221
1222 #[test]
1223 fn periodic_domains_wrap_and_validate_without_changing_bounds() {
1224 let tau = std::f64::consts::TAU;
1225 let phase = Parameter::free("phase")
1226 .with_initial(0.25)
1227 .with_bounds(0.0, tau)
1228 .with_periodic();
1229 assert_eq!(phase.periodic_bounds(), Some((0.0, tau)));
1230
1231 let layout = ParamLayout::new([phase]).unwrap();
1232 assert_eq!(
1233 layout.wrap_periodic_free_values(&[-0.25]).unwrap(),
1234 vec![tau - 0.25]
1235 );
1236 assert!(layout.validate_free_values(&[tau - 0.25]).is_ok());
1237 assert!(matches!(
1238 layout.validate_free_values(&[tau]),
1239 Err(ParamError::ValueOutsidePeriodicDomain { .. })
1240 ));
1241 }
1242
1243 #[test]
1244 fn invalid_periodic_metadata_and_initial_values_are_rejected() {
1245 assert!(matches!(
1246 ParamLayout::new([Parameter::free("phase").with_periodic()]),
1247 Err(ParamError::PeriodicRequiresFiniteBounds { .. })
1248 ));
1249 assert!(matches!(
1250 ParamLayout::new([Parameter::free("phase")
1251 .with_initial(std::f64::consts::TAU)
1252 .with_bounds(0.0, std::f64::consts::TAU)
1253 .with_periodic(),]),
1254 Err(ParamError::ValueOutsidePeriodicDomain { .. })
1255 ));
1256 }
1257
1258 #[test]
1259 fn duplicate_names_are_rejected() {
1260 let err = ParamLayout::new([Parameter::free("x"), Parameter::fixed("x", 1.0)]).unwrap_err();
1261 assert_eq!(err, ParamError::DuplicateName("x".into()));
1262 }
1263
1264 #[test]
1265 fn free_length_is_checked() {
1266 let layout = ParamLayout::new([Parameter::free("x"), Parameter::free("y")]).unwrap();
1267 let err = layout.values(&[1.0]).unwrap_err();
1268 assert_eq!(
1269 err,
1270 ParamError::FreeLengthMismatch {
1271 expected: 2,
1272 actual: 1
1273 }
1274 );
1275 }
1276
1277 #[test]
1278 fn full_and_free_vectors_round_trip_in_stable_order() {
1279 let layout = ParamLayout::new([
1280 Parameter::fixed("offset", -1.0),
1281 Parameter::free("mass").with_initial(1.2),
1282 Parameter::fixed("scale", 2.0),
1283 Parameter::free("width").with_initial(0.1),
1284 ])
1285 .unwrap();
1286
1287 let full = layout.values(&[1.4, 0.2]).unwrap();
1288 assert_eq!(full.as_slice(), &[-1.0, 1.4, 2.0, 0.2]);
1289 let mut rewritten = vec![0.0; layout.len()];
1290 layout
1291 .fill_full_from_free(&[1.5, 0.3], &mut rewritten)
1292 .unwrap();
1293 assert_eq!(rewritten, vec![-1.0, 1.5, 2.0, 0.3]);
1294 }
1295
1296 #[test]
1297 fn values_only_mutate_free_parameters() {
1298 let layout = ParamLayout::new([
1299 Parameter::fixed("fixed", 1.0),
1300 Parameter::free("x"),
1301 Parameter::free("y"),
1302 ])
1303 .unwrap();
1304 let x_id = layout.id("x").unwrap();
1305 let y_id = layout.id("y").unwrap();
1306 let x_free = layout.free_id(x_id).unwrap().unwrap();
1307 let y_free = layout.free_id(y_id).unwrap().unwrap();
1308
1309 let mut values = layout.default_values();
1310 values.set_free(x_free, 3.0).unwrap();
1311 values.set_free(y_free, 4.0).unwrap();
1312
1313 assert_eq!(values.as_slice(), &[1.0, 3.0, 4.0]);
1314 assert_eq!(values.free_values(), vec![3.0, 4.0]);
1315 }
1316
1317 #[test]
1318 fn invalid_specs_are_rejected() {
1319 assert_eq!(
1320 ParamLayout::new([Parameter::free("")]).unwrap_err(),
1321 ParamError::EmptyName
1322 );
1323
1324 assert_eq!(
1325 ParamLayout::new([Parameter::free("x").with_bounds(Some(2.0), Some(1.0))]).unwrap_err(),
1326 ParamError::InvalidBounds {
1327 name: "x".into(),
1328 min: 2.0,
1329 max: 1.0
1330 }
1331 );
1332
1333 assert_eq!(
1334 ParamLayout::new([Parameter::free("x").with_initial((2.0, 1.0))]).unwrap_err(),
1335 ParamError::InvalidInitialRange {
1336 name: "x".into(),
1337 min: 2.0,
1338 max: 1.0
1339 }
1340 );
1341
1342 assert_eq!(
1343 ParamLayout::new([Parameter::free("x")
1344 .with_initial(3.0)
1345 .with_bounds(Some(0.0), Some(2.0))])
1346 .unwrap_err(),
1347 ParamError::InitialOutOfBounds {
1348 name: "x".into(),
1349 value: 3.0
1350 }
1351 );
1352
1353 assert_eq!(
1354 ParamLayout::new([Parameter::free("x")
1355 .with_initial((-1.0, 1.0))
1356 .with_bounds(Some(0.0), Some(2.0))])
1357 .unwrap_err(),
1358 ParamError::InitialRangeOutOfBounds {
1359 name: "x".into(),
1360 min: -1.0,
1361 max: 1.0
1362 }
1363 );
1364
1365 assert_eq!(
1366 ParamLayout::new([Parameter::fixed("x", 3.0).with_bounds(Some(0.0), Some(2.0))])
1367 .unwrap_err(),
1368 ParamError::FixedValueOutOfBounds {
1369 name: "x".into(),
1370 value: 3.0
1371 }
1372 );
1373 }
1374
1375 #[test]
1376 fn direct_and_registry_layouts_report_the_same_invalid_spec_errors() {
1377 let invalid = [
1378 Parameter::fixed("fixed", 2.0).with_bounds(0.0, 1.0),
1379 Parameter::free("default").with_bounds(1.0, 2.0),
1380 Parameter::free("value")
1381 .with_initial(2.0)
1382 .with_bounds(0.0, 1.0),
1383 Parameter::free("range")
1384 .with_initial((-1.0, 0.5))
1385 .with_bounds(0.0, 1.0),
1386 Parameter::free("periodic-value")
1387 .with_initial(std::f64::consts::TAU)
1388 .with_bounds(0.0, std::f64::consts::TAU)
1389 .with_periodic(),
1390 ];
1391
1392 for parameter in invalid {
1393 let direct = ParamLayout::new([parameter.clone()]).unwrap_err();
1394 let mut registry = ParamRegistry::new();
1395 registry.register(parameter).unwrap();
1396 assert_eq!(registry.layout().unwrap_err(), direct);
1397 }
1398 }
1399
1400 #[test]
1401 fn free_vector_lengths_are_checked() {
1402 let layout = ParamLayout::new([
1403 Parameter::fixed("a", 0.0),
1404 Parameter::free("x"),
1405 Parameter::free("y"),
1406 ])
1407 .unwrap();
1408
1409 assert_eq!(
1410 layout
1411 .fill_full_from_free(&[1.0], &mut [0.0, 0.0, 0.0])
1412 .unwrap_err(),
1413 ParamError::FreeLengthMismatch {
1414 expected: 2,
1415 actual: 1
1416 }
1417 );
1418 }
1419
1420 #[test]
1421 fn free_dimension_contract_is_shared_by_projection_operations() {
1422 let layout = ParamLayout::new([
1423 Parameter::fixed("fixed", 4.0),
1424 Parameter::free("x"),
1425 Parameter::free("y"),
1426 ])
1427 .unwrap();
1428 let expected = ParamError::FreeLengthMismatch {
1429 expected: 2,
1430 actual: 1,
1431 };
1432
1433 assert_eq!(layout.values(&[1.0]).unwrap_err(), expected);
1434 assert_eq!(layout.validate_free_values(&[1.0]).unwrap_err(), expected);
1435 assert_eq!(
1436 layout.wrap_periodic_free_values(&[1.0]).unwrap_err(),
1437 expected
1438 );
1439
1440 let mut values = layout.default_values();
1441 assert_eq!(values.set_free_values(&[1.0]).unwrap_err(), expected);
1442 assert_eq!(values.as_slice(), &[4.0, 0.0, 0.0]);
1443 assert_eq!(
1444 layout.validate_free_values_classified(&[1.0]),
1445 Err(FreeValueValidationError::Parameter(
1446 ParamError::FreeLengthMismatch {
1447 expected: 2,
1448 actual: 1,
1449 }
1450 ))
1451 );
1452 }
1453
1454 #[test]
1455 fn registry_merges_identical_parameters_in_first_seen_order() {
1456 let mut registry = ParamRegistry::new();
1457 let y = registry
1458 .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1459 .unwrap();
1460 let x = registry.register(Parameter::free("x")).unwrap();
1461 let y_again = registry
1462 .register(Parameter::free("y").with_initial(1.0).with_bounds(0.0, 2.0))
1463 .unwrap();
1464
1465 assert_eq!(y.index(), 0);
1466 assert_eq!(x.index(), 1);
1467 assert_eq!(y_again, y);
1468
1469 let layout = registry.layout().unwrap();
1470 assert_eq!(
1471 layout
1472 .specs()
1473 .iter()
1474 .map(Parameter::name)
1475 .collect::<Vec<_>>(),
1476 vec!["y", "x"]
1477 );
1478 }
1479
1480 #[test]
1481 fn registry_rejects_incompatible_parameter_reuse() {
1482 let mut registry = ParamRegistry::new();
1483 registry
1484 .register(Parameter::free("x").with_initial(1.0))
1485 .unwrap();
1486
1487 assert!(matches!(
1488 registry.register(Parameter::free("x").with_initial(2.0)),
1489 Err(ParamError::ParameterConflict { name, .. }) if name == "x"
1490 ));
1491 }
1492}