1use std::error::Error;
8use std::fmt::{Display, Formatter};
9
10use nalgebra::{DMatrix, DVector};
11use phasesmith_core::{
12 Accumulation, GridView, TofError, TofInstrument, TofProfileParameters,
13 accumulate_tof_batch_with_context,
14};
15use phasesmith_execution::{ExecutionPolicy, ExecutionPolicyError};
16use phasesmith_model::{DomainError, RecordId, TofPatternRecord};
17
18use crate::{
19 DiagnosticValue, RefinementEventKind, RefinementLimits, RefinementRuntime, ResidualError,
20 ResidualEvaluation, ResidualOptions, RuntimeError, TerminationReason, evaluate_tof_residuals,
21};
22
23#[derive(Clone, Debug, PartialEq)]
25pub struct TofLeBailPhase {
26 phase_id: RecordId,
27 name: String,
28 reflection_ids: Vec<String>,
29 hkl: Vec<[i32; 3]>,
30 d_spacing_angstrom: Vec<f64>,
31 integrated_intensity: Vec<f64>,
32 scale: f64,
33}
34
35impl TofLeBailPhase {
36 pub fn new(
43 phase_id: RecordId,
44 name: impl Into<String>,
45 reflection_ids: Vec<String>,
46 hkl: Vec<[i32; 3]>,
47 d_spacing_angstrom: Vec<f64>,
48 integrated_intensity: Vec<f64>,
49 scale: f64,
50 ) -> Result<Self, TofLeBailError> {
51 let result = Self {
52 phase_id,
53 name: name.into(),
54 reflection_ids,
55 hkl,
56 d_spacing_angstrom,
57 integrated_intensity,
58 scale,
59 };
60 result.validate()?;
61 Ok(result)
62 }
63
64 pub fn validate(&self) -> Result<(), TofLeBailError> {
70 let count = self.reflection_ids.len();
71 if self.name.trim().is_empty() || count == 0 {
72 return Err(TofLeBailError::InvalidPhase(
73 "TOF Le Bail phases require a name and at least one reflection",
74 ));
75 }
76 if self.hkl.len() != count
77 || self.d_spacing_angstrom.len() != count
78 || self.integrated_intensity.len() != count
79 {
80 return Err(TofLeBailError::InvalidPhase(
81 "TOF phase reflection arrays must have equal length",
82 ));
83 }
84 if self.reflection_ids.iter().any(String::is_empty)
85 || self
86 .reflection_ids
87 .iter()
88 .collect::<std::collections::BTreeSet<_>>()
89 .len()
90 != count
91 {
92 return Err(TofLeBailError::InvalidPhase(
93 "TOF reflection IDs must be non-empty and unique within a phase",
94 ));
95 }
96 if self
97 .d_spacing_angstrom
98 .iter()
99 .any(|value| !value.is_finite() || *value <= 0.0)
100 || self
101 .integrated_intensity
102 .iter()
103 .any(|value| !value.is_finite() || *value < 0.0)
104 || !self.scale.is_finite()
105 || self.scale <= 0.0
106 {
107 return Err(TofLeBailError::InvalidPhase(
108 "TOF d-spacings and scale must be positive; intensities must be nonnegative",
109 ));
110 }
111 Ok(())
112 }
113
114 #[must_use]
116 pub const fn phase_id(&self) -> &RecordId {
117 &self.phase_id
118 }
119
120 #[must_use]
122 pub fn name(&self) -> &str {
123 &self.name
124 }
125
126 #[must_use]
128 pub fn reflection_ids(&self) -> &[String] {
129 &self.reflection_ids
130 }
131
132 #[must_use]
134 pub fn hkl(&self) -> &[[i32; 3]] {
135 &self.hkl
136 }
137
138 #[must_use]
140 pub fn d_spacing_angstrom(&self) -> &[f64] {
141 &self.d_spacing_angstrom
142 }
143
144 #[must_use]
146 pub fn integrated_intensity(&self) -> &[f64] {
147 &self.integrated_intensity
148 }
149
150 #[must_use]
152 pub const fn scale(&self) -> f64 {
153 self.scale
154 }
155
156 fn with_intensities(&self, values: &[f64]) -> Result<Self, TofLeBailError> {
157 if values.len() != self.integrated_intensity.len() {
158 return Err(TofLeBailError::IntensityLengthMismatch);
159 }
160 let mut result = self.clone();
161 result.integrated_intensity.copy_from_slice(values);
162 result.validate()?;
163 Ok(result)
164 }
165
166 pub(crate) fn with_d_spacings(&self, values: &[f64]) -> Result<Self, TofLeBailError> {
167 if values.len() != self.d_spacing_angstrom.len() {
168 return Err(TofLeBailError::DSpacingLengthMismatch);
169 }
170 let mut result = self.clone();
171 result.d_spacing_angstrom.copy_from_slice(values);
172 result.validate()?;
173 Ok(result)
174 }
175}
176
177#[derive(Clone, Debug, PartialEq)]
183pub struct TofChebyshevBackground {
184 background_id: RecordId,
185 coefficients: Vec<f64>,
186 domain_us: [f64; 2],
187}
188
189impl TofChebyshevBackground {
190 pub fn new(
196 background_id: RecordId,
197 coefficients: Vec<f64>,
198 domain_us: [f64; 2],
199 ) -> Result<Self, TofLeBailError> {
200 let result = Self {
201 background_id,
202 coefficients,
203 domain_us,
204 };
205 result.validate()?;
206 Ok(result)
207 }
208
209 pub fn validate(&self) -> Result<(), TofLeBailError> {
215 if self.coefficients.is_empty()
216 || self.coefficients.iter().any(|value| !value.is_finite())
217 || self.domain_us.iter().any(|value| !value.is_finite())
218 || self.domain_us[0] >= self.domain_us[1]
219 {
220 return Err(TofLeBailError::InvalidBackground);
221 }
222 Ok(())
223 }
224
225 #[must_use]
227 pub const fn background_id(&self) -> &RecordId {
228 &self.background_id
229 }
230
231 #[must_use]
233 pub fn coefficients(&self) -> &[f64] {
234 &self.coefficients
235 }
236
237 #[must_use]
239 pub const fn domain_us(&self) -> [f64; 2] {
240 self.domain_us
241 }
242
243 pub fn calculate(&self, tof_us: &[f64]) -> Result<Vec<f64>, TofLeBailError> {
249 let basis = self.basis(tof_us)?;
250 self.calculate_from_basis(&basis)
251 }
252
253 pub(crate) fn calculate_from_basis(
254 &self,
255 basis: &TofChebyshevBasis,
256 ) -> Result<Vec<f64>, TofLeBailError> {
257 let expected = basis
258 .rows
259 .checked_mul(basis.columns)
260 .ok_or(TofLeBailError::AllocationOverflow)?;
261 if basis.columns != self.coefficients.len() || basis.values.len() != expected {
262 return Err(TofLeBailError::BackgroundBasisShape);
263 }
264 Ok(basis
265 .values
266 .chunks_exact(basis.columns)
267 .map(|row| {
268 row.iter()
269 .zip(&self.coefficients)
270 .map(|(basis, coefficient)| basis * coefficient)
271 .sum()
272 })
273 .collect())
274 }
275
276 pub(crate) fn basis(&self, tof_us: &[f64]) -> Result<TofChebyshevBasis, TofLeBailError> {
277 self.validate()?;
278 if tof_us.is_empty()
279 || tof_us.iter().any(|value| !value.is_finite())
280 || tof_us.windows(2).any(|pair| pair[0] >= pair[1])
281 {
282 return Err(TofLeBailError::InvalidBackgroundGrid);
283 }
284 let lower = self.domain_us[0];
285 let upper = self.domain_us[1];
286 let tolerance = 64.0 * f64::EPSILON * lower.abs().max(upper.abs()).max(1.0);
287 if tof_us
288 .iter()
289 .any(|value| *value < lower - tolerance || *value > upper + tolerance)
290 {
291 return Err(TofLeBailError::BackgroundGridOutsideDomain);
292 }
293 let columns = self.coefficients.len();
294 let count = tof_us
295 .len()
296 .checked_mul(columns)
297 .ok_or(TofLeBailError::AllocationOverflow)?;
298 let mut values = vec![0.0; count];
299 for (tof, row) in tof_us.iter().zip(values.chunks_exact_mut(columns)) {
300 let normalized = 2.0 * (tof - lower) / (upper - lower) - 1.0;
301 row[0] = 1.0;
302 if columns > 1 {
303 row[1] = normalized;
304 }
305 for order in 2..columns {
306 row[order] = 2.0 * normalized * row[order - 1] - row[order - 2];
307 }
308 }
309 Ok(TofChebyshevBasis {
310 rows: tof_us.len(),
311 columns,
312 values,
313 })
314 }
315
316 pub(crate) fn with_coefficients(&self, coefficients: Vec<f64>) -> Result<Self, TofLeBailError> {
317 if coefficients.len() != self.coefficients.len() {
318 return Err(TofLeBailError::BackgroundCoefficientLengthMismatch);
319 }
320 Self::new(self.background_id.clone(), coefficients, self.domain_us)
321 }
322}
323
324#[derive(Clone, Debug, PartialEq)]
326pub struct TofChebyshevBasis {
327 pub rows: usize,
329 pub columns: usize,
331 pub values: Vec<f64>,
333}
334
335#[derive(Clone, Debug, PartialEq)]
337pub struct TofLeBailInput {
338 pub pattern: TofPatternRecord,
340 pub instrument: TofInstrument,
342 pub phases: Vec<TofLeBailPhase>,
344 pub background: Option<TofChebyshevBackground>,
346}
347
348impl TofLeBailInput {
349 pub fn new(
356 pattern: TofPatternRecord,
357 instrument: TofInstrument,
358 phases: Vec<TofLeBailPhase>,
359 ) -> Result<Self, TofLeBailError> {
360 let result = Self {
361 pattern,
362 instrument,
363 phases,
364 background: None,
365 };
366 result.validate()?;
367 Ok(result)
368 }
369
370 pub fn with_refinable_background(
376 mut self,
377 background: TofChebyshevBackground,
378 ) -> Result<Self, TofLeBailError> {
379 self.background = Some(background);
380 self.validate()?;
381 Ok(self)
382 }
383
384 pub fn validate(&self) -> Result<(), TofLeBailError> {
390 self.pattern.validate().map_err(TofLeBailError::Pattern)?;
391 if self.pattern.observed_y.is_none() {
392 return Err(TofLeBailError::MissingObservations);
393 }
394 self.instrument
395 .validate()
396 .map_err(TofLeBailError::Profile)?;
397 if self.phases.is_empty() {
398 return Err(TofLeBailError::InvalidPhase(
399 "TOF Le Bail input requires at least one phase",
400 ));
401 }
402 let mut phase_ids = std::collections::BTreeSet::new();
403 for phase in &self.phases {
404 phase.validate()?;
405 if !phase_ids.insert(phase.phase_id()) {
406 return Err(TofLeBailError::InvalidPhase(
407 "TOF Le Bail phase IDs must be unique",
408 ));
409 }
410 for d_spacing in phase.d_spacing_angstrom() {
411 TofProfileParameters::from_instrument(*d_spacing, self.instrument)
412 .map_err(TofLeBailError::Profile)?;
413 }
414 }
415 if let Some(background) = &self.background {
416 background.validate()?;
417 background.basis(&self.pattern.tof_us)?;
418 }
419 Ok(())
420 }
421}
422
423#[derive(Clone, Debug, PartialEq)]
425pub struct TofLeBailOptions {
426 pub cycles: usize,
428 pub redistribution_damping: f64,
430 pub initial_intensity_floor: f64,
432 pub minimum_calculated: f64,
434 pub support_fwhm: f64,
436 pub tail_log: f64,
438 pub use_uncertainty: bool,
440 pub redistribution_use_uncertainty: bool,
446 pub execution: ExecutionPolicy,
448}
449
450impl TofLeBailOptions {
451 #[allow(clippy::too_many_arguments)]
457 pub fn new(
458 cycles: usize,
459 redistribution_damping: f64,
460 initial_intensity_floor: f64,
461 minimum_calculated: f64,
462 support_fwhm: f64,
463 tail_log: f64,
464 use_uncertainty: bool,
465 execution: ExecutionPolicy,
466 ) -> Result<Self, TofLeBailError> {
467 let result = Self {
468 cycles,
469 redistribution_damping,
470 initial_intensity_floor,
471 minimum_calculated,
472 support_fwhm,
473 tail_log,
474 use_uncertainty,
475 redistribution_use_uncertainty: use_uncertainty,
476 execution,
477 };
478 result.validate()?;
479 Ok(result)
480 }
481
482 #[must_use]
484 pub fn with_redistribution_uncertainty(mut self, enabled: bool) -> Self {
485 self.redistribution_use_uncertainty = enabled;
486 self
487 }
488
489 pub fn scripting_defaults(execution: ExecutionPolicy) -> Result<Self, TofLeBailError> {
495 Self::new(50, 1.0, 1.0e-12, 1.0e-15, 20.0, 20.0, true, execution)
496 }
497
498 pub fn validate(&self) -> Result<(), TofLeBailError> {
504 if self.cycles == 0
505 || !self.redistribution_damping.is_finite()
506 || self.redistribution_damping <= 0.0
507 || self.redistribution_damping > 1.0
508 || !self.initial_intensity_floor.is_finite()
509 || self.initial_intensity_floor <= 0.0
510 || !self.minimum_calculated.is_finite()
511 || self.minimum_calculated <= 0.0
512 || !self.support_fwhm.is_finite()
513 || self.support_fwhm <= 0.0
514 || !self.tail_log.is_finite()
515 || self.tail_log <= 0.0
516 {
517 return Err(TofLeBailError::InvalidOptions);
518 }
519 Ok(())
520 }
521}
522
523#[derive(Clone, Debug, PartialEq)]
525pub struct TofLeBailCalculation {
526 pub y: Vec<f64>,
528 pub profile_y: Vec<f64>,
530 pub background_y: Vec<f64>,
532 pub background_basis: Option<TofChebyshevBasis>,
534 pub accumulation: Accumulation,
536 pub reflection_keys: Vec<(String, String)>,
538 pub phase_offsets: Vec<usize>,
540}
541
542#[derive(Clone, Debug, PartialEq)]
544pub struct TofLeBailIterationRecord {
545 pub iteration: usize,
547 pub metrics: ResidualEvaluation,
549 pub maximum_relative_intensity_change: f64,
551 pub maximum_absolute_background_change: f64,
553}
554
555#[derive(Clone, Debug, PartialEq)]
557pub struct TofReflectionIntensity {
558 pub phase_id: String,
560 pub reflection_id: String,
562 pub integrated_intensity: f64,
564}
565
566#[derive(Clone, Debug, PartialEq)]
568pub struct TofLeBailResult {
569 pub calculation: TofLeBailCalculation,
571 pub metrics: ResidualEvaluation,
573 pub phases: Vec<TofLeBailPhase>,
575 pub background: Option<TofChebyshevBackground>,
577 pub intensities: Vec<TofReflectionIntensity>,
579 pub history: Vec<TofLeBailIterationRecord>,
581 pub termination_reason: TerminationReason,
583 pub checkpoint: TofLeBailCheckpoint,
585}
586
587#[derive(Clone, Debug, PartialEq)]
589pub struct TofLeBailCheckpoint {
590 pub completed_iterations: usize,
592 pub phases: Vec<TofLeBailPhase>,
594 pub background: Option<TofChebyshevBackground>,
596 pub history: Vec<TofLeBailIterationRecord>,
598}
599
600impl TofLeBailCheckpoint {
601 pub fn validate_for(
608 &self,
609 input: &TofLeBailInput,
610 options: &TofLeBailOptions,
611 ) -> Result<(), TofLeBailError> {
612 if self.completed_iterations != self.history.len()
613 || self.completed_iterations > options.cycles
614 {
615 return Err(TofLeBailError::InvalidCheckpoint(
616 "checkpoint iteration count must equal history length and fit the cycle budget",
617 ));
618 }
619 if self.phases.len() != input.phases.len() {
620 return Err(TofLeBailError::InvalidCheckpoint(
621 "checkpoint phase count differs from the request",
622 ));
623 }
624 for (saved, original) in self.phases.iter().zip(&input.phases) {
625 saved.validate()?;
626 if saved.phase_id != original.phase_id
627 || saved.name != original.name
628 || saved.reflection_ids != original.reflection_ids
629 || saved.hkl != original.hkl
630 || saved.d_spacing_angstrom != original.d_spacing_angstrom
631 || saved.scale.to_bits() != original.scale.to_bits()
632 {
633 return Err(TofLeBailError::InvalidCheckpoint(
634 "checkpoint phase topology differs from the request",
635 ));
636 }
637 }
638 match (&self.background, &input.background) {
639 (None, None) => {}
640 (Some(saved), Some(original)) => {
641 saved.validate()?;
642 if saved.background_id != original.background_id
643 || saved
644 .domain_us
645 .iter()
646 .zip(original.domain_us)
647 .any(|(saved, original)| saved.to_bits() != original.to_bits())
648 || saved.coefficients.len() != original.coefficients.len()
649 {
650 return Err(TofLeBailError::InvalidCheckpoint(
651 "checkpoint background contract differs from the request",
652 ));
653 }
654 }
655 _ => {
656 return Err(TofLeBailError::InvalidCheckpoint(
657 "checkpoint background presence differs from the request",
658 ));
659 }
660 }
661 if self
662 .history
663 .iter()
664 .enumerate()
665 .any(|(index, record)| record.iteration != index + 1)
666 {
667 return Err(TofLeBailError::InvalidCheckpoint(
668 "checkpoint history must be contiguous and one-based",
669 ));
670 }
671 Ok(())
672 }
673}
674
675pub fn calculate_tof_lebail_pattern(
682 input: &TofLeBailInput,
683 options: &TofLeBailOptions,
684) -> Result<TofLeBailCalculation, TofLeBailError> {
685 input.validate()?;
686 options.validate()?;
687 let reflection_count = input
688 .phases
689 .iter()
690 .try_fold(0_usize, |count, phase| {
691 count.checked_add(phase.reflection_ids.len())
692 })
693 .ok_or(TofLeBailError::AllocationOverflow)?;
694 let mut d_spacing = Vec::with_capacity(reflection_count);
695 let mut intensity = Vec::with_capacity(reflection_count);
696 let mut reflection_keys = Vec::with_capacity(reflection_count);
697 let mut phase_offsets = Vec::with_capacity(input.phases.len() + 1);
698 phase_offsets.push(0);
699 for phase in &input.phases {
700 d_spacing.extend_from_slice(&phase.d_spacing_angstrom);
701 intensity.extend(
702 phase
703 .integrated_intensity
704 .iter()
705 .map(|value| phase.scale * value),
706 );
707 reflection_keys.extend(
708 phase.reflection_ids.iter().map(|reflection_id| {
709 (phase.phase_id.as_str().to_owned(), reflection_id.to_owned())
710 }),
711 );
712 phase_offsets.push(d_spacing.len());
713 }
714 let accumulation = accumulate_tof_batch_with_context(
715 GridView::new(&input.pattern.tof_us).map_err(TofError::from)?,
716 &d_spacing,
717 &intensity,
718 input.instrument,
719 options.support_fwhm,
720 options.tail_log,
721 options.execution.context(),
722 )?;
723 let profile_y = accumulation.y.clone();
724 let (mut background_y, background_basis) = if let Some(background) = &input.background {
725 let basis = background.basis(&input.pattern.tof_us)?;
726 let values = background.calculate_from_basis(&basis)?;
727 (values, Some(basis))
728 } else {
729 (vec![0.0; input.pattern.sample_count()], None)
730 };
731 for (residual, fixed) in background_y.iter_mut().zip(&input.pattern.background_y) {
732 *residual += fixed;
733 }
734 let y = profile_y
735 .iter()
736 .zip(&background_y)
737 .map(|(profile, background)| profile + background)
738 .collect();
739 Ok(TofLeBailCalculation {
740 y,
741 profile_y,
742 background_y,
743 background_basis,
744 accumulation,
745 reflection_keys,
746 phase_offsets,
747 })
748}
749
750pub fn refine_tof_lebail(
757 input: &TofLeBailInput,
758 options: &TofLeBailOptions,
759) -> Result<TofLeBailResult, TofLeBailError> {
760 input.validate()?;
761 options.validate()?;
762 let max_evaluations = options
763 .cycles
764 .checked_mul(3)
765 .ok_or(TofLeBailError::AllocationOverflow)?;
766 let limits = RefinementLimits::new(options.cycles, max_evaluations, None, 1)?;
767 let mut runtime = RefinementRuntime::new(limits, None)?;
768 refine_tof_lebail_with_runtime(input, options, None, &mut runtime)
769}
770
771#[allow(clippy::too_many_lines)]
782pub fn refine_tof_lebail_with_runtime(
783 input: &TofLeBailInput,
784 options: &TofLeBailOptions,
785 checkpoint: Option<&TofLeBailCheckpoint>,
786 runtime: &mut RefinementRuntime<TofLeBailCheckpoint>,
787) -> Result<TofLeBailResult, TofLeBailError> {
788 input.validate()?;
789 options.validate()?;
790 let restored = restore_tof_state(input, options, checkpoint)?;
791 let mut phases = restored.phases;
792 let mut background = restored.background;
793 let mut history = restored.history;
794 let first_iteration = restored.first_iteration;
795 if let Some(checkpoint) = checkpoint {
796 runtime.resume_accepted(checkpoint.completed_iterations)?;
797 }
798 runtime.emit(
799 RefinementEventKind::Start,
800 "tof_lebail",
801 "TOF Le Bail extraction started",
802 Vec::new(),
803 )?;
804 let mut calculation = None;
805 let mut termination = TerminationReason::MaxIterations;
806 for iteration in first_iteration..=options.cycles {
807 if let Err(error) = runtime.begin_iteration(iteration) {
808 termination = normal_tof_stop(error)?;
809 break;
810 }
811 if let Err(error) = runtime.begin_evaluation() {
812 termination = normal_tof_stop(error)?;
813 break;
814 }
815 let current_input = state_input(input, phases.clone(), background.clone())?;
816 let current_calculation = calculate_tof_lebail_pattern(¤t_input, options)?;
817 let current = flatten_intensities(&phases);
818 let updated = redistribute(&input.pattern, ¤t_calculation, ¤t, options)?;
819 let maximum_relative_intensity_change = updated
820 .iter()
821 .zip(¤t)
822 .map(|(updated, current)| {
823 (updated - current).abs() / current.abs().max(options.initial_intensity_floor)
824 })
825 .fold(0.0_f64, f64::max);
826 let candidate_phases = install_intensities(&phases, &updated)?;
827 if let Err(error) = runtime.begin_evaluation() {
828 termination = normal_tof_stop(error)?;
829 break;
830 }
831 let intensity_input = state_input(input, candidate_phases.clone(), background.clone())?;
832 let intensity_calculation = calculate_tof_lebail_pattern(&intensity_input, options)?;
833 let candidate_background = refine_background(
834 &input.pattern,
835 &intensity_calculation.profile_y,
836 background.as_ref(),
837 options,
838 )?;
839 let maximum_absolute_background_change =
840 maximum_background_change(background.as_ref(), candidate_background.as_ref());
841 if let Err(error) = runtime.begin_evaluation() {
842 termination = normal_tof_stop(error)?;
843 break;
844 }
845 let accepted_input = state_input(
846 input,
847 candidate_phases.clone(),
848 candidate_background.clone(),
849 )?;
850 let accepted = calculate_tof_lebail_pattern(&accepted_input, options)?;
851 let background_parameter_count = candidate_background
852 .as_ref()
853 .map_or(0, |background| background.coefficients.len());
854 let metrics = evaluate_tof_residuals(
855 &input.pattern,
856 &accepted.y,
857 ResidualOptions {
858 use_uncertainty: options.use_uncertainty,
859 parameter_count: updated.len() + background_parameter_count,
860 },
861 )?;
862 phases = candidate_phases;
863 background = candidate_background;
864 history.push(TofLeBailIterationRecord {
865 iteration,
866 metrics: metrics.clone(),
867 maximum_relative_intensity_change,
868 maximum_absolute_background_change,
869 });
870 calculation = Some(accepted);
871 let accepted_checkpoint = TofLeBailCheckpoint {
872 completed_iterations: history.len(),
873 phases: phases.clone(),
874 background: background.clone(),
875 history: history.clone(),
876 };
877 runtime.accept_step(Some(&accepted_checkpoint))?;
878 runtime.emit(
879 RefinementEventKind::Iteration,
880 "tof_lebail_iteration",
881 "TOF Le Bail cycle accepted",
882 vec![
883 ("rwp".to_owned(), DiagnosticValue::Float(metrics.rwp)),
884 (
885 "maximum_relative_intensity_change".to_owned(),
886 DiagnosticValue::Float(maximum_relative_intensity_change),
887 ),
888 (
889 "maximum_absolute_background_change".to_owned(),
890 DiagnosticValue::Float(maximum_absolute_background_change),
891 ),
892 ],
893 )?;
894 }
895 let calculation = if let Some(calculation) = calculation {
896 calculation
897 } else {
898 let final_input = state_input(input, phases.clone(), background.clone())?;
899 calculate_tof_lebail_pattern(&final_input, options)?
900 };
901 let background_parameter_count = background
902 .as_ref()
903 .map_or(0, |background| background.coefficients.len());
904 let metrics = evaluate_tof_residuals(
905 &input.pattern,
906 &calculation.y,
907 ResidualOptions {
908 use_uncertainty: options.use_uncertainty,
909 parameter_count: flatten_intensities(&phases).len() + background_parameter_count,
910 },
911 )?;
912 let final_checkpoint = TofLeBailCheckpoint {
913 completed_iterations: history.len(),
914 phases: phases.clone(),
915 background: background.clone(),
916 history: history.clone(),
917 };
918 final_checkpoint.validate_for(input, options)?;
919 let intensities = phases
920 .iter()
921 .flat_map(|phase| {
922 phase
923 .reflection_ids
924 .iter()
925 .zip(&phase.integrated_intensity)
926 .map(
927 |(reflection_id, integrated_intensity)| TofReflectionIntensity {
928 phase_id: phase.phase_id.as_str().to_owned(),
929 reflection_id: reflection_id.clone(),
930 integrated_intensity: *integrated_intensity,
931 },
932 )
933 })
934 .collect();
935 runtime.emit(
936 RefinementEventKind::Termination,
937 "tof_lebail",
938 "TOF Le Bail extraction terminated",
939 vec![(
940 "termination_reason".to_owned(),
941 DiagnosticValue::String(termination.as_str().to_owned()),
942 )],
943 )?;
944 Ok(TofLeBailResult {
945 calculation,
946 metrics,
947 phases,
948 background,
949 intensities,
950 history,
951 termination_reason: termination,
952 checkpoint: final_checkpoint,
953 })
954}
955
956struct RestoredTofState {
957 phases: Vec<TofLeBailPhase>,
958 background: Option<TofChebyshevBackground>,
959 history: Vec<TofLeBailIterationRecord>,
960 first_iteration: usize,
961}
962
963fn restore_tof_state(
964 input: &TofLeBailInput,
965 options: &TofLeBailOptions,
966 checkpoint: Option<&TofLeBailCheckpoint>,
967) -> Result<RestoredTofState, TofLeBailError> {
968 let Some(checkpoint) = checkpoint else {
969 return Ok(RestoredTofState {
970 phases: initialize_intensities(input, options)?,
971 background: input.background.clone(),
972 history: Vec::new(),
973 first_iteration: 1,
974 });
975 };
976 checkpoint.validate_for(input, options)?;
977 let first_iteration = checkpoint
978 .completed_iterations
979 .checked_add(1)
980 .ok_or(TofLeBailError::AllocationOverflow)?;
981 Ok(RestoredTofState {
982 phases: checkpoint.phases.clone(),
983 background: checkpoint.background.clone(),
984 history: checkpoint.history.clone(),
985 first_iteration,
986 })
987}
988
989pub(crate) fn normal_tof_stop(error: RuntimeError) -> Result<TerminationReason, TofLeBailError> {
990 match error {
991 RuntimeError::Stopped(stop) => Ok(stop.reason),
992 other => Err(TofLeBailError::Runtime(other)),
993 }
994}
995
996pub(crate) fn initialize_intensities(
997 input: &TofLeBailInput,
998 options: &TofLeBailOptions,
999) -> Result<Vec<TofLeBailPhase>, TofLeBailError> {
1000 let current = flatten_intensities(&input.phases);
1001 if current.iter().any(|value| *value > 0.0) {
1002 let values = current
1003 .iter()
1004 .map(|value| value.max(options.initial_intensity_floor))
1005 .collect::<Vec<_>>();
1006 return install_intensities(&input.phases, &values);
1007 }
1008 let observed = input
1009 .pattern
1010 .observed_y
1011 .as_deref()
1012 .ok_or(TofLeBailError::MissingObservations)?;
1013 let mut background = input.pattern.background_y.clone();
1014 if let Some(residual) = &input.background {
1015 for (fixed, value) in background
1016 .iter_mut()
1017 .zip(residual.calculate(&input.pattern.tof_us)?)
1018 {
1019 *fixed += value;
1020 }
1021 }
1022 let widths = bin_integration_weights(&input.pattern.tof_us);
1023 let area = observed
1024 .iter()
1025 .zip(&background)
1026 .zip(widths)
1027 .map(|((observed, background), width)| (observed - background).max(0.0) * width)
1028 .sum::<f64>();
1029 let count = current.len().max(1);
1030 #[allow(clippy::cast_precision_loss)]
1031 let starting = (area / count as f64).max(options.initial_intensity_floor);
1032 install_intensities(&input.phases, &vec![starting; current.len()])
1033}
1034
1035pub(crate) fn redistribute(
1036 pattern: &TofPatternRecord,
1037 calculation: &TofLeBailCalculation,
1038 current: &[f64],
1039 options: &TofLeBailOptions,
1040) -> Result<Vec<f64>, TofLeBailError> {
1041 let observed = pattern
1042 .observed_y
1043 .as_deref()
1044 .ok_or(TofLeBailError::MissingObservations)?;
1045 let included = pattern
1046 .mask
1047 .clone()
1048 .unwrap_or_else(|| vec![true; pattern.sample_count()]);
1049 let ratio = observed
1050 .iter()
1051 .zip(&calculation.background_y)
1052 .zip(&calculation.profile_y)
1053 .zip(&included)
1054 .map(|(((observed, background), calculated), included)| {
1055 if *included && *calculated > options.minimum_calculated {
1056 (observed - background).max(0.0) / calculated
1057 } else {
1058 0.0
1059 }
1060 })
1061 .collect::<Vec<_>>();
1062 let mut weights = bin_integration_weights(&pattern.tof_us);
1063 if options.redistribution_use_uncertainty
1064 && let Some(uncertainty) = &pattern.uncertainty
1065 {
1066 for (weight, uncertainty) in weights.iter_mut().zip(uncertainty) {
1067 *weight /= uncertainty * uncertainty;
1068 }
1069 }
1070 for (weight, included) in weights.iter_mut().zip(&included) {
1071 if !included {
1072 *weight = 0.0;
1073 }
1074 }
1075 let local = &calculation.accumulation.derivatives.local;
1076 if local.peak_count() != current.len() {
1077 return Err(TofLeBailError::IntensityLengthMismatch);
1078 }
1079 let mut updated = vec![0.0; current.len()];
1080 for reflection in 0..current.len() {
1081 let begin = local.offsets[reflection];
1082 let end = local.offsets[reflection + 1];
1083 let start = local.starts[reflection];
1084 let mut denominator = 0.0;
1085 let mut numerator = 0.0;
1086 for active in begin..end {
1087 let sample = start + active - begin;
1088 let profile = local.values[active * local.parameter_count];
1089 let weighted_profile = weights[sample] * profile;
1090 denominator += weighted_profile;
1091 numerator += weighted_profile * ratio[sample];
1092 }
1093 let raw = if denominator > 0.0 {
1094 (current[reflection] * numerator / denominator).max(0.0)
1095 } else {
1096 current[reflection]
1097 };
1098 updated[reflection] =
1099 current[reflection] + options.redistribution_damping * (raw - current[reflection]);
1100 }
1101 Ok(updated)
1102}
1103
1104pub(crate) fn state_input(
1105 original: &TofLeBailInput,
1106 phases: Vec<TofLeBailPhase>,
1107 background: Option<TofChebyshevBackground>,
1108) -> Result<TofLeBailInput, TofLeBailError> {
1109 let mut result = TofLeBailInput::new(original.pattern.clone(), original.instrument, phases)?;
1110 result.background = background;
1111 result.validate()?;
1112 Ok(result)
1113}
1114
1115pub(crate) fn refine_background(
1116 pattern: &TofPatternRecord,
1117 profile_y: &[f64],
1118 background: Option<&TofChebyshevBackground>,
1119 options: &TofLeBailOptions,
1120) -> Result<Option<TofChebyshevBackground>, TofLeBailError> {
1121 let Some(background) = background else {
1122 return Ok(None);
1123 };
1124 let observed = pattern
1125 .observed_y
1126 .as_deref()
1127 .ok_or(TofLeBailError::MissingObservations)?;
1128 let basis = background.basis(&pattern.tof_us)?;
1129 if profile_y.len() != pattern.sample_count() || basis.rows != pattern.sample_count() {
1130 return Err(TofLeBailError::BackgroundBasisShape);
1131 }
1132 let included_count = (0..pattern.sample_count())
1133 .filter(|index| pattern.mask.as_ref().is_none_or(|mask| mask[*index]))
1134 .count();
1135 if included_count < basis.columns {
1136 return Err(TofLeBailError::InsufficientBackgroundObservations);
1137 }
1138 let count = included_count
1139 .checked_mul(basis.columns)
1140 .ok_or(TofLeBailError::AllocationOverflow)?;
1141 let mut design = Vec::with_capacity(count);
1142 let mut target = Vec::with_capacity(included_count);
1143 for sample in 0..pattern.sample_count() {
1144 if pattern.mask.as_ref().is_some_and(|mask| !mask[sample]) {
1145 continue;
1146 }
1147 let sigma = if options.use_uncertainty {
1148 pattern
1149 .uncertainty
1150 .as_ref()
1151 .map_or(1.0, |values| values[sample])
1152 } else {
1153 1.0
1154 };
1155 let row = basis
1156 .values
1157 .get(sample * basis.columns..(sample + 1) * basis.columns)
1158 .ok_or(TofLeBailError::BackgroundBasisShape)?;
1159 design.extend(row.iter().map(|value| value / sigma));
1160 target.push((observed[sample] - pattern.background_y[sample] - profile_y[sample]) / sigma);
1161 }
1162 let matrix = DMatrix::from_row_slice(included_count, basis.columns, &design);
1163 let target = DVector::from_vec(target);
1164 let coefficients = matrix
1165 .svd(true, true)
1166 .solve(&target, 1.0e-12)
1167 .map_err(|_| TofLeBailError::BackgroundLinearSolve)?;
1168 if coefficients.iter().any(|value| !value.is_finite()) {
1169 return Err(TofLeBailError::BackgroundLinearSolve);
1170 }
1171 background
1172 .with_coefficients(coefficients.as_slice().to_vec())
1173 .map(Some)
1174}
1175
1176pub(crate) fn maximum_background_change(
1177 previous: Option<&TofChebyshevBackground>,
1178 updated: Option<&TofChebyshevBackground>,
1179) -> f64 {
1180 previous.zip(updated).map_or(0.0, |(previous, updated)| {
1181 previous
1182 .coefficients
1183 .iter()
1184 .zip(&updated.coefficients)
1185 .map(|(previous, updated)| (updated - previous).abs())
1186 .fold(0.0_f64, f64::max)
1187 })
1188}
1189
1190pub(crate) fn flatten_intensities(phases: &[TofLeBailPhase]) -> Vec<f64> {
1191 phases
1192 .iter()
1193 .flat_map(|phase| phase.integrated_intensity.iter().copied())
1194 .collect()
1195}
1196
1197pub(crate) fn install_intensities(
1198 phases: &[TofLeBailPhase],
1199 values: &[f64],
1200) -> Result<Vec<TofLeBailPhase>, TofLeBailError> {
1201 if phases
1202 .iter()
1203 .map(|phase| phase.reflection_ids.len())
1204 .sum::<usize>()
1205 != values.len()
1206 {
1207 return Err(TofLeBailError::IntensityLengthMismatch);
1208 }
1209 let mut offset = 0;
1210 phases
1211 .iter()
1212 .map(|phase| {
1213 let end = offset + phase.reflection_ids.len();
1214 let result = phase.with_intensities(&values[offset..end]);
1215 offset = end;
1216 result
1217 })
1218 .collect()
1219}
1220
1221fn bin_integration_weights(x: &[f64]) -> Vec<f64> {
1222 match x.len() {
1223 0 => Vec::new(),
1224 1 => vec![1.0],
1225 count => (0..count)
1226 .map(|index| {
1227 if index == 0 {
1228 0.5 * (x[1] - x[0])
1229 } else if index + 1 == count {
1230 0.5 * (x[count - 1] - x[count - 2])
1231 } else {
1232 0.5 * (x[index + 1] - x[index - 1])
1233 }
1234 })
1235 .collect(),
1236 }
1237}
1238
1239#[derive(Debug)]
1241pub enum TofLeBailError {
1242 Pattern(DomainError),
1244 MissingObservations,
1246 Profile(TofError),
1248 InvalidPhase(&'static str),
1250 InvalidOptions,
1252 InvalidBackground,
1254 InvalidBackgroundGrid,
1256 BackgroundGridOutsideDomain,
1258 BackgroundCoefficientLengthMismatch,
1260 BackgroundBasisShape,
1262 InsufficientBackgroundObservations,
1264 BackgroundLinearSolve,
1266 IntensityLengthMismatch,
1268 DSpacingLengthMismatch,
1270 AllocationOverflow,
1272 InvalidCheckpoint(&'static str),
1274 Residual(ResidualError),
1276 Execution(ExecutionPolicyError),
1278 Runtime(RuntimeError),
1280}
1281
1282impl Display for TofLeBailError {
1283 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
1284 match self {
1285 Self::Pattern(error) => Display::fmt(error, formatter),
1286 Self::MissingObservations => {
1287 formatter.write_str("observed_y is required for TOF Le Bail extraction")
1288 }
1289 Self::Profile(error) => Display::fmt(error, formatter),
1290 Self::InvalidPhase(message) | Self::InvalidCheckpoint(message) => {
1291 formatter.write_str(message)
1292 }
1293 Self::InvalidOptions => formatter.write_str("invalid TOF Le Bail options"),
1294 Self::InvalidBackground => {
1295 formatter.write_str("invalid TOF Chebyshev background coefficients or domain")
1296 }
1297 Self::InvalidBackgroundGrid => {
1298 formatter.write_str("TOF Chebyshev background grid must be finite and increasing")
1299 }
1300 Self::BackgroundGridOutsideDomain => {
1301 formatter.write_str("TOF grid extends outside the Chebyshev background domain")
1302 }
1303 Self::BackgroundCoefficientLengthMismatch => {
1304 formatter.write_str("TOF Chebyshev background coefficient length mismatch")
1305 }
1306 Self::BackgroundBasisShape => {
1307 formatter.write_str("TOF Chebyshev background basis shape mismatch")
1308 }
1309 Self::InsufficientBackgroundObservations => formatter.write_str(
1310 "TOF Chebyshev refinement has fewer included observations than coefficients",
1311 ),
1312 Self::BackgroundLinearSolve => {
1313 formatter.write_str("TOF Chebyshev weighted linear solve failed")
1314 }
1315 Self::IntensityLengthMismatch => {
1316 formatter.write_str("TOF reflection intensity length mismatch")
1317 }
1318 Self::DSpacingLengthMismatch => {
1319 formatter.write_str("TOF reflection d-spacing length mismatch")
1320 }
1321 Self::AllocationOverflow => formatter.write_str("TOF Le Bail allocation overflow"),
1322 Self::Residual(error) => Display::fmt(error, formatter),
1323 Self::Execution(error) => Display::fmt(error, formatter),
1324 Self::Runtime(error) => Display::fmt(error, formatter),
1325 }
1326 }
1327}
1328
1329impl Error for TofLeBailError {
1330 fn source(&self) -> Option<&(dyn Error + 'static)> {
1331 match self {
1332 Self::Pattern(error) => Some(error),
1333 Self::Profile(error) => Some(error),
1334 Self::Residual(error) => Some(error),
1335 Self::Execution(error) => Some(error),
1336 Self::Runtime(error) => Some(error),
1337 _ => None,
1338 }
1339 }
1340}
1341
1342impl From<TofError> for TofLeBailError {
1343 fn from(value: TofError) -> Self {
1344 Self::Profile(value)
1345 }
1346}
1347
1348impl From<ResidualError> for TofLeBailError {
1349 fn from(value: ResidualError) -> Self {
1350 Self::Residual(value)
1351 }
1352}
1353
1354impl From<ExecutionPolicyError> for TofLeBailError {
1355 fn from(value: ExecutionPolicyError) -> Self {
1356 Self::Execution(value)
1357 }
1358}
1359
1360impl From<RuntimeError> for TofLeBailError {
1361 fn from(value: RuntimeError) -> Self {
1362 Self::Runtime(value)
1363 }
1364}