1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use phasesmith_core::{
7 ConstantWavelengthInstrument, CwContributionsError, FcjGeometry, OwnedCwContributionArrays,
8 OwnedCwContributions, SupportPolicy,
9};
10use phasesmith_crystallography::IntegratedIntensityCorrectionModel;
11use phasesmith_engine::{
12 MonochromaticPositionCorrection, PreparedStructuralModel, PreparedStructuralMultiphase,
13 PreparedStructuralPhase, PreparedStructuralSpectrum, StructuralCalculationRequest,
14 StructuralModelInput, StructuralMultiphaseError, StructuralPatternError,
15 StructuralPatternResult, StructuralPhaseDefinition, StructuralSpectrumError,
16 calculate_monochromatic_reflection_geometry,
17};
18use phasesmith_execution::ExecutionPolicy;
19use phasesmith_model::{DomainError, FixedWavelengthSpectrum, PatternRecord, RecordId};
20
21use crate::{
22 BackgroundError, BackgroundModel, DifferentiableBackground, LatticeError,
23 LatticeReflectionDomain, ResidualError, ResidualEvaluation, ResidualOptions,
24 RietveldSamplePhysicsModel, SamplePhysicsError, evaluate_residuals,
25};
26
27#[derive(Clone, Debug, PartialEq)]
29pub struct RietveldPhase {
30 phase_id: RecordId,
31 name: String,
32 site_ids: Vec<RecordId>,
33 reflection_ids: Vec<String>,
34 definition: StructuralPhaseDefinition,
35 contributions: OwnedCwContributions,
36 sample_physics: Option<RietveldSamplePhysicsModel>,
37 reflection_domain: Option<LatticeReflectionDomain>,
38}
39
40impl RietveldPhase {
41 pub fn new(
48 phase_id: RecordId,
49 name: impl Into<String>,
50 definition: StructuralPhaseDefinition,
51 contributions: OwnedCwContributions,
52 ) -> Result<Self, RietveldError> {
53 let site_ids = (0..definition.fractional_xyz.len())
54 .map(|index| RecordId::new(format!("site-{index}")))
55 .collect::<Result<Vec<_>, _>>()
56 .map_err(RietveldError::Pattern)?;
57 Self::new_with_site_ids(phase_id, name, site_ids, definition, contributions)
58 }
59
60 pub fn new_with_site_ids(
67 phase_id: RecordId,
68 name: impl Into<String>,
69 site_ids: Vec<RecordId>,
70 definition: StructuralPhaseDefinition,
71 contributions: OwnedCwContributions,
72 ) -> Result<Self, RietveldError> {
73 let reflection_ids = definition
74 .hkl
75 .iter()
76 .map(|hkl| reflection_id(*hkl))
77 .collect();
78 let phase = Self {
79 phase_id,
80 name: name.into(),
81 site_ids,
82 reflection_ids,
83 definition,
84 contributions,
85 sample_physics: None,
86 reflection_domain: None,
87 };
88 phase.validate()?;
89 Ok(phase)
90 }
91
92 pub fn from_lattice_domain(
102 phase_id: RecordId,
103 name: impl Into<String>,
104 site_ids: Vec<RecordId>,
105 mut definition: StructuralPhaseDefinition,
106 reflection_domain: LatticeReflectionDomain,
107 ) -> Result<Self, RietveldError> {
108 let generated = reflection_domain
109 .generate(definition.cell, None)
110 .map_err(RietveldError::Lattice)?;
111 definition.hkl = generated.hkl;
112 definition.multiplicity = generated.multiplicity;
113 let phase = Self {
114 phase_id,
115 name: name.into(),
116 site_ids,
117 reflection_ids: generated.reflection_ids,
118 contributions: OwnedCwContributions::neutral(definition.hkl.len()),
119 sample_physics: None,
120 definition,
121 reflection_domain: Some(reflection_domain),
122 };
123 phase.validate()?;
124 Ok(phase)
125 }
126
127 pub fn validate(&self) -> Result<(), RietveldError> {
133 if self.name.trim().is_empty() {
134 return Err(RietveldError::InvalidPhaseName);
135 }
136 self.definition
137 .validate()
138 .map_err(RietveldError::StructuralPattern)?;
139 if self.site_ids.len() != self.definition.fractional_xyz.len() {
140 return Err(RietveldError::SiteIdCountMismatch);
141 }
142 if self
143 .site_ids
144 .iter()
145 .collect::<std::collections::BTreeSet<_>>()
146 .len()
147 != self.site_ids.len()
148 {
149 return Err(RietveldError::DuplicateSiteId);
150 }
151 if self.contributions.reflection_count() != self.definition.hkl.len() {
152 return Err(RietveldError::ContributionCountMismatch);
153 }
154 if self.reflection_ids.len() != self.definition.hkl.len()
155 || self
156 .reflection_ids
157 .iter()
158 .collect::<std::collections::BTreeSet<_>>()
159 .len()
160 != self.reflection_ids.len()
161 {
162 return Err(RietveldError::ReflectionIdentityMismatch);
163 }
164 if self
165 .reflection_ids
166 .iter()
167 .zip(&self.definition.hkl)
168 .any(|(id, hkl)| id != &reflection_id(*hkl))
169 {
170 return Err(RietveldError::ReflectionTopologyMismatch);
171 }
172 if let Some(domain) = &self.reflection_domain {
173 domain
174 .validate_cell(self.definition.cell)
175 .map_err(RietveldError::Lattice)?;
176 }
177 Ok(())
178 }
179
180 #[must_use]
182 pub const fn phase_id(&self) -> &RecordId {
183 &self.phase_id
184 }
185
186 #[must_use]
188 pub fn name(&self) -> &str {
189 &self.name
190 }
191
192 #[must_use]
194 pub fn site_ids(&self) -> &[RecordId] {
195 &self.site_ids
196 }
197
198 #[must_use]
200 pub fn reflection_ids(&self) -> &[String] {
201 &self.reflection_ids
202 }
203
204 #[must_use]
206 pub const fn definition(&self) -> &StructuralPhaseDefinition {
207 &self.definition
208 }
209
210 #[must_use]
212 pub const fn contributions(&self) -> &OwnedCwContributions {
213 &self.contributions
214 }
215
216 #[must_use]
218 pub const fn sample_physics(&self) -> Option<&RietveldSamplePhysicsModel> {
219 self.sample_physics.as_ref()
220 }
221
222 #[must_use]
227 pub fn with_sample_physics(mut self, model: RietveldSamplePhysicsModel) -> Self {
228 self.sample_physics = Some(model);
229 self
230 }
231
232 pub(crate) fn replace_sample_physics(&self, model: RietveldSamplePhysicsModel) -> Self {
233 let mut phase = self.clone();
234 phase.sample_physics = Some(model);
235 phase
236 }
237
238 pub(crate) fn resolved_sample_physics(
239 &self,
240 instrument: ConstantWavelengthInstrument,
241 position_correction: MonochromaticPositionCorrection,
242 ) -> Result<(OwnedCwContributions, Vec<String>), RietveldError> {
243 let Some(model) = &self.sample_physics else {
244 return Ok((self.contributions.clone(), Vec::new()));
245 };
246 let geometry = calculate_monochromatic_reflection_geometry(
247 self.definition.cell,
248 &self.definition.hkl,
249 instrument,
250 position_correction,
251 )
252 .map_err(RietveldError::StructuralPattern)?;
253 let evaluated = model
254 .evaluate(
255 &self.definition.hkl,
256 &geometry.two_theta_deg,
257 self.definition.cell,
258 instrument.wavelength_angstrom,
259 )
260 .map_err(RietveldError::SamplePhysics)?;
261 Ok((evaluated.contributions, evaluated.parameter_names))
262 }
263
264 pub fn with_contributions(
271 &self,
272 contributions: OwnedCwContributions,
273 ) -> Result<Self, RietveldError> {
274 let mut phase = self.clone();
275 phase.contributions = contributions;
276 phase.validate()?;
277 Ok(phase)
278 }
279
280 #[must_use]
282 pub const fn reflection_domain(&self) -> Option<&LatticeReflectionDomain> {
283 self.reflection_domain.as_ref()
284 }
285
286 pub fn regenerate_lattice_at_cell(
296 &self,
297 cell: phasesmith_crystallography::UnitCell,
298 ) -> Result<(Self, RietveldTopologyChange), RietveldError> {
299 let domain = self
300 .reflection_domain
301 .as_ref()
302 .ok_or(RietveldError::FixedReflectionTopology)?;
303 let previous = self
304 .reflection_ids
305 .iter()
306 .cloned()
307 .map(|reflection_id| (reflection_id, 1.0))
308 .collect::<std::collections::BTreeMap<_, _>>();
309 let generated = domain
310 .generate(cell, Some(&previous))
311 .map_err(RietveldError::Lattice)?;
312 let contributions = transfer_contributions(
313 &self.reflection_ids,
314 &generated.reflection_ids,
315 &self.contributions,
316 )?;
317 let change = RietveldTopologyChange {
318 phase_id: self.phase_id.clone(),
319 added_reflection_ids: generated.added_reflection_ids.clone(),
320 removed_reflection_ids: generated.removed_reflection_ids.clone(),
321 preserved_reflection_count: generated.preserved_reflection_count,
322 };
323 let mut phase = self.clone();
324 phase.definition.cell = cell;
325 phase.definition.hkl = generated.hkl;
326 phase.definition.multiplicity = generated.multiplicity;
327 phase.reflection_ids = generated.reflection_ids;
328 phase.contributions = contributions;
329 phase.validate()?;
330 Ok((phase, change))
331 }
332
333 pub(crate) fn with_definition(
334 &self,
335 definition: StructuralPhaseDefinition,
336 ) -> Result<Self, RietveldError> {
337 if self.reflection_domain.is_some() && definition.cell != self.definition.cell {
338 let (mut phase, _) = self.regenerate_lattice_at_cell(definition.cell)?;
339 let hkl = std::mem::take(&mut phase.definition.hkl);
340 let multiplicity = std::mem::take(&mut phase.definition.multiplicity);
341 phase.definition = definition;
342 phase.definition.hkl = hkl;
343 phase.definition.multiplicity = multiplicity;
344 phase.validate()?;
345 return Ok(phase);
346 }
347 let mut phase = self.clone();
348 phase.definition = definition;
349 phase.validate()?;
350 Ok(phase)
351 }
352
353 pub(crate) fn restart_compatible(&self, requested: &Self) -> bool {
354 self.restart_compatible_with_wavelength(requested, false)
355 }
356
357 pub(crate) fn restart_compatible_with_wavelength(
358 &self,
359 requested: &Self,
360 allow_wavelength_change: bool,
361 ) -> bool {
362 self.phase_id == requested.phase_id
363 && self.site_ids == requested.site_ids
364 && self.definition.space_group == requested.definition.space_group
365 && self.definition.anisotropic_mask == requested.definition.anisotropic_mask
366 && self.definition.u_aniso_cif_angstrom2 == requested.definition.u_aniso_cif_angstrom2
367 && self.definition.scattering_species == requested.definition.scattering_species
368 && self.definition.scattering_real_offset == requested.definition.scattering_real_offset
369 && self.definition.scattering_imag_offset == requested.definition.scattering_imag_offset
370 && self.definition.coordinate_tolerance.to_bits()
371 == requested.definition.coordinate_tolerance.to_bits()
372 && self.definition.scattering_model == requested.definition.scattering_model
373 && (self.definition.correction_model == requested.definition.correction_model
374 || (allow_wavelength_change
375 && correction_identity_matches(
376 self.definition.correction_model,
377 requested.definition.correction_model,
378 )))
379 && sample_physics_identity_matches(
380 self.sample_physics.as_ref(),
381 requested.sample_physics.as_ref(),
382 )
383 && (self.reflection_domain == requested.reflection_domain
384 || (allow_wavelength_change
385 && reflection_domain_identity_matches(
386 self.reflection_domain.as_ref(),
387 requested.reflection_domain.as_ref(),
388 )))
389 && (self.reflection_domain.is_some()
390 || (self.reflection_ids == requested.reflection_ids
391 && self.definition.hkl == requested.definition.hkl
392 && self.definition.multiplicity == requested.definition.multiplicity))
393 }
394
395 pub(crate) fn with_wavelength(&self, wavelength_angstrom: f64) -> Result<Self, RietveldError> {
396 let mut phase = self.clone();
397 phase.definition.correction_model = match phase.definition.correction_model {
398 IntegratedIntensityCorrectionModel::Neutral => {
399 IntegratedIntensityCorrectionModel::Neutral
400 }
401 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp { .. } => {
402 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
403 wavelength_angstrom,
404 }
405 }
406 IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
407 polarization, ..
408 } => IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
409 wavelength_angstrom,
410 polarization,
411 },
412 IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz { .. } => {
413 IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz {
414 wavelength_angstrom,
415 }
416 }
417 };
418 if let Some(domain) = &self.reflection_domain {
419 phase.reflection_domain = Some(
420 domain
421 .with_wavelength(wavelength_angstrom)
422 .map_err(RietveldError::Lattice)?,
423 );
424 phase = phase.regenerate_lattice_at_cell(phase.definition.cell)?.0;
425 }
426 phase.validate()?;
427 Ok(phase)
428 }
429
430 fn correction_wavelength(&self) -> Option<f64> {
431 match self.definition.correction_model {
432 IntegratedIntensityCorrectionModel::Neutral => None,
433 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
434 wavelength_angstrom,
435 }
436 | IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
437 wavelength_angstrom,
438 ..
439 }
440 | IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz {
441 wavelength_angstrom,
442 } => Some(wavelength_angstrom),
443 }
444 }
445}
446
447fn correction_identity_matches(
448 left: IntegratedIntensityCorrectionModel,
449 right: IntegratedIntensityCorrectionModel,
450) -> bool {
451 match (left, right) {
452 (
453 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp { .. },
454 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp { .. },
455 )
456 | (
457 IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz { .. },
458 IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz { .. },
459 ) => true,
460 (
461 IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
462 polarization: left, ..
463 },
464 IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
465 polarization: right,
466 ..
467 },
468 ) => left.to_bits() == right.to_bits(),
469 _ => false,
470 }
471}
472
473fn reflection_domain_identity_matches(
474 left: Option<&LatticeReflectionDomain>,
475 right: Option<&LatticeReflectionDomain>,
476) -> bool {
477 match (left, right) {
478 (None, None) => true,
479 (Some(left), Some(right)) => left
480 .with_wavelength(right.wavelength_angstrom())
481 .is_ok_and(|updated| updated == *right),
482 _ => false,
483 }
484}
485
486fn sample_physics_identity_matches(
487 left: Option<&RietveldSamplePhysicsModel>,
488 right: Option<&RietveldSamplePhysicsModel>,
489) -> bool {
490 match (left, right) {
491 (None, None) => true,
492 (Some(left), Some(right)) => sample_physics_model_identity_matches(left, right),
493 _ => false,
494 }
495}
496
497fn sample_physics_model_identity_matches(
498 left: &RietveldSamplePhysicsModel,
499 right: &RietveldSamplePhysicsModel,
500) -> bool {
501 match (left, right) {
502 (
503 RietveldSamplePhysicsModel::IsotropicSize {
504 shape_factor: left, ..
505 },
506 RietveldSamplePhysicsModel::IsotropicSize {
507 shape_factor: right,
508 ..
509 },
510 ) => left.to_bits() == right.to_bits(),
511 (
512 RietveldSamplePhysicsModel::IsotropicMicrostrain { .. },
513 RietveldSamplePhysicsModel::IsotropicMicrostrain { .. },
514 ) => true,
515 (
516 RietveldSamplePhysicsModel::MarchDollase {
517 preferred_axis_hkl: left,
518 ..
519 },
520 RietveldSamplePhysicsModel::MarchDollase {
521 preferred_axis_hkl: right,
522 ..
523 },
524 ) => left
525 .iter()
526 .zip(right)
527 .all(|(left, right)| left.to_bits() == right.to_bits()),
528 (
529 RietveldSamplePhysicsModel::Composite(left),
530 RietveldSamplePhysicsModel::Composite(right),
531 ) => {
532 left.len() == right.len()
533 && left
534 .iter()
535 .zip(right)
536 .all(|(left, right)| sample_physics_model_identity_matches(left, right))
537 }
538 _ => false,
539 }
540}
541
542#[derive(Clone, Debug, PartialEq, Eq)]
544pub struct RietveldTopologyChange {
545 pub phase_id: RecordId,
547 pub added_reflection_ids: Vec<String>,
549 pub removed_reflection_ids: Vec<String>,
551 pub preserved_reflection_count: usize,
553}
554
555#[derive(Clone, Debug, PartialEq)]
557pub struct RietveldInput {
558 pub pattern: PatternRecord,
560 pub instrument: ConstantWavelengthInstrument,
562 pub fixed_spectrum: Option<FixedWavelengthSpectrum>,
564 pub axial_geometry: Option<FcjGeometry>,
566 pub position_correction: MonochromaticPositionCorrection,
568 pub background: Option<BackgroundModel>,
570 pub phases: Vec<RietveldPhase>,
572}
573
574impl RietveldInput {
575 pub fn new(
582 pattern: PatternRecord,
583 instrument: ConstantWavelengthInstrument,
584 axial_geometry: Option<FcjGeometry>,
585 position_correction: MonochromaticPositionCorrection,
586 phases: Vec<RietveldPhase>,
587 ) -> Result<Self, RietveldError> {
588 let input = Self {
589 pattern,
590 instrument,
591 fixed_spectrum: None,
592 axial_geometry,
593 position_correction,
594 background: None,
595 phases,
596 };
597 input.validate()?;
598 Ok(input)
599 }
600
601 pub fn new_fixed_spectrum(
612 pattern: PatternRecord,
613 instrument: ConstantWavelengthInstrument,
614 spectrum: FixedWavelengthSpectrum,
615 axial_geometry: Option<FcjGeometry>,
616 position_correction: MonochromaticPositionCorrection,
617 phases: Vec<RietveldPhase>,
618 ) -> Result<Self, RietveldError> {
619 let input = Self {
620 pattern,
621 instrument,
622 fixed_spectrum: Some(spectrum),
623 axial_geometry,
624 position_correction,
625 background: None,
626 phases,
627 };
628 input.validate()?;
629 Ok(input)
630 }
631
632 pub fn new_with_background(
639 pattern: PatternRecord,
640 instrument: ConstantWavelengthInstrument,
641 axial_geometry: Option<FcjGeometry>,
642 position_correction: MonochromaticPositionCorrection,
643 background: BackgroundModel,
644 phases: Vec<RietveldPhase>,
645 ) -> Result<Self, RietveldError> {
646 let mut input = Self::new(
647 pattern,
648 instrument,
649 axial_geometry,
650 position_correction,
651 phases,
652 )?;
653 input.background = Some(background);
654 input.validate()?;
655 Ok(input)
656 }
657
658 pub fn new_fixed_spectrum_with_background(
665 pattern: PatternRecord,
666 instrument: ConstantWavelengthInstrument,
667 spectrum: FixedWavelengthSpectrum,
668 axial_geometry: Option<FcjGeometry>,
669 position_correction: MonochromaticPositionCorrection,
670 background: BackgroundModel,
671 phases: Vec<RietveldPhase>,
672 ) -> Result<Self, RietveldError> {
673 let mut input = Self::new_fixed_spectrum(
674 pattern,
675 instrument,
676 spectrum,
677 axial_geometry,
678 position_correction,
679 phases,
680 )?;
681 input.background = Some(background);
682 input.validate()?;
683 Ok(input)
684 }
685
686 pub fn validate(&self) -> Result<(), RietveldError> {
693 self.pattern.validate().map_err(RietveldError::Pattern)?;
694 if self.pattern.observed_y.is_none() {
695 return Err(RietveldError::MissingObservations);
696 }
697 self.instrument
698 .validate()
699 .map_err(|_| RietveldError::InvalidInstrument)?;
700 if self.fixed_spectrum.as_ref().is_some_and(|spectrum| {
701 spectrum.wavelengths_angstrom()[0].to_bits()
702 != self.instrument.wavelength_angstrom.to_bits()
703 }) {
704 return Err(RietveldError::SpectrumReferenceWavelengthMismatch);
705 }
706 if self.axial_geometry.is_some_and(|geometry| {
707 !geometry.sample_over_radius.is_finite()
708 || !geometry.detector_over_radius.is_finite()
709 || geometry.sample_over_radius < 0.0
710 || geometry.detector_over_radius < 0.0
711 }) {
712 return Err(RietveldError::InvalidAxialGeometry);
713 }
714 let correction = self.position_correction;
715 if !correction.zero_shift_deg.is_finite()
716 || (correction.bragg_brentano_mm.is_some()
717 && correction.debye_scherrer_micrometre.is_some())
718 || correction
719 .bragg_brentano_mm
720 .is_some_and(|(displacement, radius)| {
721 !displacement.is_finite() || !radius.is_finite() || radius <= 0.0
722 })
723 || correction
724 .debye_scherrer_micrometre
725 .is_some_and(|(x, y, radius)| {
726 !x.is_finite() || !y.is_finite() || !radius.is_finite() || radius <= 0.0
727 })
728 {
729 return Err(RietveldError::InvalidPositionCorrection);
730 }
731 if self.phases.is_empty() {
732 return Err(RietveldError::EmptyPhases);
733 }
734 if let Some(background) = &self.background {
735 background
736 .basis(&self.pattern.x_deg)
737 .map_err(RietveldError::Background)?;
738 background
739 .calculate(&self.pattern.x_deg)
740 .map_err(RietveldError::Background)?;
741 }
742 let mut identities = std::collections::BTreeSet::new();
743 for phase in &self.phases {
744 phase.validate()?;
745 phase.resolved_sample_physics(self.instrument, self.position_correction)?;
746 if self.fixed_spectrum.is_some() && phase.reflection_domain().is_some() {
747 return Err(RietveldError::SpectrumReflectionDomain);
748 }
749 if phase.correction_wavelength().is_some_and(|wavelength| {
750 wavelength.to_bits() != self.instrument.wavelength_angstrom.to_bits()
751 }) {
752 return Err(RietveldError::CorrectionWavelengthMismatch);
753 }
754 if phase.reflection_domain().is_some_and(|domain| {
755 domain.wavelength_angstrom().to_bits()
756 != self.instrument.wavelength_angstrom.to_bits()
757 }) {
758 return Err(RietveldError::ReflectionWavelengthMismatch);
759 }
760 if !identities.insert(phase.phase_id.clone()) {
761 return Err(RietveldError::DuplicatePhaseId);
762 }
763 }
764 Ok(())
765 }
766}
767
768pub(crate) fn prepare_phase_model(
769 phase: &RietveldPhase,
770 spectrum: Option<&FixedWavelengthSpectrum>,
771 execution: &ExecutionPolicy,
772) -> Result<PreparedStructuralModel, RietveldError> {
773 match spectrum {
774 None => PreparedStructuralPhase::new(phase.definition.clone(), execution.context().clone())
775 .map(PreparedStructuralModel::monochromatic)
776 .map_err(RietveldError::StructuralPattern),
777 Some(spectrum) => PreparedStructuralSpectrum::new(
778 &phase.definition,
779 spectrum.wavelengths_angstrom().to_vec(),
780 spectrum.relative_intensities(),
781 execution.clone(),
782 )
783 .map(PreparedStructuralModel::fixed_spectrum)
784 .map_err(RietveldError::StructuralSpectrum),
785 }
786}
787
788pub(crate) fn resolve_phase_contributions(
789 phase: &RietveldPhase,
790 input: &RietveldInput,
791) -> Result<Vec<OwnedCwContributions>, RietveldError> {
792 let wavelengths = input.fixed_spectrum.as_ref().map_or_else(
793 || vec![input.instrument.wavelength_angstrom],
794 |spectrum| spectrum.wavelengths_angstrom().to_vec(),
795 );
796 wavelengths
797 .into_iter()
798 .map(|wavelength_angstrom| {
799 let mut instrument = input.instrument;
800 instrument.wavelength_angstrom = wavelength_angstrom;
801 phase
802 .resolved_sample_physics(instrument, input.position_correction)
803 .map(|value| value.0)
804 })
805 .collect()
806}
807
808fn reflection_id(hkl: [i32; 3]) -> String {
809 format!("hkl:{},{},{}", hkl[0], hkl[1], hkl[2])
810}
811
812fn transfer_contributions(
813 previous_ids: &[String],
814 current_ids: &[String],
815 previous: &OwnedCwContributions,
816) -> Result<OwnedCwContributions, RietveldError> {
817 let old_count = previous_ids.len();
818 let new_count = current_ids.len();
819 let parameter_count = previous.parameter_count();
820 let derivative_count =
821 parameter_count
822 .checked_mul(new_count)
823 .ok_or(RietveldError::Contributions(
824 CwContributionsError::AllocationOverflow,
825 ))?;
826 let old = previous.arrays();
827 let mut arrays = OwnedCwContributionArrays {
828 gaussian_variance_deg2: vec![0.0; new_count],
829 lorentzian_fwhm_deg: vec![0.0; new_count],
830 intensity_multiplier: vec![1.0; new_count],
831 d_gaussian_variance_d_position: vec![0.0; new_count],
832 d_lorentzian_fwhm_d_position: vec![0.0; new_count],
833 d_intensity_multiplier_d_position: vec![0.0; new_count],
834 d_gaussian_variance_d_parameters: vec![0.0; derivative_count],
835 d_lorentzian_fwhm_d_parameters: vec![0.0; derivative_count],
836 d_intensity_multiplier_d_parameters: vec![0.0; derivative_count],
837 };
838 let previous_index = previous_ids
839 .iter()
840 .enumerate()
841 .map(|(index, id)| (id, index))
842 .collect::<std::collections::BTreeMap<_, _>>();
843 for (new_index, id) in current_ids.iter().enumerate() {
844 let Some(&old_index) = previous_index.get(id) else {
845 continue;
846 };
847 for (target, source) in [
848 (
849 &mut arrays.gaussian_variance_deg2,
850 &old.gaussian_variance_deg2,
851 ),
852 (&mut arrays.lorentzian_fwhm_deg, &old.lorentzian_fwhm_deg),
853 (&mut arrays.intensity_multiplier, &old.intensity_multiplier),
854 (
855 &mut arrays.d_gaussian_variance_d_position,
856 &old.d_gaussian_variance_d_position,
857 ),
858 (
859 &mut arrays.d_lorentzian_fwhm_d_position,
860 &old.d_lorentzian_fwhm_d_position,
861 ),
862 (
863 &mut arrays.d_intensity_multiplier_d_position,
864 &old.d_intensity_multiplier_d_position,
865 ),
866 ] {
867 target[new_index] = source[old_index];
868 }
869 for parameter in 0..parameter_count {
870 let old_offset = parameter * old_count + old_index;
871 let new_offset = parameter * new_count + new_index;
872 arrays.d_gaussian_variance_d_parameters[new_offset] =
873 old.d_gaussian_variance_d_parameters[old_offset];
874 arrays.d_lorentzian_fwhm_d_parameters[new_offset] =
875 old.d_lorentzian_fwhm_d_parameters[old_offset];
876 arrays.d_intensity_multiplier_d_parameters[new_offset] =
877 old.d_intensity_multiplier_d_parameters[old_offset];
878 }
879 }
880 OwnedCwContributions::new(new_count, parameter_count, arrays)
881 .map_err(RietveldError::Contributions)
882}
883
884#[derive(Clone, Debug, PartialEq)]
886pub struct RietveldCalculationOptions {
887 pub support_fwhm: f64,
889 pub use_uncertainty: bool,
891 pub execution: ExecutionPolicy,
893}
894
895impl RietveldCalculationOptions {
896 pub fn new(
902 support_fwhm: f64,
903 use_uncertainty: bool,
904 execution: ExecutionPolicy,
905 ) -> Result<Self, RietveldError> {
906 let options = Self {
907 support_fwhm,
908 use_uncertainty,
909 execution,
910 };
911 options.validate()?;
912 Ok(options)
913 }
914
915 pub(crate) fn validate(&self) -> Result<(), RietveldError> {
916 if !self.support_fwhm.is_finite() || self.support_fwhm <= 0.0 {
917 return Err(RietveldError::InvalidOptions);
918 }
919 Ok(())
920 }
921}
922
923#[derive(Clone, Debug, PartialEq)]
925pub struct RietveldPhaseCalculation {
926 pub phase_id: RecordId,
928 pub name: String,
930 pub result: StructuralPatternResult,
932}
933
934#[derive(Clone, Debug, PartialEq)]
936pub struct RietveldCalculation {
937 pub profile_y: Vec<f64>,
939 pub background_y: Vec<f64>,
941 pub y: Vec<f64>,
943 pub phases: Vec<RietveldPhaseCalculation>,
945 pub metrics: ResidualEvaluation,
947}
948
949pub fn calculate_rietveld_pattern(
956 input: &RietveldInput,
957 options: &RietveldCalculationOptions,
958) -> Result<RietveldCalculation, RietveldError> {
959 input.validate()?;
960 options.validate()?;
961 let models = input
962 .phases
963 .iter()
964 .map(|phase| prepare_phase_model(phase, input.fixed_spectrum.as_ref(), &options.execution))
965 .collect::<Result<Vec<_>, _>>()?;
966 let prepared = PreparedStructuralMultiphase::new(models, options.execution.clone())
967 .map_err(RietveldError::StructuralMultiphase)?;
968 let contributions = input
969 .phases
970 .iter()
971 .map(|phase| resolve_phase_contributions(phase, input))
972 .collect::<Result<Vec<_>, _>>()?;
973 let request = StructuralCalculationRequest {
974 x_deg: input.pattern.x_deg.clone(),
975 instrument: input.instrument,
976 axial_geometry: input.axial_geometry,
977 position_correction: input.position_correction,
978 phase_inputs: contributions
979 .into_iter()
980 .map(|contributions| StructuralModelInput { contributions })
981 .collect(),
982 support: SupportPolicy::FwhmMultiple(options.support_fwhm),
983 };
984 let calculated = prepared
985 .calculate_request(request)
986 .map_err(RietveldError::StructuralMultiphase)?;
987 assemble_rietveld_calculation(input, options, calculated.phases)
988}
989
990pub(crate) fn assemble_rietveld_calculation(
991 input: &RietveldInput,
992 options: &RietveldCalculationOptions,
993 phase_results: Vec<StructuralPatternResult>,
994) -> Result<RietveldCalculation, RietveldError> {
995 if phase_results.len() != input.phases.len() {
996 return Err(RietveldError::CalculationShapeMismatch);
997 }
998 let sample_count = input.pattern.sample_count();
999 let mut profile_y = vec![0.0; sample_count];
1000 for result in &phase_results {
1001 if result.accumulation.sample_count != sample_count
1002 || result.accumulation.y.len() != sample_count
1003 {
1004 return Err(RietveldError::CalculationShapeMismatch);
1005 }
1006 for (combined, value) in profile_y.iter_mut().zip(&result.accumulation.y) {
1007 *combined += value;
1008 }
1009 }
1010 let mut background_y = input.pattern.background_y.clone();
1011 if let Some(background) = &input.background {
1012 for (target, value) in background_y.iter_mut().zip(
1013 background
1014 .calculate(&input.pattern.x_deg)
1015 .map_err(RietveldError::Background)?,
1016 ) {
1017 *target += value;
1018 }
1019 }
1020 let y = profile_y
1021 .iter()
1022 .zip(&background_y)
1023 .map(|(profile, background)| profile + background)
1024 .collect::<Vec<_>>();
1025 if y.iter().any(|value| !value.is_finite()) {
1026 return Err(RietveldError::NonFiniteCalculation);
1027 }
1028 let metrics = evaluate_residuals(
1029 &input.pattern,
1030 &y,
1031 ResidualOptions {
1032 use_uncertainty: options.use_uncertainty,
1033 parameter_count: 0,
1034 },
1035 )
1036 .map_err(RietveldError::Residual)?;
1037 let phases = input
1038 .phases
1039 .iter()
1040 .zip(phase_results)
1041 .map(|(phase, result)| RietveldPhaseCalculation {
1042 phase_id: phase.phase_id.clone(),
1043 name: phase.name.clone(),
1044 result,
1045 })
1046 .collect();
1047 Ok(RietveldCalculation {
1048 profile_y,
1049 background_y,
1050 y,
1051 phases,
1052 metrics,
1053 })
1054}
1055
1056#[derive(Debug)]
1058pub enum RietveldError {
1059 Pattern(DomainError),
1061 MissingObservations,
1063 InvalidInstrument,
1065 InvalidAxialGeometry,
1067 InvalidPositionCorrection,
1069 EmptyPhases,
1071 InvalidPhaseName,
1073 DuplicatePhaseId,
1075 ContributionCountMismatch,
1077 ReflectionIdentityMismatch,
1079 ReflectionTopologyMismatch,
1081 ReflectionWavelengthMismatch,
1083 CorrectionWavelengthMismatch,
1085 SpectrumReferenceWavelengthMismatch,
1087 SpectrumReflectionDomain,
1089 FixedReflectionTopology,
1091 SiteIdCountMismatch,
1093 DuplicateSiteId,
1095 StructuralPattern(StructuralPatternError),
1097 StructuralSpectrum(StructuralSpectrumError),
1099 StructuralMultiphase(StructuralMultiphaseError),
1101 Lattice(LatticeError),
1103 Contributions(CwContributionsError),
1105 SamplePhysics(SamplePhysicsError),
1107 Residual(ResidualError),
1109 Background(BackgroundError),
1111 InvalidOptions,
1113 NonFiniteCalculation,
1115 CalculationShapeMismatch,
1117}
1118
1119impl Display for RietveldError {
1120 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
1121 match self {
1122 Self::Pattern(error) => Display::fmt(error, formatter),
1123 Self::MissingObservations => formatter.write_str("observed_y is required for Rietveld"),
1124 Self::InvalidInstrument => formatter.write_str("Rietveld instrument is invalid"),
1125 Self::InvalidAxialGeometry => formatter.write_str("Rietveld axial geometry is invalid"),
1126 Self::InvalidPositionCorrection => {
1127 formatter.write_str("Rietveld position correction is invalid")
1128 }
1129 Self::EmptyPhases => formatter.write_str("at least one Rietveld phase is required"),
1130 Self::InvalidPhaseName => formatter.write_str("Rietveld phase names must be non-empty"),
1131 Self::DuplicatePhaseId => formatter.write_str("Rietveld phase IDs must be unique"),
1132 Self::ContributionCountMismatch => formatter
1133 .write_str("sample-physics contributions must match the phase reflection count"),
1134 Self::ReflectionIdentityMismatch => {
1135 formatter.write_str("Rietveld reflection identities are invalid")
1136 }
1137 Self::ReflectionTopologyMismatch => formatter
1138 .write_str("Rietveld reflection topology does not match the current cell/domain"),
1139 Self::ReflectionWavelengthMismatch => formatter
1140 .write_str("Rietveld reflection domain wavelength differs from the instrument"),
1141 Self::CorrectionWavelengthMismatch => formatter
1142 .write_str("Rietveld intensity-correction wavelength differs from the instrument"),
1143 Self::SpectrumReferenceWavelengthMismatch => formatter.write_str(
1144 "fixed spectrum reference wavelength differs from the Rietveld instrument",
1145 ),
1146 Self::SpectrumReflectionDomain => formatter
1147 .write_str("fixed-spectrum Rietveld inputs cannot use dynamic reflection domains"),
1148 Self::FixedReflectionTopology => {
1149 formatter.write_str("fixed Rietveld phases cannot regenerate topology")
1150 }
1151 Self::SiteIdCountMismatch => {
1152 formatter.write_str("Rietveld site IDs must match the asymmetric-site count")
1153 }
1154 Self::DuplicateSiteId => {
1155 formatter.write_str("Rietveld site IDs must be unique within a phase")
1156 }
1157 Self::StructuralPattern(error) => Display::fmt(error, formatter),
1158 Self::StructuralSpectrum(error) => Display::fmt(error, formatter),
1159 Self::StructuralMultiphase(error) => Display::fmt(error, formatter),
1160 Self::Lattice(error) => Display::fmt(error, formatter),
1161 Self::Contributions(error) => Display::fmt(error, formatter),
1162 Self::SamplePhysics(error) => Display::fmt(error, formatter),
1163 Self::Residual(error) => Display::fmt(error, formatter),
1164 Self::Background(error) => Display::fmt(error, formatter),
1165 Self::InvalidOptions => formatter.write_str("Rietveld calculation options are invalid"),
1166 Self::NonFiniteCalculation => {
1167 formatter.write_str("Rietveld calculated pattern is non-finite")
1168 }
1169 Self::CalculationShapeMismatch => {
1170 formatter.write_str("Rietveld phase calculation shape mismatch")
1171 }
1172 }
1173 }
1174}
1175
1176impl Error for RietveldError {
1177 fn source(&self) -> Option<&(dyn Error + 'static)> {
1178 match self {
1179 Self::Pattern(error) => Some(error),
1180 Self::StructuralPattern(error) => Some(error),
1181 Self::StructuralSpectrum(error) => Some(error),
1182 Self::StructuralMultiphase(error) => Some(error),
1183 Self::Lattice(error) => Some(error),
1184 Self::Contributions(error) => Some(error),
1185 Self::SamplePhysics(error) => Some(error),
1186 Self::Residual(error) => Some(error),
1187 Self::Background(error) => Some(error),
1188 Self::MissingObservations
1189 | Self::InvalidInstrument
1190 | Self::InvalidAxialGeometry
1191 | Self::InvalidPositionCorrection
1192 | Self::EmptyPhases
1193 | Self::InvalidPhaseName
1194 | Self::DuplicatePhaseId
1195 | Self::ContributionCountMismatch
1196 | Self::ReflectionIdentityMismatch
1197 | Self::ReflectionTopologyMismatch
1198 | Self::ReflectionWavelengthMismatch
1199 | Self::CorrectionWavelengthMismatch
1200 | Self::SpectrumReferenceWavelengthMismatch
1201 | Self::SpectrumReflectionDomain
1202 | Self::FixedReflectionTopology
1203 | Self::SiteIdCountMismatch
1204 | Self::DuplicateSiteId
1205 | Self::InvalidOptions
1206 | Self::NonFiniteCalculation
1207 | Self::CalculationShapeMismatch => None,
1208 }
1209 }
1210}