1use std::collections::BTreeSet;
4use std::error::Error;
5use std::f64::consts::PI;
6use std::fmt::{Display, Formatter};
7
8use crate::ParameterBounds;
9
10#[derive(Clone, Debug, PartialEq)]
12pub struct BackgroundBasis {
13 pub rows: usize,
15 pub columns: usize,
17 pub values: Vec<f64>,
19}
20
21impl BackgroundBasis {
22 fn zeros(rows: usize, columns: usize) -> Result<Self, BackgroundError> {
23 let count = rows
24 .checked_mul(columns)
25 .ok_or(BackgroundError::SizeOverflow)?;
26 Ok(Self {
27 rows,
28 columns,
29 values: vec![0.0; count],
30 })
31 }
32
33 #[must_use]
35 pub fn row(&self, index: usize) -> Option<&[f64]> {
36 let start = index.checked_mul(self.columns)?;
37 self.values.get(start..start.checked_add(self.columns)?)
38 }
39
40 #[must_use]
42 pub fn column(&self, index: usize) -> Option<Vec<f64>> {
43 if index >= self.columns {
44 return None;
45 }
46 Some(
47 self.values
48 .chunks_exact(self.columns)
49 .map(|row| row[index])
50 .collect(),
51 )
52 }
53}
54
55pub trait DifferentiableBackground {
57 fn background_id(&self) -> &str;
59 fn parameter_names(&self) -> Vec<String>;
61 fn coefficients(&self) -> Vec<f64>;
63 fn parameter_bounds(&self) -> Vec<ParameterBounds>;
65 fn basis(&self, x_deg: &[f64]) -> Result<BackgroundBasis, BackgroundError>;
71 fn calculate(&self, x_deg: &[f64]) -> Result<Vec<f64>, BackgroundError>;
77 fn replace_coefficients(&self, coefficients: &[f64]) -> Result<Self, BackgroundError>
83 where
84 Self: Sized;
85 fn basis_is_invariant(&self) -> bool;
87}
88
89#[derive(Clone, Debug, PartialEq)]
91pub struct PolynomialBackground {
92 background_id: String,
93 coefficients: Vec<f64>,
94}
95
96impl PolynomialBackground {
97 pub fn new(
103 background_id: impl Into<String>,
104 coefficients: Vec<f64>,
105 ) -> Result<Self, BackgroundError> {
106 let background_id = validate_id(background_id.into())?;
107 validate_coefficients(&coefficients)?;
108 Ok(Self {
109 background_id,
110 coefficients,
111 })
112 }
113}
114
115impl DifferentiableBackground for PolynomialBackground {
116 fn background_id(&self) -> &str {
117 &self.background_id
118 }
119
120 fn parameter_names(&self) -> Vec<String> {
121 indexed_names("coefficient", self.coefficients.len())
122 }
123
124 fn coefficients(&self) -> Vec<f64> {
125 self.coefficients.clone()
126 }
127
128 fn parameter_bounds(&self) -> Vec<ParameterBounds> {
129 vec![ParameterBounds::default(); self.coefficients.len()]
130 }
131
132 fn basis(&self, x_deg: &[f64]) -> Result<BackgroundBasis, BackgroundError> {
133 validate_grid(x_deg)?;
134 let mut result = BackgroundBasis::zeros(x_deg.len(), self.coefficients.len())?;
135 for (row_index, row) in result.values.chunks_exact_mut(result.columns).enumerate() {
136 let normalized = normalized_grid_value(x_deg, row_index);
137 let mut power = 1.0;
138 for value in row {
139 *value = power;
140 power *= normalized;
141 }
142 }
143 Ok(result)
144 }
145
146 fn calculate(&self, x_deg: &[f64]) -> Result<Vec<f64>, BackgroundError> {
147 linear_calculate(&self.basis(x_deg)?, &self.coefficients)
148 }
149
150 fn replace_coefficients(&self, coefficients: &[f64]) -> Result<Self, BackgroundError> {
151 validate_replacement(coefficients, self.coefficients.len())?;
152 Self::new(self.background_id.clone(), coefficients.to_vec())
153 }
154
155 fn basis_is_invariant(&self) -> bool {
156 true
157 }
158}
159
160#[derive(Clone, Debug, PartialEq)]
162pub struct ChebyshevBackground {
163 background_id: String,
164 coefficients: Vec<f64>,
165 domain_deg: [f64; 2],
166}
167
168impl ChebyshevBackground {
169 pub fn new(
175 background_id: impl Into<String>,
176 coefficients: Vec<f64>,
177 domain_deg: [f64; 2],
178 ) -> Result<Self, BackgroundError> {
179 let background_id = validate_id(background_id.into())?;
180 validate_coefficients(&coefficients)?;
181 if domain_deg.iter().any(|value| !value.is_finite()) || domain_deg[0] >= domain_deg[1] {
182 return Err(BackgroundError::InvalidDomain);
183 }
184 Ok(Self {
185 background_id,
186 coefficients,
187 domain_deg,
188 })
189 }
190
191 #[must_use]
193 pub const fn domain_deg(&self) -> [f64; 2] {
194 self.domain_deg
195 }
196}
197
198impl DifferentiableBackground for ChebyshevBackground {
199 fn background_id(&self) -> &str {
200 &self.background_id
201 }
202
203 fn parameter_names(&self) -> Vec<String> {
204 indexed_names("coefficient", self.coefficients.len())
205 }
206
207 fn coefficients(&self) -> Vec<f64> {
208 self.coefficients.clone()
209 }
210
211 fn parameter_bounds(&self) -> Vec<ParameterBounds> {
212 vec![ParameterBounds::default(); self.coefficients.len()]
213 }
214
215 fn basis(&self, x_deg: &[f64]) -> Result<BackgroundBasis, BackgroundError> {
216 validate_grid(x_deg)?;
217 let lower = self.domain_deg[0];
218 let upper = self.domain_deg[1];
219 let tolerance = 64.0 * f64::EPSILON * lower.abs().max(upper.abs()).max(1.0);
220 if x_deg
221 .iter()
222 .any(|value| *value < lower - tolerance || *value > upper + tolerance)
223 {
224 return Err(BackgroundError::GridOutsideDomain);
225 }
226 let mut result = BackgroundBasis::zeros(x_deg.len(), self.coefficients.len())?;
227 for (x, row) in x_deg
228 .iter()
229 .zip(result.values.chunks_exact_mut(result.columns))
230 {
231 let normalized = 2.0 * (x - lower) / (upper - lower) - 1.0;
232 row[0] = 1.0;
233 if row.len() > 1 {
234 row[1] = normalized;
235 }
236 for order in 2..row.len() {
237 row[order] = 2.0 * normalized * row[order - 1] - row[order - 2];
238 }
239 }
240 Ok(result)
241 }
242
243 fn calculate(&self, x_deg: &[f64]) -> Result<Vec<f64>, BackgroundError> {
244 linear_calculate(&self.basis(x_deg)?, &self.coefficients)
245 }
246
247 fn replace_coefficients(&self, coefficients: &[f64]) -> Result<Self, BackgroundError> {
248 validate_replacement(coefficients, self.coefficients.len())?;
249 Self::new(
250 self.background_id.clone(),
251 coefficients.to_vec(),
252 self.domain_deg,
253 )
254 }
255
256 fn basis_is_invariant(&self) -> bool {
257 true
258 }
259}
260
261#[derive(Clone, Debug, PartialEq)]
263pub struct PointBackground {
264 background_id: String,
265 knot_x: Vec<f64>,
266 values: Vec<f64>,
267}
268
269impl PointBackground {
270 pub fn new(
276 background_id: impl Into<String>,
277 knot_x: Vec<f64>,
278 values: Vec<f64>,
279 ) -> Result<Self, BackgroundError> {
280 let background_id = validate_id(background_id.into())?;
281 if knot_x.len() < 2 || values.len() != knot_x.len() {
282 return Err(BackgroundError::InvalidKnots);
283 }
284 validate_grid(&knot_x).map_err(|_| BackgroundError::InvalidKnots)?;
285 if values.iter().any(|value| !value.is_finite()) {
286 return Err(BackgroundError::NonFiniteCoefficients);
287 }
288 Ok(Self {
289 background_id,
290 knot_x,
291 values,
292 })
293 }
294
295 #[must_use]
297 pub fn knot_x(&self) -> &[f64] {
298 &self.knot_x
299 }
300}
301
302impl DifferentiableBackground for PointBackground {
303 fn background_id(&self) -> &str {
304 &self.background_id
305 }
306
307 fn parameter_names(&self) -> Vec<String> {
308 indexed_names("value", self.values.len())
309 }
310
311 fn coefficients(&self) -> Vec<f64> {
312 self.values.clone()
313 }
314
315 fn parameter_bounds(&self) -> Vec<ParameterBounds> {
316 vec![ParameterBounds::default(); self.values.len()]
317 }
318
319 fn basis(&self, x_deg: &[f64]) -> Result<BackgroundBasis, BackgroundError> {
320 validate_grid(x_deg)?;
321 let mut result = BackgroundBasis::zeros(x_deg.len(), self.knot_x.len())?;
322 for (x, row) in x_deg
323 .iter()
324 .zip(result.values.chunks_exact_mut(result.columns))
325 {
326 let right = self.knot_x.partition_point(|knot| knot <= x);
327 if right == 0 {
328 row[0] = 1.0;
329 } else if right == self.knot_x.len() {
330 row[right - 1] = 1.0;
331 } else {
332 let lower = right - 1;
333 let fraction = (x - self.knot_x[lower]) / (self.knot_x[right] - self.knot_x[lower]);
334 row[lower] = 1.0 - fraction;
335 row[right] = fraction;
336 }
337 }
338 Ok(result)
339 }
340
341 fn calculate(&self, x_deg: &[f64]) -> Result<Vec<f64>, BackgroundError> {
342 linear_calculate(&self.basis(x_deg)?, &self.values)
343 }
344
345 fn replace_coefficients(&self, coefficients: &[f64]) -> Result<Self, BackgroundError> {
346 validate_replacement(coefficients, self.values.len())?;
347 Self::new(
348 self.background_id.clone(),
349 self.knot_x.clone(),
350 coefficients.to_vec(),
351 )
352 }
353
354 fn basis_is_invariant(&self) -> bool {
355 true
356 }
357}
358
359#[derive(Clone, Copy, Debug, PartialEq)]
361pub struct AmorphousPeak {
362 area: f64,
363 center_deg: f64,
364 fwhm_deg: f64,
365}
366
367impl AmorphousPeak {
368 pub fn new(area: f64, center_deg: f64, fwhm_deg: f64) -> Result<Self, BackgroundError> {
374 if !area.is_finite()
375 || !center_deg.is_finite()
376 || !fwhm_deg.is_finite()
377 || area < 0.0
378 || fwhm_deg <= 0.0
379 {
380 return Err(BackgroundError::InvalidAmorphousPeak);
381 }
382 Ok(Self {
383 area,
384 center_deg,
385 fwhm_deg,
386 })
387 }
388
389 #[must_use]
391 pub const fn area(self) -> f64 {
392 self.area
393 }
394
395 #[must_use]
397 pub const fn center_deg(self) -> f64 {
398 self.center_deg
399 }
400
401 #[must_use]
403 pub const fn fwhm_deg(self) -> f64 {
404 self.fwhm_deg
405 }
406}
407
408#[derive(Clone, Debug, PartialEq)]
410pub struct AmorphousBackground {
411 background_id: String,
412 peaks: Vec<AmorphousPeak>,
413}
414
415impl AmorphousBackground {
416 pub fn new(
422 background_id: impl Into<String>,
423 peaks: Vec<AmorphousPeak>,
424 ) -> Result<Self, BackgroundError> {
425 let background_id = validate_id(background_id.into())?;
426 if peaks.is_empty() {
427 return Err(BackgroundError::EmptyComponents);
428 }
429 Ok(Self {
430 background_id,
431 peaks,
432 })
433 }
434
435 #[must_use]
437 pub fn peaks(&self) -> &[AmorphousPeak] {
438 &self.peaks
439 }
440}
441
442impl DifferentiableBackground for AmorphousBackground {
443 fn background_id(&self) -> &str {
444 &self.background_id
445 }
446
447 fn parameter_names(&self) -> Vec<String> {
448 self.peaks
449 .iter()
450 .enumerate()
451 .flat_map(|(index, _)| {
452 ["area", "center_deg", "fwhm_deg"]
453 .into_iter()
454 .map(move |name| format!("peak_{index}.{name}"))
455 })
456 .collect()
457 }
458
459 fn coefficients(&self) -> Vec<f64> {
460 self.peaks
461 .iter()
462 .flat_map(|peak| [peak.area, peak.center_deg, peak.fwhm_deg])
463 .collect()
464 }
465
466 fn parameter_bounds(&self) -> Vec<ParameterBounds> {
467 self.peaks
468 .iter()
469 .flat_map(|_| {
470 [
471 ParameterBounds::new(0.0, f64::INFINITY).expect("valid area bounds"),
472 ParameterBounds::default(),
473 ParameterBounds::new(f64::MIN_POSITIVE, f64::INFINITY)
474 .expect("valid FWHM bounds"),
475 ]
476 })
477 .collect()
478 }
479
480 fn basis(&self, x_deg: &[f64]) -> Result<BackgroundBasis, BackgroundError> {
481 validate_grid(x_deg)?;
482 let columns = self
483 .peaks
484 .len()
485 .checked_mul(3)
486 .ok_or(BackgroundError::SizeOverflow)?;
487 let mut result = BackgroundBasis::zeros(x_deg.len(), columns)?;
488 let factor = 4.0 * 2.0_f64.ln();
489 let normalization = (factor / PI).sqrt();
490 for (x, row) in x_deg
491 .iter()
492 .zip(result.values.chunks_exact_mut(result.columns))
493 {
494 for (peak_index, peak) in self.peaks.iter().enumerate() {
495 let delta = x - peak.center_deg;
496 let ratio = delta / peak.fwhm_deg;
497 let gaussian = normalization / peak.fwhm_deg * (-factor * ratio * ratio).exp();
498 let value = peak.area * gaussian;
499 let offset = 3 * peak_index;
500 row[offset] = gaussian;
501 row[offset + 1] = value * 2.0 * factor * delta / (peak.fwhm_deg * peak.fwhm_deg);
502 row[offset + 2] = value
503 * (-1.0 / peak.fwhm_deg
504 + 2.0 * factor * delta * delta
505 / (peak.fwhm_deg * peak.fwhm_deg * peak.fwhm_deg));
506 }
507 }
508 Ok(result)
509 }
510
511 fn calculate(&self, x_deg: &[f64]) -> Result<Vec<f64>, BackgroundError> {
512 validate_grid(x_deg)?;
513 let factor = 4.0 * 2.0_f64.ln();
514 let normalization = (factor / PI).sqrt();
515 Ok(x_deg
516 .iter()
517 .map(|x| {
518 self.peaks
519 .iter()
520 .map(|peak| {
521 let ratio = (x - peak.center_deg) / peak.fwhm_deg;
522 peak.area * normalization / peak.fwhm_deg * (-factor * ratio * ratio).exp()
523 })
524 .sum()
525 })
526 .collect())
527 }
528
529 fn replace_coefficients(&self, coefficients: &[f64]) -> Result<Self, BackgroundError> {
530 let expected = self
531 .peaks
532 .len()
533 .checked_mul(3)
534 .ok_or(BackgroundError::SizeOverflow)?;
535 validate_replacement(coefficients, expected)?;
536 let peaks = coefficients
537 .chunks_exact(3)
538 .map(|values| AmorphousPeak::new(values[0], values[1], values[2]))
539 .collect::<Result<Vec<_>, _>>()?;
540 Self::new(self.background_id.clone(), peaks)
541 }
542
543 fn basis_is_invariant(&self) -> bool {
544 false
545 }
546}
547
548#[derive(Clone, Debug, PartialEq)]
550pub enum BackgroundModel {
551 Polynomial(PolynomialBackground),
553 Chebyshev(ChebyshevBackground),
555 Point(PointBackground),
557 Amorphous(AmorphousBackground),
559 Composite(CompositeBackground),
561}
562
563#[derive(Clone, Debug, PartialEq)]
565pub struct CompositeBackground {
566 background_id: String,
567 components: Vec<BackgroundModel>,
568}
569
570impl CompositeBackground {
571 pub fn new(
578 background_id: impl Into<String>,
579 components: Vec<BackgroundModel>,
580 ) -> Result<Self, BackgroundError> {
581 let background_id = validate_id(background_id.into())?;
582 if components.is_empty() {
583 return Err(BackgroundError::EmptyComponents);
584 }
585 let mut ids = BTreeSet::new();
586 for component in &components {
587 if !ids.insert(component.background_id().to_owned()) {
588 return Err(BackgroundError::DuplicateComponentId {
589 background_id: component.background_id().to_owned(),
590 });
591 }
592 }
593 Ok(Self {
594 background_id,
595 components,
596 })
597 }
598
599 #[must_use]
601 pub fn components(&self) -> &[BackgroundModel] {
602 &self.components
603 }
604}
605
606macro_rules! delegate_background {
607 ($self:ident, $method:ident $(, $argument:expr)*) => {
608 match $self {
609 Self::Polynomial(value) => value.$method($($argument),*),
610 Self::Chebyshev(value) => value.$method($($argument),*),
611 Self::Point(value) => value.$method($($argument),*),
612 Self::Amorphous(value) => value.$method($($argument),*),
613 Self::Composite(value) => value.$method($($argument),*),
614 }
615 };
616}
617
618impl DifferentiableBackground for BackgroundModel {
619 fn background_id(&self) -> &str {
620 delegate_background!(self, background_id)
621 }
622
623 fn parameter_names(&self) -> Vec<String> {
624 delegate_background!(self, parameter_names)
625 }
626
627 fn coefficients(&self) -> Vec<f64> {
628 delegate_background!(self, coefficients)
629 }
630
631 fn parameter_bounds(&self) -> Vec<ParameterBounds> {
632 delegate_background!(self, parameter_bounds)
633 }
634
635 fn basis(&self, x_deg: &[f64]) -> Result<BackgroundBasis, BackgroundError> {
636 delegate_background!(self, basis, x_deg)
637 }
638
639 fn calculate(&self, x_deg: &[f64]) -> Result<Vec<f64>, BackgroundError> {
640 delegate_background!(self, calculate, x_deg)
641 }
642
643 fn replace_coefficients(&self, coefficients: &[f64]) -> Result<Self, BackgroundError> {
644 Ok(match self {
645 Self::Polynomial(value) => Self::Polynomial(value.replace_coefficients(coefficients)?),
646 Self::Chebyshev(value) => Self::Chebyshev(value.replace_coefficients(coefficients)?),
647 Self::Point(value) => Self::Point(value.replace_coefficients(coefficients)?),
648 Self::Amorphous(value) => Self::Amorphous(value.replace_coefficients(coefficients)?),
649 Self::Composite(value) => Self::Composite(value.replace_coefficients(coefficients)?),
650 })
651 }
652
653 fn basis_is_invariant(&self) -> bool {
654 delegate_background!(self, basis_is_invariant)
655 }
656}
657
658impl BackgroundModel {
659 pub(crate) fn restart_compatible(&self, requested: &Self) -> bool {
660 match (self, requested) {
661 (Self::Polynomial(left), Self::Polynomial(right)) => {
662 left.background_id == right.background_id
663 && left.coefficients.len() == right.coefficients.len()
664 }
665 (Self::Chebyshev(left), Self::Chebyshev(right)) => {
666 left.background_id == right.background_id
667 && left.coefficients.len() == right.coefficients.len()
668 && left
669 .domain_deg
670 .iter()
671 .zip(right.domain_deg)
672 .all(|(left, right)| left.to_bits() == right.to_bits())
673 }
674 (Self::Point(left), Self::Point(right)) => {
675 left.background_id == right.background_id && left.knot_x == right.knot_x
676 }
677 (Self::Amorphous(left), Self::Amorphous(right)) => {
678 left.background_id == right.background_id && left.peaks.len() == right.peaks.len()
679 }
680 (Self::Composite(left), Self::Composite(right)) => {
681 left.background_id == right.background_id
682 && left.components.len() == right.components.len()
683 && left
684 .components
685 .iter()
686 .zip(&right.components)
687 .all(|(left, right)| left.restart_compatible(right))
688 }
689 _ => false,
690 }
691 }
692}
693
694impl DifferentiableBackground for CompositeBackground {
695 fn background_id(&self) -> &str {
696 &self.background_id
697 }
698
699 fn parameter_names(&self) -> Vec<String> {
700 self.components
701 .iter()
702 .flat_map(|component| {
703 component
704 .parameter_names()
705 .into_iter()
706 .map(move |name| format!("{}.{}", component.background_id(), name))
707 })
708 .collect()
709 }
710
711 fn coefficients(&self) -> Vec<f64> {
712 self.components
713 .iter()
714 .flat_map(DifferentiableBackground::coefficients)
715 .collect()
716 }
717
718 fn parameter_bounds(&self) -> Vec<ParameterBounds> {
719 self.components
720 .iter()
721 .flat_map(DifferentiableBackground::parameter_bounds)
722 .collect()
723 }
724
725 fn basis(&self, x_deg: &[f64]) -> Result<BackgroundBasis, BackgroundError> {
726 validate_grid(x_deg)?;
727 let component_bases = self
728 .components
729 .iter()
730 .map(|component| component.basis(x_deg))
731 .collect::<Result<Vec<_>, _>>()?;
732 let columns = component_bases.iter().try_fold(0_usize, |total, basis| {
733 total
734 .checked_add(basis.columns)
735 .ok_or(BackgroundError::SizeOverflow)
736 })?;
737 let mut result = BackgroundBasis::zeros(x_deg.len(), columns)?;
738 for row_index in 0..x_deg.len() {
739 let target = result
740 .values
741 .get_mut(row_index * columns..(row_index + 1) * columns)
742 .ok_or(BackgroundError::InternalInvariant)?;
743 let mut offset = 0;
744 for basis in &component_bases {
745 let source = basis
746 .row(row_index)
747 .ok_or(BackgroundError::InternalInvariant)?;
748 let end = offset + source.len();
749 target
750 .get_mut(offset..end)
751 .ok_or(BackgroundError::InternalInvariant)?
752 .copy_from_slice(source);
753 offset = end;
754 }
755 }
756 Ok(result)
757 }
758
759 fn calculate(&self, x_deg: &[f64]) -> Result<Vec<f64>, BackgroundError> {
760 validate_grid(x_deg)?;
761 let mut result = vec![0.0; x_deg.len()];
762 for component in &self.components {
763 let values = component.calculate(x_deg)?;
764 for (target, value) in result.iter_mut().zip(values) {
765 *target += value;
766 }
767 }
768 Ok(result)
769 }
770
771 fn replace_coefficients(&self, coefficients: &[f64]) -> Result<Self, BackgroundError> {
772 validate_replacement(coefficients, self.coefficients().len())?;
773 let mut offset = 0_usize;
774 let mut components = Vec::with_capacity(self.components.len());
775 for component in &self.components {
776 let count = component.coefficients().len();
777 let end = offset
778 .checked_add(count)
779 .ok_or(BackgroundError::SizeOverflow)?;
780 components.push(
781 component.replace_coefficients(
782 coefficients
783 .get(offset..end)
784 .ok_or(BackgroundError::InternalInvariant)?,
785 )?,
786 );
787 offset = end;
788 }
789 Self::new(self.background_id.clone(), components)
790 }
791
792 fn basis_is_invariant(&self) -> bool {
793 self.components
794 .iter()
795 .all(DifferentiableBackground::basis_is_invariant)
796 }
797}
798
799#[derive(Clone, Debug, PartialEq)]
801pub enum BackgroundError {
802 InvalidId,
804 EmptyCoefficients,
806 NonFiniteCoefficients,
808 InvalidDomain,
810 InvalidKnots,
812 NonFiniteGrid {
814 index: usize,
816 },
817 UnorderedGrid,
819 GridOutsideDomain,
821 InvalidAmorphousPeak,
823 EmptyComponents,
825 DuplicateComponentId {
827 background_id: String,
829 },
830 CoefficientLengthMismatch {
832 expected: usize,
834 actual: usize,
836 },
837 SizeOverflow,
839 InternalInvariant,
841}
842
843impl Display for BackgroundError {
844 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
845 match self {
846 Self::InvalidId => formatter.write_str("background_id must be non-empty and trimmed"),
847 Self::EmptyCoefficients => {
848 formatter.write_str("background coefficients must not be empty")
849 }
850 Self::NonFiniteCoefficients => {
851 formatter.write_str("background coefficients must be finite")
852 }
853 Self::InvalidDomain => {
854 formatter.write_str("background domain must contain two increasing finite values")
855 }
856 Self::InvalidKnots => formatter.write_str(
857 "point background requires matching finite strictly increasing knots and values",
858 ),
859 Self::NonFiniteGrid { index } => {
860 write!(formatter, "background grid is non-finite at index {index}")
861 }
862 Self::UnorderedGrid => {
863 formatter.write_str("background grid must be strictly increasing")
864 }
865 Self::GridOutsideDomain => {
866 formatter.write_str("background grid lies outside the explicit domain")
867 }
868 Self::InvalidAmorphousPeak => formatter.write_str(
869 "amorphous area/center/FWHM must be finite with non-negative area and positive FWHM",
870 ),
871 Self::EmptyComponents => {
872 formatter.write_str("background components must not be empty")
873 }
874 Self::DuplicateComponentId { background_id } => {
875 write!(formatter, "duplicate background component ID {background_id:?}")
876 }
877 Self::CoefficientLengthMismatch { expected, actual } => write!(
878 formatter,
879 "replacement coefficient length {actual} does not match {expected}"
880 ),
881 Self::SizeOverflow => formatter.write_str("background matrix size overflow"),
882 Self::InternalInvariant => {
883 formatter.write_str("validated background state is inconsistent")
884 }
885 }
886 }
887}
888
889impl Error for BackgroundError {}
890
891fn validate_id(value: String) -> Result<String, BackgroundError> {
892 if value.is_empty() || value.trim() != value {
893 return Err(BackgroundError::InvalidId);
894 }
895 Ok(value)
896}
897
898fn validate_coefficients(values: &[f64]) -> Result<(), BackgroundError> {
899 if values.is_empty() {
900 return Err(BackgroundError::EmptyCoefficients);
901 }
902 if values.iter().any(|value| !value.is_finite()) {
903 return Err(BackgroundError::NonFiniteCoefficients);
904 }
905 Ok(())
906}
907
908fn validate_replacement(values: &[f64], expected: usize) -> Result<(), BackgroundError> {
909 if values.len() != expected {
910 return Err(BackgroundError::CoefficientLengthMismatch {
911 expected,
912 actual: values.len(),
913 });
914 }
915 if values.iter().any(|value| !value.is_finite()) {
916 return Err(BackgroundError::NonFiniteCoefficients);
917 }
918 Ok(())
919}
920
921fn validate_grid(x_deg: &[f64]) -> Result<(), BackgroundError> {
922 if let Some(index) = x_deg.iter().position(|value| !value.is_finite()) {
923 return Err(BackgroundError::NonFiniteGrid { index });
924 }
925 if x_deg.windows(2).any(|pair| pair[1] <= pair[0]) {
926 return Err(BackgroundError::UnorderedGrid);
927 }
928 Ok(())
929}
930
931fn indexed_names(stem: &str, count: usize) -> Vec<String> {
932 (0..count).map(|index| format!("{stem}_{index}")).collect()
933}
934
935fn normalized_grid_value(x_deg: &[f64], index: usize) -> f64 {
936 if x_deg.len() <= 1 {
937 0.0
938 } else {
939 2.0 * (x_deg[index] - x_deg[0]) / (x_deg[x_deg.len() - 1] - x_deg[0]) - 1.0
940 }
941}
942
943fn linear_calculate(
944 basis: &BackgroundBasis,
945 coefficients: &[f64],
946) -> Result<Vec<f64>, BackgroundError> {
947 if basis.columns != coefficients.len() {
948 return Err(BackgroundError::InternalInvariant);
949 }
950 Ok(basis
951 .values
952 .chunks_exact(basis.columns)
953 .map(|row| row.iter().zip(coefficients).map(|(a, b)| a * b).sum())
954 .collect())
955}