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 ) => true,
539 (
540 RietveldSamplePhysicsModel::MarchDollase {
541 preferred_axis_hkl: left,
542 ..
543 },
544 RietveldSamplePhysicsModel::MarchDollase {
545 preferred_axis_hkl: right,
546 ..
547 },
548 ) => left
549 .iter()
550 .zip(right)
551 .all(|(left, right)| left.to_bits() == right.to_bits()),
552 (
553 RietveldSamplePhysicsModel::Composite(left),
554 RietveldSamplePhysicsModel::Composite(right),
555 ) => {
556 left.len() == right.len()
557 && left
558 .iter()
559 .zip(right)
560 .all(|(left, right)| sample_physics_model_identity_matches(left, right))
561 }
562 _ => false,
563 }
564}
565
566#[derive(Clone, Debug, PartialEq, Eq)]
568pub struct RietveldTopologyChange {
569 pub phase_id: RecordId,
571 pub added_reflection_ids: Vec<String>,
573 pub removed_reflection_ids: Vec<String>,
575 pub preserved_reflection_count: usize,
577}
578
579#[derive(Clone, Debug, PartialEq)]
581pub struct RietveldInput {
582 pub pattern: PatternRecord,
584 pub instrument: ConstantWavelengthInstrument,
586 pub fixed_spectrum: Option<FixedWavelengthSpectrum>,
588 pub axial_geometry: Option<FcjGeometry>,
590 pub position_correction: MonochromaticPositionCorrection,
592 pub background: Option<BackgroundModel>,
594 pub phases: Vec<RietveldPhase>,
596}
597
598impl RietveldInput {
599 pub fn new(
606 pattern: PatternRecord,
607 instrument: ConstantWavelengthInstrument,
608 axial_geometry: Option<FcjGeometry>,
609 position_correction: MonochromaticPositionCorrection,
610 phases: Vec<RietveldPhase>,
611 ) -> Result<Self, RietveldError> {
612 let input = Self {
613 pattern,
614 instrument,
615 fixed_spectrum: None,
616 axial_geometry,
617 position_correction,
618 background: None,
619 phases,
620 };
621 input.validate()?;
622 Ok(input)
623 }
624
625 pub fn new_fixed_spectrum(
636 pattern: PatternRecord,
637 instrument: ConstantWavelengthInstrument,
638 spectrum: FixedWavelengthSpectrum,
639 axial_geometry: Option<FcjGeometry>,
640 position_correction: MonochromaticPositionCorrection,
641 phases: Vec<RietveldPhase>,
642 ) -> Result<Self, RietveldError> {
643 let input = Self {
644 pattern,
645 instrument,
646 fixed_spectrum: Some(spectrum),
647 axial_geometry,
648 position_correction,
649 background: None,
650 phases,
651 };
652 input.validate()?;
653 Ok(input)
654 }
655
656 pub fn new_with_background(
663 pattern: PatternRecord,
664 instrument: ConstantWavelengthInstrument,
665 axial_geometry: Option<FcjGeometry>,
666 position_correction: MonochromaticPositionCorrection,
667 background: BackgroundModel,
668 phases: Vec<RietveldPhase>,
669 ) -> Result<Self, RietveldError> {
670 let mut input = Self::new(
671 pattern,
672 instrument,
673 axial_geometry,
674 position_correction,
675 phases,
676 )?;
677 input.background = Some(background);
678 input.validate()?;
679 Ok(input)
680 }
681
682 pub fn new_fixed_spectrum_with_background(
689 pattern: PatternRecord,
690 instrument: ConstantWavelengthInstrument,
691 spectrum: FixedWavelengthSpectrum,
692 axial_geometry: Option<FcjGeometry>,
693 position_correction: MonochromaticPositionCorrection,
694 background: BackgroundModel,
695 phases: Vec<RietveldPhase>,
696 ) -> Result<Self, RietveldError> {
697 let mut input = Self::new_fixed_spectrum(
698 pattern,
699 instrument,
700 spectrum,
701 axial_geometry,
702 position_correction,
703 phases,
704 )?;
705 input.background = Some(background);
706 input.validate()?;
707 Ok(input)
708 }
709
710 pub fn validate(&self) -> Result<(), RietveldError> {
717 self.pattern.validate().map_err(RietveldError::Pattern)?;
718 if self.pattern.observed_y.is_none() {
719 return Err(RietveldError::MissingObservations);
720 }
721 self.instrument
722 .validate()
723 .map_err(|_| RietveldError::InvalidInstrument)?;
724 if self.fixed_spectrum.as_ref().is_some_and(|spectrum| {
725 spectrum.wavelengths_angstrom()[0].to_bits()
726 != self.instrument.wavelength_angstrom.to_bits()
727 }) {
728 return Err(RietveldError::SpectrumReferenceWavelengthMismatch);
729 }
730 if self.axial_geometry.is_some_and(|geometry| {
731 !geometry.sample_over_radius.is_finite()
732 || !geometry.detector_over_radius.is_finite()
733 || geometry.sample_over_radius < 0.0
734 || geometry.detector_over_radius < 0.0
735 }) {
736 return Err(RietveldError::InvalidAxialGeometry);
737 }
738 let correction = self.position_correction;
739 if !correction.zero_shift_deg.is_finite()
740 || (correction.bragg_brentano_mm.is_some()
741 && correction.debye_scherrer_micrometre.is_some())
742 || correction
743 .bragg_brentano_mm
744 .is_some_and(|(displacement, radius)| {
745 !displacement.is_finite() || !radius.is_finite() || radius <= 0.0
746 })
747 || correction
748 .debye_scherrer_micrometre
749 .is_some_and(|(x, y, radius)| {
750 !x.is_finite() || !y.is_finite() || !radius.is_finite() || radius <= 0.0
751 })
752 {
753 return Err(RietveldError::InvalidPositionCorrection);
754 }
755 if self.phases.is_empty() {
756 return Err(RietveldError::EmptyPhases);
757 }
758 if let Some(background) = &self.background {
759 background
760 .basis(&self.pattern.x_deg)
761 .map_err(RietveldError::Background)?;
762 background
763 .calculate(&self.pattern.x_deg)
764 .map_err(RietveldError::Background)?;
765 }
766 let mut identities = std::collections::BTreeSet::new();
767 for phase in &self.phases {
768 phase.validate()?;
769 phase.resolved_sample_physics(self.instrument, self.position_correction)?;
770 if self.fixed_spectrum.is_some() && phase.reflection_domain().is_some() {
771 return Err(RietveldError::SpectrumReflectionDomain);
772 }
773 if phase.correction_wavelength().is_some_and(|wavelength| {
774 wavelength.to_bits() != self.instrument.wavelength_angstrom.to_bits()
775 }) {
776 return Err(RietveldError::CorrectionWavelengthMismatch);
777 }
778 if phase.reflection_domain().is_some_and(|domain| {
779 domain.wavelength_angstrom().to_bits()
780 != self.instrument.wavelength_angstrom.to_bits()
781 }) {
782 return Err(RietveldError::ReflectionWavelengthMismatch);
783 }
784 if !identities.insert(phase.phase_id.clone()) {
785 return Err(RietveldError::DuplicatePhaseId);
786 }
787 }
788 Ok(())
789 }
790}
791
792pub(crate) fn prepare_phase_model(
793 phase: &RietveldPhase,
794 spectrum: Option<&FixedWavelengthSpectrum>,
795 execution: &ExecutionPolicy,
796) -> Result<PreparedStructuralModel, RietveldError> {
797 match spectrum {
798 None => PreparedStructuralPhase::new(phase.definition.clone(), execution.context().clone())
799 .map(PreparedStructuralModel::monochromatic)
800 .map_err(RietveldError::StructuralPattern),
801 Some(spectrum) => PreparedStructuralSpectrum::new(
802 &phase.definition,
803 spectrum.wavelengths_angstrom().to_vec(),
804 spectrum.relative_intensities(),
805 execution.clone(),
806 )
807 .map(PreparedStructuralModel::fixed_spectrum)
808 .map_err(RietveldError::StructuralSpectrum),
809 }
810}
811
812pub(crate) fn resolve_phase_contributions(
813 phase: &RietveldPhase,
814 input: &RietveldInput,
815) -> Result<Vec<OwnedCwContributions>, RietveldError> {
816 let wavelengths = input.fixed_spectrum.as_ref().map_or_else(
817 || vec![input.instrument.wavelength_angstrom],
818 |spectrum| spectrum.wavelengths_angstrom().to_vec(),
819 );
820 wavelengths
821 .into_iter()
822 .map(|wavelength_angstrom| {
823 let mut instrument = input.instrument;
824 instrument.wavelength_angstrom = wavelength_angstrom;
825 phase
826 .resolved_sample_physics(instrument, input.position_correction)
827 .map(|value| value.0)
828 })
829 .collect()
830}
831
832fn reflection_id(hkl: [i32; 3]) -> String {
833 format!("hkl:{},{},{}", hkl[0], hkl[1], hkl[2])
834}
835
836fn transfer_contributions(
837 previous_ids: &[String],
838 current_ids: &[String],
839 previous: &OwnedCwContributions,
840) -> Result<OwnedCwContributions, RietveldError> {
841 let old_count = previous_ids.len();
842 let new_count = current_ids.len();
843 let parameter_count = previous.parameter_count();
844 let derivative_count =
845 parameter_count
846 .checked_mul(new_count)
847 .ok_or(RietveldError::Contributions(
848 CwContributionsError::AllocationOverflow,
849 ))?;
850 let old = previous.arrays();
851 let mut arrays = OwnedCwContributionArrays {
852 gaussian_variance_deg2: vec![0.0; new_count],
853 lorentzian_fwhm_deg: vec![0.0; new_count],
854 intensity_multiplier: vec![1.0; new_count],
855 d_gaussian_variance_d_position: vec![0.0; new_count],
856 d_lorentzian_fwhm_d_position: vec![0.0; new_count],
857 d_intensity_multiplier_d_position: vec![0.0; new_count],
858 d_gaussian_variance_d_parameters: vec![0.0; derivative_count],
859 d_lorentzian_fwhm_d_parameters: vec![0.0; derivative_count],
860 d_intensity_multiplier_d_parameters: vec![0.0; derivative_count],
861 };
862 let previous_index = previous_ids
863 .iter()
864 .enumerate()
865 .map(|(index, id)| (id, index))
866 .collect::<std::collections::BTreeMap<_, _>>();
867 for (new_index, id) in current_ids.iter().enumerate() {
868 let Some(&old_index) = previous_index.get(id) else {
869 continue;
870 };
871 for (target, source) in [
872 (
873 &mut arrays.gaussian_variance_deg2,
874 &old.gaussian_variance_deg2,
875 ),
876 (&mut arrays.lorentzian_fwhm_deg, &old.lorentzian_fwhm_deg),
877 (&mut arrays.intensity_multiplier, &old.intensity_multiplier),
878 (
879 &mut arrays.d_gaussian_variance_d_position,
880 &old.d_gaussian_variance_d_position,
881 ),
882 (
883 &mut arrays.d_lorentzian_fwhm_d_position,
884 &old.d_lorentzian_fwhm_d_position,
885 ),
886 (
887 &mut arrays.d_intensity_multiplier_d_position,
888 &old.d_intensity_multiplier_d_position,
889 ),
890 ] {
891 target[new_index] = source[old_index];
892 }
893 for parameter in 0..parameter_count {
894 let old_offset = parameter * old_count + old_index;
895 let new_offset = parameter * new_count + new_index;
896 arrays.d_gaussian_variance_d_parameters[new_offset] =
897 old.d_gaussian_variance_d_parameters[old_offset];
898 arrays.d_lorentzian_fwhm_d_parameters[new_offset] =
899 old.d_lorentzian_fwhm_d_parameters[old_offset];
900 arrays.d_intensity_multiplier_d_parameters[new_offset] =
901 old.d_intensity_multiplier_d_parameters[old_offset];
902 }
903 }
904 OwnedCwContributions::new(new_count, parameter_count, arrays)
905 .map_err(RietveldError::Contributions)
906}
907
908#[derive(Clone, Debug, PartialEq)]
910pub struct RietveldCalculationOptions {
911 pub support_fwhm: f64,
913 pub use_uncertainty: bool,
915 pub execution: ExecutionPolicy,
917}
918
919impl RietveldCalculationOptions {
920 pub fn new(
926 support_fwhm: f64,
927 use_uncertainty: bool,
928 execution: ExecutionPolicy,
929 ) -> Result<Self, RietveldError> {
930 let options = Self {
931 support_fwhm,
932 use_uncertainty,
933 execution,
934 };
935 options.validate()?;
936 Ok(options)
937 }
938
939 pub(crate) fn validate(&self) -> Result<(), RietveldError> {
940 if !self.support_fwhm.is_finite() || self.support_fwhm <= 0.0 {
941 return Err(RietveldError::InvalidOptions);
942 }
943 Ok(())
944 }
945}
946
947#[derive(Clone, Debug, PartialEq)]
949pub struct RietveldPhaseCalculation {
950 pub phase_id: RecordId,
952 pub name: String,
954 pub result: StructuralPatternResult,
956}
957
958#[derive(Clone, Debug, PartialEq)]
960pub struct RietveldCalculation {
961 pub profile_y: Vec<f64>,
963 pub background_y: Vec<f64>,
965 pub y: Vec<f64>,
967 pub phases: Vec<RietveldPhaseCalculation>,
969 pub metrics: ResidualEvaluation,
971}
972
973pub fn calculate_rietveld_pattern(
980 input: &RietveldInput,
981 options: &RietveldCalculationOptions,
982) -> Result<RietveldCalculation, RietveldError> {
983 input.validate()?;
984 options.validate()?;
985 let models = input
986 .phases
987 .iter()
988 .map(|phase| prepare_phase_model(phase, input.fixed_spectrum.as_ref(), &options.execution))
989 .collect::<Result<Vec<_>, _>>()?;
990 let prepared = PreparedStructuralMultiphase::new(models, options.execution.clone())
991 .map_err(RietveldError::StructuralMultiphase)?;
992 let contributions = input
993 .phases
994 .iter()
995 .map(|phase| resolve_phase_contributions(phase, input))
996 .collect::<Result<Vec<_>, _>>()?;
997 let request = StructuralCalculationRequest {
998 x_deg: input.pattern.x_deg.clone(),
999 instrument: input.instrument,
1000 axial_geometry: input.axial_geometry,
1001 position_correction: input.position_correction,
1002 phase_inputs: contributions
1003 .into_iter()
1004 .map(|contributions| StructuralModelInput { contributions })
1005 .collect(),
1006 support: SupportPolicy::FwhmMultiple(options.support_fwhm),
1007 };
1008 let calculated = prepared
1009 .calculate_request(request)
1010 .map_err(RietveldError::StructuralMultiphase)?;
1011 assemble_rietveld_calculation(input, options, calculated.phases)
1012}
1013
1014pub(crate) fn assemble_rietveld_calculation(
1015 input: &RietveldInput,
1016 options: &RietveldCalculationOptions,
1017 phase_results: Vec<StructuralPatternResult>,
1018) -> Result<RietveldCalculation, RietveldError> {
1019 if phase_results.len() != input.phases.len() {
1020 return Err(RietveldError::CalculationShapeMismatch);
1021 }
1022 let sample_count = input.pattern.sample_count();
1023 let mut profile_y = vec![0.0; sample_count];
1024 for result in &phase_results {
1025 if result.accumulation.sample_count != sample_count
1026 || result.accumulation.y.len() != sample_count
1027 {
1028 return Err(RietveldError::CalculationShapeMismatch);
1029 }
1030 for (combined, value) in profile_y.iter_mut().zip(&result.accumulation.y) {
1031 *combined += value;
1032 }
1033 }
1034 let mut background_y = input.pattern.background_y.clone();
1035 if let Some(background) = &input.background {
1036 for (target, value) in background_y.iter_mut().zip(
1037 background
1038 .calculate(&input.pattern.x_deg)
1039 .map_err(RietveldError::Background)?,
1040 ) {
1041 *target += value;
1042 }
1043 }
1044 let y = profile_y
1045 .iter()
1046 .zip(&background_y)
1047 .map(|(profile, background)| profile + background)
1048 .collect::<Vec<_>>();
1049 if y.iter().any(|value| !value.is_finite()) {
1050 return Err(RietveldError::NonFiniteCalculation);
1051 }
1052 let metrics = evaluate_residuals(
1053 &input.pattern,
1054 &y,
1055 ResidualOptions {
1056 use_uncertainty: options.use_uncertainty,
1057 parameter_count: 0,
1058 },
1059 )
1060 .map_err(RietveldError::Residual)?;
1061 let phases = input
1062 .phases
1063 .iter()
1064 .zip(phase_results)
1065 .map(|(phase, result)| RietveldPhaseCalculation {
1066 phase_id: phase.phase_id.clone(),
1067 name: phase.name.clone(),
1068 result,
1069 })
1070 .collect();
1071 Ok(RietveldCalculation {
1072 profile_y,
1073 background_y,
1074 y,
1075 phases,
1076 metrics,
1077 })
1078}
1079
1080#[derive(Debug)]
1082pub enum RietveldError {
1083 Pattern(DomainError),
1085 MissingObservations,
1087 InvalidInstrument,
1089 InvalidAxialGeometry,
1091 InvalidPositionCorrection,
1093 EmptyPhases,
1095 InvalidPhaseName,
1097 DuplicatePhaseId,
1099 ContributionCountMismatch,
1101 ReflectionIdentityMismatch,
1103 ReflectionTopologyMismatch,
1105 ReflectionWavelengthMismatch,
1107 CorrectionWavelengthMismatch,
1109 SpectrumReferenceWavelengthMismatch,
1111 SpectrumReflectionDomain,
1113 FixedReflectionTopology,
1115 SiteIdCountMismatch,
1117 DuplicateSiteId,
1119 StructuralPattern(StructuralPatternError),
1121 StructuralSpectrum(StructuralSpectrumError),
1123 StructuralMultiphase(StructuralMultiphaseError),
1125 Lattice(LatticeError),
1127 Contributions(CwContributionsError),
1129 SamplePhysics(SamplePhysicsError),
1131 Residual(ResidualError),
1133 Background(BackgroundError),
1135 InvalidOptions,
1137 NonFiniteCalculation,
1139 CalculationShapeMismatch,
1141}
1142
1143impl Display for RietveldError {
1144 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
1145 match self {
1146 Self::Pattern(error) => Display::fmt(error, formatter),
1147 Self::MissingObservations => formatter.write_str("observed_y is required for Rietveld"),
1148 Self::InvalidInstrument => formatter.write_str("Rietveld instrument is invalid"),
1149 Self::InvalidAxialGeometry => formatter.write_str("Rietveld axial geometry is invalid"),
1150 Self::InvalidPositionCorrection => {
1151 formatter.write_str("Rietveld position correction is invalid")
1152 }
1153 Self::EmptyPhases => formatter.write_str("at least one Rietveld phase is required"),
1154 Self::InvalidPhaseName => formatter.write_str("Rietveld phase names must be non-empty"),
1155 Self::DuplicatePhaseId => formatter.write_str("Rietveld phase IDs must be unique"),
1156 Self::ContributionCountMismatch => formatter
1157 .write_str("sample-physics contributions must match the phase reflection count"),
1158 Self::ReflectionIdentityMismatch => {
1159 formatter.write_str("Rietveld reflection identities are invalid")
1160 }
1161 Self::ReflectionTopologyMismatch => formatter
1162 .write_str("Rietveld reflection topology does not match the current cell/domain"),
1163 Self::ReflectionWavelengthMismatch => formatter
1164 .write_str("Rietveld reflection domain wavelength differs from the instrument"),
1165 Self::CorrectionWavelengthMismatch => formatter
1166 .write_str("Rietveld intensity-correction wavelength differs from the instrument"),
1167 Self::SpectrumReferenceWavelengthMismatch => formatter.write_str(
1168 "fixed spectrum reference wavelength differs from the Rietveld instrument",
1169 ),
1170 Self::SpectrumReflectionDomain => formatter
1171 .write_str("fixed-spectrum Rietveld inputs cannot use dynamic reflection domains"),
1172 Self::FixedReflectionTopology => {
1173 formatter.write_str("fixed Rietveld phases cannot regenerate topology")
1174 }
1175 Self::SiteIdCountMismatch => {
1176 formatter.write_str("Rietveld site IDs must match the asymmetric-site count")
1177 }
1178 Self::DuplicateSiteId => {
1179 formatter.write_str("Rietveld site IDs must be unique within a phase")
1180 }
1181 Self::StructuralPattern(error) => Display::fmt(error, formatter),
1182 Self::StructuralSpectrum(error) => Display::fmt(error, formatter),
1183 Self::StructuralMultiphase(error) => Display::fmt(error, formatter),
1184 Self::Lattice(error) => Display::fmt(error, formatter),
1185 Self::Contributions(error) => Display::fmt(error, formatter),
1186 Self::SamplePhysics(error) => Display::fmt(error, formatter),
1187 Self::Residual(error) => Display::fmt(error, formatter),
1188 Self::Background(error) => Display::fmt(error, formatter),
1189 Self::InvalidOptions => formatter.write_str("Rietveld calculation options are invalid"),
1190 Self::NonFiniteCalculation => {
1191 formatter.write_str("Rietveld calculated pattern is non-finite")
1192 }
1193 Self::CalculationShapeMismatch => {
1194 formatter.write_str("Rietveld phase calculation shape mismatch")
1195 }
1196 }
1197 }
1198}
1199
1200impl Error for RietveldError {
1201 fn source(&self) -> Option<&(dyn Error + 'static)> {
1202 match self {
1203 Self::Pattern(error) => Some(error),
1204 Self::StructuralPattern(error) => Some(error),
1205 Self::StructuralSpectrum(error) => Some(error),
1206 Self::StructuralMultiphase(error) => Some(error),
1207 Self::Lattice(error) => Some(error),
1208 Self::Contributions(error) => Some(error),
1209 Self::SamplePhysics(error) => Some(error),
1210 Self::Residual(error) => Some(error),
1211 Self::Background(error) => Some(error),
1212 Self::MissingObservations
1213 | Self::InvalidInstrument
1214 | Self::InvalidAxialGeometry
1215 | Self::InvalidPositionCorrection
1216 | Self::EmptyPhases
1217 | Self::InvalidPhaseName
1218 | Self::DuplicatePhaseId
1219 | Self::ContributionCountMismatch
1220 | Self::ReflectionIdentityMismatch
1221 | Self::ReflectionTopologyMismatch
1222 | Self::ReflectionWavelengthMismatch
1223 | Self::CorrectionWavelengthMismatch
1224 | Self::SpectrumReferenceWavelengthMismatch
1225 | Self::SpectrumReflectionDomain
1226 | Self::FixedReflectionTopology
1227 | Self::SiteIdCountMismatch
1228 | Self::DuplicateSiteId
1229 | Self::InvalidOptions
1230 | Self::NonFiniteCalculation
1231 | Self::CalculationShapeMismatch => None,
1232 }
1233 }
1234}