1use std::error::Error;
4use std::f64::consts::PI;
5use std::fmt::{Display, Formatter};
6
7use nalgebra::{Matrix3, Vector3};
8use phasesmith_core::{CwContributionsError, OwnedCwContributionArrays, OwnedCwContributions};
9use phasesmith_crystallography::UnitCell;
10
11use crate::ParameterBounds;
12
13const DEG_PER_RAD: f64 = 180.0 / PI;
14const HALF_ANGLE_RAD_PER_DEG: f64 = PI / 360.0;
15const CELL_PARAMETER_NAMES: [&str; 6] = [
16 "a_angstrom",
17 "b_angstrom",
18 "c_angstrom",
19 "alpha_deg",
20 "beta_deg",
21 "gamma_deg",
22];
23#[derive(Clone, Debug, PartialEq)]
25pub enum RietveldSamplePhysicsModel {
26 IsotropicSize {
28 crystallite_size_nm: f64,
30 shape_factor: f64,
32 },
33 IsotropicMicrostrain {
35 rms_microstrain: f64,
37 },
38 IsotropicLorentzianMicrostrain {
40 microstrain: f64,
42 },
43 MarchDollase {
45 ratio: f64,
47 preferred_axis_hkl: [f64; 3],
49 },
50 Composite(Vec<Self>),
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct EvaluatedSamplePhysics {
57 pub contributions: OwnedCwContributions,
59 pub parameter_names: Vec<String>,
61}
62
63#[derive(Clone, Debug, PartialEq)]
65pub struct SamplePhysicsParameter {
66 pub name: String,
68 pub value: f64,
70 pub unit: &'static str,
72 pub bounds: ParameterBounds,
74 pub scale: f64,
76}
77
78impl RietveldSamplePhysicsModel {
79 pub fn parameters(&self) -> Result<Vec<SamplePhysicsParameter>, SamplePhysicsError> {
85 let result = match self {
86 Self::IsotropicSize {
87 crystallite_size_nm,
88 shape_factor,
89 } => {
90 if crystallite_size_nm.is_nan()
91 || *crystallite_size_nm <= 0.0
92 || !crystallite_size_nm.is_finite()
93 || !shape_factor.is_finite()
94 || *shape_factor <= 0.0
95 {
96 return Err(SamplePhysicsError::InvalidModel);
97 }
98 vec![SamplePhysicsParameter {
99 name: "isotropic_size.crystallite_size_nm".to_owned(),
100 value: *crystallite_size_nm,
101 unit: "nanometre",
102 bounds: ParameterBounds::new(f64::MIN_POSITIVE, f64::INFINITY)
103 .map_err(|_| SamplePhysicsError::InvalidModel)?,
104 scale: crystallite_size_nm.abs().max(1.0),
105 }]
106 }
107 Self::IsotropicMicrostrain { rms_microstrain } => {
108 if !rms_microstrain.is_finite() || *rms_microstrain < 0.0 {
109 return Err(SamplePhysicsError::InvalidModel);
110 }
111 vec![SamplePhysicsParameter {
112 name: "isotropic_microstrain.rms".to_owned(),
113 value: *rms_microstrain,
114 unit: "fraction",
115 bounds: ParameterBounds::new(0.0, f64::INFINITY)
116 .map_err(|_| SamplePhysicsError::InvalidModel)?,
117 scale: rms_microstrain.abs().max(1.0e-4),
118 }]
119 }
120 Self::IsotropicLorentzianMicrostrain { microstrain } => {
121 if !microstrain.is_finite() || *microstrain < 0.0 {
122 return Err(SamplePhysicsError::InvalidModel);
123 }
124 vec![SamplePhysicsParameter {
125 name: "isotropic_lorentzian_microstrain.fraction".to_owned(),
126 value: *microstrain,
127 unit: "fraction",
128 bounds: ParameterBounds::new(0.0, f64::INFINITY)
129 .map_err(|_| SamplePhysicsError::InvalidModel)?,
130 scale: microstrain.abs().max(1.0e-4),
131 }]
132 }
133 Self::MarchDollase {
134 ratio,
135 preferred_axis_hkl,
136 } => {
137 if !ratio.is_finite()
138 || *ratio <= 0.0
139 || preferred_axis_hkl.iter().any(|value| !value.is_finite())
140 || preferred_axis_hkl.iter().all(|value| *value == 0.0)
141 {
142 return Err(SamplePhysicsError::InvalidModel);
143 }
144 vec![SamplePhysicsParameter {
145 name: "march_dollase.ratio".to_owned(),
146 value: *ratio,
147 unit: "relative",
148 bounds: ParameterBounds::new(f64::MIN_POSITIVE, f64::INFINITY)
149 .map_err(|_| SamplePhysicsError::InvalidModel)?,
150 scale: ratio.abs().max(1.0),
151 }]
152 }
153 Self::Composite(models) => {
154 if models.is_empty() {
155 return Err(SamplePhysicsError::EmptyComposite);
156 }
157 models
158 .iter()
159 .map(Self::parameters)
160 .collect::<Result<Vec<_>, _>>()?
161 .into_iter()
162 .flatten()
163 .collect()
164 }
165 };
166 if result
167 .iter()
168 .map(|parameter| ¶meter.name)
169 .collect::<std::collections::BTreeSet<_>>()
170 .len()
171 != result.len()
172 {
173 return Err(SamplePhysicsError::DuplicateParameterName);
174 }
175 Ok(result)
176 }
177
178 pub fn replace_parameters(
184 &self,
185 values: &std::collections::BTreeMap<String, f64>,
186 ) -> Result<Self, SamplePhysicsError> {
187 let expected = self
188 .parameters()?
189 .into_iter()
190 .map(|parameter| parameter.name)
191 .collect::<std::collections::BTreeSet<_>>();
192 if values.len() != expected.len() || values.keys().any(|name| !expected.contains(name)) {
193 return Err(SamplePhysicsError::ParameterSetMismatch);
194 }
195 let result = match self {
196 Self::IsotropicSize { shape_factor, .. } => Self::IsotropicSize {
197 crystallite_size_nm: values["isotropic_size.crystallite_size_nm"],
198 shape_factor: *shape_factor,
199 },
200 Self::IsotropicMicrostrain { .. } => Self::IsotropicMicrostrain {
201 rms_microstrain: values["isotropic_microstrain.rms"],
202 },
203 Self::IsotropicLorentzianMicrostrain { .. } => Self::IsotropicLorentzianMicrostrain {
204 microstrain: values["isotropic_lorentzian_microstrain.fraction"],
205 },
206 Self::MarchDollase {
207 preferred_axis_hkl, ..
208 } => Self::MarchDollase {
209 ratio: values["march_dollase.ratio"],
210 preferred_axis_hkl: *preferred_axis_hkl,
211 },
212 Self::Composite(models) => Self::Composite(
213 models
214 .iter()
215 .map(|model| {
216 let names = model
217 .parameters()?
218 .into_iter()
219 .map(|parameter| parameter.name)
220 .collect::<std::collections::BTreeSet<_>>();
221 let child = values
222 .iter()
223 .filter(|(name, _)| names.contains(*name))
224 .map(|(name, value)| (name.clone(), *value))
225 .collect();
226 model.replace_parameters(&child)
227 })
228 .collect::<Result<Vec<_>, _>>()?,
229 ),
230 };
231 result.parameters()?;
232 Ok(result)
233 }
234
235 pub fn evaluate(
242 &self,
243 hkl: &[[i32; 3]],
244 two_theta_deg: &[f64],
245 cell: UnitCell,
246 wavelength_angstrom: f64,
247 ) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
248 if hkl.len() != two_theta_deg.len()
249 || hkl.is_empty()
250 || two_theta_deg
251 .iter()
252 .any(|value| !value.is_finite() || !(0.0..180.0).contains(value))
253 || !wavelength_angstrom.is_finite()
254 || wavelength_angstrom <= 0.0
255 {
256 return Err(SamplePhysicsError::InvalidInput);
257 }
258 cell.geometry()
259 .map_err(|_| SamplePhysicsError::InvalidInput)?;
260 match self {
261 Self::IsotropicSize {
262 crystallite_size_nm,
263 shape_factor,
264 } => size(
265 *crystallite_size_nm,
266 *shape_factor,
267 two_theta_deg,
268 wavelength_angstrom,
269 ),
270 Self::IsotropicMicrostrain { rms_microstrain } => {
271 microstrain(*rms_microstrain, two_theta_deg)
272 }
273 Self::IsotropicLorentzianMicrostrain { microstrain } => {
274 lorentzian_microstrain(*microstrain, two_theta_deg)
275 }
276 Self::MarchDollase {
277 ratio,
278 preferred_axis_hkl,
279 } => march(*ratio, *preferred_axis_hkl, hkl, cell),
280 Self::Composite(models) => {
281 if models.is_empty() {
282 return Err(SamplePhysicsError::EmptyComposite);
283 }
284 let evaluated = models
285 .iter()
286 .map(|model| model.evaluate(hkl, two_theta_deg, cell, wavelength_angstrom))
287 .collect::<Result<Vec<_>, _>>()?;
288 compose(&evaluated)
289 }
290 }
291 }
292}
293
294fn size(
295 size_nm: f64,
296 shape_factor: f64,
297 positions: &[f64],
298 wavelength: f64,
299) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
300 if size_nm.is_nan() || size_nm <= 0.0 || !shape_factor.is_finite() || shape_factor <= 0.0 {
301 return Err(SamplePhysicsError::InvalidModel);
302 }
303 let count = positions.len();
304 let mut lorentzian = vec![0.0; count];
305 let mut d_position = vec![0.0; count];
306 let mut d_parameter = vec![0.0; count];
307 if size_nm.is_finite() {
308 let scale = DEG_PER_RAD * shape_factor * wavelength / (10.0 * size_nm);
309 for (index, position) in positions.iter().enumerate() {
310 let theta = position * HALF_ANGLE_RAD_PER_DEG;
311 lorentzian[index] = scale / theta.cos();
312 d_position[index] = lorentzian[index] * HALF_ANGLE_RAD_PER_DEG * theta.tan();
313 d_parameter[index] = -lorentzian[index] / size_nm;
314 }
315 }
316 width_result(
317 vec![0.0; count],
318 lorentzian,
319 vec![0.0; count],
320 d_position,
321 "isotropic_size.crystallite_size_nm",
322 vec![0.0; count],
323 d_parameter,
324 )
325}
326
327fn microstrain(
328 strain: f64,
329 positions: &[f64],
330) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
331 if !strain.is_finite() || strain < 0.0 {
332 return Err(SamplePhysicsError::InvalidModel);
333 }
334 let coefficient = (2.0 * DEG_PER_RAD).powi(2);
335 let mut variance = Vec::with_capacity(positions.len());
336 let mut d_position = Vec::with_capacity(positions.len());
337 let mut d_parameter = Vec::with_capacity(positions.len());
338 for position in positions {
339 let theta = position * HALF_ANGLE_RAD_PER_DEG;
340 let tangent = theta.tan();
341 variance.push(coefficient * strain * strain * tangent * tangent);
342 d_parameter.push(2.0 * coefficient * strain * tangent * tangent);
343 d_position.push(
344 2.0 * coefficient * strain * strain * tangent / theta.cos().powi(2)
345 * HALF_ANGLE_RAD_PER_DEG,
346 );
347 }
348 let count = positions.len();
349 width_result(
350 variance,
351 vec![0.0; count],
352 d_position,
353 vec![0.0; count],
354 "isotropic_microstrain.rms",
355 d_parameter,
356 vec![0.0; count],
357 )
358}
359
360fn lorentzian_microstrain(
361 strain: f64,
362 positions: &[f64],
363) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
364 if !strain.is_finite() || strain < 0.0 {
365 return Err(SamplePhysicsError::InvalidModel);
366 }
367 let mut lorentzian = Vec::with_capacity(positions.len());
368 let mut d_position = Vec::with_capacity(positions.len());
369 let mut d_parameter = Vec::with_capacity(positions.len());
370 for position in positions {
371 let theta = position * HALF_ANGLE_RAD_PER_DEG;
372 lorentzian.push(DEG_PER_RAD * strain * theta.tan());
373 d_parameter.push(DEG_PER_RAD * theta.tan());
374 d_position.push(0.5 * strain / theta.cos().powi(2));
375 }
376 let count = positions.len();
377 width_result(
378 vec![0.0; count],
379 lorentzian,
380 vec![0.0; count],
381 d_position,
382 "isotropic_lorentzian_microstrain.fraction",
383 vec![0.0; count],
384 d_parameter,
385 )
386}
387
388#[allow(clippy::too_many_arguments)]
389fn width_result(
390 gaussian: Vec<f64>,
391 lorentzian: Vec<f64>,
392 d_gaussian_position: Vec<f64>,
393 d_lorentzian_position: Vec<f64>,
394 name: &str,
395 d_gaussian_parameter: Vec<f64>,
396 d_lorentzian_parameter: Vec<f64>,
397) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
398 let count = gaussian.len();
399 Ok(EvaluatedSamplePhysics {
400 contributions: OwnedCwContributions::new(
401 count,
402 1,
403 OwnedCwContributionArrays {
404 gaussian_variance_deg2: gaussian,
405 lorentzian_fwhm_deg: lorentzian,
406 intensity_multiplier: vec![1.0; count],
407 d_gaussian_variance_d_position: d_gaussian_position,
408 d_lorentzian_fwhm_d_position: d_lorentzian_position,
409 d_intensity_multiplier_d_position: vec![0.0; count],
410 d_gaussian_variance_d_parameters: d_gaussian_parameter,
411 d_lorentzian_fwhm_d_parameters: d_lorentzian_parameter,
412 d_intensity_multiplier_d_parameters: vec![0.0; count],
413 },
414 )?,
415 parameter_names: vec![name.to_owned()],
416 })
417}
418
419fn march(
420 ratio: f64,
421 axis: [f64; 3],
422 hkl: &[[i32; 3]],
423 cell: UnitCell,
424) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
425 if !ratio.is_finite()
426 || ratio <= 0.0
427 || axis.iter().any(|value| !value.is_finite())
428 || axis.iter().all(|value| *value == 0.0)
429 {
430 return Err(SamplePhysicsError::InvalidModel);
431 }
432 let reciprocal = cell
433 .geometry()
434 .map_err(|_| SamplePhysicsError::InvalidInput)?
435 .reciprocal_metric;
436 let metric = Matrix3::from_row_slice(&reciprocal.concat());
437 let metric_derivatives = reciprocal_metric_derivatives(cell, metric)?;
438 let axis = Vector3::from_row_slice(&axis);
439 let axis_norm = (axis.transpose() * metric * axis)[0];
440 let count = hkl.len();
441 let mut multiplier = Vec::with_capacity(count);
442 let mut d_ratio = Vec::with_capacity(count);
443 let cell_derivative_count =
444 CELL_PARAMETER_NAMES
445 .len()
446 .checked_mul(count)
447 .ok_or(SamplePhysicsError::Contributions(
448 CwContributionsError::AllocationOverflow,
449 ))?;
450 let mut d_cell = vec![0.0; cell_derivative_count];
451 for reflection in hkl {
452 let vector = Vector3::new(
453 f64::from(reflection[0]),
454 f64::from(reflection[1]),
455 f64::from(reflection[2]),
456 );
457 let reflection_norm = (vector.transpose() * metric * vector)[0];
458 let projection = (vector.transpose() * metric * axis)[0];
459 if reflection_norm <= 0.0 || axis_norm <= 0.0 {
460 return Err(SamplePhysicsError::InvalidInput);
461 }
462 let raw_cosine = projection * projection / (reflection_norm * axis_norm);
463 let tolerance = 64.0 * f64::EPSILON;
464 if !raw_cosine.is_finite() || raw_cosine < -tolerance || raw_cosine > 1.0 + tolerance {
465 return Err(SamplePhysicsError::InvalidInput);
466 }
467 let cosine = raw_cosine.clamp(0.0, 1.0);
468 let sine = 1.0 - cosine;
469 let denominator = ratio * ratio * cosine + sine / ratio;
470 multiplier.push(denominator.powf(-1.5));
471 let derivative = 2.0 * ratio * cosine - sine / (ratio * ratio);
472 d_ratio.push(-1.5 * denominator.powf(-2.5) * derivative);
473 let d_multiplier_d_cosine = -1.5 * denominator.powf(-2.5) * (ratio * ratio - ratio.recip());
474 for (parameter, derivative_metric) in metric_derivatives.iter().enumerate() {
475 let d_reflection_norm = (vector.transpose() * derivative_metric * vector)[0];
476 let d_axis_norm = (axis.transpose() * derivative_metric * axis)[0];
477 let d_projection = (vector.transpose() * derivative_metric * axis)[0];
478 let d_cosine = 2.0 * projection * d_projection / (reflection_norm * axis_norm)
479 - cosine * (d_reflection_norm / reflection_norm + d_axis_norm / axis_norm);
480 d_cell[parameter * count + multiplier.len() - 1] = d_multiplier_d_cosine * d_cosine;
481 }
482 }
483 let mut intensity_derivatives = d_ratio;
484 intensity_derivatives.extend(d_cell);
485 let zeros = vec![0.0; count];
486 Ok(EvaluatedSamplePhysics {
487 contributions: OwnedCwContributions::new(
488 count,
489 1 + CELL_PARAMETER_NAMES.len(),
490 OwnedCwContributionArrays {
491 gaussian_variance_deg2: zeros.clone(),
492 lorentzian_fwhm_deg: zeros.clone(),
493 intensity_multiplier: multiplier,
494 d_gaussian_variance_d_position: zeros.clone(),
495 d_lorentzian_fwhm_d_position: zeros.clone(),
496 d_intensity_multiplier_d_position: zeros.clone(),
497 d_gaussian_variance_d_parameters: vec![0.0; intensity_derivatives.len()],
498 d_lorentzian_fwhm_d_parameters: vec![0.0; intensity_derivatives.len()],
499 d_intensity_multiplier_d_parameters: intensity_derivatives,
500 },
501 )?,
502 parameter_names: std::iter::once("march_dollase.ratio".to_owned())
503 .chain(
504 CELL_PARAMETER_NAMES
505 .iter()
506 .map(|name| format!("march_dollase.cell.{name}")),
507 )
508 .collect(),
509 })
510}
511
512fn reciprocal_metric_derivatives(
513 cell: UnitCell,
514 reciprocal: Matrix3<f64>,
515) -> Result<[Matrix3<f64>; 6], SamplePhysicsError> {
516 let [a, b, c, alpha_deg, beta_deg, gamma_deg] = [
517 cell.a_angstrom,
518 cell.b_angstrom,
519 cell.c_angstrom,
520 cell.alpha_deg,
521 cell.beta_deg,
522 cell.gamma_deg,
523 ];
524 let [alpha, beta, gamma] = [alpha_deg, beta_deg, gamma_deg].map(f64::to_radians);
525 let mut direct = std::array::from_fn(|_| Matrix3::zeros());
526 direct[0] = Matrix3::new(
527 2.0 * a,
528 b * gamma.cos(),
529 c * beta.cos(),
530 b * gamma.cos(),
531 0.0,
532 0.0,
533 c * beta.cos(),
534 0.0,
535 0.0,
536 );
537 direct[1] = Matrix3::new(
538 0.0,
539 a * gamma.cos(),
540 0.0,
541 a * gamma.cos(),
542 2.0 * b,
543 c * alpha.cos(),
544 0.0,
545 c * alpha.cos(),
546 0.0,
547 );
548 direct[2] = Matrix3::new(
549 0.0,
550 0.0,
551 a * beta.cos(),
552 0.0,
553 0.0,
554 b * alpha.cos(),
555 a * beta.cos(),
556 b * alpha.cos(),
557 2.0 * c,
558 );
559 let per_degree = PI / 180.0;
560 direct[3][(1, 2)] = -b * c * alpha.sin() * per_degree;
561 direct[3][(2, 1)] = direct[3][(1, 2)];
562 direct[4][(0, 2)] = -a * c * beta.sin() * per_degree;
563 direct[4][(2, 0)] = direct[4][(0, 2)];
564 direct[5][(0, 1)] = -a * b * gamma.sin() * per_degree;
565 direct[5][(1, 0)] = direct[5][(0, 1)];
566 if direct
567 .iter()
568 .flat_map(Matrix3::iter)
569 .any(|value| !value.is_finite())
570 {
571 return Err(SamplePhysicsError::InvalidInput);
572 }
573 Ok(direct.map(|derivative| -reciprocal * derivative * reciprocal))
574}
575
576fn compose(items: &[EvaluatedSamplePhysics]) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
577 let count = items[0].contributions.reflection_count();
578 if items
579 .iter()
580 .any(|item| item.contributions.reflection_count() != count)
581 {
582 return Err(SamplePhysicsError::InvalidInput);
583 }
584 let names = items
585 .iter()
586 .flat_map(|item| item.parameter_names.iter().cloned())
587 .collect::<Vec<_>>();
588 if names
589 .iter()
590 .collect::<std::collections::BTreeSet<_>>()
591 .len()
592 != names.len()
593 {
594 return Err(SamplePhysicsError::DuplicateParameterName);
595 }
596 let parameter_count = names.len();
597 let derivative_count =
598 parameter_count
599 .checked_mul(count)
600 .ok_or(SamplePhysicsError::Contributions(
601 CwContributionsError::AllocationOverflow,
602 ))?;
603 let mut arrays = OwnedCwContributionArrays {
604 gaussian_variance_deg2: vec![0.0; count],
605 lorentzian_fwhm_deg: vec![0.0; count],
606 intensity_multiplier: vec![1.0; count],
607 d_gaussian_variance_d_position: vec![0.0; count],
608 d_lorentzian_fwhm_d_position: vec![0.0; count],
609 d_intensity_multiplier_d_position: vec![0.0; count],
610 d_gaussian_variance_d_parameters: vec![0.0; derivative_count],
611 d_lorentzian_fwhm_d_parameters: vec![0.0; derivative_count],
612 d_intensity_multiplier_d_parameters: vec![0.0; derivative_count],
613 };
614 let mut row_offset = 0;
615 for item in items {
616 let source = item.contributions.arrays();
617 let previous_multipliers = arrays.intensity_multiplier.clone();
618 for (reflection, old_multiplier) in previous_multipliers.iter().copied().enumerate() {
619 let child_multiplier = source.intensity_multiplier[reflection];
620 arrays.gaussian_variance_deg2[reflection] += source.gaussian_variance_deg2[reflection];
621 arrays.lorentzian_fwhm_deg[reflection] += source.lorentzian_fwhm_deg[reflection];
622 arrays.d_gaussian_variance_d_position[reflection] +=
623 source.d_gaussian_variance_d_position[reflection];
624 arrays.d_lorentzian_fwhm_d_position[reflection] +=
625 source.d_lorentzian_fwhm_d_position[reflection];
626 arrays.d_intensity_multiplier_d_position[reflection] =
627 arrays.d_intensity_multiplier_d_position[reflection] * child_multiplier
628 + old_multiplier * source.d_intensity_multiplier_d_position[reflection];
629 arrays.intensity_multiplier[reflection] *= child_multiplier;
630 for prior in 0..row_offset {
631 arrays.d_intensity_multiplier_d_parameters[prior * count + reflection] *=
632 child_multiplier;
633 }
634 }
635 for row in 0..item.parameter_names.len() {
636 for (reflection, previous_multiplier) in
637 previous_multipliers.iter().copied().enumerate()
638 {
639 let source_index = row * count + reflection;
640 let target_index = (row_offset + row) * count + reflection;
641 arrays.d_gaussian_variance_d_parameters[target_index] =
642 source.d_gaussian_variance_d_parameters[source_index];
643 arrays.d_lorentzian_fwhm_d_parameters[target_index] =
644 source.d_lorentzian_fwhm_d_parameters[source_index];
645 arrays.d_intensity_multiplier_d_parameters[target_index] =
646 source.d_intensity_multiplier_d_parameters[source_index] * previous_multiplier;
647 }
648 }
649 row_offset += item.parameter_names.len();
650 }
651 Ok(EvaluatedSamplePhysics {
652 contributions: OwnedCwContributions::new(count, parameter_count, arrays)?,
653 parameter_names: names,
654 })
655}
656
657#[derive(Debug)]
659pub enum SamplePhysicsError {
660 InvalidModel,
662 InvalidInput,
664 EmptyComposite,
666 DuplicateParameterName,
668 ParameterSetMismatch,
670 Contributions(CwContributionsError),
672}
673
674impl Display for SamplePhysicsError {
675 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
676 match self {
677 Self::InvalidModel => formatter.write_str("native sample-physics model is invalid"),
678 Self::InvalidInput => formatter.write_str("native sample-physics input is invalid"),
679 Self::EmptyComposite => formatter.write_str("sample-physics composite is empty"),
680 Self::DuplicateParameterName => {
681 formatter.write_str("sample-physics parameter names are duplicated")
682 }
683 Self::ParameterSetMismatch => {
684 formatter.write_str("sample-physics replacement parameters do not match")
685 }
686 Self::Contributions(error) => Display::fmt(error, formatter),
687 }
688 }
689}
690
691impl Error for SamplePhysicsError {
692 fn source(&self) -> Option<&(dyn Error + 'static)> {
693 match self {
694 Self::Contributions(error) => Some(error),
695 Self::InvalidModel
696 | Self::InvalidInput
697 | Self::EmptyComposite
698 | Self::DuplicateParameterName
699 | Self::ParameterSetMismatch => None,
700 }
701 }
702}
703
704impl From<CwContributionsError> for SamplePhysicsError {
705 fn from(value: CwContributionsError) -> Self {
706 Self::Contributions(value)
707 }
708}