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 #[must_use]
235 pub fn without_sample_physics(mut self) -> Self {
236 self.sample_physics = None;
237 self
238 }
239
240 pub(crate) fn replace_sample_physics(&self, model: RietveldSamplePhysicsModel) -> Self {
241 let mut phase = self.clone();
242 phase.sample_physics = Some(model);
243 phase
244 }
245
246 pub(crate) fn resolved_sample_physics(
247 &self,
248 instrument: ConstantWavelengthInstrument,
249 position_correction: MonochromaticPositionCorrection,
250 ) -> Result<(OwnedCwContributions, Vec<String>), RietveldError> {
251 let Some(model) = &self.sample_physics else {
252 return Ok((self.contributions.clone(), Vec::new()));
253 };
254 let geometry = calculate_monochromatic_reflection_geometry(
255 self.definition.cell,
256 &self.definition.hkl,
257 instrument,
258 position_correction,
259 )
260 .map_err(RietveldError::StructuralPattern)?;
261 let evaluated = model
262 .evaluate(
263 &self.definition.hkl,
264 &geometry.two_theta_deg,
265 self.definition.cell,
266 instrument.wavelength_angstrom,
267 )
268 .map_err(RietveldError::SamplePhysics)?;
269 Ok((evaluated.contributions, evaluated.parameter_names))
270 }
271
272 pub fn with_contributions(
279 &self,
280 contributions: OwnedCwContributions,
281 ) -> Result<Self, RietveldError> {
282 let mut phase = self.clone();
283 phase.contributions = contributions;
284 phase.validate()?;
285 Ok(phase)
286 }
287
288 #[must_use]
290 pub const fn reflection_domain(&self) -> Option<&LatticeReflectionDomain> {
291 self.reflection_domain.as_ref()
292 }
293
294 pub fn regenerate_lattice_at_cell(
304 &self,
305 cell: phasesmith_crystallography::UnitCell,
306 ) -> Result<(Self, RietveldTopologyChange), RietveldError> {
307 let domain = self
308 .reflection_domain
309 .as_ref()
310 .ok_or(RietveldError::FixedReflectionTopology)?;
311 let previous = self
312 .reflection_ids
313 .iter()
314 .cloned()
315 .map(|reflection_id| (reflection_id, 1.0))
316 .collect::<std::collections::BTreeMap<_, _>>();
317 let generated = domain
318 .generate(cell, Some(&previous))
319 .map_err(RietveldError::Lattice)?;
320 let contributions = transfer_contributions(
321 &self.reflection_ids,
322 &generated.reflection_ids,
323 &self.contributions,
324 )?;
325 let change = RietveldTopologyChange {
326 phase_id: self.phase_id.clone(),
327 added_reflection_ids: generated.added_reflection_ids.clone(),
328 removed_reflection_ids: generated.removed_reflection_ids.clone(),
329 preserved_reflection_count: generated.preserved_reflection_count,
330 };
331 let mut phase = self.clone();
332 phase.definition.cell = cell;
333 phase.definition.hkl = generated.hkl;
334 phase.definition.multiplicity = generated.multiplicity;
335 phase.reflection_ids = generated.reflection_ids;
336 phase.contributions = contributions;
337 phase.validate()?;
338 Ok((phase, change))
339 }
340
341 pub(crate) fn with_definition(
342 &self,
343 definition: StructuralPhaseDefinition,
344 ) -> Result<Self, RietveldError> {
345 if self.reflection_domain.is_some() && definition.cell != self.definition.cell {
346 let (mut phase, _) = self.regenerate_lattice_at_cell(definition.cell)?;
347 let hkl = std::mem::take(&mut phase.definition.hkl);
348 let multiplicity = std::mem::take(&mut phase.definition.multiplicity);
349 phase.definition = definition;
350 phase.definition.hkl = hkl;
351 phase.definition.multiplicity = multiplicity;
352 phase.validate()?;
353 return Ok(phase);
354 }
355 let mut phase = self.clone();
356 phase.definition = definition;
357 phase.validate()?;
358 Ok(phase)
359 }
360
361 pub(crate) fn restart_compatible(&self, requested: &Self) -> bool {
362 self.restart_compatible_with_wavelength(requested, false)
363 }
364
365 pub(crate) fn restart_compatible_with_wavelength(
366 &self,
367 requested: &Self,
368 allow_wavelength_change: bool,
369 ) -> bool {
370 self.phase_id == requested.phase_id
371 && self.site_ids == requested.site_ids
372 && self.definition.space_group == requested.definition.space_group
373 && self.definition.anisotropic_mask == requested.definition.anisotropic_mask
374 && self.definition.u_aniso_cif_angstrom2 == requested.definition.u_aniso_cif_angstrom2
375 && self.definition.scattering_species == requested.definition.scattering_species
376 && self.definition.scattering_real_offset == requested.definition.scattering_real_offset
377 && self.definition.scattering_imag_offset == requested.definition.scattering_imag_offset
378 && self.definition.coordinate_tolerance.to_bits()
379 == requested.definition.coordinate_tolerance.to_bits()
380 && self.definition.scattering_model == requested.definition.scattering_model
381 && (self.definition.correction_model == requested.definition.correction_model
382 || (allow_wavelength_change
383 && correction_identity_matches(
384 self.definition.correction_model,
385 requested.definition.correction_model,
386 )))
387 && sample_physics_identity_matches(
388 self.sample_physics.as_ref(),
389 requested.sample_physics.as_ref(),
390 )
391 && (self.reflection_domain == requested.reflection_domain
392 || (allow_wavelength_change
393 && reflection_domain_identity_matches(
394 self.reflection_domain.as_ref(),
395 requested.reflection_domain.as_ref(),
396 )))
397 && (self.reflection_domain.is_some()
398 || (self.reflection_ids == requested.reflection_ids
399 && self.definition.hkl == requested.definition.hkl
400 && self.definition.multiplicity == requested.definition.multiplicity))
401 }
402
403 pub(crate) fn with_wavelength(&self, wavelength_angstrom: f64) -> Result<Self, RietveldError> {
404 let mut phase = self.clone();
405 phase.definition.correction_model = match phase.definition.correction_model {
406 IntegratedIntensityCorrectionModel::Neutral => {
407 IntegratedIntensityCorrectionModel::Neutral
408 }
409 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp { .. } => {
410 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
411 wavelength_angstrom,
412 }
413 }
414 IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
415 polarization, ..
416 } => IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
417 wavelength_angstrom,
418 polarization,
419 },
420 IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz { .. } => {
421 IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz {
422 wavelength_angstrom,
423 }
424 }
425 IntegratedIntensityCorrectionModel::TimeOfFlightNeutronLorentz { two_theta_deg } => {
426 IntegratedIntensityCorrectionModel::TimeOfFlightNeutronLorentz { two_theta_deg }
427 }
428 };
429 if let Some(domain) = &self.reflection_domain {
430 phase.reflection_domain = Some(
431 domain
432 .with_wavelength(wavelength_angstrom)
433 .map_err(RietveldError::Lattice)?,
434 );
435 phase = phase.regenerate_lattice_at_cell(phase.definition.cell)?.0;
436 }
437 phase.validate()?;
438 Ok(phase)
439 }
440
441 fn correction_wavelength(&self) -> Option<f64> {
442 match self.definition.correction_model {
443 IntegratedIntensityCorrectionModel::Neutral
444 | IntegratedIntensityCorrectionModel::TimeOfFlightNeutronLorentz { .. } => None,
445 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp {
446 wavelength_angstrom,
447 }
448 | IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
449 wavelength_angstrom,
450 ..
451 }
452 | IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz {
453 wavelength_angstrom,
454 } => Some(wavelength_angstrom),
455 }
456 }
457}
458
459fn correction_identity_matches(
460 left: IntegratedIntensityCorrectionModel,
461 right: IntegratedIntensityCorrectionModel,
462) -> bool {
463 match (left, right) {
464 (
465 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp { .. },
466 IntegratedIntensityCorrectionModel::BraggBrentanoUnpolarizedLp { .. },
467 )
468 | (
469 IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz { .. },
470 IntegratedIntensityCorrectionModel::ConstantWavelengthNeutronLorentz { .. },
471 ) => true,
472 (
473 IntegratedIntensityCorrectionModel::TimeOfFlightNeutronLorentz {
474 two_theta_deg: left,
475 },
476 IntegratedIntensityCorrectionModel::TimeOfFlightNeutronLorentz {
477 two_theta_deg: right,
478 },
479 )
480 | (
481 IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
482 polarization: left, ..
483 },
484 IntegratedIntensityCorrectionModel::BraggBrentanoPolarizedLp {
485 polarization: right,
486 ..
487 },
488 ) => left.to_bits() == right.to_bits(),
489 _ => false,
490 }
491}
492
493fn reflection_domain_identity_matches(
494 left: Option<&LatticeReflectionDomain>,
495 right: Option<&LatticeReflectionDomain>,
496) -> bool {
497 match (left, right) {
498 (None, None) => true,
499 (Some(left), Some(right)) => left
500 .with_wavelength(right.wavelength_angstrom())
501 .is_ok_and(|updated| updated == *right),
502 _ => false,
503 }
504}
505
506fn sample_physics_identity_matches(
507 left: Option<&RietveldSamplePhysicsModel>,
508 right: Option<&RietveldSamplePhysicsModel>,
509) -> bool {
510 match (left, right) {
511 (None, None) => true,
512 (Some(left), Some(right)) => sample_physics_model_identity_matches(left, right),
513 _ => false,
514 }
515}
516
517fn sample_physics_model_identity_matches(
518 left: &RietveldSamplePhysicsModel,
519 right: &RietveldSamplePhysicsModel,
520) -> bool {
521 match (left, right) {
522 (
523 RietveldSamplePhysicsModel::IsotropicSize {
524 shape_factor: left, ..
525 },
526 RietveldSamplePhysicsModel::IsotropicSize {
527 shape_factor: right,
528 ..
529 },
530 ) => left.to_bits() == right.to_bits(),
531 (
532 RietveldSamplePhysicsModel::IsotropicMicrostrain { .. },
533 RietveldSamplePhysicsModel::IsotropicMicrostrain { .. },
534 )
535 | (
536 RietveldSamplePhysicsModel::IsotropicLorentzianMicrostrain { .. },
537 RietveldSamplePhysicsModel::IsotropicLorentzianMicrostrain { .. },
538 )
539 | (
540 RietveldSamplePhysicsModel::StephensOrthorhombic { .. },
541 RietveldSamplePhysicsModel::StephensOrthorhombic { .. },
542 ) => true,
543 (
544 RietveldSamplePhysicsModel::MarchDollase {
545 preferred_axis_hkl: left,
546 ..
547 },
548 RietveldSamplePhysicsModel::MarchDollase {
549 preferred_axis_hkl: right,
550 ..
551 },
552 ) => left
553 .iter()
554 .zip(right)
555 .all(|(left, right)| left.to_bits() == right.to_bits()),
556 (
557 RietveldSamplePhysicsModel::Composite(left),
558 RietveldSamplePhysicsModel::Composite(right),
559 ) => {
560 left.len() == right.len()
561 && left
562 .iter()
563 .zip(right)
564 .all(|(left, right)| sample_physics_model_identity_matches(left, right))
565 }
566 _ => false,
567 }
568}
569
570#[derive(Clone, Debug, PartialEq, Eq)]
572pub struct RietveldTopologyChange {
573 pub phase_id: RecordId,
575 pub added_reflection_ids: Vec<String>,
577 pub removed_reflection_ids: Vec<String>,
579 pub preserved_reflection_count: usize,
581}
582
583#[derive(Clone, Debug, PartialEq)]
585pub struct RietveldInput {
586 pub pattern: PatternRecord,
588 pub instrument: ConstantWavelengthInstrument,
590 pub fixed_spectrum: Option<FixedWavelengthSpectrum>,
592 pub axial_geometry: Option<FcjGeometry>,
594 pub position_correction: MonochromaticPositionCorrection,
596 pub background: Option<BackgroundModel>,
598 pub phases: Vec<RietveldPhase>,
600}
601
602impl RietveldInput {
603 pub fn new(
610 pattern: PatternRecord,
611 instrument: ConstantWavelengthInstrument,
612 axial_geometry: Option<FcjGeometry>,
613 position_correction: MonochromaticPositionCorrection,
614 phases: Vec<RietveldPhase>,
615 ) -> Result<Self, RietveldError> {
616 let input = Self {
617 pattern,
618 instrument,
619 fixed_spectrum: None,
620 axial_geometry,
621 position_correction,
622 background: None,
623 phases,
624 };
625 input.validate()?;
626 Ok(input)
627 }
628
629 pub fn new_fixed_spectrum(
640 pattern: PatternRecord,
641 instrument: ConstantWavelengthInstrument,
642 spectrum: FixedWavelengthSpectrum,
643 axial_geometry: Option<FcjGeometry>,
644 position_correction: MonochromaticPositionCorrection,
645 phases: Vec<RietveldPhase>,
646 ) -> Result<Self, RietveldError> {
647 let input = Self {
648 pattern,
649 instrument,
650 fixed_spectrum: Some(spectrum),
651 axial_geometry,
652 position_correction,
653 background: None,
654 phases,
655 };
656 input.validate()?;
657 Ok(input)
658 }
659
660 pub fn new_with_background(
667 pattern: PatternRecord,
668 instrument: ConstantWavelengthInstrument,
669 axial_geometry: Option<FcjGeometry>,
670 position_correction: MonochromaticPositionCorrection,
671 background: BackgroundModel,
672 phases: Vec<RietveldPhase>,
673 ) -> Result<Self, RietveldError> {
674 let mut input = Self::new(
675 pattern,
676 instrument,
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 new_fixed_spectrum_with_background(
693 pattern: PatternRecord,
694 instrument: ConstantWavelengthInstrument,
695 spectrum: FixedWavelengthSpectrum,
696 axial_geometry: Option<FcjGeometry>,
697 position_correction: MonochromaticPositionCorrection,
698 background: BackgroundModel,
699 phases: Vec<RietveldPhase>,
700 ) -> Result<Self, RietveldError> {
701 let mut input = Self::new_fixed_spectrum(
702 pattern,
703 instrument,
704 spectrum,
705 axial_geometry,
706 position_correction,
707 phases,
708 )?;
709 input.background = Some(background);
710 input.validate()?;
711 Ok(input)
712 }
713
714 pub fn validate(&self) -> Result<(), RietveldError> {
721 self.pattern.validate().map_err(RietveldError::Pattern)?;
722 if self.pattern.observed_y.is_none() {
723 return Err(RietveldError::MissingObservations);
724 }
725 self.instrument
726 .validate()
727 .map_err(|_| RietveldError::InvalidInstrument)?;
728 if self.fixed_spectrum.as_ref().is_some_and(|spectrum| {
729 spectrum.wavelengths_angstrom()[0].to_bits()
730 != self.instrument.wavelength_angstrom.to_bits()
731 }) {
732 return Err(RietveldError::SpectrumReferenceWavelengthMismatch);
733 }
734 if self.axial_geometry.is_some_and(|geometry| {
735 !geometry.sample_over_radius.is_finite()
736 || !geometry.detector_over_radius.is_finite()
737 || geometry.sample_over_radius < 0.0
738 || geometry.detector_over_radius < 0.0
739 }) {
740 return Err(RietveldError::InvalidAxialGeometry);
741 }
742 let correction = self.position_correction;
743 if !correction.zero_shift_deg.is_finite()
744 || (correction.bragg_brentano_mm.is_some()
745 && correction.debye_scherrer_micrometre.is_some())
746 || correction
747 .bragg_brentano_mm
748 .is_some_and(|(displacement, radius)| {
749 !displacement.is_finite() || !radius.is_finite() || radius <= 0.0
750 })
751 || correction
752 .debye_scherrer_micrometre
753 .is_some_and(|(x, y, radius)| {
754 !x.is_finite() || !y.is_finite() || !radius.is_finite() || radius <= 0.0
755 })
756 {
757 return Err(RietveldError::InvalidPositionCorrection);
758 }
759 if self.phases.is_empty() {
760 return Err(RietveldError::EmptyPhases);
761 }
762 if let Some(background) = &self.background {
763 background
764 .basis(&self.pattern.x_deg)
765 .map_err(RietveldError::Background)?;
766 background
767 .calculate(&self.pattern.x_deg)
768 .map_err(RietveldError::Background)?;
769 }
770 let mut identities = std::collections::BTreeSet::new();
771 for phase in &self.phases {
772 phase.validate()?;
773 phase.resolved_sample_physics(self.instrument, self.position_correction)?;
774 if self.fixed_spectrum.is_some() && phase.reflection_domain().is_some() {
775 return Err(RietveldError::SpectrumReflectionDomain);
776 }
777 if phase.correction_wavelength().is_some_and(|wavelength| {
778 wavelength.to_bits() != self.instrument.wavelength_angstrom.to_bits()
779 }) {
780 return Err(RietveldError::CorrectionWavelengthMismatch);
781 }
782 if phase.reflection_domain().is_some_and(|domain| {
783 domain.wavelength_angstrom().to_bits()
784 != self.instrument.wavelength_angstrom.to_bits()
785 }) {
786 return Err(RietveldError::ReflectionWavelengthMismatch);
787 }
788 if !identities.insert(phase.phase_id.clone()) {
789 return Err(RietveldError::DuplicatePhaseId);
790 }
791 }
792 Ok(())
793 }
794}
795
796pub(crate) fn prepare_phase_model(
797 phase: &RietveldPhase,
798 spectrum: Option<&FixedWavelengthSpectrum>,
799 execution: &ExecutionPolicy,
800) -> Result<PreparedStructuralModel, RietveldError> {
801 match spectrum {
802 None => PreparedStructuralPhase::new(phase.definition.clone(), execution.context().clone())
803 .map(PreparedStructuralModel::monochromatic)
804 .map_err(RietveldError::StructuralPattern),
805 Some(spectrum) => PreparedStructuralSpectrum::new(
806 &phase.definition,
807 spectrum.wavelengths_angstrom().to_vec(),
808 spectrum.relative_intensities(),
809 execution.clone(),
810 )
811 .map(PreparedStructuralModel::fixed_spectrum)
812 .map_err(RietveldError::StructuralSpectrum),
813 }
814}
815
816pub(crate) fn resolve_phase_contributions(
817 phase: &RietveldPhase,
818 input: &RietveldInput,
819) -> Result<Vec<OwnedCwContributions>, RietveldError> {
820 let wavelengths = input.fixed_spectrum.as_ref().map_or_else(
821 || vec![input.instrument.wavelength_angstrom],
822 |spectrum| spectrum.wavelengths_angstrom().to_vec(),
823 );
824 wavelengths
825 .into_iter()
826 .map(|wavelength_angstrom| {
827 let mut instrument = input.instrument;
828 instrument.wavelength_angstrom = wavelength_angstrom;
829 phase
830 .resolved_sample_physics(instrument, input.position_correction)
831 .map(|value| value.0)
832 })
833 .collect()
834}
835
836fn reflection_id(hkl: [i32; 3]) -> String {
837 format!("hkl:{},{},{}", hkl[0], hkl[1], hkl[2])
838}
839
840fn transfer_contributions(
841 previous_ids: &[String],
842 current_ids: &[String],
843 previous: &OwnedCwContributions,
844) -> Result<OwnedCwContributions, RietveldError> {
845 let old_count = previous_ids.len();
846 let new_count = current_ids.len();
847 let parameter_count = previous.parameter_count();
848 let derivative_count =
849 parameter_count
850 .checked_mul(new_count)
851 .ok_or(RietveldError::Contributions(
852 CwContributionsError::AllocationOverflow,
853 ))?;
854 let old = previous.arrays();
855 let mut arrays = OwnedCwContributionArrays {
856 gaussian_variance_deg2: vec![0.0; new_count],
857 lorentzian_fwhm_deg: vec![0.0; new_count],
858 intensity_multiplier: vec![1.0; new_count],
859 d_gaussian_variance_d_position: vec![0.0; new_count],
860 d_lorentzian_fwhm_d_position: vec![0.0; new_count],
861 d_intensity_multiplier_d_position: vec![0.0; new_count],
862 d_gaussian_variance_d_parameters: vec![0.0; derivative_count],
863 d_lorentzian_fwhm_d_parameters: vec![0.0; derivative_count],
864 d_intensity_multiplier_d_parameters: vec![0.0; derivative_count],
865 };
866 let previous_index = previous_ids
867 .iter()
868 .enumerate()
869 .map(|(index, id)| (id, index))
870 .collect::<std::collections::BTreeMap<_, _>>();
871 for (new_index, id) in current_ids.iter().enumerate() {
872 let Some(&old_index) = previous_index.get(id) else {
873 continue;
874 };
875 for (target, source) in [
876 (
877 &mut arrays.gaussian_variance_deg2,
878 &old.gaussian_variance_deg2,
879 ),
880 (&mut arrays.lorentzian_fwhm_deg, &old.lorentzian_fwhm_deg),
881 (&mut arrays.intensity_multiplier, &old.intensity_multiplier),
882 (
883 &mut arrays.d_gaussian_variance_d_position,
884 &old.d_gaussian_variance_d_position,
885 ),
886 (
887 &mut arrays.d_lorentzian_fwhm_d_position,
888 &old.d_lorentzian_fwhm_d_position,
889 ),
890 (
891 &mut arrays.d_intensity_multiplier_d_position,
892 &old.d_intensity_multiplier_d_position,
893 ),
894 ] {
895 target[new_index] = source[old_index];
896 }
897 for parameter in 0..parameter_count {
898 let old_offset = parameter * old_count + old_index;
899 let new_offset = parameter * new_count + new_index;
900 arrays.d_gaussian_variance_d_parameters[new_offset] =
901 old.d_gaussian_variance_d_parameters[old_offset];
902 arrays.d_lorentzian_fwhm_d_parameters[new_offset] =
903 old.d_lorentzian_fwhm_d_parameters[old_offset];
904 arrays.d_intensity_multiplier_d_parameters[new_offset] =
905 old.d_intensity_multiplier_d_parameters[old_offset];
906 }
907 }
908 OwnedCwContributions::new(new_count, parameter_count, arrays)
909 .map_err(RietveldError::Contributions)
910}
911
912#[derive(Clone, Debug, PartialEq)]
914pub struct RietveldCalculationOptions {
915 pub support_fwhm: f64,
917 pub use_uncertainty: bool,
919 pub execution: ExecutionPolicy,
921}
922
923impl RietveldCalculationOptions {
924 pub fn new(
930 support_fwhm: f64,
931 use_uncertainty: bool,
932 execution: ExecutionPolicy,
933 ) -> Result<Self, RietveldError> {
934 let options = Self {
935 support_fwhm,
936 use_uncertainty,
937 execution,
938 };
939 options.validate()?;
940 Ok(options)
941 }
942
943 pub(crate) fn validate(&self) -> Result<(), RietveldError> {
944 if !self.support_fwhm.is_finite() || self.support_fwhm <= 0.0 {
945 return Err(RietveldError::InvalidOptions);
946 }
947 Ok(())
948 }
949}
950
951#[derive(Clone, Debug, PartialEq)]
953pub struct RietveldPhaseCalculation {
954 pub phase_id: RecordId,
956 pub name: String,
958 pub result: StructuralPatternResult,
960}
961
962#[derive(Clone, Debug, PartialEq)]
964pub struct RietveldCalculation {
965 pub profile_y: Vec<f64>,
967 pub background_y: Vec<f64>,
969 pub y: Vec<f64>,
971 pub phases: Vec<RietveldPhaseCalculation>,
973 pub metrics: ResidualEvaluation,
975}
976
977pub fn calculate_rietveld_pattern(
984 input: &RietveldInput,
985 options: &RietveldCalculationOptions,
986) -> Result<RietveldCalculation, RietveldError> {
987 input.validate()?;
988 options.validate()?;
989 let models = input
990 .phases
991 .iter()
992 .map(|phase| prepare_phase_model(phase, input.fixed_spectrum.as_ref(), &options.execution))
993 .collect::<Result<Vec<_>, _>>()?;
994 let prepared = PreparedStructuralMultiphase::new(models, options.execution.clone())
995 .map_err(RietveldError::StructuralMultiphase)?;
996 let contributions = input
997 .phases
998 .iter()
999 .map(|phase| resolve_phase_contributions(phase, input))
1000 .collect::<Result<Vec<_>, _>>()?;
1001 let request = StructuralCalculationRequest {
1002 x_deg: input.pattern.x_deg.clone(),
1003 instrument: input.instrument,
1004 axial_geometry: input.axial_geometry,
1005 position_correction: input.position_correction,
1006 phase_inputs: contributions
1007 .into_iter()
1008 .map(|contributions| StructuralModelInput { contributions })
1009 .collect(),
1010 support: SupportPolicy::FwhmMultiple(options.support_fwhm),
1011 };
1012 let calculated = prepared
1013 .calculate_request(request)
1014 .map_err(RietveldError::StructuralMultiphase)?;
1015 assemble_rietveld_calculation(input, options, calculated.phases)
1016}
1017
1018pub(crate) fn assemble_rietveld_calculation(
1019 input: &RietveldInput,
1020 options: &RietveldCalculationOptions,
1021 phase_results: Vec<StructuralPatternResult>,
1022) -> Result<RietveldCalculation, RietveldError> {
1023 if phase_results.len() != input.phases.len() {
1024 return Err(RietveldError::CalculationShapeMismatch);
1025 }
1026 let sample_count = input.pattern.sample_count();
1027 let mut profile_y = vec![0.0; sample_count];
1028 for result in &phase_results {
1029 if result.accumulation.sample_count != sample_count
1030 || result.accumulation.y.len() != sample_count
1031 {
1032 return Err(RietveldError::CalculationShapeMismatch);
1033 }
1034 for (combined, value) in profile_y.iter_mut().zip(&result.accumulation.y) {
1035 *combined += value;
1036 }
1037 }
1038 let mut background_y = input.pattern.background_y.clone();
1039 if let Some(background) = &input.background {
1040 for (target, value) in background_y.iter_mut().zip(
1041 background
1042 .calculate(&input.pattern.x_deg)
1043 .map_err(RietveldError::Background)?,
1044 ) {
1045 *target += value;
1046 }
1047 }
1048 let y = profile_y
1049 .iter()
1050 .zip(&background_y)
1051 .map(|(profile, background)| profile + background)
1052 .collect::<Vec<_>>();
1053 if y.iter().any(|value| !value.is_finite()) {
1054 return Err(RietveldError::NonFiniteCalculation);
1055 }
1056 let metrics = evaluate_residuals(
1057 &input.pattern,
1058 &y,
1059 ResidualOptions {
1060 use_uncertainty: options.use_uncertainty,
1061 parameter_count: 0,
1062 },
1063 )
1064 .map_err(RietveldError::Residual)?;
1065 let phases = input
1066 .phases
1067 .iter()
1068 .zip(phase_results)
1069 .map(|(phase, result)| RietveldPhaseCalculation {
1070 phase_id: phase.phase_id.clone(),
1071 name: phase.name.clone(),
1072 result,
1073 })
1074 .collect();
1075 Ok(RietveldCalculation {
1076 profile_y,
1077 background_y,
1078 y,
1079 phases,
1080 metrics,
1081 })
1082}
1083
1084#[derive(Debug)]
1086pub enum RietveldError {
1087 Pattern(DomainError),
1089 MissingObservations,
1091 InvalidInstrument,
1093 InvalidAxialGeometry,
1095 InvalidPositionCorrection,
1097 EmptyPhases,
1099 InvalidPhaseName,
1101 DuplicatePhaseId,
1103 ContributionCountMismatch,
1105 ReflectionIdentityMismatch,
1107 ReflectionTopologyMismatch,
1109 ReflectionWavelengthMismatch,
1111 CorrectionWavelengthMismatch,
1113 SpectrumReferenceWavelengthMismatch,
1115 SpectrumReflectionDomain,
1117 FixedReflectionTopology,
1119 SiteIdCountMismatch,
1121 DuplicateSiteId,
1123 StructuralPattern(StructuralPatternError),
1125 StructuralSpectrum(StructuralSpectrumError),
1127 StructuralMultiphase(StructuralMultiphaseError),
1129 Lattice(LatticeError),
1131 Contributions(CwContributionsError),
1133 SamplePhysics(SamplePhysicsError),
1135 Residual(ResidualError),
1137 Background(BackgroundError),
1139 InvalidOptions,
1141 NonFiniteCalculation,
1143 CalculationShapeMismatch,
1145}
1146
1147impl Display for RietveldError {
1148 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
1149 match self {
1150 Self::Pattern(error) => Display::fmt(error, formatter),
1151 Self::MissingObservations => formatter.write_str("observed_y is required for Rietveld"),
1152 Self::InvalidInstrument => formatter.write_str("Rietveld instrument is invalid"),
1153 Self::InvalidAxialGeometry => formatter.write_str("Rietveld axial geometry is invalid"),
1154 Self::InvalidPositionCorrection => {
1155 formatter.write_str("Rietveld position correction is invalid")
1156 }
1157 Self::EmptyPhases => formatter.write_str("at least one Rietveld phase is required"),
1158 Self::InvalidPhaseName => formatter.write_str("Rietveld phase names must be non-empty"),
1159 Self::DuplicatePhaseId => formatter.write_str("Rietveld phase IDs must be unique"),
1160 Self::ContributionCountMismatch => formatter
1161 .write_str("sample-physics contributions must match the phase reflection count"),
1162 Self::ReflectionIdentityMismatch => {
1163 formatter.write_str("Rietveld reflection identities are invalid")
1164 }
1165 Self::ReflectionTopologyMismatch => formatter
1166 .write_str("Rietveld reflection topology does not match the current cell/domain"),
1167 Self::ReflectionWavelengthMismatch => formatter
1168 .write_str("Rietveld reflection domain wavelength differs from the instrument"),
1169 Self::CorrectionWavelengthMismatch => formatter
1170 .write_str("Rietveld intensity-correction wavelength differs from the instrument"),
1171 Self::SpectrumReferenceWavelengthMismatch => formatter.write_str(
1172 "fixed spectrum reference wavelength differs from the Rietveld instrument",
1173 ),
1174 Self::SpectrumReflectionDomain => formatter
1175 .write_str("fixed-spectrum Rietveld inputs cannot use dynamic reflection domains"),
1176 Self::FixedReflectionTopology => {
1177 formatter.write_str("fixed Rietveld phases cannot regenerate topology")
1178 }
1179 Self::SiteIdCountMismatch => {
1180 formatter.write_str("Rietveld site IDs must match the asymmetric-site count")
1181 }
1182 Self::DuplicateSiteId => {
1183 formatter.write_str("Rietveld site IDs must be unique within a phase")
1184 }
1185 Self::StructuralPattern(error) => Display::fmt(error, formatter),
1186 Self::StructuralSpectrum(error) => Display::fmt(error, formatter),
1187 Self::StructuralMultiphase(error) => Display::fmt(error, formatter),
1188 Self::Lattice(error) => Display::fmt(error, formatter),
1189 Self::Contributions(error) => Display::fmt(error, formatter),
1190 Self::SamplePhysics(error) => Display::fmt(error, formatter),
1191 Self::Residual(error) => Display::fmt(error, formatter),
1192 Self::Background(error) => Display::fmt(error, formatter),
1193 Self::InvalidOptions => formatter.write_str("Rietveld calculation options are invalid"),
1194 Self::NonFiniteCalculation => {
1195 formatter.write_str("Rietveld calculated pattern is non-finite")
1196 }
1197 Self::CalculationShapeMismatch => {
1198 formatter.write_str("Rietveld phase calculation shape mismatch")
1199 }
1200 }
1201 }
1202}
1203
1204impl Error for RietveldError {
1205 fn source(&self) -> Option<&(dyn Error + 'static)> {
1206 match self {
1207 Self::Pattern(error) => Some(error),
1208 Self::StructuralPattern(error) => Some(error),
1209 Self::StructuralSpectrum(error) => Some(error),
1210 Self::StructuralMultiphase(error) => Some(error),
1211 Self::Lattice(error) => Some(error),
1212 Self::Contributions(error) => Some(error),
1213 Self::SamplePhysics(error) => Some(error),
1214 Self::Residual(error) => Some(error),
1215 Self::Background(error) => Some(error),
1216 Self::MissingObservations
1217 | Self::InvalidInstrument
1218 | Self::InvalidAxialGeometry
1219 | Self::InvalidPositionCorrection
1220 | Self::EmptyPhases
1221 | Self::InvalidPhaseName
1222 | Self::DuplicatePhaseId
1223 | Self::ContributionCountMismatch
1224 | Self::ReflectionIdentityMismatch
1225 | Self::ReflectionTopologyMismatch
1226 | Self::ReflectionWavelengthMismatch
1227 | Self::CorrectionWavelengthMismatch
1228 | Self::SpectrumReferenceWavelengthMismatch
1229 | Self::SpectrumReflectionDomain
1230 | Self::FixedReflectionTopology
1231 | Self::SiteIdCountMismatch
1232 | Self::DuplicateSiteId
1233 | Self::InvalidOptions
1234 | Self::NonFiniteCalculation
1235 | Self::CalculationShapeMismatch => None,
1236 }
1237 }
1238}