1use std::collections::BTreeMap;
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7use nalgebra::{DMatrix, DVector};
8use phasesmith_core::{
9 Accumulation, ConstantWavelengthInstrument, CwContributionsError, GridView,
10 OwnedCwContributionArrays, OwnedCwContributions, ProfileError, SupportPolicy,
11 accumulate_cw_contributions_batch_with_context,
12};
13use phasesmith_crystallography::UnitCell;
14use phasesmith_execution::{ExecutionPolicy, ExecutionPolicyError};
15use phasesmith_model::{DomainError, PatternRecord};
16
17use crate::{
18 BackgroundError, BackgroundModel, Constraint, ConstraintError, ConstraintTransform,
19 DiagnosticValue, DifferentiableBackground, GeneratedLatticeDomain, LatticeError,
20 LatticeReflectionDomain, ParameterBounds, ParameterError, ParameterKey, ParameterSet,
21 ParameterSpec, RefinementEventKind, RefinementLimits, RefinementRuntime, ResidualError,
22 ResidualEvaluation, ResidualOptions, RuntimeError, TerminationReason, cw_lattice_geometry,
23 evaluate_residuals,
24};
25
26const INSTRUMENT_PARAMETER_NAMES: [&str; 5] = ["u_deg2", "v_deg2", "w_deg2", "x_deg", "y_deg"];
27const LATTICE_PARAMETER_NAMES: [&str; 6] = [
28 "a_angstrom",
29 "b_angstrom",
30 "c_angstrom",
31 "alpha_deg",
32 "beta_deg",
33 "gamma_deg",
34];
35
36#[derive(Clone, Debug, PartialEq)]
38pub struct LeBailPhase {
39 phase_id: String,
40 name: String,
41 reflection_ids: Vec<String>,
42 hkl: Vec<[i32; 3]>,
43 d_spacing_angstrom: Vec<f64>,
44 two_theta_deg: Vec<f64>,
45 integrated_intensity: Vec<f64>,
46 scale: f64,
47 preserve_unobserved: Vec<bool>,
48 cell: Option<UnitCell>,
49 reflection_domain: Option<LatticeReflectionDomain>,
50}
51
52impl LeBailPhase {
53 #[allow(clippy::too_many_arguments)]
65 pub fn new(
66 phase_id: impl Into<String>,
67 name: impl Into<String>,
68 reflection_ids: Vec<String>,
69 hkl: Vec<[i32; 3]>,
70 d_spacing_angstrom: Vec<f64>,
71 two_theta_deg: Vec<f64>,
72 integrated_intensity: Vec<f64>,
73 scale: f64,
74 preserve_unobserved: Vec<bool>,
75 ) -> Result<Self, LeBailError> {
76 let phase = Self {
77 phase_id: phase_id.into(),
78 name: name.into(),
79 reflection_ids,
80 hkl,
81 d_spacing_angstrom,
82 two_theta_deg,
83 integrated_intensity,
84 scale,
85 preserve_unobserved,
86 cell: None,
87 reflection_domain: None,
88 };
89 phase.validate()?;
90 Ok(phase)
91 }
92
93 pub fn from_lattice_domain(
100 phase_id: impl Into<String>,
101 name: impl Into<String>,
102 cell: UnitCell,
103 scale: f64,
104 reflection_domain: LatticeReflectionDomain,
105 ) -> Result<Self, LeBailError> {
106 let generated = reflection_domain
107 .generate(cell, None)
108 .map_err(LeBailError::Lattice)?;
109 let mut phase = Self::new(
110 phase_id,
111 name,
112 generated.reflection_ids.clone(),
113 generated.hkl.clone(),
114 generated.d_spacing_angstrom.clone(),
115 generated.two_theta_deg.clone(),
116 generated.integrated_intensity.clone(),
117 scale,
118 generated.visible.iter().map(|visible| !visible).collect(),
119 )?;
120 phase.cell = Some(cell);
121 phase.reflection_domain = Some(reflection_domain);
122 phase.validate()?;
123 Ok(phase)
124 }
125
126 fn validate(&self) -> Result<(), LeBailError> {
127 validate_stable_label("phase_id", &self.phase_id)?;
128 if self.name.trim().is_empty() {
129 return Err(invalid_phase("phase name must be non-empty"));
130 }
131 let count = self.reflection_ids.len();
132 if count == 0 {
133 return Err(invalid_phase("at least one reflection is required"));
134 }
135 if self.hkl.len() != count
136 || self.d_spacing_angstrom.len() != count
137 || self.two_theta_deg.len() != count
138 || self.integrated_intensity.len() != count
139 || (!self.preserve_unobserved.is_empty() && self.preserve_unobserved.len() != count)
140 {
141 return Err(invalid_phase("reflection arrays must have equal lengths"));
142 }
143 let mut identities = std::collections::BTreeSet::new();
144 for reflection_id in &self.reflection_ids {
145 validate_stable_label("reflection_id", reflection_id)?;
146 if !identities.insert(reflection_id) {
147 return Err(invalid_phase(
148 "reflection IDs must be unique within a phase",
149 ));
150 }
151 }
152 if self
153 .d_spacing_angstrom
154 .iter()
155 .any(|value| !value.is_finite() || *value <= 0.0)
156 {
157 return Err(invalid_phase("d-spacings must be positive and finite"));
158 }
159 if self
160 .two_theta_deg
161 .iter()
162 .any(|value| !value.is_finite() || *value <= 0.0 || *value >= 180.0)
163 {
164 return Err(invalid_phase(
165 "reflection positions must lie strictly inside (0, 180) degrees",
166 ));
167 }
168 if self
169 .integrated_intensity
170 .iter()
171 .any(|value| !value.is_finite() || *value < 0.0)
172 {
173 return Err(invalid_phase(
174 "integrated intensities must be non-negative and finite",
175 ));
176 }
177 if !self.scale.is_finite() || self.scale < 0.0 {
178 return Err(invalid_phase("phase scale must be non-negative and finite"));
179 }
180 match (&self.cell, &self.reflection_domain) {
181 (None, None) => {}
182 (Some(cell), Some(domain)) => {
183 domain.validate_cell(*cell).map_err(LeBailError::Lattice)?;
184 if self.preserve_unobserved.len() != count {
185 return Err(invalid_phase(
186 "dynamic phases require one visibility marker per reflection",
187 ));
188 }
189 }
190 _ => {
191 return Err(invalid_phase(
192 "dynamic phases require both a cell and reflection domain",
193 ));
194 }
195 }
196 Ok(())
197 }
198
199 #[must_use]
201 pub fn phase_id(&self) -> &str {
202 &self.phase_id
203 }
204
205 #[must_use]
207 pub fn name(&self) -> &str {
208 &self.name
209 }
210
211 #[must_use]
213 pub fn reflection_ids(&self) -> &[String] {
214 &self.reflection_ids
215 }
216
217 #[must_use]
219 pub fn hkl(&self) -> &[[i32; 3]] {
220 &self.hkl
221 }
222
223 #[must_use]
225 pub fn d_spacing_angstrom(&self) -> &[f64] {
226 &self.d_spacing_angstrom
227 }
228
229 #[must_use]
231 pub fn two_theta_deg(&self) -> &[f64] {
232 &self.two_theta_deg
233 }
234
235 #[must_use]
237 pub fn integrated_intensity(&self) -> &[f64] {
238 &self.integrated_intensity
239 }
240
241 pub fn with_integrated_intensities(&self, values: &[f64]) -> Result<Self, LeBailError> {
248 self.replace_intensities(values)
249 }
250
251 #[must_use]
253 pub const fn scale(&self) -> f64 {
254 self.scale
255 }
256
257 #[must_use]
259 pub fn preserve_unobserved(&self) -> &[bool] {
260 &self.preserve_unobserved
261 }
262
263 #[must_use]
265 pub const fn cell(&self) -> Option<UnitCell> {
266 self.cell
267 }
268
269 #[must_use]
271 pub const fn reflection_domain(&self) -> Option<&LatticeReflectionDomain> {
272 self.reflection_domain.as_ref()
273 }
274
275 pub fn regenerate_lattice_at_cell(&self, cell: UnitCell) -> Result<Self, LeBailError> {
285 let domain = self
286 .reflection_domain
287 .as_ref()
288 .ok_or_else(|| invalid_phase("only dynamic phases have a lattice reflection domain"))?;
289 let previous = self
290 .reflection_ids
291 .iter()
292 .cloned()
293 .zip(self.integrated_intensity.iter().copied())
294 .collect::<BTreeMap<_, _>>();
295 let generated = domain
296 .generate(cell, Some(&previous))
297 .map_err(LeBailError::Lattice)?;
298 self.replace_generated_domain(cell, generated)
299 }
300
301 fn replace_intensities(&self, values: &[f64]) -> Result<Self, LeBailError> {
302 if values.len() != self.integrated_intensity.len()
303 || values
304 .iter()
305 .any(|value| !value.is_finite() || *value < 0.0)
306 {
307 return Err(invalid_phase(
308 "replacement intensities must match and remain non-negative",
309 ));
310 }
311 let mut phase = self.clone();
312 phase.integrated_intensity.copy_from_slice(values);
313 Ok(phase)
314 }
315
316 fn replace_scale_and_positions(
317 &self,
318 scale: f64,
319 positions: Vec<f64>,
320 ) -> Result<Self, LeBailError> {
321 let mut phase = self.clone();
322 phase.scale = scale;
323 phase.two_theta_deg = positions;
324 phase.validate()?;
325 Ok(phase)
326 }
327
328 fn replace_cell_geometry(
329 &self,
330 cell: UnitCell,
331 wavelength_angstrom: f64,
332 ) -> Result<Self, LeBailError> {
333 let domain = self.reflection_domain.as_ref().ok_or_else(|| {
334 invalid_phase("lattice parameters require a bounded reflection domain")
335 })?;
336 domain.validate_cell(cell).map_err(LeBailError::Lattice)?;
337 let geometry = cw_lattice_geometry(
338 domain.parameterization(),
339 cell,
340 &self.hkl,
341 wavelength_angstrom,
342 )
343 .map_err(LeBailError::Lattice)?;
344 let mut phase = self.clone();
345 phase.cell = Some(cell);
346 phase.d_spacing_angstrom = geometry.d_spacing_angstrom;
347 phase.two_theta_deg = geometry.two_theta_deg;
348 phase.validate()?;
349 Ok(phase)
350 }
351
352 fn replace_generated_domain(
353 &self,
354 cell: UnitCell,
355 generated: GeneratedLatticeDomain,
356 ) -> Result<Self, LeBailError> {
357 let mut phase = self.clone();
358 phase.cell = Some(cell);
359 phase.reflection_ids = generated.reflection_ids;
360 phase.hkl = generated.hkl;
361 phase.d_spacing_angstrom = generated.d_spacing_angstrom;
362 phase.two_theta_deg = generated.two_theta_deg;
363 phase.integrated_intensity = generated.integrated_intensity;
364 phase.preserve_unobserved = generated
365 .visible
366 .into_iter()
367 .map(|visible| !visible)
368 .collect();
369 phase.validate()?;
370 Ok(phase)
371 }
372}
373
374pub fn lebail_instrument_parameter_key(name: &str) -> Result<ParameterKey, LeBailError> {
380 if !INSTRUMENT_PARAMETER_NAMES.contains(&name) {
381 return Err(LeBailError::UnsupportedParameter {
382 label: format!("instrument[cw].{name}"),
383 });
384 }
385 ParameterKey::new("instrument", "cw", name).map_err(LeBailError::Parameter)
386}
387
388pub fn lebail_phase_scale_key(phase_id: &str) -> Result<ParameterKey, LeBailError> {
394 ParameterKey::new("phase", phase_id, "scale").map_err(LeBailError::Parameter)
395}
396
397pub fn lebail_background_parameter_key(
403 background_id: &str,
404 name: &str,
405) -> Result<ParameterKey, LeBailError> {
406 ParameterKey::new("background", background_id, name).map_err(LeBailError::Parameter)
407}
408
409pub fn lebail_lattice_parameter_key(
415 phase_id: &str,
416 name: &str,
417) -> Result<ParameterKey, LeBailError> {
418 if !LATTICE_PARAMETER_NAMES.contains(&name) {
419 return Err(LeBailError::UnsupportedParameter {
420 label: format!("lattice[{phase_id}].{name}"),
421 });
422 }
423 ParameterKey::new("lattice", phase_id, name).map_err(LeBailError::Parameter)
424}
425
426pub fn lebail_reflection_position_key(
432 phase_id: &str,
433 reflection_id: &str,
434) -> Result<ParameterKey, LeBailError> {
435 ParameterKey::new(
436 "reflection",
437 format!("{phase_id}/{reflection_id}"),
438 "two_theta_deg",
439 )
440 .map_err(LeBailError::Parameter)
441}
442
443pub fn build_lebail_parameter_set(
449 instrument: ConstantWavelengthInstrument,
450 phases: &[LeBailPhase],
451 instrument_parameters: &[&str],
452 phase_scales: bool,
453 reflection_positions: bool,
454) -> Result<ParameterSet, LeBailError> {
455 build_lebail_parameter_set_with_lattice(
456 instrument,
457 phases,
458 instrument_parameters,
459 phase_scales,
460 reflection_positions,
461 false,
462 )
463}
464
465pub fn build_lebail_parameter_set_with_lattice(
471 instrument: ConstantWavelengthInstrument,
472 phases: &[LeBailPhase],
473 instrument_parameters: &[&str],
474 phase_scales: bool,
475 reflection_positions: bool,
476 lattice_parameters: bool,
477) -> Result<ParameterSet, LeBailError> {
478 if lattice_parameters && reflection_positions {
479 return Err(invalid_phase(
480 "lattice parameters and independent reflection positions are redundant",
481 ));
482 }
483 let mut specs = Vec::new();
484 for name in instrument_parameters {
485 let key = lebail_instrument_parameter_key(name)?;
486 let value = instrument_parameter(instrument, name)
487 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
488 specs.push(
489 ParameterSpec::new(
490 key,
491 value,
492 if name.ends_with("deg2") {
493 "degree^2"
494 } else {
495 "degree"
496 },
497 ParameterBounds::default(),
498 value.abs().max(if name.ends_with("deg2") {
499 1.0e-5
500 } else {
501 1.0e-4
502 }),
503 true,
504 )
505 .map_err(LeBailError::Parameter)?,
506 );
507 }
508 for phase in phases {
509 if lattice_parameters {
510 append_lattice_parameter_specs(&mut specs, phase)?;
511 }
512 if phase_scales {
513 specs.push(
514 ParameterSpec::new(
515 lebail_phase_scale_key(phase.phase_id())?,
516 phase.scale(),
517 "dimensionless",
518 ParameterBounds::new(0.0, f64::INFINITY).map_err(LeBailError::Parameter)?,
519 phase.scale().max(1.0),
520 true,
521 )
522 .map_err(LeBailError::Parameter)?,
523 );
524 }
525 if reflection_positions {
526 if phase.reflection_domain.is_some() {
527 return Err(invalid_phase(
528 "independent reflection positions require fixed-topology phases",
529 ));
530 }
531 for (reflection_id, position) in phase.reflection_ids.iter().zip(&phase.two_theta_deg) {
532 specs.push(
533 ParameterSpec::new(
534 lebail_reflection_position_key(phase.phase_id(), reflection_id)?,
535 *position,
536 "degree_2theta",
537 ParameterBounds::new(
538 f64::from_bits(1),
539 f64::from_bits(180.0_f64.to_bits() - 1),
540 )
541 .map_err(LeBailError::Parameter)?,
542 0.01,
543 true,
544 )
545 .map_err(LeBailError::Parameter)?,
546 );
547 }
548 }
549 }
550 ParameterSet::new(specs).map_err(LeBailError::Parameter)
551}
552
553fn append_lattice_parameter_specs(
554 specs: &mut Vec<ParameterSpec>,
555 phase: &LeBailPhase,
556) -> Result<(), LeBailError> {
557 let cell = phase
558 .cell
559 .ok_or_else(|| invalid_phase("lattice parameters require bounded dynamic phases"))?;
560 let domain = phase
561 .reflection_domain
562 .as_ref()
563 .ok_or_else(|| invalid_phase("lattice parameters require bounded dynamic phases"))?;
564 let values = domain
565 .parameterization()
566 .values_from_cell(cell)
567 .map_err(LeBailError::Lattice)?;
568 for (((name, value), lower), upper) in domain
569 .parameterization()
570 .parameter_names()
571 .iter()
572 .zip(values)
573 .zip(domain.bounds().lower())
574 .zip(domain.bounds().upper())
575 {
576 specs.push(
577 ParameterSpec::new(
578 lebail_lattice_parameter_key(phase.phase_id(), name)?,
579 value,
580 if name.ends_with("_angstrom") {
581 "angstrom"
582 } else {
583 "degree"
584 },
585 ParameterBounds::new(*lower, *upper).map_err(LeBailError::Parameter)?,
586 value.abs().max(1.0),
587 true,
588 )
589 .map_err(LeBailError::Parameter)?,
590 );
591 }
592 Ok(())
593}
594
595#[derive(Clone, Debug, PartialEq)]
597pub struct LeBailInput {
598 pub pattern: PatternRecord,
600 pub instrument: ConstantWavelengthInstrument,
602 pub phases: Vec<LeBailPhase>,
604 pub background: Option<BackgroundModel>,
606 pub parameters: Option<ParameterSet>,
608 pub constraints: Vec<Constraint>,
610}
611
612impl LeBailInput {
613 pub fn new(
620 pattern: PatternRecord,
621 instrument: ConstantWavelengthInstrument,
622 phases: Vec<LeBailPhase>,
623 ) -> Result<Self, LeBailError> {
624 pattern.validate().map_err(LeBailError::Pattern)?;
625 if pattern.observed_y.is_none() {
626 return Err(LeBailError::MissingObservations);
627 }
628 instrument
629 .validate()
630 .map_err(|error| LeBailError::Profile {
631 message: error.to_string(),
632 })?;
633 if phases.is_empty() {
634 return Err(invalid_phase("at least one phase is required"));
635 }
636 let mut phase_ids = std::collections::BTreeSet::new();
637 for phase in &phases {
638 phase.validate()?;
639 if phase.reflection_domain.as_ref().is_some_and(|domain| {
640 domain.wavelength_angstrom().to_bits() != instrument.wavelength_angstrom.to_bits()
641 }) {
642 return Err(invalid_phase(
643 "dynamic phase wavelength must match the Le Bail instrument",
644 ));
645 }
646 if !phase_ids.insert(phase.phase_id()) {
647 return Err(invalid_phase("phase IDs must be unique"));
648 }
649 }
650 Ok(Self {
651 pattern,
652 instrument,
653 phases,
654 background: None,
655 parameters: None,
656 constraints: Vec::new(),
657 })
658 }
659
660 pub fn new_with_parameters(
667 pattern: PatternRecord,
668 instrument: ConstantWavelengthInstrument,
669 phases: Vec<LeBailPhase>,
670 parameters: ParameterSet,
671 constraints: Vec<Constraint>,
672 ) -> Result<Self, LeBailError> {
673 let mut input = Self::new(pattern, instrument, phases)?;
674 validate_parameter_selection(&input.phases, input.background.as_ref(), ¶meters)?;
675 domain_parameter_values(
676 input.instrument,
677 &input.phases,
678 input.background.as_ref(),
679 ¶meters,
680 )?;
681 ConstraintTransform::new(parameters.clone(), constraints.clone())
682 .map_err(LeBailError::Constraint)?;
683 input.parameters = Some(parameters);
684 input.constraints = constraints;
685 Ok(input)
686 }
687
688 pub fn with_refinable_background(
698 mut self,
699 background: BackgroundModel,
700 ) -> Result<Self, LeBailError> {
701 background
702 .basis(&self.pattern.x_deg)
703 .map_err(LeBailError::Background)?;
704 background
705 .calculate(&self.pattern.x_deg)
706 .map_err(LeBailError::Background)?;
707 let names = background.parameter_names();
708 let coefficients = background.coefficients();
709 let bounds = background.parameter_bounds();
710 let refine_in_nonlinear_step = !background.basis_is_invariant();
711 let mut specs = self
712 .parameters
713 .as_ref()
714 .map_or_else(Vec::new, |parameters| parameters.specs().to_vec());
715 let background_scale = self
716 .pattern
717 .background_y
718 .iter()
719 .map(|value| value.abs())
720 .fold(1.0_f64, f64::max);
721 for ((name, value), bounds) in names.iter().zip(coefficients).zip(bounds) {
722 specs.push(
723 ParameterSpec::new(
724 lebail_background_parameter_key(background.background_id(), name)?,
725 value,
726 "intensity",
727 bounds,
728 value.abs().max(background_scale),
729 refine_in_nonlinear_step,
730 )
731 .map_err(LeBailError::Parameter)?,
732 );
733 }
734 self.parameters = Some(ParameterSet::new(specs).map_err(LeBailError::Parameter)?);
735 self.background = Some(background);
736 validate_parameter_selection(
737 &self.phases,
738 self.background.as_ref(),
739 self.parameters
740 .as_ref()
741 .ok_or(LeBailError::InternalInvariant)?,
742 )?;
743 ConstraintTransform::new(
744 self.parameters
745 .clone()
746 .ok_or(LeBailError::InternalInvariant)?,
747 self.constraints.clone(),
748 )
749 .map_err(LeBailError::Constraint)?;
750 Ok(self)
751 }
752}
753
754#[derive(Clone, Debug, PartialEq)]
756pub struct LeBailOptions {
757 pub max_iterations: usize,
759 pub min_iterations: usize,
761 pub intensity_tolerance: f64,
763 pub rwp_tolerance: f64,
765 pub redistribution_damping: f64,
767 pub minimum_calculated: f64,
769 pub initial_intensity_floor: f64,
771 pub use_uncertainty: bool,
773 pub profile_damping: f64,
775 pub max_scaled_parameter_step: f64,
777 pub max_profile_backtracks: usize,
779 pub unresolved_correlation: f64,
781 pub diagnose_rank_deficiency: bool,
783 pub support_fwhm: f64,
785 pub execution: ExecutionPolicy,
787}
788
789impl LeBailOptions {
790 #[allow(clippy::too_many_arguments)]
796 pub fn new(
797 max_iterations: usize,
798 min_iterations: usize,
799 intensity_tolerance: f64,
800 rwp_tolerance: f64,
801 redistribution_damping: f64,
802 minimum_calculated: f64,
803 initial_intensity_floor: f64,
804 use_uncertainty: bool,
805 unresolved_correlation: f64,
806 diagnose_rank_deficiency: bool,
807 support_fwhm: f64,
808 execution: ExecutionPolicy,
809 ) -> Result<Self, LeBailError> {
810 let options = Self {
811 max_iterations,
812 min_iterations,
813 intensity_tolerance,
814 rwp_tolerance,
815 redistribution_damping,
816 minimum_calculated,
817 initial_intensity_floor,
818 use_uncertainty,
819 profile_damping: 1.0e-10,
820 max_scaled_parameter_step: 0.25,
821 max_profile_backtracks: 8,
822 unresolved_correlation,
823 diagnose_rank_deficiency,
824 support_fwhm,
825 execution,
826 };
827 options.validate()?;
828 Ok(options)
829 }
830
831 fn validate(&self) -> Result<(), LeBailError> {
832 if self.max_iterations == 0
833 || self.min_iterations == 0
834 || self.min_iterations > self.max_iterations
835 {
836 return Err(invalid_options(
837 "iteration counts must be positive and minimum must not exceed maximum",
838 ));
839 }
840 for (name, value) in [
841 ("intensity_tolerance", self.intensity_tolerance),
842 ("rwp_tolerance", self.rwp_tolerance),
843 ("minimum_calculated", self.minimum_calculated),
844 ("initial_intensity_floor", self.initial_intensity_floor),
845 ("support_fwhm", self.support_fwhm),
846 ] {
847 if !value.is_finite() || value <= 0.0 {
848 return Err(LeBailError::InvalidOptions {
849 message: format!("{name} must be positive and finite"),
850 });
851 }
852 }
853 if !self.redistribution_damping.is_finite()
854 || self.redistribution_damping <= 0.0
855 || self.redistribution_damping > 1.0
856 {
857 return Err(invalid_options("redistribution_damping must lie in (0, 1]"));
858 }
859 if !self.unresolved_correlation.is_finite()
860 || !(0.0..=1.0).contains(&self.unresolved_correlation)
861 {
862 return Err(invalid_options("unresolved_correlation must lie in [0, 1]"));
863 }
864 if !self.profile_damping.is_finite() || self.profile_damping < 0.0 {
865 return Err(invalid_options(
866 "profile_damping must be non-negative and finite",
867 ));
868 }
869 if !self.max_scaled_parameter_step.is_finite() || self.max_scaled_parameter_step <= 0.0 {
870 return Err(invalid_options(
871 "max_scaled_parameter_step must be positive and finite",
872 ));
873 }
874 Ok(())
875 }
876
877 pub fn with_profile_controls(
883 mut self,
884 profile_damping: f64,
885 max_scaled_parameter_step: f64,
886 max_profile_backtracks: usize,
887 ) -> Result<Self, LeBailError> {
888 self.profile_damping = profile_damping;
889 self.max_scaled_parameter_step = max_scaled_parameter_step;
890 self.max_profile_backtracks = max_profile_backtracks;
891 self.validate()?;
892 Ok(self)
893 }
894
895 pub fn scripting_defaults(execution: ExecutionPolicy) -> Result<Self, LeBailError> {
901 Self::new(
902 50,
903 2,
904 1.0e-6,
905 1.0e-8,
906 1.0,
907 1.0e-15,
908 1.0e-12,
909 true,
910 1.0 - 1.0e-10,
911 false,
912 20.0,
913 execution,
914 )
915 }
916}
917
918#[derive(Clone, Debug, PartialEq)]
920pub struct PhasePatternComponent {
921 pub phase_id: String,
923 pub y: Vec<f64>,
925}
926
927#[derive(Clone, Debug, PartialEq)]
929pub struct LeBailCalculation {
930 pub y: Vec<f64>,
932 pub profile_y: Vec<f64>,
934 pub background_y: Vec<f64>,
936 pub accumulation: Accumulation,
938 pub reflection_keys: Vec<(String, String)>,
940 pub phase_offsets: Vec<usize>,
942 pub phase_components: Vec<PhasePatternComponent>,
944}
945
946#[derive(Clone, Debug, PartialEq)]
948pub struct IntensityExtractionResult {
949 pub intensities: Vec<f64>,
951 pub maximum_relative_change: f64,
953 pub unobserved_reflections: Vec<(String, String)>,
955}
956
957#[derive(Clone, Debug, PartialEq)]
959pub struct ParameterChange {
960 pub key: ParameterKey,
962 pub before: f64,
964 pub after: f64,
966 pub scaled_change: f64,
968}
969
970#[derive(Clone, Debug, PartialEq)]
972pub struct LeBailIterationRecord {
973 pub iteration: usize,
975 pub rp: f64,
977 pub rwp: f64,
979 pub chi_square: f64,
981 pub reduced_chi_square: f64,
983 pub maximum_relative_intensity_change: f64,
985 pub scaled_profile_step_norm: f64,
987 pub parameter_changes: Vec<ParameterChange>,
989 pub warnings: Vec<String>,
991}
992
993#[derive(Clone, Debug, PartialEq)]
995pub struct ReflectionIntensity {
996 pub phase_id: String,
998 pub reflection_id: String,
1000 pub integrated_intensity: f64,
1002}
1003
1004#[derive(Clone, Debug, PartialEq, Eq)]
1006pub struct CoincidentReflectionGroup {
1007 pub reflection_keys: Vec<(String, String)>,
1009 pub rank: usize,
1011}
1012
1013#[derive(Clone, Debug, PartialEq)]
1015pub struct LeBailCheckpoint {
1016 pub completed_iterations: usize,
1018 pub phases: Vec<LeBailPhase>,
1020 pub instrument: ConstantWavelengthInstrument,
1022 pub background: Option<BackgroundModel>,
1024 pub intensities: Vec<f64>,
1026 pub parameters: Option<ParameterSet>,
1028 pub previous_rwp: f64,
1030 pub history: Vec<LeBailIterationRecord>,
1032}
1033
1034impl LeBailCheckpoint {
1035 fn validate(&self) -> Result<(), LeBailError> {
1036 if self.completed_iterations != self.history.len() {
1037 return Err(LeBailError::InvalidCheckpoint {
1038 message: "checkpoint iteration count must equal its history length".to_owned(),
1039 });
1040 }
1041 if self.previous_rwp.is_nan() || self.previous_rwp == f64::NEG_INFINITY {
1042 return Err(LeBailError::InvalidCheckpoint {
1043 message: "checkpoint previous_rwp must be finite or positive infinity".to_owned(),
1044 });
1045 }
1046 for phase in &self.phases {
1047 phase.validate()?;
1048 }
1049 self.instrument
1050 .validate()
1051 .map_err(|error| LeBailError::Profile {
1052 message: error.to_string(),
1053 })?;
1054 if self.phases.iter().any(|phase| {
1055 phase.reflection_domain.as_ref().is_some_and(|domain| {
1056 domain.wavelength_angstrom().to_bits()
1057 != self.instrument.wavelength_angstrom.to_bits()
1058 })
1059 }) {
1060 return Err(LeBailError::InvalidCheckpoint {
1061 message: "checkpoint dynamic phase wavelength must match its instrument".to_owned(),
1062 });
1063 }
1064 if let Some(parameters) = &self.parameters {
1065 validate_parameter_selection(&self.phases, self.background.as_ref(), parameters)?;
1066 let domain_values = domain_parameter_values(
1067 self.instrument,
1068 &self.phases,
1069 self.background.as_ref(),
1070 parameters,
1071 )?;
1072 if domain_values != parameters.values() {
1073 return Err(LeBailError::InvalidCheckpoint {
1074 message: "checkpoint parameters disagree with its live domain".to_owned(),
1075 });
1076 }
1077 }
1078 let expected = self.phases.iter().map(reflection_count).sum::<usize>();
1079 if self.intensities.len() != expected
1080 || self
1081 .intensities
1082 .iter()
1083 .any(|value| !value.is_finite() || *value < 0.0)
1084 {
1085 return Err(LeBailError::InvalidCheckpoint {
1086 message: "checkpoint intensities must match its phases".to_owned(),
1087 });
1088 }
1089 if flatten_intensities(&self.phases) != self.intensities {
1090 return Err(LeBailError::InvalidCheckpoint {
1091 message: "checkpoint phase and flattened intensities disagree".to_owned(),
1092 });
1093 }
1094 if self
1095 .history
1096 .iter()
1097 .enumerate()
1098 .any(|(index, record)| record.iteration != index + 1)
1099 {
1100 return Err(LeBailError::InvalidCheckpoint {
1101 message: "checkpoint history iterations must be contiguous and one-based"
1102 .to_owned(),
1103 });
1104 }
1105 Ok(())
1106 }
1107}
1108
1109#[derive(Clone, Debug, PartialEq)]
1111pub struct LeBailResult {
1112 pub calculation: LeBailCalculation,
1114 pub phases: Vec<LeBailPhase>,
1116 pub instrument: ConstantWavelengthInstrument,
1118 pub background: Option<BackgroundModel>,
1120 pub intensities: Vec<ReflectionIntensity>,
1122 pub metrics: ResidualEvaluation,
1124 pub history: Vec<LeBailIterationRecord>,
1126 pub termination_reason: TerminationReason,
1128 pub rank_deficient_groups: Vec<CoincidentReflectionGroup>,
1130 pub parameters: Option<ParameterSet>,
1132 pub covariance: Option<CovarianceMatrix>,
1134 pub checkpoint: LeBailCheckpoint,
1136}
1137
1138#[derive(Clone, Debug, PartialEq)]
1143pub struct CovarianceMatrix {
1144 pub size: usize,
1146 pub values: Vec<f64>,
1148}
1149
1150pub fn calculate_lebail_pattern(
1156 pattern: &PatternRecord,
1157 instrument: ConstantWavelengthInstrument,
1158 phases: &[LeBailPhase],
1159 support_fwhm: f64,
1160 execution: &ExecutionPolicy,
1161) -> Result<LeBailCalculation, LeBailError> {
1162 calculate_lebail_pattern_with_background(
1163 pattern,
1164 instrument,
1165 phases,
1166 None,
1167 support_fwhm,
1168 execution,
1169 )
1170}
1171
1172pub fn calculate_lebail_pattern_with_background(
1181 pattern: &PatternRecord,
1182 instrument: ConstantWavelengthInstrument,
1183 phases: &[LeBailPhase],
1184 background: Option<&BackgroundModel>,
1185 support_fwhm: f64,
1186 execution: &ExecutionPolicy,
1187) -> Result<LeBailCalculation, LeBailError> {
1188 pattern.validate().map_err(LeBailError::Pattern)?;
1189 if phases.is_empty() {
1190 return Err(invalid_phase("at least one phase is required"));
1191 }
1192 if !support_fwhm.is_finite() || support_fwhm <= 0.0 {
1193 return Err(invalid_options("support_fwhm must be positive and finite"));
1194 }
1195 let reflection_count = phases.iter().map(reflection_count).sum::<usize>();
1196 let mut positions = Vec::with_capacity(reflection_count);
1197 let mut intensities = Vec::with_capacity(reflection_count);
1198 let mut multipliers = Vec::with_capacity(reflection_count);
1199 let mut reflection_keys = Vec::with_capacity(reflection_count);
1200 let mut phase_offsets = Vec::with_capacity(phases.len() + 1);
1201 let phase_derivative_count = phases
1202 .len()
1203 .checked_mul(reflection_count)
1204 .ok_or(LeBailError::SizeOverflow)?;
1205 let mut derivative_multipliers = vec![0.0; phase_derivative_count];
1206 phase_offsets.push(0);
1207 for (phase_index, phase) in phases.iter().enumerate() {
1208 phase.validate()?;
1209 let begin = positions.len();
1210 positions.extend_from_slice(&phase.two_theta_deg);
1211 intensities.extend_from_slice(&phase.integrated_intensity);
1212 multipliers.extend(std::iter::repeat_n(phase.scale, reflection_count_of(phase)));
1213 reflection_keys.extend(
1214 phase
1215 .reflection_ids
1216 .iter()
1217 .map(|reflection_id| (phase.phase_id.clone(), reflection_id.clone())),
1218 );
1219 let end = positions.len();
1220 derivative_multipliers
1221 [phase_index * reflection_count + begin..phase_index * reflection_count + end]
1222 .fill(1.0);
1223 phase_offsets.push(end);
1224 }
1225 let contributions = OwnedCwContributions::new(
1226 reflection_count,
1227 phases.len(),
1228 OwnedCwContributionArrays {
1229 gaussian_variance_deg2: vec![0.0; reflection_count],
1230 lorentzian_fwhm_deg: vec![0.0; reflection_count],
1231 intensity_multiplier: multipliers,
1232 d_gaussian_variance_d_position: vec![0.0; reflection_count],
1233 d_lorentzian_fwhm_d_position: vec![0.0; reflection_count],
1234 d_intensity_multiplier_d_position: vec![0.0; reflection_count],
1235 d_gaussian_variance_d_parameters: vec![0.0; phase_derivative_count],
1236 d_lorentzian_fwhm_d_parameters: vec![0.0; phase_derivative_count],
1237 d_intensity_multiplier_d_parameters: derivative_multipliers,
1238 },
1239 )
1240 .map_err(LeBailError::Calculation)?;
1241 let grid = GridView::new(&pattern.x_deg).map_err(LeBailError::Grid)?;
1242 let accumulation = accumulate_cw_contributions_batch_with_context(
1243 grid,
1244 &positions,
1245 &intensities,
1246 instrument,
1247 contributions.as_view(),
1248 SupportPolicy::FwhmMultiple(support_fwhm),
1249 execution.context(),
1250 )
1251 .map_err(LeBailError::Calculation)?;
1252 let profile_y = accumulation.y.clone();
1253 let background_y = combined_background_values(pattern, background)?;
1254 let y = profile_y
1255 .iter()
1256 .zip(&background_y)
1257 .map(|(profile, background)| profile + background)
1258 .collect::<Vec<_>>();
1259 let phase_components = build_phase_components(
1260 phases,
1261 &phase_offsets,
1262 &intensities,
1263 &accumulation,
1264 pattern.sample_count(),
1265 );
1266 Ok(LeBailCalculation {
1267 y,
1268 profile_y,
1269 background_y,
1270 accumulation,
1271 reflection_keys,
1272 phase_offsets,
1273 phase_components,
1274 })
1275}
1276
1277fn combined_background_values(
1278 pattern: &PatternRecord,
1279 background: Option<&BackgroundModel>,
1280) -> Result<Vec<f64>, LeBailError> {
1281 let mut values = pattern.background_y.clone();
1282 if let Some(background) = background {
1283 for (fixed, residual) in values.iter_mut().zip(
1284 background
1285 .calculate(&pattern.x_deg)
1286 .map_err(LeBailError::Background)?,
1287 ) {
1288 *fixed += residual;
1289 }
1290 }
1291 Ok(values)
1292}
1293
1294fn build_phase_components(
1295 phases: &[LeBailPhase],
1296 phase_offsets: &[usize],
1297 intensities: &[f64],
1298 accumulation: &Accumulation,
1299 sample_count: usize,
1300) -> Vec<PhasePatternComponent> {
1301 phases
1302 .iter()
1303 .enumerate()
1304 .map(|(phase_index, phase)| {
1305 let mut phase_y = vec![0.0; sample_count];
1306 let first = phase_offsets[phase_index];
1307 let last = phase_offsets[phase_index + 1];
1308 for (reflection, intensity) in intensities.iter().enumerate().take(last).skip(first) {
1309 let begin = accumulation.derivatives.local.offsets[reflection];
1310 let end = accumulation.derivatives.local.offsets[reflection + 1];
1311 let start = accumulation.derivatives.local.starts[reflection];
1312 for active in begin..end {
1313 let sample = start + active - begin;
1314 phase_y[sample] += intensity
1315 * accumulation.derivatives.local.values
1316 [active * accumulation.derivatives.local.parameter_count];
1317 }
1318 }
1319 PhasePatternComponent {
1320 phase_id: phase.phase_id.clone(),
1321 y: phase_y,
1322 }
1323 })
1324 .collect()
1325}
1326
1327pub fn initialize_lebail_intensities(
1333 input: &LeBailInput,
1334 options: &LeBailOptions,
1335) -> Result<Vec<f64>, LeBailError> {
1336 options.validate()?;
1337 let values = flatten_intensities(&input.phases);
1338 if values
1339 .iter()
1340 .any(|value| !value.is_finite() || *value < 0.0)
1341 {
1342 return Err(invalid_phase(
1343 "starting intensities must be non-negative and finite",
1344 ));
1345 }
1346 if values.iter().any(|value| *value > 0.0) {
1347 return Ok(values
1348 .into_iter()
1349 .map(|value| value.max(options.initial_intensity_floor))
1350 .collect());
1351 }
1352 let observed = input
1353 .pattern
1354 .observed_y
1355 .as_deref()
1356 .ok_or(LeBailError::MissingObservations)?;
1357 let weights = bin_integration_weights(&input.pattern.x_deg);
1358 let mut background_y = input.pattern.background_y.clone();
1359 if let Some(background) = &input.background {
1360 for (fixed, residual) in background_y.iter_mut().zip(
1361 background
1362 .calculate(&input.pattern.x_deg)
1363 .map_err(LeBailError::Background)?,
1364 ) {
1365 *fixed += residual;
1366 }
1367 }
1368 let area = observed
1369 .iter()
1370 .zip(&background_y)
1371 .zip(weights)
1372 .map(|((observed, background), width)| (observed - background).max(0.0) * width)
1373 .sum::<f64>();
1374 let starting = (area / count_as_f64(values.len().max(1))).max(options.initial_intensity_floor);
1375 Ok(vec![starting; values.len()])
1376}
1377
1378pub fn extract_lebail_intensities(
1384 pattern: &PatternRecord,
1385 calculation: &LeBailCalculation,
1386 current: &[f64],
1387 options: &LeBailOptions,
1388 preserve_unobserved: &[bool],
1389) -> Result<IntensityExtractionResult, LeBailError> {
1390 options.validate()?;
1391 let observed = pattern
1392 .observed_y
1393 .as_deref()
1394 .ok_or(LeBailError::MissingObservations)?;
1395 let reflection_count = calculation.accumulation.derivatives.local.peak_count();
1396 if current.len() != reflection_count
1397 || current
1398 .iter()
1399 .any(|value| !value.is_finite() || *value < 0.0)
1400 {
1401 return Err(LeBailError::IntensityShapeMismatch);
1402 }
1403 if preserve_unobserved.len() != reflection_count {
1404 return Err(LeBailError::PreserveMaskLengthMismatch);
1405 }
1406 let included = pattern
1407 .mask
1408 .clone()
1409 .unwrap_or_else(|| vec![true; pattern.sample_count()]);
1410 let ratio = observed
1411 .iter()
1412 .zip(&calculation.background_y)
1413 .zip(&calculation.profile_y)
1414 .zip(&included)
1415 .map(|(((observed, background), calculated), included)| {
1416 if *included && *calculated > options.minimum_calculated {
1417 (observed - background).max(0.0) / calculated
1418 } else {
1419 0.0
1420 }
1421 })
1422 .collect::<Vec<_>>();
1423 let mut weights = bin_integration_weights(&pattern.x_deg);
1424 if options.use_uncertainty
1425 && let Some(uncertainty) = &pattern.uncertainty
1426 {
1427 for (weight, uncertainty) in weights.iter_mut().zip(uncertainty) {
1428 *weight /= uncertainty * uncertainty;
1429 }
1430 }
1431 for (weight, included) in weights.iter_mut().zip(&included) {
1432 if !included {
1433 *weight = 0.0;
1434 }
1435 }
1436 let local = &calculation.accumulation.derivatives.local;
1437 let mut updated = vec![0.0; reflection_count];
1438 let mut unobserved_reflections = Vec::new();
1439 for reflection in 0..reflection_count {
1440 let begin = local.offsets[reflection];
1441 let end = local.offsets[reflection + 1];
1442 let start = local.starts[reflection];
1443 let mut denominator = 0.0;
1444 let mut numerator = 0.0;
1445 for active in begin..end {
1446 let sample = start + active - begin;
1447 let profile = local.values[active * local.parameter_count];
1448 let weighted_profile = weights[sample] * profile;
1449 denominator += weighted_profile;
1450 numerator += weighted_profile * ratio[sample];
1451 }
1452 if denominator <= 0.0 {
1453 unobserved_reflections.push(calculation.reflection_keys[reflection].clone());
1454 if preserve_unobserved[reflection] {
1455 updated[reflection] = current[reflection];
1456 }
1457 continue;
1458 }
1459 let raw = (current[reflection] * numerator / denominator).max(0.0);
1460 updated[reflection] =
1461 current[reflection] + options.redistribution_damping * (raw - current[reflection]);
1462 }
1463 let maximum_relative_change = updated
1464 .iter()
1465 .zip(current)
1466 .map(|(updated, current)| {
1467 (updated - current).abs() / current.abs().max(options.initial_intensity_floor)
1468 })
1469 .fold(0.0_f64, f64::max);
1470 Ok(IntensityExtractionResult {
1471 intensities: updated,
1472 maximum_relative_change,
1473 unobserved_reflections,
1474 })
1475}
1476
1477pub fn refine_lebail(
1484 input: &LeBailInput,
1485 options: &LeBailOptions,
1486 checkpoint: Option<&LeBailCheckpoint>,
1487) -> Result<LeBailResult, LeBailError> {
1488 let evaluations_per_iteration = options
1489 .max_profile_backtracks
1490 .checked_add(2)
1491 .ok_or(LeBailError::SizeOverflow)?;
1492 let max_evaluations = options
1493 .max_iterations
1494 .checked_mul(evaluations_per_iteration)
1495 .and_then(|value| value.checked_add(1))
1496 .ok_or(LeBailError::SizeOverflow)?;
1497 let limits = RefinementLimits::new(options.max_iterations, max_evaluations, None, 1)
1498 .map_err(LeBailError::Runtime)?;
1499 let mut runtime = RefinementRuntime::new(limits, None).map_err(LeBailError::Runtime)?;
1500 refine_lebail_with_runtime(input, options, checkpoint, &mut runtime)
1501}
1502
1503pub fn iterate_lebail_once(
1514 input: &LeBailInput,
1515 options: &LeBailOptions,
1516 checkpoint: Option<&LeBailCheckpoint>,
1517) -> Result<LeBailResult, LeBailError> {
1518 let completed = checkpoint.map_or(0, |value| value.completed_iterations);
1519 let iteration = completed.checked_add(1).ok_or(LeBailError::SizeOverflow)?;
1520 let mut selected = options.clone();
1521 selected.min_iterations = iteration;
1522 selected.max_iterations = iteration;
1523 selected.validate()?;
1524 refine_lebail(input, &selected, checkpoint)
1525}
1526
1527pub fn refine_lebail_with_runtime(
1537 input: &LeBailInput,
1538 options: &LeBailOptions,
1539 checkpoint: Option<&LeBailCheckpoint>,
1540 runtime: &mut RefinementRuntime<LeBailCheckpoint>,
1541) -> Result<LeBailResult, LeBailError> {
1542 options.validate()?;
1543 let mut state = restore_state(input, options, checkpoint)?;
1544 if let Some(checkpoint) = checkpoint {
1545 runtime
1546 .resume_accepted(checkpoint.completed_iterations)
1547 .map_err(LeBailError::Runtime)?;
1548 }
1549 runtime
1550 .emit(
1551 RefinementEventKind::Start,
1552 "lebail",
1553 "Le Bail extraction started",
1554 Vec::new(),
1555 )
1556 .map_err(LeBailError::Runtime)?;
1557 state.calculation = Some(calculate_lebail_pattern_with_background(
1558 &input.pattern,
1559 state.instrument,
1560 &state.phases,
1561 state.background.as_ref(),
1562 options.support_fwhm,
1563 &options.execution,
1564 )?);
1565 let termination = run_lebail_iterations(input, options, &mut state, runtime)?;
1566 finish_result(
1567 input,
1568 options,
1569 state.phases,
1570 state.instrument,
1571 state.background,
1572 &state.intensities,
1573 state.parameters,
1574 state.history,
1575 state.previous_rwp,
1576 state.calculation.ok_or(LeBailError::InternalInvariant)?,
1577 termination,
1578 runtime,
1579 )
1580}
1581
1582fn run_lebail_iterations(
1583 input: &LeBailInput,
1584 options: &LeBailOptions,
1585 state: &mut RestoredLeBailState,
1586 runtime: &mut RefinementRuntime<LeBailCheckpoint>,
1587) -> Result<TerminationReason, LeBailError> {
1588 if let Err(error) = runtime.begin_evaluation() {
1589 return stop_reason_or_error(error);
1590 }
1591 for iteration in state.first_iteration..=options.max_iterations {
1592 if let Err(error) = runtime.begin_iteration(iteration) {
1593 return stop_reason_or_error(error);
1594 }
1595 if let Err(error) = runtime.begin_evaluation() {
1596 return stop_reason_or_error(error);
1597 }
1598 let candidate = match evaluate_lebail_iteration(input, options, state, runtime) {
1599 Ok(candidate) => candidate,
1600 Err(LeBailError::Runtime(error)) if normal_stop_reason(&error).is_some() => {
1601 return stop_reason_or_error(error);
1602 }
1603 Err(error) => return Err(error),
1604 };
1605 state.history.push(LeBailIterationRecord {
1606 iteration,
1607 rp: candidate.metrics.rp,
1608 rwp: candidate.metrics.rwp,
1609 chi_square: candidate.metrics.chi_square,
1610 reduced_chi_square: candidate.metrics.reduced_chi_square,
1611 maximum_relative_intensity_change: candidate.extraction.maximum_relative_change,
1612 scaled_profile_step_norm: candidate.profile_step_norm,
1613 parameter_changes: candidate.parameter_changes,
1614 warnings: candidate.warnings,
1615 });
1616 state.instrument = candidate.instrument;
1617 state.background = candidate.background;
1618 state.phases = candidate.phases;
1619 state.parameters = candidate.parameters;
1620 state.intensities = candidate.extraction.intensities;
1621 state.calculation = Some(candidate.calculation);
1622 accept_lebail_iteration(runtime, state, &candidate.metrics)?;
1623 if iteration >= options.min_iterations
1624 && candidate.extraction.maximum_relative_change < options.intensity_tolerance
1625 && (state.previous_rwp - candidate.metrics.rwp).abs() < options.rwp_tolerance
1626 {
1627 return Ok(TerminationReason::Converged);
1628 }
1629 state.previous_rwp = candidate.metrics.rwp;
1630 }
1631 Ok(TerminationReason::MaxIterations)
1632}
1633
1634struct EvaluatedLeBailIteration {
1635 extraction: IntensityExtractionResult,
1636 phases: Vec<LeBailPhase>,
1637 calculation: LeBailCalculation,
1638 metrics: ResidualEvaluation,
1639 warnings: Vec<String>,
1640 instrument: ConstantWavelengthInstrument,
1641 background: Option<BackgroundModel>,
1642 parameters: Option<ParameterSet>,
1643 profile_step_norm: f64,
1644 parameter_changes: Vec<ParameterChange>,
1645}
1646
1647fn evaluate_lebail_iteration(
1648 input: &LeBailInput,
1649 options: &LeBailOptions,
1650 state: &RestoredLeBailState,
1651 runtime: &mut RefinementRuntime<LeBailCheckpoint>,
1652) -> Result<EvaluatedLeBailIteration, LeBailError> {
1653 let mut extraction = extract_lebail_intensities(
1654 &input.pattern,
1655 state.calculation()?,
1656 &state.intensities,
1657 options,
1658 &flatten_preserve_mask(&state.phases),
1659 )?;
1660 let phases = replace_flat_intensities(&state.phases, &extraction.intensities)?;
1661 let calculation = calculate_lebail_pattern_with_background(
1662 &input.pattern,
1663 state.instrument,
1664 &phases,
1665 state.background.as_ref(),
1666 options.support_fwhm,
1667 &options.execution,
1668 )?;
1669 let (background, parameters) = fit_linear_background(
1670 &input.pattern,
1671 &calculation.profile_y,
1672 state.background.as_ref(),
1673 state.parameters.as_ref(),
1674 options.use_uncertainty,
1675 )?;
1676 let calculation = calculate_lebail_pattern_with_background(
1677 &input.pattern,
1678 state.instrument,
1679 &phases,
1680 background.as_ref(),
1681 options.support_fwhm,
1682 &options.execution,
1683 )?;
1684 let profile = profile_update(
1685 &input.pattern,
1686 state.instrument,
1687 phases,
1688 calculation,
1689 parameters.as_ref(),
1690 background,
1691 &input.constraints,
1692 options,
1693 runtime,
1694 )?;
1695 extraction.intensities = flatten_intensities(&profile.phases);
1696 let parameter_count = free_parameter_count(profile.parameters.as_ref(), &input.constraints)?;
1697 let metrics = evaluate_residuals(
1698 &input.pattern,
1699 &profile.calculation.y,
1700 ResidualOptions {
1701 use_uncertainty: options.use_uncertainty,
1702 parameter_count,
1703 },
1704 )
1705 .map_err(LeBailError::Residual)?;
1706 let warnings = if extraction.unobserved_reflections.is_empty() {
1707 Vec::new()
1708 } else {
1709 vec![format!(
1710 "{} reflections have no included support",
1711 extraction.unobserved_reflections.len()
1712 )]
1713 };
1714 Ok(EvaluatedLeBailIteration {
1715 extraction,
1716 phases: profile.phases,
1717 calculation: profile.calculation,
1718 metrics,
1719 warnings: [warnings, profile.warnings].concat(),
1720 instrument: profile.instrument,
1721 background: profile.background,
1722 parameters: profile.parameters,
1723 profile_step_norm: profile.step_norm,
1724 parameter_changes: profile.parameter_changes,
1725 })
1726}
1727
1728fn accept_lebail_iteration(
1729 runtime: &mut RefinementRuntime<LeBailCheckpoint>,
1730 state: &RestoredLeBailState,
1731 metrics: &ResidualEvaluation,
1732) -> Result<(), LeBailError> {
1733 let checkpoint = LeBailCheckpoint {
1734 completed_iterations: state.history.len(),
1735 phases: state.phases.clone(),
1736 instrument: state.instrument,
1737 background: state.background.clone(),
1738 intensities: state.intensities.clone(),
1739 parameters: state.parameters.clone(),
1740 previous_rwp: metrics.rwp,
1741 history: state.history.clone(),
1742 };
1743 runtime
1744 .accept_step(Some(&checkpoint))
1745 .map_err(LeBailError::Runtime)?;
1746 runtime
1747 .emit(
1748 RefinementEventKind::Iteration,
1749 "lebail_iteration",
1750 "Le Bail iteration accepted",
1751 vec![
1752 ("rwp".to_owned(), DiagnosticValue::Float(metrics.rwp)),
1753 (
1754 "maximum_relative_intensity_change".to_owned(),
1755 DiagnosticValue::Float(
1756 state
1757 .history
1758 .last()
1759 .ok_or(LeBailError::InternalInvariant)?
1760 .maximum_relative_intensity_change,
1761 ),
1762 ),
1763 ],
1764 )
1765 .map_err(LeBailError::Runtime)?;
1766 Ok(())
1767}
1768
1769#[allow(clippy::too_many_arguments)]
1770fn finish_result(
1771 input: &LeBailInput,
1772 options: &LeBailOptions,
1773 phases: Vec<LeBailPhase>,
1774 instrument: ConstantWavelengthInstrument,
1775 background: Option<BackgroundModel>,
1776 intensities: &[f64],
1777 parameters: Option<ParameterSet>,
1778 history: Vec<LeBailIterationRecord>,
1779 previous_rwp: f64,
1780 calculation: LeBailCalculation,
1781 termination: TerminationReason,
1782 runtime: &mut RefinementRuntime<LeBailCheckpoint>,
1783) -> Result<LeBailResult, LeBailError> {
1784 let metrics = evaluate_residuals(
1785 &input.pattern,
1786 &calculation.y,
1787 ResidualOptions {
1788 use_uncertainty: options.use_uncertainty,
1789 parameter_count: free_parameter_count(parameters.as_ref(), &input.constraints)?,
1790 },
1791 )
1792 .map_err(LeBailError::Residual)?;
1793 let checkpoint = LeBailCheckpoint {
1794 completed_iterations: history.len(),
1795 phases: phases.clone(),
1796 instrument,
1797 background: background.clone(),
1798 intensities: intensities.to_owned(),
1799 parameters: parameters.clone(),
1800 previous_rwp: if termination == TerminationReason::Cancelled {
1801 previous_rwp
1802 } else {
1803 metrics.rwp
1804 },
1805 history: history.clone(),
1806 };
1807 checkpoint.validate()?;
1808 let labeled = calculation
1809 .reflection_keys
1810 .iter()
1811 .zip(intensities)
1812 .map(
1813 |((phase_id, reflection_id), intensity)| ReflectionIntensity {
1814 phase_id: phase_id.clone(),
1815 reflection_id: reflection_id.clone(),
1816 integrated_intensity: *intensity,
1817 },
1818 )
1819 .collect();
1820 let rank_deficient_groups = if options.diagnose_rank_deficiency {
1821 rank_deficient_groups(&calculation, options.unresolved_correlation)
1822 } else {
1823 Vec::new()
1824 };
1825 let covariance = covariance(
1826 &input.pattern,
1827 &calculation,
1828 instrument,
1829 background.as_ref(),
1830 &phases,
1831 parameters.as_ref(),
1832 &input.constraints,
1833 options.use_uncertainty,
1834 metrics.reduced_chi_square,
1835 )?;
1836 runtime
1837 .emit(
1838 RefinementEventKind::Termination,
1839 "lebail",
1840 "Le Bail extraction terminated",
1841 vec![(
1842 "termination_reason".to_owned(),
1843 DiagnosticValue::String(termination.as_str().to_owned()),
1844 )],
1845 )
1846 .map_err(LeBailError::Runtime)?;
1847 Ok(LeBailResult {
1848 calculation,
1849 phases,
1850 instrument,
1851 background,
1852 intensities: labeled,
1853 metrics,
1854 history,
1855 termination_reason: termination,
1856 rank_deficient_groups,
1857 parameters,
1858 covariance,
1859 checkpoint,
1860 })
1861}
1862
1863struct RestoredLeBailState {
1864 phases: Vec<LeBailPhase>,
1865 instrument: ConstantWavelengthInstrument,
1866 background: Option<BackgroundModel>,
1867 intensities: Vec<f64>,
1868 history: Vec<LeBailIterationRecord>,
1869 parameters: Option<ParameterSet>,
1870 previous_rwp: f64,
1871 first_iteration: usize,
1872 calculation: Option<LeBailCalculation>,
1873}
1874
1875impl RestoredLeBailState {
1876 fn calculation(&self) -> Result<&LeBailCalculation, LeBailError> {
1877 self.calculation
1878 .as_ref()
1879 .ok_or(LeBailError::InternalInvariant)
1880 }
1881}
1882
1883fn restore_state(
1884 input: &LeBailInput,
1885 options: &LeBailOptions,
1886 checkpoint: Option<&LeBailCheckpoint>,
1887) -> Result<RestoredLeBailState, LeBailError> {
1888 let Some(checkpoint) = checkpoint else {
1889 let intensities = initialize_lebail_intensities(input, options)?;
1890 let phases = replace_flat_intensities(&input.phases, &intensities)?;
1891 return Ok(RestoredLeBailState {
1892 phases,
1893 instrument: input.instrument,
1894 background: input.background.clone(),
1895 intensities,
1896 history: Vec::new(),
1897 parameters: input.parameters.clone(),
1898 previous_rwp: f64::INFINITY,
1899 first_iteration: 1,
1900 calculation: None,
1901 });
1902 };
1903 checkpoint.validate()?;
1904 if checkpoint.completed_iterations >= options.max_iterations {
1905 return Err(LeBailError::InvalidCheckpoint {
1906 message: "checkpoint already reached the configured maximum iteration".to_owned(),
1907 });
1908 }
1909 if !phases_restart_compatible(&input.phases, &checkpoint.phases) {
1910 return Err(LeBailError::InvalidCheckpoint {
1911 message: "checkpoint phase/reflection domain does not match the input".to_owned(),
1912 });
1913 }
1914 if !backgrounds_restart_compatible(input.background.as_ref(), checkpoint.background.as_ref()) {
1915 return Err(LeBailError::InvalidCheckpoint {
1916 message: "checkpoint background does not match the input".to_owned(),
1917 });
1918 }
1919 let input_parameter_keys = input.parameters.as_ref().map(parameter_keys);
1920 let checkpoint_parameter_keys = checkpoint.parameters.as_ref().map(parameter_keys);
1921 if input_parameter_keys != checkpoint_parameter_keys {
1922 return Err(LeBailError::InvalidCheckpoint {
1923 message: "checkpoint parameter identities do not match the input".to_owned(),
1924 });
1925 }
1926 Ok(RestoredLeBailState {
1927 phases: checkpoint.phases.clone(),
1928 instrument: checkpoint.instrument,
1929 background: checkpoint.background.clone(),
1930 intensities: checkpoint.intensities.clone(),
1931 history: checkpoint.history.clone(),
1932 parameters: checkpoint.parameters.clone(),
1933 previous_rwp: checkpoint.previous_rwp,
1934 first_iteration: checkpoint.completed_iterations + 1,
1935 calculation: None,
1936 })
1937}
1938
1939struct ProfileUpdate {
1940 instrument: ConstantWavelengthInstrument,
1941 phases: Vec<LeBailPhase>,
1942 background: Option<BackgroundModel>,
1943 calculation: LeBailCalculation,
1944 parameters: Option<ParameterSet>,
1945 step_norm: f64,
1946 parameter_changes: Vec<ParameterChange>,
1947 warnings: Vec<String>,
1948}
1949
1950fn fit_linear_background(
1951 pattern: &PatternRecord,
1952 profile_y: &[f64],
1953 background: Option<&BackgroundModel>,
1954 parameters: Option<&ParameterSet>,
1955 use_uncertainty: bool,
1956) -> Result<(Option<BackgroundModel>, Option<ParameterSet>), LeBailError> {
1957 let Some(background) = background else {
1958 return Ok((None, parameters.cloned()));
1959 };
1960 if !background.basis_is_invariant() {
1961 return Ok((Some(background.clone()), parameters.cloned()));
1962 }
1963 let observed = pattern
1964 .observed_y
1965 .as_deref()
1966 .ok_or(LeBailError::MissingObservations)?;
1967 let basis = background
1968 .basis(&pattern.x_deg)
1969 .map_err(LeBailError::Background)?;
1970 let included = pattern
1971 .mask
1972 .clone()
1973 .unwrap_or_else(|| vec![true; pattern.sample_count()]);
1974 let row_count = included.iter().filter(|value| **value).count();
1975 if row_count < basis.columns || profile_y.len() != pattern.sample_count() {
1976 return Err(LeBailError::LinearSolve);
1977 }
1978 let mut design = Vec::with_capacity(row_count * basis.columns);
1979 let mut target = Vec::with_capacity(row_count);
1980 for sample in 0..pattern.sample_count() {
1981 if !included[sample] {
1982 continue;
1983 }
1984 let sigma = if use_uncertainty {
1985 pattern
1986 .uncertainty
1987 .as_ref()
1988 .map_or(1.0, |values| values[sample])
1989 } else {
1990 1.0
1991 };
1992 let row = basis.row(sample).ok_or(LeBailError::InternalInvariant)?;
1993 design.extend(row.iter().map(|value| value / sigma));
1994 target.push((observed[sample] - pattern.background_y[sample] - profile_y[sample]) / sigma);
1995 }
1996 let matrix = DMatrix::from_row_slice(row_count, basis.columns, &design);
1997 let target = DVector::from_vec(target);
1998 let coefficients = matrix
1999 .svd(true, true)
2000 .solve(&target, 1.0e-12)
2001 .map_err(|_| LeBailError::LinearSolve)?;
2002 if coefficients.iter().any(|value| !value.is_finite()) {
2003 return Err(LeBailError::LinearSolve);
2004 }
2005 let updated = background
2006 .replace_coefficients(coefficients.as_slice())
2007 .map_err(LeBailError::Background)?;
2008 let updated_parameters = if let Some(parameters) = parameters {
2009 let values = updated
2010 .parameter_names()
2011 .iter()
2012 .zip(updated.coefficients())
2013 .map(|(name, value)| {
2014 Ok((
2015 lebail_background_parameter_key(updated.background_id(), name)?,
2016 value,
2017 ))
2018 })
2019 .collect::<Result<BTreeMap<_, _>, LeBailError>>()?;
2020 Some(
2021 parameters
2022 .replace_values(&values)
2023 .map_err(LeBailError::Parameter)?,
2024 )
2025 } else {
2026 None
2027 };
2028 Ok((Some(updated), updated_parameters))
2029}
2030
2031#[allow(clippy::too_many_arguments)]
2032#[allow(clippy::too_many_lines)]
2036fn profile_update(
2037 pattern: &PatternRecord,
2038 instrument: ConstantWavelengthInstrument,
2039 phases: Vec<LeBailPhase>,
2040 calculation: LeBailCalculation,
2041 parameters: Option<&ParameterSet>,
2042 background: Option<BackgroundModel>,
2043 constraints: &[Constraint],
2044 options: &LeBailOptions,
2045 runtime: &mut RefinementRuntime<LeBailCheckpoint>,
2046) -> Result<ProfileUpdate, LeBailError> {
2047 let Some(parameters) = parameters else {
2048 return Ok(ProfileUpdate {
2049 instrument,
2050 phases,
2051 background,
2052 calculation,
2053 parameters: None,
2054 step_norm: 0.0,
2055 parameter_changes: Vec::new(),
2056 warnings: Vec::new(),
2057 });
2058 };
2059 let domain_values =
2060 domain_parameter_values(instrument, &phases, background.as_ref(), parameters)?;
2061 let current = parameters
2062 .replace_values(&domain_values)
2063 .map_err(LeBailError::Parameter)?;
2064 let transform = ConstraintTransform::new(current.clone(), constraints.to_vec())
2065 .map_err(LeBailError::Constraint)?;
2066 if transform.free_keys().is_empty() {
2067 return Ok(ProfileUpdate {
2068 instrument,
2069 phases,
2070 background,
2071 calculation,
2072 parameters: Some(current),
2073 step_norm: 0.0,
2074 parameter_changes: Vec::new(),
2075 warnings: Vec::new(),
2076 });
2077 }
2078 let physical = parameter_columns(
2079 &calculation,
2080 ¤t,
2081 instrument,
2082 &phases,
2083 background.as_ref(),
2084 &pattern.x_deg,
2085 )?;
2086 let derivative = transform
2087 .derivative_matrix()
2088 .map_err(LeBailError::Constraint)?;
2089 let chain = DMatrix::from_row_slice(derivative.rows, derivative.columns, &derivative.values);
2090 let jacobian = physical * chain;
2091 let observed = pattern
2092 .observed_y
2093 .as_deref()
2094 .ok_or(LeBailError::MissingObservations)?;
2095 let included = pattern
2096 .mask
2097 .clone()
2098 .unwrap_or_else(|| vec![true; pattern.sample_count()]);
2099 let selected_count = included.iter().filter(|value| **value).count();
2100 let free_count = transform.free_keys().len();
2101 let mut selected_jacobian = DMatrix::zeros(selected_count, free_count);
2102 let mut selected_residual = DVector::zeros(selected_count);
2103 let mut selected_row = 0;
2104 for sample in 0..pattern.sample_count() {
2105 if !included[sample] {
2106 continue;
2107 }
2108 let weight = if options.use_uncertainty {
2109 pattern
2110 .uncertainty
2111 .as_ref()
2112 .map_or(1.0, |values| values[sample].recip())
2113 } else {
2114 1.0
2115 };
2116 selected_residual[selected_row] = (observed[sample] - calculation.y[sample]) * weight;
2117 for column in 0..free_count {
2118 selected_jacobian[(selected_row, column)] = jacobian[(sample, column)] * weight;
2119 }
2120 selected_row += 1;
2121 }
2122 let normal = selected_jacobian.transpose() * &selected_jacobian;
2123 let mut warnings = Vec::new();
2124 if matrix_rank(&normal) != free_count {
2125 warnings.push("profile Jacobian is rank deficient".to_owned());
2126 }
2127 if current
2128 .specs()
2129 .iter()
2130 .any(|spec| spec.key().module() == "phase" && spec.key().name() == "scale")
2131 {
2132 warnings.push(
2133 "phase scale is not identifiable independently of extracted Le Bail intensities"
2134 .to_owned(),
2135 );
2136 }
2137 let base = transform.pack().map_err(LeBailError::Constraint)?;
2138 let mut lower = vec![-options.max_scaled_parameter_step; free_count];
2139 let mut upper = vec![options.max_scaled_parameter_step; free_count];
2140 for (index, key) in transform.free_keys().iter().enumerate() {
2141 let spec = current.spec(key).ok_or(LeBailError::InternalInvariant)?;
2142 lower[index] = lower[index].max(spec.bounds().lower() / spec.scale() - base[index]);
2143 upper[index] = upper[index].min(spec.bounds().upper() / spec.scale() - base[index]);
2144 }
2145 let rhs = selected_jacobian.transpose() * &selected_residual;
2146 let mut regularized = normal;
2147 for index in 0..free_count {
2148 regularized[(index, index)] += options.profile_damping;
2149 }
2150 let mut step = if let Some(solution) = regularized.lu().solve(&rhs) {
2151 solution
2152 } else {
2153 warnings.push("profile normal equations used least-squares fallback".to_owned());
2154 selected_jacobian
2155 .clone()
2156 .svd(true, true)
2157 .solve(&selected_residual, f64::EPSILON)
2158 .map_err(|_| LeBailError::LinearSolve)?
2159 };
2160 for index in 0..free_count {
2161 step[index] = step[index].clamp(lower[index], upper[index]);
2162 }
2163 let baseline = evaluate_residuals(
2164 pattern,
2165 &calculation.y,
2166 ResidualOptions {
2167 use_uncertainty: options.use_uncertainty,
2168 parameter_count: free_count,
2169 },
2170 )
2171 .map_err(LeBailError::Residual)?;
2172 let mut factor = 1.0;
2173 for _ in 0..=options.max_profile_backtracks {
2174 let trial = base
2175 .iter()
2176 .zip(step.iter())
2177 .map(|(base, step)| base + factor * step)
2178 .collect::<Vec<_>>();
2179 let Ok(values) = transform.unpack(&trial, true) else {
2180 factor *= 0.5;
2181 continue;
2182 };
2183 let Ok((candidate_instrument, candidate_phases, candidate_background)) =
2184 apply_parameter_values(instrument, &phases, background.as_ref(), &values)
2185 else {
2186 factor *= 0.5;
2187 continue;
2188 };
2189 runtime.begin_evaluation().map_err(LeBailError::Runtime)?;
2190 let Ok(candidate_calculation) = calculate_lebail_pattern_with_background(
2191 pattern,
2192 candidate_instrument,
2193 &candidate_phases,
2194 candidate_background.as_ref(),
2195 options.support_fwhm,
2196 &options.execution,
2197 ) else {
2198 factor *= 0.5;
2199 continue;
2200 };
2201 let candidate_metrics = evaluate_residuals(
2202 pattern,
2203 &candidate_calculation.y,
2204 ResidualOptions {
2205 use_uncertainty: options.use_uncertainty,
2206 parameter_count: free_count,
2207 },
2208 )
2209 .map_err(LeBailError::Residual)?;
2210 if candidate_metrics.chi_square < baseline.chi_square {
2211 let candidate_parameters = current
2212 .replace_values(&values)
2213 .map_err(LeBailError::Parameter)?;
2214 let (candidate_phases, domain_warnings, topology_changed) =
2215 regenerate_accepted_domains(candidate_phases)?;
2216 let candidate_calculation = if topology_changed {
2217 runtime.begin_evaluation().map_err(LeBailError::Runtime)?;
2218 calculate_lebail_pattern_with_background(
2219 pattern,
2220 candidate_instrument,
2221 &candidate_phases,
2222 candidate_background.as_ref(),
2223 options.support_fwhm,
2224 &options.execution,
2225 )?
2226 } else {
2227 candidate_calculation
2228 };
2229 let parameter_changes = current
2230 .specs()
2231 .iter()
2232 .filter_map(|spec| {
2233 let after = candidate_parameters.spec(spec.key())?.value();
2234 (after.to_bits() != spec.value().to_bits()).then(|| ParameterChange {
2235 key: spec.key().clone(),
2236 before: spec.value(),
2237 after,
2238 scaled_change: (after - spec.value()) / spec.scale(),
2239 })
2240 })
2241 .collect();
2242 return Ok(ProfileUpdate {
2243 instrument: candidate_instrument,
2244 phases: candidate_phases,
2245 background: candidate_background,
2246 calculation: candidate_calculation,
2247 parameters: Some(candidate_parameters),
2248 step_norm: factor * step.norm(),
2249 parameter_changes,
2250 warnings: [warnings, domain_warnings].concat(),
2251 });
2252 }
2253 factor *= 0.5;
2254 }
2255 warnings.push("profile step rejected by backtracking".to_owned());
2256 Ok(ProfileUpdate {
2257 instrument,
2258 phases,
2259 background,
2260 calculation,
2261 parameters: Some(current),
2262 step_norm: 0.0,
2263 parameter_changes: Vec::new(),
2264 warnings,
2265 })
2266}
2267
2268fn domain_parameter_values(
2269 instrument: ConstantWavelengthInstrument,
2270 phases: &[LeBailPhase],
2271 background: Option<&BackgroundModel>,
2272 parameters: &ParameterSet,
2273) -> Result<BTreeMap<ParameterKey, f64>, LeBailError> {
2274 let mut values = BTreeMap::new();
2275 for spec in parameters.specs() {
2276 let key = spec.key();
2277 let value = if key.module() == "instrument" && key.owner_id() == "cw" {
2278 instrument_parameter(instrument, key.name())
2279 } else if key.module() == "phase" && key.name() == "scale" {
2280 phases
2281 .iter()
2282 .find(|phase| phase.phase_id() == key.owner_id())
2283 .map(LeBailPhase::scale)
2284 } else if key.module() == "background" {
2285 background.and_then(|background| {
2286 if background.background_id() != key.owner_id() {
2287 return None;
2288 }
2289 background
2290 .parameter_names()
2291 .iter()
2292 .position(|name| name == key.name())
2293 .and_then(|index| background.coefficients().get(index).copied())
2294 })
2295 } else if key.module() == "lattice" {
2296 phases
2297 .iter()
2298 .find(|phase| phase.phase_id() == key.owner_id())
2299 .and_then(|phase| {
2300 let cell = phase.cell?;
2301 let parameterization = phase.reflection_domain.as_ref()?.parameterization();
2302 let index = parameterization
2303 .parameter_names()
2304 .iter()
2305 .position(|name| name == key.name())?;
2306 parameterization
2307 .values_from_cell(cell)
2308 .ok()?
2309 .get(index)
2310 .copied()
2311 })
2312 } else if key.module() == "reflection" && key.name() == "two_theta_deg" {
2313 phases.iter().find_map(|phase| {
2314 phase
2315 .reflection_ids
2316 .iter()
2317 .position(|reflection_id| {
2318 format!("{}/{}", phase.phase_id(), reflection_id) == key.owner_id()
2319 })
2320 .map(|index| phase.two_theta_deg[index])
2321 })
2322 } else {
2323 None
2324 }
2325 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2326 if !spec.bounds().contains(value) {
2327 return Err(LeBailError::ParameterDomainOutsideBounds { label: key.label() });
2328 }
2329 values.insert(key.clone(), value);
2330 }
2331 Ok(values)
2332}
2333
2334fn parameter_columns(
2335 calculation: &LeBailCalculation,
2336 parameters: &ParameterSet,
2337 instrument: ConstantWavelengthInstrument,
2338 phases: &[LeBailPhase],
2339 background: Option<&BackgroundModel>,
2340 x_deg: &[f64],
2341) -> Result<DMatrix<f64>, LeBailError> {
2342 let samples = calculation.y.len();
2343 let mut matrix = DMatrix::zeros(samples, parameters.specs().len());
2344 let global = calculation
2345 .accumulation
2346 .derivatives
2347 .global
2348 .as_ref()
2349 .ok_or(LeBailError::InternalInvariant)?;
2350 for (column, spec) in parameters.specs().iter().enumerate() {
2351 let key = spec.key();
2352 if key.module() == "instrument" {
2353 let row = INSTRUMENT_PARAMETER_NAMES
2354 .iter()
2355 .position(|name| *name == key.name())
2356 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2357 for sample in 0..samples {
2358 matrix[(sample, column)] = global.values[row * samples + sample];
2359 }
2360 } else if key.module() == "phase" {
2361 let phase = phases
2362 .iter()
2363 .position(|phase| phase.phase_id() == key.owner_id())
2364 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2365 let row = 5 + phase;
2366 for sample in 0..samples {
2367 matrix[(sample, column)] = global.values[row * samples + sample];
2368 }
2369 } else if key.module() == "reflection" {
2370 let reflection = calculation
2371 .reflection_keys
2372 .iter()
2373 .position(|(phase_id, reflection_id)| {
2374 format!("{phase_id}/{reflection_id}") == key.owner_id()
2375 })
2376 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2377 let local = &calculation.accumulation.derivatives.local;
2378 let begin = local.offsets[reflection];
2379 let end = local.offsets[reflection + 1];
2380 let start = local.starts[reflection];
2381 for active in begin..end {
2382 matrix[(start + active - begin, column)] =
2383 local.values[active * local.parameter_count + 1];
2384 }
2385 } else if key.module() == "lattice" {
2386 let phase = phases
2387 .iter()
2388 .find(|phase| phase.phase_id() == key.owner_id())
2389 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2390 let cell = phase
2391 .cell
2392 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2393 let domain = phase
2394 .reflection_domain
2395 .as_ref()
2396 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2397 let geometry = cw_lattice_geometry(
2398 domain.parameterization(),
2399 cell,
2400 &phase.hkl,
2401 instrument.wavelength_angstrom,
2402 )
2403 .map_err(LeBailError::Lattice)?;
2404 let parameter = geometry
2405 .parameter_names
2406 .iter()
2407 .position(|name| name == key.name())
2408 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2409 let local = &calculation.accumulation.derivatives.local;
2410 for (phase_reflection, reflection_id) in phase.reflection_ids.iter().enumerate() {
2411 let reflection = calculation
2412 .reflection_keys
2413 .iter()
2414 .position(|(phase_id, candidate_id)| {
2415 phase_id == phase.phase_id() && candidate_id == reflection_id
2416 })
2417 .ok_or(LeBailError::InternalInvariant)?;
2418 let derivative = geometry.d_two_theta_d_parameters
2419 [phase_reflection * geometry.parameter_names.len() + parameter];
2420 let begin = local.offsets[reflection];
2421 let end = local.offsets[reflection + 1];
2422 let start = local.starts[reflection];
2423 for active in begin..end {
2424 matrix[(start + active - begin, column)] +=
2425 local.values[active * local.parameter_count + 1] * derivative;
2426 }
2427 }
2428 } else if key.module() == "background" {
2429 fill_background_parameter_column(&mut matrix, column, key, background, x_deg)?;
2430 } else {
2431 return Err(LeBailError::UnsupportedParameter { label: key.label() });
2432 }
2433 }
2434 Ok(matrix)
2435}
2436
2437fn fill_background_parameter_column(
2438 matrix: &mut DMatrix<f64>,
2439 column: usize,
2440 key: &ParameterKey,
2441 background: Option<&BackgroundModel>,
2442 x_deg: &[f64],
2443) -> Result<(), LeBailError> {
2444 let background = background
2445 .filter(|background| background.background_id() == key.owner_id())
2446 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2447 let parameter = background
2448 .parameter_names()
2449 .iter()
2450 .position(|name| name == key.name())
2451 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2452 let basis = background.basis(x_deg).map_err(LeBailError::Background)?;
2453 let values = basis
2454 .column(parameter)
2455 .ok_or(LeBailError::InternalInvariant)?;
2456 for sample in 0..matrix.nrows() {
2457 matrix[(sample, column)] = values[sample];
2458 }
2459 Ok(())
2460}
2461
2462fn apply_parameter_values(
2463 instrument: ConstantWavelengthInstrument,
2464 phases: &[LeBailPhase],
2465 background: Option<&BackgroundModel>,
2466 values: &BTreeMap<ParameterKey, f64>,
2467) -> Result<
2468 (
2469 ConstantWavelengthInstrument,
2470 Vec<LeBailPhase>,
2471 Option<BackgroundModel>,
2472 ),
2473 LeBailError,
2474> {
2475 let mut updated_instrument = instrument;
2476 for (key, value) in values {
2477 if key.module() == "instrument" {
2478 set_instrument_parameter(&mut updated_instrument, key.name(), *value)?;
2479 }
2480 }
2481 updated_instrument
2482 .validate()
2483 .map_err(|error| LeBailError::Profile {
2484 message: error.to_string(),
2485 })?;
2486 let mut updated_phases = Vec::with_capacity(phases.len());
2487 for phase in phases {
2488 let scale = values
2489 .get(&lebail_phase_scale_key(phase.phase_id())?)
2490 .copied()
2491 .unwrap_or(phase.scale());
2492 let mut positions = phase.two_theta_deg.clone();
2493 for (index, reflection_id) in phase.reflection_ids.iter().enumerate() {
2494 if let Some(value) = values.get(&lebail_reflection_position_key(
2495 phase.phase_id(),
2496 reflection_id,
2497 )?) {
2498 positions[index] = *value;
2499 }
2500 }
2501 let mut updated = phase.replace_scale_and_positions(scale, positions)?;
2502 let lattice_values = values
2503 .iter()
2504 .filter(|(key, _)| key.module() == "lattice" && key.owner_id() == phase.phase_id())
2505 .collect::<Vec<_>>();
2506 if !lattice_values.is_empty() {
2507 let cell = phase.cell.ok_or_else(|| {
2508 invalid_phase("lattice parameters require a bounded reflection domain")
2509 })?;
2510 let domain = phase.reflection_domain.as_ref().ok_or_else(|| {
2511 invalid_phase("lattice parameters require a bounded reflection domain")
2512 })?;
2513 let mut independent = domain
2514 .parameterization()
2515 .values_from_cell(cell)
2516 .map_err(LeBailError::Lattice)?;
2517 for (key, value) in lattice_values {
2518 let index = domain
2519 .parameterization()
2520 .parameter_names()
2521 .iter()
2522 .position(|name| name == key.name())
2523 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2524 independent[index] = *value;
2525 }
2526 let cell = domain
2527 .parameterization()
2528 .to_cell(&independent)
2529 .map_err(LeBailError::Lattice)?;
2530 updated =
2531 updated.replace_cell_geometry(cell, updated_instrument.wavelength_angstrom)?;
2532 }
2533 updated_phases.push(updated);
2534 }
2535 let updated_background = if let Some(background) = background {
2536 let names = background.parameter_names();
2537 let coefficients = names
2538 .iter()
2539 .zip(background.coefficients())
2540 .map(|(name, current)| {
2541 values
2542 .get(&lebail_background_parameter_key(
2543 background.background_id(),
2544 name,
2545 )?)
2546 .copied()
2547 .map_or(Ok(current), Ok)
2548 })
2549 .collect::<Result<Vec<_>, LeBailError>>()?;
2550 Some(
2551 background
2552 .replace_coefficients(&coefficients)
2553 .map_err(LeBailError::Background)?,
2554 )
2555 } else {
2556 None
2557 };
2558 Ok((updated_instrument, updated_phases, updated_background))
2559}
2560
2561fn regenerate_accepted_domains(
2562 phases: Vec<LeBailPhase>,
2563) -> Result<(Vec<LeBailPhase>, Vec<String>, bool), LeBailError> {
2564 let mut updated = Vec::with_capacity(phases.len());
2565 let mut warnings = Vec::new();
2566 let mut topology_changed = false;
2567 for phase in phases {
2568 let Some(domain) = phase.reflection_domain.as_ref() else {
2569 updated.push(phase);
2570 continue;
2571 };
2572 let cell = phase.cell.ok_or(LeBailError::InternalInvariant)?;
2573 let previous = phase
2574 .reflection_ids
2575 .iter()
2576 .cloned()
2577 .zip(phase.integrated_intensity.iter().copied())
2578 .collect::<BTreeMap<_, _>>();
2579 let generated = domain
2580 .generate(cell, Some(&previous))
2581 .map_err(LeBailError::Lattice)?;
2582 let changed = generated.reflection_ids != phase.reflection_ids;
2583 topology_changed |= changed;
2584 if !generated.added_reflection_ids.is_empty()
2585 || !generated.removed_reflection_ids.is_empty()
2586 {
2587 warnings.push(format!(
2588 "phase {} reflection domain regenerated: {} added, {} removed",
2589 phase.phase_id(),
2590 generated.added_reflection_ids.len(),
2591 generated.removed_reflection_ids.len()
2592 ));
2593 }
2594 updated.push(phase.replace_generated_domain(cell, generated)?);
2595 }
2596 Ok((updated, warnings, topology_changed))
2597}
2598
2599#[allow(clippy::too_many_arguments)]
2600fn covariance(
2601 pattern: &PatternRecord,
2602 calculation: &LeBailCalculation,
2603 instrument: ConstantWavelengthInstrument,
2604 background: Option<&BackgroundModel>,
2605 phases: &[LeBailPhase],
2606 parameters: Option<&ParameterSet>,
2607 constraints: &[Constraint],
2608 use_uncertainty: bool,
2609 reduced_chi_square: f64,
2610) -> Result<Option<CovarianceMatrix>, LeBailError> {
2611 let Some(parameters) = parameters else {
2612 return Ok(None);
2613 };
2614 let transform = ConstraintTransform::new(parameters.clone(), constraints.to_vec())
2615 .map_err(LeBailError::Constraint)?;
2616 let free_count = transform.free_keys().len();
2617 if free_count == 0 {
2618 return Ok(Some(CovarianceMatrix {
2619 size: 0,
2620 values: Vec::new(),
2621 }));
2622 }
2623 let derivative = transform
2624 .derivative_matrix()
2625 .map_err(LeBailError::Constraint)?;
2626 for (row, spec) in parameters.specs().iter().enumerate() {
2627 if spec.key().module() == "phase"
2628 && spec.key().name() == "scale"
2629 && derivative
2630 .row(row)
2631 .is_some_and(|values| values.iter().any(|value| *value != 0.0))
2632 {
2633 return Ok(None);
2634 }
2635 }
2636 let physical = parameter_columns(
2637 calculation,
2638 parameters,
2639 instrument,
2640 phases,
2641 background,
2642 &pattern.x_deg,
2643 )?;
2644 let chain = DMatrix::from_row_slice(derivative.rows, derivative.columns, &derivative.values);
2645 let jacobian = physical * chain;
2646 let included = pattern
2647 .mask
2648 .clone()
2649 .unwrap_or_else(|| vec![true; pattern.sample_count()]);
2650 let row_count = included.iter().filter(|value| **value).count();
2651 let mut selected = DMatrix::zeros(row_count, free_count);
2652 let mut row = 0;
2653 for sample in 0..pattern.sample_count() {
2654 if !included[sample] {
2655 continue;
2656 }
2657 let weight = if use_uncertainty {
2658 pattern
2659 .uncertainty
2660 .as_ref()
2661 .map_or(1.0, |values| values[sample].recip())
2662 } else {
2663 1.0
2664 };
2665 for column in 0..free_count {
2666 selected[(row, column)] = jacobian[(sample, column)] * weight;
2667 }
2668 row += 1;
2669 }
2670 let normal = selected.transpose() * selected;
2671 if matrix_rank(&normal) != free_count {
2672 return Ok(None);
2673 }
2674 let Some(mut inverse) = normal.try_inverse() else {
2675 return Ok(None);
2676 };
2677 let known_uncertainties = use_uncertainty && pattern.uncertainty.is_some();
2678 if !known_uncertainties && reduced_chi_square.is_finite() {
2679 inverse *= reduced_chi_square;
2680 }
2681 let mut values = Vec::with_capacity(free_count * free_count);
2682 for row in 0..free_count {
2683 for column in 0..free_count {
2684 values.push(inverse[(row, column)]);
2685 }
2686 }
2687 Ok(Some(CovarianceMatrix {
2688 size: free_count,
2689 values,
2690 }))
2691}
2692
2693fn free_parameter_count(
2694 parameters: Option<&ParameterSet>,
2695 constraints: &[Constraint],
2696) -> Result<usize, LeBailError> {
2697 parameters.map_or(Ok(0), |parameters| {
2698 ConstraintTransform::new(parameters.clone(), constraints.to_vec())
2699 .map(|transform| transform.free_keys().len())
2700 .map_err(LeBailError::Constraint)
2701 })
2702}
2703
2704fn matrix_rank(matrix: &DMatrix<f64>) -> usize {
2705 let singular = matrix.clone().svd(false, false).singular_values;
2706 let maximum = singular.iter().copied().fold(0.0_f64, f64::max);
2707 let tolerance = count_as_f64(matrix.nrows().max(matrix.ncols())) * f64::EPSILON * maximum;
2708 singular.iter().filter(|value| **value > tolerance).count()
2709}
2710
2711fn instrument_parameter(instrument: ConstantWavelengthInstrument, name: &str) -> Option<f64> {
2712 match name {
2713 "u_deg2" => Some(instrument.u_deg2),
2714 "v_deg2" => Some(instrument.v_deg2),
2715 "w_deg2" => Some(instrument.w_deg2),
2716 "x_deg" => Some(instrument.x_deg),
2717 "y_deg" => Some(instrument.y_deg),
2718 _ => None,
2719 }
2720}
2721
2722fn set_instrument_parameter(
2723 instrument: &mut ConstantWavelengthInstrument,
2724 name: &str,
2725 value: f64,
2726) -> Result<(), LeBailError> {
2727 match name {
2728 "u_deg2" => instrument.u_deg2 = value,
2729 "v_deg2" => instrument.v_deg2 = value,
2730 "w_deg2" => instrument.w_deg2 = value,
2731 "x_deg" => instrument.x_deg = value,
2732 "y_deg" => instrument.y_deg = value,
2733 _ => {
2734 return Err(LeBailError::UnsupportedParameter {
2735 label: format!("instrument[cw].{name}"),
2736 });
2737 }
2738 }
2739 Ok(())
2740}
2741
2742fn rank_deficient_groups(
2743 calculation: &LeBailCalculation,
2744 threshold: f64,
2745) -> Vec<CoincidentReflectionGroup> {
2746 let local = &calculation.accumulation.derivatives.local;
2747 let count = local.peak_count();
2748 let mut parents = (0..count).collect::<Vec<_>>();
2749 let norms = (0..count)
2750 .map(|reflection| {
2751 let begin = local.offsets[reflection];
2752 let end = local.offsets[reflection + 1];
2753 (begin..end)
2754 .map(|active| {
2755 let value = local.values[active * local.parameter_count];
2756 value * value
2757 })
2758 .sum::<f64>()
2759 .sqrt()
2760 })
2761 .collect::<Vec<_>>();
2762 for left in 0..count {
2763 let left_begin = local.offsets[left];
2764 let left_end = local.offsets[left + 1];
2765 let left_start = local.starts[left];
2766 let left_stop = left_start + left_end - left_begin;
2767 for right in left + 1..count {
2768 let right_begin = local.offsets[right];
2769 let right_end = local.offsets[right + 1];
2770 let right_start = local.starts[right];
2771 let right_stop = right_start + right_end - right_begin;
2772 let start = left_start.max(right_start);
2773 let stop = left_stop.min(right_stop);
2774 if start >= stop || norms[left] == 0.0 || norms[right] == 0.0 {
2775 continue;
2776 }
2777 let correlation = (start..stop)
2778 .map(|sample| {
2779 let left_active = left_begin + sample - left_start;
2780 let right_active = right_begin + sample - right_start;
2781 local.values[left_active * local.parameter_count]
2782 * local.values[right_active * local.parameter_count]
2783 })
2784 .sum::<f64>()
2785 / (norms[left] * norms[right]);
2786 if correlation >= threshold {
2787 union(&mut parents, left, right);
2788 }
2789 }
2790 }
2791 let mut grouped = std::collections::BTreeMap::<usize, Vec<usize>>::new();
2792 for reflection in 0..count {
2793 let root = root(&mut parents, reflection);
2794 grouped.entry(root).or_default().push(reflection);
2795 }
2796 grouped
2797 .into_values()
2798 .filter(|indices| indices.len() > 1)
2799 .map(|indices| {
2800 let first = indices
2801 .iter()
2802 .map(|index| local.starts[*index])
2803 .min()
2804 .unwrap_or(0);
2805 let last = indices
2806 .iter()
2807 .map(|index| {
2808 local.starts[*index] + local.offsets[*index + 1] - local.offsets[*index]
2809 })
2810 .max()
2811 .unwrap_or(first);
2812 let mut matrix = DMatrix::zeros(last - first, indices.len());
2813 for (column, reflection) in indices.iter().enumerate() {
2814 let begin = local.offsets[*reflection];
2815 let end = local.offsets[*reflection + 1];
2816 let start = local.starts[*reflection] - first;
2817 for active in begin..end {
2818 matrix[(start + active - begin, column)] =
2819 local.values[active * local.parameter_count];
2820 }
2821 }
2822 let singular_values = matrix.svd(false, false).singular_values;
2823 let maximum = singular_values.iter().copied().fold(0.0_f64, f64::max);
2824 let tolerance =
2825 count_as_f64((last - first).max(indices.len())) * f64::EPSILON * maximum;
2826 let rank = singular_values
2827 .iter()
2828 .filter(|value| **value > tolerance)
2829 .count();
2830 CoincidentReflectionGroup {
2831 reflection_keys: indices
2832 .iter()
2833 .map(|index| calculation.reflection_keys[*index].clone())
2834 .collect(),
2835 rank,
2836 }
2837 })
2838 .collect()
2839}
2840
2841fn root(parents: &mut [usize], mut index: usize) -> usize {
2842 while parents[index] != index {
2843 parents[index] = parents[parents[index]];
2844 index = parents[index];
2845 }
2846 index
2847}
2848
2849fn union(parents: &mut [usize], left: usize, right: usize) {
2850 let left_root = root(parents, left);
2851 let right_root = root(parents, right);
2852 if left_root != right_root {
2853 parents[right_root] = left_root;
2854 }
2855}
2856
2857fn bin_integration_weights(x: &[f64]) -> Vec<f64> {
2858 match x.len() {
2859 0 => Vec::new(),
2860 1 => vec![1.0],
2861 count => {
2862 let mut widths = vec![0.0; count];
2863 widths[0] = 0.5 * (x[1] - x[0]);
2864 widths[count - 1] = 0.5 * (x[count - 1] - x[count - 2]);
2865 for index in 1..count - 1 {
2866 widths[index] = 0.5 * (x[index + 1] - x[index - 1]);
2867 }
2868 widths
2869 }
2870 }
2871}
2872
2873fn replace_flat_intensities(
2874 phases: &[LeBailPhase],
2875 intensities: &[f64],
2876) -> Result<Vec<LeBailPhase>, LeBailError> {
2877 let expected = phases.iter().map(reflection_count).sum::<usize>();
2878 if intensities.len() != expected {
2879 return Err(LeBailError::IntensityShapeMismatch);
2880 }
2881 let mut offset = 0;
2882 phases
2883 .iter()
2884 .map(|phase| {
2885 let end = offset + reflection_count(phase);
2886 let updated = phase.replace_intensities(&intensities[offset..end]);
2887 offset = end;
2888 updated
2889 })
2890 .collect()
2891}
2892
2893fn flatten_intensities(phases: &[LeBailPhase]) -> Vec<f64> {
2894 phases
2895 .iter()
2896 .flat_map(|phase| phase.integrated_intensity.iter().copied())
2897 .collect()
2898}
2899
2900fn flatten_preserve_mask(phases: &[LeBailPhase]) -> Vec<bool> {
2901 phases
2902 .iter()
2903 .flat_map(|phase| {
2904 if phase.preserve_unobserved.is_empty() {
2905 vec![false; reflection_count(phase)]
2906 } else {
2907 phase.preserve_unobserved.clone()
2908 }
2909 })
2910 .collect()
2911}
2912
2913fn parameter_keys(parameters: &ParameterSet) -> Vec<&ParameterKey> {
2914 parameters.specs().iter().map(ParameterSpec::key).collect()
2915}
2916
2917fn validate_parameter_selection(
2918 phases: &[LeBailPhase],
2919 background: Option<&BackgroundModel>,
2920 parameters: &ParameterSet,
2921) -> Result<(), LeBailError> {
2922 for spec in parameters.specs() {
2923 let key = spec.key();
2924 if key.module() == "background" {
2925 let supported = background.is_some_and(|background| {
2926 background.background_id() == key.owner_id()
2927 && background
2928 .parameter_names()
2929 .iter()
2930 .any(|name| name == key.name())
2931 });
2932 if !supported {
2933 return Err(LeBailError::UnsupportedParameter { label: key.label() });
2934 }
2935 } else if key.module() == "lattice" {
2936 let phase = phases
2937 .iter()
2938 .find(|phase| phase.phase_id() == key.owner_id())
2939 .ok_or_else(|| LeBailError::UnsupportedParameter { label: key.label() })?;
2940 let Some(domain) = phase.reflection_domain.as_ref() else {
2941 return Err(invalid_phase(
2942 "lattice parameters require bounded dynamic phases",
2943 ));
2944 };
2945 if !domain
2946 .parameterization()
2947 .parameter_names()
2948 .iter()
2949 .any(|name| name == key.name())
2950 {
2951 return Err(LeBailError::UnsupportedParameter { label: key.label() });
2952 }
2953 } else if key.module() == "reflection" {
2954 let dynamic = phases.iter().any(|phase| {
2955 phase.reflection_domain.is_some()
2956 && key
2957 .owner_id()
2958 .strip_prefix(phase.phase_id())
2959 .is_some_and(|suffix| suffix.starts_with('/'))
2960 });
2961 if dynamic {
2962 return Err(invalid_phase(
2963 "independent reflection positions require fixed-topology phases",
2964 ));
2965 }
2966 }
2967 }
2968 Ok(())
2969}
2970
2971fn phases_restart_compatible(input: &[LeBailPhase], checkpoint: &[LeBailPhase]) -> bool {
2972 input.len() == checkpoint.len()
2973 && input.iter().zip(checkpoint).all(|(left, right)| {
2974 if left.phase_id() != right.phase_id() {
2975 return false;
2976 }
2977 match (&left.reflection_domain, &right.reflection_domain) {
2978 (None, None) => left.reflection_ids == right.reflection_ids,
2979 (Some(left_domain), Some(right_domain)) => left_domain == right_domain,
2980 _ => false,
2981 }
2982 })
2983}
2984
2985fn backgrounds_restart_compatible(
2986 input: Option<&BackgroundModel>,
2987 checkpoint: Option<&BackgroundModel>,
2988) -> bool {
2989 match (input, checkpoint) {
2990 (None, None) => true,
2991 (Some(input), Some(checkpoint)) => checkpoint.restart_compatible(input),
2992 _ => false,
2993 }
2994}
2995
2996fn reflection_count(phase: &LeBailPhase) -> usize {
2997 phase.reflection_ids.len()
2998}
2999
3000fn reflection_count_of(phase: &LeBailPhase) -> usize {
3001 reflection_count(phase)
3002}
3003
3004fn validate_stable_label(name: &'static str, value: &str) -> Result<(), LeBailError> {
3005 if value.is_empty()
3006 || value.trim() != value
3007 || value.chars().any(char::is_control)
3008 || value.contains('/')
3009 {
3010 return Err(LeBailError::InvalidPhase {
3011 message: format!(
3012 "{name} must be non-empty, trimmed, and contain neither '/' nor control characters"
3013 ),
3014 });
3015 }
3016 Ok(())
3017}
3018
3019fn normal_stop_reason(error: &RuntimeError) -> Option<TerminationReason> {
3020 match error {
3021 RuntimeError::Stopped(stop) => Some(stop.reason),
3022 _ => None,
3023 }
3024}
3025
3026fn stop_reason_or_error(error: RuntimeError) -> Result<TerminationReason, LeBailError> {
3027 normal_stop_reason(&error).ok_or(LeBailError::Runtime(error))
3028}
3029
3030#[allow(clippy::cast_precision_loss)]
3031fn count_as_f64(value: usize) -> f64 {
3032 value as f64
3033}
3034
3035fn invalid_phase(message: &str) -> LeBailError {
3036 LeBailError::InvalidPhase {
3037 message: message.to_owned(),
3038 }
3039}
3040
3041fn invalid_options(message: &str) -> LeBailError {
3042 LeBailError::InvalidOptions {
3043 message: message.to_owned(),
3044 }
3045}
3046
3047#[derive(Debug)]
3049pub enum LeBailError {
3050 Pattern(DomainError),
3052 MissingObservations,
3054 Parameter(ParameterError),
3056 Constraint(ConstraintError),
3058 Lattice(LatticeError),
3060 Background(BackgroundError),
3062 UnsupportedParameter {
3064 label: String,
3066 },
3067 ParameterDomainOutsideBounds {
3069 label: String,
3071 },
3072 LinearSolve,
3074 InvalidPhase {
3076 message: String,
3078 },
3079 InvalidOptions {
3081 message: String,
3083 },
3084 InvalidCheckpoint {
3086 message: String,
3088 },
3089 IntensityShapeMismatch,
3091 PreserveMaskLengthMismatch,
3093 Grid(ProfileError),
3095 Calculation(CwContributionsError),
3097 Residual(ResidualError),
3099 Runtime(RuntimeError),
3101 Execution(ExecutionPolicyError),
3103 SizeOverflow,
3105 Profile {
3107 message: String,
3109 },
3110 InternalInvariant,
3112}
3113
3114impl Display for LeBailError {
3115 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
3116 match self {
3117 Self::Pattern(error) => Display::fmt(error, formatter),
3118 Self::MissingObservations => {
3119 formatter.write_str("observed_y is required for Le Bail extraction")
3120 }
3121 Self::Parameter(error) => Display::fmt(error, formatter),
3122 Self::Constraint(error) => Display::fmt(error, formatter),
3123 Self::Lattice(error) => Display::fmt(error, formatter),
3124 Self::Background(error) => Display::fmt(error, formatter),
3125 Self::UnsupportedParameter { label } => {
3126 write!(formatter, "unsupported Le Bail parameter {label}")
3127 }
3128 Self::ParameterDomainOutsideBounds { label } => {
3129 write!(
3130 formatter,
3131 "domain value for {label} lies outside its bounds"
3132 )
3133 }
3134 Self::LinearSolve => formatter.write_str("profile least-squares solve failed"),
3135 Self::InvalidPhase { message }
3136 | Self::InvalidOptions { message }
3137 | Self::InvalidCheckpoint { message }
3138 | Self::Profile { message } => formatter.write_str(message),
3139 Self::IntensityShapeMismatch => {
3140 formatter.write_str("current intensities must match the reflection count")
3141 }
3142 Self::PreserveMaskLengthMismatch => {
3143 formatter.write_str("preserve_unobserved must match the reflection count")
3144 }
3145 Self::Grid(error) => Display::fmt(error, formatter),
3146 Self::Calculation(error) => Display::fmt(error, formatter),
3147 Self::Residual(error) => Display::fmt(error, formatter),
3148 Self::Runtime(error) => Display::fmt(error, formatter),
3149 Self::Execution(error) => Display::fmt(error, formatter),
3150 Self::SizeOverflow => formatter.write_str("Le Bail workflow size or budget overflowed"),
3151 Self::InternalInvariant => {
3152 formatter.write_str("internal Le Bail workflow state is inconsistent")
3153 }
3154 }
3155 }
3156}
3157
3158impl Error for LeBailError {
3159 fn source(&self) -> Option<&(dyn Error + 'static)> {
3160 match self {
3161 Self::Pattern(error) => Some(error),
3162 Self::Parameter(error) => Some(error),
3163 Self::Constraint(error) => Some(error),
3164 Self::Lattice(error) => Some(error),
3165 Self::Background(error) => Some(error),
3166 Self::Grid(error) => Some(error),
3167 Self::Calculation(error) => Some(error),
3168 Self::Residual(error) => Some(error),
3169 Self::Runtime(error) => Some(error),
3170 Self::Execution(error) => Some(error),
3171 Self::MissingObservations
3172 | Self::UnsupportedParameter { .. }
3173 | Self::ParameterDomainOutsideBounds { .. }
3174 | Self::LinearSolve
3175 | Self::InvalidPhase { .. }
3176 | Self::InvalidOptions { .. }
3177 | Self::InvalidCheckpoint { .. }
3178 | Self::IntensityShapeMismatch
3179 | Self::PreserveMaskLengthMismatch
3180 | Self::SizeOverflow
3181 | Self::Profile { .. }
3182 | Self::InternalInvariant => None,
3183 }
3184 }
3185}