nereids_fitting/parameters.rs
1//! Fit parameter types, bounds, and constraints.
2//!
3//! Parameters for the forward model that can be fitted or held fixed.
4//! Supports non-negativity constraints and sum-to-one constraints for
5//! isotopes of the same element.
6
7use std::borrow::Cow;
8
9/// A single fit parameter with value, bounds, and fixed/free flag.
10///
11/// The `name` field uses `Cow<'static, str>` so that static strings
12/// (e.g. `"U-238"`, `"temperature_k"`) avoid heap allocation entirely,
13/// while dynamic strings from `format!()` still work via the `Owned` variant.
14/// When a `ParameterSet` template is cloned per-pixel, `Cow::Borrowed` names
15/// are copied as a pointer+length (16 bytes, no heap) instead of allocating
16/// a new `String` on the heap.
17#[derive(Debug, Clone)]
18pub struct FitParameter {
19 /// Parameter name (for reporting).
20 pub name: Cow<'static, str>,
21 /// Current value.
22 pub value: f64,
23 /// Lower bound (f64::NEG_INFINITY if unbounded).
24 pub lower: f64,
25 /// Upper bound (f64::INFINITY if unbounded).
26 pub upper: f64,
27 /// If true, parameter is held fixed during fitting.
28 pub fixed: bool,
29}
30
31impl FitParameter {
32 /// Create a new free parameter with non-negativity constraint.
33 pub fn non_negative(name: impl Into<Cow<'static, str>>, value: f64) -> Self {
34 Self {
35 name: name.into(),
36 value,
37 lower: 0.0,
38 upper: f64::INFINITY,
39 fixed: false,
40 }
41 }
42
43 /// Create a new free parameter with no bounds.
44 pub fn unbounded(name: impl Into<Cow<'static, str>>, value: f64) -> Self {
45 Self {
46 name: name.into(),
47 value,
48 lower: f64::NEG_INFINITY,
49 upper: f64::INFINITY,
50 fixed: false,
51 }
52 }
53
54 /// Create a fixed parameter.
55 pub fn fixed(name: impl Into<Cow<'static, str>>, value: f64) -> Self {
56 Self {
57 name: name.into(),
58 value,
59 lower: f64::NEG_INFINITY,
60 upper: f64::INFINITY,
61 fixed: true,
62 }
63 }
64
65 /// Clamp value to bounds.
66 pub fn clamp(&mut self) {
67 self.value = self.value.clamp(self.lower, self.upper);
68 }
69}
70
71/// Collection of fit parameters for a forward model fit.
72#[derive(Debug, Clone)]
73pub struct ParameterSet {
74 /// All parameters (fixed + free).
75 pub params: Vec<FitParameter>,
76}
77
78impl ParameterSet {
79 pub fn new(params: Vec<FitParameter>) -> Self {
80 Self { params }
81 }
82
83 /// Number of free (non-fixed) parameters.
84 pub fn n_free(&self) -> usize {
85 self.params.iter().filter(|p| !p.fixed).count()
86 }
87
88 /// Get the values of all free parameters as a vector.
89 pub fn free_values(&self) -> Vec<f64> {
90 self.params
91 .iter()
92 .filter(|p| !p.fixed)
93 .map(|p| p.value)
94 .collect()
95 }
96
97 /// Set the values of free parameters from a vector.
98 ///
99 /// # Panics
100 /// Panics if `values.len() != self.n_free()`.
101 pub fn set_free_values(&mut self, values: &[f64]) {
102 let n_free = self.n_free();
103 assert_eq!(
104 values.len(),
105 n_free,
106 "set_free_values: values length ({}) must match number of free parameters ({})",
107 values.len(),
108 n_free,
109 );
110 let mut j = 0;
111 for p in &mut self.params {
112 if !p.fixed {
113 p.value = values[j];
114 p.clamp();
115 j += 1;
116 }
117 }
118 }
119
120 /// Get the value of all parameters (fixed + free) as a vector.
121 pub fn all_values(&self) -> Vec<f64> {
122 self.params.iter().map(|p| p.value).collect()
123 }
124
125 /// Write all parameter values into the provided buffer, resizing if needed.
126 ///
127 /// This is the buffer-reuse counterpart of [`all_values`](Self::all_values).
128 /// Callers that fit many pixels in a loop can allocate one `Vec<f64>` and
129 /// reuse it across iterations to avoid per-pixel allocation churn.
130 pub fn all_values_into(&self, buf: &mut Vec<f64>) {
131 buf.clear();
132 buf.extend(self.params.iter().map(|p| p.value));
133 }
134
135 /// Write free parameter values into the provided buffer, resizing if needed.
136 ///
137 /// This is the buffer-reuse counterpart of [`free_values`](Self::free_values).
138 /// Callers that fit many pixels in a loop can allocate one `Vec<f64>` and
139 /// reuse it across iterations to avoid per-pixel allocation churn.
140 pub fn free_values_into(&self, buf: &mut Vec<f64>) {
141 buf.clear();
142 buf.extend(self.params.iter().filter(|p| !p.fixed).map(|p| p.value));
143 }
144
145 /// Indices (into `params`) of free parameters.
146 pub fn free_indices(&self) -> Vec<usize> {
147 self.params
148 .iter()
149 .enumerate()
150 .filter(|(_, p)| !p.fixed)
151 .map(|(i, _)| i)
152 .collect()
153 }
154
155 /// Write free parameter indices into the provided buffer, resizing if needed.
156 ///
157 /// This is the buffer-reuse counterpart of [`free_indices`](Self::free_indices).
158 /// Callers that compute the Jacobian many times in a loop can allocate one
159 /// `Vec<usize>` and reuse it across iterations to avoid per-call allocation.
160 pub fn free_indices_into(&self, buf: &mut Vec<usize>) {
161 buf.clear();
162 buf.extend(
163 self.params
164 .iter()
165 .enumerate()
166 .filter(|(_, p)| !p.fixed)
167 .map(|(i, _)| i),
168 );
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_parameter_set_free_values() {
178 let params = ParameterSet::new(vec![
179 FitParameter::non_negative("a", 1.0),
180 FitParameter::fixed("b", 2.0),
181 FitParameter::non_negative("c", 3.0),
182 ]);
183
184 assert_eq!(params.n_free(), 2);
185 assert_eq!(params.free_values(), vec![1.0, 3.0]);
186 assert_eq!(params.all_values(), vec![1.0, 2.0, 3.0]);
187 }
188
189 #[test]
190 #[should_panic(expected = "set_free_values: values length")]
191 fn test_set_free_values_length_mismatch() {
192 let mut params = ParameterSet::new(vec![
193 FitParameter::non_negative("a", 1.0),
194 FitParameter::non_negative("b", 2.0),
195 ]);
196 // 2 free params but only 1 value — should panic
197 params.set_free_values(&[1.0]);
198 }
199
200 #[test]
201 fn test_set_free_values_with_clamping() {
202 let mut params = ParameterSet::new(vec![
203 FitParameter::non_negative("a", 1.0),
204 FitParameter::fixed("b", 2.0),
205 FitParameter::non_negative("c", 3.0),
206 ]);
207
208 // Set a to -0.5 (should clamp to 0.0) and c to 5.0
209 params.set_free_values(&[-0.5, 5.0]);
210 assert_eq!(params.params[0].value, 0.0); // clamped
211 assert_eq!(params.params[1].value, 2.0); // fixed, unchanged
212 assert_eq!(params.params[2].value, 5.0);
213 }
214}