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 EIGHT_LN_TWO: f64 = 8.0 * std::f64::consts::LN_2;
16const STEPHENS_ORTHORHOMBIC_NAMES: [&str; 6] = ["S400", "S040", "S004", "S220", "S202", "S022"];
17const CELL_PARAMETER_NAMES: [&str; 6] = [
18 "a_angstrom",
19 "b_angstrom",
20 "c_angstrom",
21 "alpha_deg",
22 "beta_deg",
23 "gamma_deg",
24];
25#[derive(Clone, Debug, PartialEq)]
27pub enum RietveldSamplePhysicsModel {
28 IsotropicSize {
30 crystallite_size_nm: f64,
32 shape_factor: f64,
34 },
35 IsotropicMicrostrain {
37 rms_microstrain: f64,
39 },
40 IsotropicLorentzianMicrostrain {
42 microstrain: f64,
44 },
45 StephensOrthorhombic {
47 coefficients_angstrom_minus4: [f64; 6],
49 lorentzian_fraction: f64,
51 },
52 MarchDollase {
54 ratio: f64,
56 preferred_axis_hkl: [f64; 3],
58 },
59 Composite(Vec<Self>),
61}
62
63#[derive(Clone, Debug, PartialEq)]
65pub struct EvaluatedSamplePhysics {
66 pub contributions: OwnedCwContributions,
68 pub parameter_names: Vec<String>,
70}
71
72#[derive(Clone, Debug, PartialEq)]
74pub struct SamplePhysicsParameter {
75 pub name: String,
77 pub value: f64,
79 pub unit: &'static str,
81 pub bounds: ParameterBounds,
83 pub scale: f64,
85}
86
87impl RietveldSamplePhysicsModel {
88 pub fn parameters(&self) -> Result<Vec<SamplePhysicsParameter>, SamplePhysicsError> {
94 let result = match self {
95 Self::IsotropicSize {
96 crystallite_size_nm,
97 shape_factor,
98 } => {
99 if crystallite_size_nm.is_nan()
100 || *crystallite_size_nm <= 0.0
101 || !crystallite_size_nm.is_finite()
102 || !shape_factor.is_finite()
103 || *shape_factor <= 0.0
104 {
105 return Err(SamplePhysicsError::InvalidModel);
106 }
107 vec![SamplePhysicsParameter {
108 name: "isotropic_size.crystallite_size_nm".to_owned(),
109 value: *crystallite_size_nm,
110 unit: "nanometre",
111 bounds: ParameterBounds::new(f64::MIN_POSITIVE, f64::INFINITY)
112 .map_err(|_| SamplePhysicsError::InvalidModel)?,
113 scale: crystallite_size_nm.abs().max(1.0),
114 }]
115 }
116 Self::IsotropicMicrostrain { rms_microstrain } => {
117 if !rms_microstrain.is_finite() || *rms_microstrain < 0.0 {
118 return Err(SamplePhysicsError::InvalidModel);
119 }
120 vec![SamplePhysicsParameter {
121 name: "isotropic_microstrain.rms".to_owned(),
122 value: *rms_microstrain,
123 unit: "fraction",
124 bounds: ParameterBounds::new(0.0, f64::INFINITY)
125 .map_err(|_| SamplePhysicsError::InvalidModel)?,
126 scale: rms_microstrain.abs().max(1.0e-4),
127 }]
128 }
129 Self::IsotropicLorentzianMicrostrain { microstrain } => {
130 if !microstrain.is_finite() || *microstrain < 0.0 {
131 return Err(SamplePhysicsError::InvalidModel);
132 }
133 vec![SamplePhysicsParameter {
134 name: "isotropic_lorentzian_microstrain.fraction".to_owned(),
135 value: *microstrain,
136 unit: "fraction",
137 bounds: ParameterBounds::new(0.0, f64::INFINITY)
138 .map_err(|_| SamplePhysicsError::InvalidModel)?,
139 scale: microstrain.abs().max(1.0e-4),
140 }]
141 }
142 Self::StephensOrthorhombic {
143 coefficients_angstrom_minus4,
144 lorentzian_fraction,
145 } => stephens_parameters(coefficients_angstrom_minus4, *lorentzian_fraction)?,
146 Self::MarchDollase {
147 ratio,
148 preferred_axis_hkl,
149 } => {
150 if !ratio.is_finite()
151 || *ratio <= 0.0
152 || preferred_axis_hkl.iter().any(|value| !value.is_finite())
153 || preferred_axis_hkl.iter().all(|value| *value == 0.0)
154 {
155 return Err(SamplePhysicsError::InvalidModel);
156 }
157 vec![SamplePhysicsParameter {
158 name: "march_dollase.ratio".to_owned(),
159 value: *ratio,
160 unit: "relative",
161 bounds: ParameterBounds::new(f64::MIN_POSITIVE, f64::INFINITY)
162 .map_err(|_| SamplePhysicsError::InvalidModel)?,
163 scale: ratio.abs().max(1.0),
164 }]
165 }
166 Self::Composite(models) => {
167 if models.is_empty() {
168 return Err(SamplePhysicsError::EmptyComposite);
169 }
170 models
171 .iter()
172 .map(Self::parameters)
173 .collect::<Result<Vec<_>, _>>()?
174 .into_iter()
175 .flatten()
176 .collect()
177 }
178 };
179 if result
180 .iter()
181 .map(|parameter| ¶meter.name)
182 .collect::<std::collections::BTreeSet<_>>()
183 .len()
184 != result.len()
185 {
186 return Err(SamplePhysicsError::DuplicateParameterName);
187 }
188 Ok(result)
189 }
190
191 pub fn replace_parameters(
197 &self,
198 values: &std::collections::BTreeMap<String, f64>,
199 ) -> Result<Self, SamplePhysicsError> {
200 let expected = self
201 .parameters()?
202 .into_iter()
203 .map(|parameter| parameter.name)
204 .collect::<std::collections::BTreeSet<_>>();
205 if values.len() != expected.len() || values.keys().any(|name| !expected.contains(name)) {
206 return Err(SamplePhysicsError::ParameterSetMismatch);
207 }
208 let result = match self {
209 Self::IsotropicSize { shape_factor, .. } => Self::IsotropicSize {
210 crystallite_size_nm: values["isotropic_size.crystallite_size_nm"],
211 shape_factor: *shape_factor,
212 },
213 Self::IsotropicMicrostrain { .. } => Self::IsotropicMicrostrain {
214 rms_microstrain: values["isotropic_microstrain.rms"],
215 },
216 Self::IsotropicLorentzianMicrostrain { .. } => Self::IsotropicLorentzianMicrostrain {
217 microstrain: values["isotropic_lorentzian_microstrain.fraction"],
218 },
219 Self::StephensOrthorhombic { .. } => Self::StephensOrthorhombic {
220 coefficients_angstrom_minus4: STEPHENS_ORTHORHOMBIC_NAMES
221 .map(|name| values[&format!("stephens.{name}")]),
222 lorentzian_fraction: values["stephens.lorentzian_fraction"],
223 },
224 Self::MarchDollase {
225 preferred_axis_hkl, ..
226 } => Self::MarchDollase {
227 ratio: values["march_dollase.ratio"],
228 preferred_axis_hkl: *preferred_axis_hkl,
229 },
230 Self::Composite(models) => Self::Composite(
231 models
232 .iter()
233 .map(|model| {
234 let names = model
235 .parameters()?
236 .into_iter()
237 .map(|parameter| parameter.name)
238 .collect::<std::collections::BTreeSet<_>>();
239 let child = values
240 .iter()
241 .filter(|(name, _)| names.contains(*name))
242 .map(|(name, value)| (name.clone(), *value))
243 .collect();
244 model.replace_parameters(&child)
245 })
246 .collect::<Result<Vec<_>, _>>()?,
247 ),
248 };
249 result.parameters()?;
250 Ok(result)
251 }
252
253 pub fn evaluate(
260 &self,
261 hkl: &[[i32; 3]],
262 two_theta_deg: &[f64],
263 cell: UnitCell,
264 wavelength_angstrom: f64,
265 ) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
266 if hkl.len() != two_theta_deg.len()
267 || hkl.is_empty()
268 || two_theta_deg
269 .iter()
270 .any(|value| !value.is_finite() || !(0.0..180.0).contains(value))
271 || !wavelength_angstrom.is_finite()
272 || wavelength_angstrom <= 0.0
273 {
274 return Err(SamplePhysicsError::InvalidInput);
275 }
276 cell.geometry()
277 .map_err(|_| SamplePhysicsError::InvalidInput)?;
278 match self {
279 Self::IsotropicSize {
280 crystallite_size_nm,
281 shape_factor,
282 } => size(
283 *crystallite_size_nm,
284 *shape_factor,
285 two_theta_deg,
286 wavelength_angstrom,
287 ),
288 Self::IsotropicMicrostrain { rms_microstrain } => {
289 microstrain(*rms_microstrain, two_theta_deg)
290 }
291 Self::IsotropicLorentzianMicrostrain { microstrain } => {
292 lorentzian_microstrain(*microstrain, two_theta_deg)
293 }
294 Self::StephensOrthorhombic {
295 coefficients_angstrom_minus4,
296 lorentzian_fraction,
297 } => stephens_orthorhombic(
298 *coefficients_angstrom_minus4,
299 *lorentzian_fraction,
300 hkl,
301 two_theta_deg,
302 cell,
303 ),
304 Self::MarchDollase {
305 ratio,
306 preferred_axis_hkl,
307 } => march(*ratio, *preferred_axis_hkl, hkl, cell),
308 Self::Composite(models) => {
309 if models.is_empty() {
310 return Err(SamplePhysicsError::EmptyComposite);
311 }
312 let evaluated = models
313 .iter()
314 .map(|model| model.evaluate(hkl, two_theta_deg, cell, wavelength_angstrom))
315 .collect::<Result<Vec<_>, _>>()?;
316 compose(&evaluated)
317 }
318 }
319 }
320}
321
322fn stephens_parameters(
323 coefficients: &[f64; 6],
324 mixing: f64,
325) -> Result<Vec<SamplePhysicsParameter>, SamplePhysicsError> {
326 if coefficients.iter().any(|value| !value.is_finite())
327 || !mixing.is_finite()
328 || !(0.0..=1.0).contains(&mixing)
329 {
330 return Err(SamplePhysicsError::InvalidModel);
331 }
332 let coefficient_bounds = ParameterBounds::new(f64::NEG_INFINITY, f64::INFINITY)
333 .map_err(|_| SamplePhysicsError::InvalidModel)?;
334 let mixing_bounds =
335 ParameterBounds::new(0.0, 1.0).map_err(|_| SamplePhysicsError::InvalidModel)?;
336 Ok(STEPHENS_ORTHORHOMBIC_NAMES
337 .iter()
338 .zip(coefficients)
339 .map(|(name, value)| SamplePhysicsParameter {
340 name: format!("stephens.{name}"),
341 value: *value,
342 unit: "angstrom^-4",
343 bounds: coefficient_bounds,
344 scale: value.abs().max(1.0e-12),
345 })
346 .chain(std::iter::once(SamplePhysicsParameter {
347 name: "stephens.lorentzian_fraction".to_owned(),
348 value: mixing,
349 unit: "fraction",
350 bounds: mixing_bounds,
351 scale: 1.0,
352 }))
353 .collect())
354}
355
356fn stephens_orthorhombic_basis([h, k, l]: [i32; 3]) -> [f64; 6] {
357 let [h, k, l] = [h, k, l].map(f64::from);
358 [
359 h.powi(4),
360 k.powi(4),
361 l.powi(4),
362 h.powi(2) * k.powi(2),
363 h.powi(2) * l.powi(2),
364 k.powi(2) * l.powi(2),
365 ]
366}
367
368fn size(
369 size_nm: f64,
370 shape_factor: f64,
371 positions: &[f64],
372 wavelength: f64,
373) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
374 if size_nm.is_nan() || size_nm <= 0.0 || !shape_factor.is_finite() || shape_factor <= 0.0 {
375 return Err(SamplePhysicsError::InvalidModel);
376 }
377 let count = positions.len();
378 let mut lorentzian = vec![0.0; count];
379 let mut d_position = vec![0.0; count];
380 let mut d_parameter = vec![0.0; count];
381 if size_nm.is_finite() {
382 let scale = DEG_PER_RAD * shape_factor * wavelength / (10.0 * size_nm);
383 for (index, position) in positions.iter().enumerate() {
384 let theta = position * HALF_ANGLE_RAD_PER_DEG;
385 lorentzian[index] = scale / theta.cos();
386 d_position[index] = lorentzian[index] * HALF_ANGLE_RAD_PER_DEG * theta.tan();
387 d_parameter[index] = -lorentzian[index] / size_nm;
388 }
389 }
390 width_result(
391 vec![0.0; count],
392 lorentzian,
393 vec![0.0; count],
394 d_position,
395 "isotropic_size.crystallite_size_nm",
396 vec![0.0; count],
397 d_parameter,
398 )
399}
400
401fn microstrain(
402 strain: f64,
403 positions: &[f64],
404) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
405 if !strain.is_finite() || strain < 0.0 {
406 return Err(SamplePhysicsError::InvalidModel);
407 }
408 let coefficient = (2.0 * DEG_PER_RAD).powi(2);
409 let mut variance = Vec::with_capacity(positions.len());
410 let mut d_position = Vec::with_capacity(positions.len());
411 let mut d_parameter = Vec::with_capacity(positions.len());
412 for position in positions {
413 let theta = position * HALF_ANGLE_RAD_PER_DEG;
414 let tangent = theta.tan();
415 variance.push(coefficient * strain * strain * tangent * tangent);
416 d_parameter.push(2.0 * coefficient * strain * tangent * tangent);
417 d_position.push(
418 2.0 * coefficient * strain * strain * tangent / theta.cos().powi(2)
419 * HALF_ANGLE_RAD_PER_DEG,
420 );
421 }
422 let count = positions.len();
423 width_result(
424 variance,
425 vec![0.0; count],
426 d_position,
427 vec![0.0; count],
428 "isotropic_microstrain.rms",
429 d_parameter,
430 vec![0.0; count],
431 )
432}
433
434fn lorentzian_microstrain(
435 strain: f64,
436 positions: &[f64],
437) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
438 if !strain.is_finite() || strain < 0.0 {
439 return Err(SamplePhysicsError::InvalidModel);
440 }
441 let mut lorentzian = Vec::with_capacity(positions.len());
442 let mut d_position = Vec::with_capacity(positions.len());
443 let mut d_parameter = Vec::with_capacity(positions.len());
444 for position in positions {
445 let theta = position * HALF_ANGLE_RAD_PER_DEG;
446 lorentzian.push(DEG_PER_RAD * strain * theta.tan());
447 d_parameter.push(DEG_PER_RAD * theta.tan());
448 d_position.push(0.5 * strain / theta.cos().powi(2));
449 }
450 let count = positions.len();
451 width_result(
452 vec![0.0; count],
453 lorentzian,
454 vec![0.0; count],
455 d_position,
456 "isotropic_lorentzian_microstrain.fraction",
457 vec![0.0; count],
458 d_parameter,
459 )
460}
461
462fn stephens_orthorhombic(
463 coefficients: [f64; 6],
464 mixing: f64,
465 hkl: &[[i32; 3]],
466 positions: &[f64],
467 cell: UnitCell,
468) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
469 if coefficients.iter().any(|value| !value.is_finite())
470 || !mixing.is_finite()
471 || !(0.0..=1.0).contains(&mixing)
472 || [cell.alpha_deg, cell.beta_deg, cell.gamma_deg]
473 .iter()
474 .any(|angle| (angle - 90.0).abs() > 1.0e-10)
475 {
476 return Err(SamplePhysicsError::InvalidModel);
477 }
478 let geometry = cell
479 .geometry()
480 .map_err(|_| SamplePhysicsError::InvalidInput)?;
481 let count = positions.len();
482 let parameter_count = STEPHENS_ORTHORHOMBIC_NAMES.len() + 1 + CELL_PARAMETER_NAMES.len();
483 let derivative_count =
484 parameter_count
485 .checked_mul(count)
486 .ok_or(SamplePhysicsError::Contributions(
487 CwContributionsError::AllocationOverflow,
488 ))?;
489 let mut gaussian = Vec::with_capacity(count);
490 let mut lorentzian = Vec::with_capacity(count);
491 let mut d_gaussian_position = Vec::with_capacity(count);
492 let mut d_lorentzian_position = Vec::with_capacity(count);
493 let mut d_gaussian_parameters = vec![0.0; derivative_count];
494 let mut d_lorentzian_parameters = vec![0.0; derivative_count];
495 let gaussian_weight = (1.0 - mixing).powi(2);
496
497 for (reflection_index, (reflection, position)) in hkl.iter().zip(positions).enumerate() {
498 let basis = stephens_orthorhombic_basis(*reflection);
499 let terms = std::array::from_fn::<_, 6, _>(|index| coefficients[index] * basis[index]);
500 let raw_variance = terms.iter().sum::<f64>();
501 let tolerance = 64.0 * f64::EPSILON * terms.iter().map(|value| value.abs()).sum::<f64>();
502 if raw_variance < -tolerance {
503 return Err(SamplePhysicsError::InvalidModel);
504 }
505 let inverse_metric_variance = raw_variance.max(0.0);
506 if mixing > 0.0 && inverse_metric_variance == 0.0 {
507 return Err(SamplePhysicsError::InvalidModel);
508 }
509 let (d_spacing, d_spacing_cell) = geometry
510 .d_spacing_and_derivatives(*reflection)
511 .map_err(|_| SamplePhysicsError::InvalidInput)?;
512 let theta = position * HALF_ANGLE_RAD_PER_DEG;
513 let angular_scale = DEG_PER_RAD.powi(2) * d_spacing.powi(4) * theta.tan().powi(2);
514 let equivalent_fwhm = (EIGHT_LN_TWO * angular_scale * inverse_metric_variance).sqrt();
515 let gaussian_value = gaussian_weight * angular_scale * inverse_metric_variance;
516 let lorentzian_value = mixing * equivalent_fwhm;
517 gaussian.push(gaussian_value);
518 lorentzian.push(lorentzian_value);
519 let position_log_scale = (PI / 180.0) / (theta.sin() * theta.cos());
520 d_gaussian_position.push(gaussian_value * position_log_scale);
521 d_lorentzian_position.push(0.5 * lorentzian_value * position_log_scale);
522
523 for (coefficient_index, basis_value) in basis.iter().copied().enumerate() {
524 let target = coefficient_index * count + reflection_index;
525 d_gaussian_parameters[target] = gaussian_weight * angular_scale * basis_value;
526 if mixing > 0.0 {
527 d_lorentzian_parameters[target] =
528 lorentzian_value * basis_value / (2.0 * inverse_metric_variance);
529 }
530 }
531 let mixing_row = STEPHENS_ORTHORHOMBIC_NAMES.len();
532 d_gaussian_parameters[mixing_row * count + reflection_index] =
533 -2.0 * (1.0 - mixing) * angular_scale * inverse_metric_variance;
534 d_lorentzian_parameters[mixing_row * count + reflection_index] = equivalent_fwhm;
535 for (cell_index, d_spacing_value) in d_spacing_cell.iter().enumerate() {
536 let target = (mixing_row + 1 + cell_index) * count + reflection_index;
537 d_gaussian_parameters[target] = 4.0 * gaussian_value * d_spacing_value / d_spacing;
538 d_lorentzian_parameters[target] = 2.0 * lorentzian_value * d_spacing_value / d_spacing;
539 }
540 }
541
542 Ok(EvaluatedSamplePhysics {
543 contributions: OwnedCwContributions::new(
544 count,
545 parameter_count,
546 OwnedCwContributionArrays {
547 gaussian_variance_deg2: gaussian,
548 lorentzian_fwhm_deg: lorentzian,
549 intensity_multiplier: vec![1.0; count],
550 d_gaussian_variance_d_position: d_gaussian_position,
551 d_lorentzian_fwhm_d_position: d_lorentzian_position,
552 d_intensity_multiplier_d_position: vec![0.0; count],
553 d_gaussian_variance_d_parameters: d_gaussian_parameters,
554 d_lorentzian_fwhm_d_parameters: d_lorentzian_parameters,
555 d_intensity_multiplier_d_parameters: vec![0.0; derivative_count],
556 },
557 )?,
558 parameter_names: STEPHENS_ORTHORHOMBIC_NAMES
559 .iter()
560 .map(|name| format!("stephens.{name}"))
561 .chain(std::iter::once("stephens.lorentzian_fraction".to_owned()))
562 .chain(
563 CELL_PARAMETER_NAMES
564 .iter()
565 .map(|name| format!("stephens.cell.{name}")),
566 )
567 .collect(),
568 })
569}
570
571#[allow(clippy::too_many_arguments)]
572fn width_result(
573 gaussian: Vec<f64>,
574 lorentzian: Vec<f64>,
575 d_gaussian_position: Vec<f64>,
576 d_lorentzian_position: Vec<f64>,
577 name: &str,
578 d_gaussian_parameter: Vec<f64>,
579 d_lorentzian_parameter: Vec<f64>,
580) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
581 let count = gaussian.len();
582 Ok(EvaluatedSamplePhysics {
583 contributions: OwnedCwContributions::new(
584 count,
585 1,
586 OwnedCwContributionArrays {
587 gaussian_variance_deg2: gaussian,
588 lorentzian_fwhm_deg: lorentzian,
589 intensity_multiplier: vec![1.0; count],
590 d_gaussian_variance_d_position: d_gaussian_position,
591 d_lorentzian_fwhm_d_position: d_lorentzian_position,
592 d_intensity_multiplier_d_position: vec![0.0; count],
593 d_gaussian_variance_d_parameters: d_gaussian_parameter,
594 d_lorentzian_fwhm_d_parameters: d_lorentzian_parameter,
595 d_intensity_multiplier_d_parameters: vec![0.0; count],
596 },
597 )?,
598 parameter_names: vec![name.to_owned()],
599 })
600}
601
602fn march(
603 ratio: f64,
604 axis: [f64; 3],
605 hkl: &[[i32; 3]],
606 cell: UnitCell,
607) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
608 if !ratio.is_finite()
609 || ratio <= 0.0
610 || axis.iter().any(|value| !value.is_finite())
611 || axis.iter().all(|value| *value == 0.0)
612 {
613 return Err(SamplePhysicsError::InvalidModel);
614 }
615 let reciprocal = cell
616 .geometry()
617 .map_err(|_| SamplePhysicsError::InvalidInput)?
618 .reciprocal_metric;
619 let metric = Matrix3::from_row_slice(&reciprocal.concat());
620 let metric_derivatives = reciprocal_metric_derivatives(cell, metric)?;
621 let axis = Vector3::from_row_slice(&axis);
622 let axis_norm = (axis.transpose() * metric * axis)[0];
623 let count = hkl.len();
624 let mut multiplier = Vec::with_capacity(count);
625 let mut d_ratio = Vec::with_capacity(count);
626 let cell_derivative_count =
627 CELL_PARAMETER_NAMES
628 .len()
629 .checked_mul(count)
630 .ok_or(SamplePhysicsError::Contributions(
631 CwContributionsError::AllocationOverflow,
632 ))?;
633 let mut d_cell = vec![0.0; cell_derivative_count];
634 for reflection in hkl {
635 let vector = Vector3::new(
636 f64::from(reflection[0]),
637 f64::from(reflection[1]),
638 f64::from(reflection[2]),
639 );
640 let reflection_norm = (vector.transpose() * metric * vector)[0];
641 let projection = (vector.transpose() * metric * axis)[0];
642 if reflection_norm <= 0.0 || axis_norm <= 0.0 {
643 return Err(SamplePhysicsError::InvalidInput);
644 }
645 let raw_cosine = projection * projection / (reflection_norm * axis_norm);
646 let tolerance = 64.0 * f64::EPSILON;
647 if !raw_cosine.is_finite() || raw_cosine < -tolerance || raw_cosine > 1.0 + tolerance {
648 return Err(SamplePhysicsError::InvalidInput);
649 }
650 let cosine = raw_cosine.clamp(0.0, 1.0);
651 let sine = 1.0 - cosine;
652 let denominator = ratio * ratio * cosine + sine / ratio;
653 multiplier.push(denominator.powf(-1.5));
654 let derivative = 2.0 * ratio * cosine - sine / (ratio * ratio);
655 d_ratio.push(-1.5 * denominator.powf(-2.5) * derivative);
656 let d_multiplier_d_cosine = -1.5 * denominator.powf(-2.5) * (ratio * ratio - ratio.recip());
657 for (parameter, derivative_metric) in metric_derivatives.iter().enumerate() {
658 let d_reflection_norm = (vector.transpose() * derivative_metric * vector)[0];
659 let d_axis_norm = (axis.transpose() * derivative_metric * axis)[0];
660 let d_projection = (vector.transpose() * derivative_metric * axis)[0];
661 let d_cosine = 2.0 * projection * d_projection / (reflection_norm * axis_norm)
662 - cosine * (d_reflection_norm / reflection_norm + d_axis_norm / axis_norm);
663 d_cell[parameter * count + multiplier.len() - 1] = d_multiplier_d_cosine * d_cosine;
664 }
665 }
666 let mut intensity_derivatives = d_ratio;
667 intensity_derivatives.extend(d_cell);
668 let zeros = vec![0.0; count];
669 Ok(EvaluatedSamplePhysics {
670 contributions: OwnedCwContributions::new(
671 count,
672 1 + CELL_PARAMETER_NAMES.len(),
673 OwnedCwContributionArrays {
674 gaussian_variance_deg2: zeros.clone(),
675 lorentzian_fwhm_deg: zeros.clone(),
676 intensity_multiplier: multiplier,
677 d_gaussian_variance_d_position: zeros.clone(),
678 d_lorentzian_fwhm_d_position: zeros.clone(),
679 d_intensity_multiplier_d_position: zeros.clone(),
680 d_gaussian_variance_d_parameters: vec![0.0; intensity_derivatives.len()],
681 d_lorentzian_fwhm_d_parameters: vec![0.0; intensity_derivatives.len()],
682 d_intensity_multiplier_d_parameters: intensity_derivatives,
683 },
684 )?,
685 parameter_names: std::iter::once("march_dollase.ratio".to_owned())
686 .chain(
687 CELL_PARAMETER_NAMES
688 .iter()
689 .map(|name| format!("march_dollase.cell.{name}")),
690 )
691 .collect(),
692 })
693}
694
695fn reciprocal_metric_derivatives(
696 cell: UnitCell,
697 reciprocal: Matrix3<f64>,
698) -> Result<[Matrix3<f64>; 6], SamplePhysicsError> {
699 let [a, b, c, alpha_deg, beta_deg, gamma_deg] = [
700 cell.a_angstrom,
701 cell.b_angstrom,
702 cell.c_angstrom,
703 cell.alpha_deg,
704 cell.beta_deg,
705 cell.gamma_deg,
706 ];
707 let [alpha, beta, gamma] = [alpha_deg, beta_deg, gamma_deg].map(f64::to_radians);
708 let mut direct = std::array::from_fn(|_| Matrix3::zeros());
709 direct[0] = Matrix3::new(
710 2.0 * a,
711 b * gamma.cos(),
712 c * beta.cos(),
713 b * gamma.cos(),
714 0.0,
715 0.0,
716 c * beta.cos(),
717 0.0,
718 0.0,
719 );
720 direct[1] = Matrix3::new(
721 0.0,
722 a * gamma.cos(),
723 0.0,
724 a * gamma.cos(),
725 2.0 * b,
726 c * alpha.cos(),
727 0.0,
728 c * alpha.cos(),
729 0.0,
730 );
731 direct[2] = Matrix3::new(
732 0.0,
733 0.0,
734 a * beta.cos(),
735 0.0,
736 0.0,
737 b * alpha.cos(),
738 a * beta.cos(),
739 b * alpha.cos(),
740 2.0 * c,
741 );
742 let per_degree = PI / 180.0;
743 direct[3][(1, 2)] = -b * c * alpha.sin() * per_degree;
744 direct[3][(2, 1)] = direct[3][(1, 2)];
745 direct[4][(0, 2)] = -a * c * beta.sin() * per_degree;
746 direct[4][(2, 0)] = direct[4][(0, 2)];
747 direct[5][(0, 1)] = -a * b * gamma.sin() * per_degree;
748 direct[5][(1, 0)] = direct[5][(0, 1)];
749 if direct
750 .iter()
751 .flat_map(Matrix3::iter)
752 .any(|value| !value.is_finite())
753 {
754 return Err(SamplePhysicsError::InvalidInput);
755 }
756 Ok(direct.map(|derivative| -reciprocal * derivative * reciprocal))
757}
758
759fn compose(items: &[EvaluatedSamplePhysics]) -> Result<EvaluatedSamplePhysics, SamplePhysicsError> {
760 let count = items[0].contributions.reflection_count();
761 if items
762 .iter()
763 .any(|item| item.contributions.reflection_count() != count)
764 {
765 return Err(SamplePhysicsError::InvalidInput);
766 }
767 let names = items
768 .iter()
769 .flat_map(|item| item.parameter_names.iter().cloned())
770 .collect::<Vec<_>>();
771 if names
772 .iter()
773 .collect::<std::collections::BTreeSet<_>>()
774 .len()
775 != names.len()
776 {
777 return Err(SamplePhysicsError::DuplicateParameterName);
778 }
779 let parameter_count = names.len();
780 let derivative_count =
781 parameter_count
782 .checked_mul(count)
783 .ok_or(SamplePhysicsError::Contributions(
784 CwContributionsError::AllocationOverflow,
785 ))?;
786 let mut arrays = OwnedCwContributionArrays {
787 gaussian_variance_deg2: vec![0.0; count],
788 lorentzian_fwhm_deg: vec![0.0; count],
789 intensity_multiplier: vec![1.0; count],
790 d_gaussian_variance_d_position: vec![0.0; count],
791 d_lorentzian_fwhm_d_position: vec![0.0; count],
792 d_intensity_multiplier_d_position: vec![0.0; count],
793 d_gaussian_variance_d_parameters: vec![0.0; derivative_count],
794 d_lorentzian_fwhm_d_parameters: vec![0.0; derivative_count],
795 d_intensity_multiplier_d_parameters: vec![0.0; derivative_count],
796 };
797 let mut row_offset = 0;
798 for item in items {
799 let source = item.contributions.arrays();
800 let previous_multipliers = arrays.intensity_multiplier.clone();
801 for (reflection, old_multiplier) in previous_multipliers.iter().copied().enumerate() {
802 let child_multiplier = source.intensity_multiplier[reflection];
803 arrays.gaussian_variance_deg2[reflection] += source.gaussian_variance_deg2[reflection];
804 arrays.lorentzian_fwhm_deg[reflection] += source.lorentzian_fwhm_deg[reflection];
805 arrays.d_gaussian_variance_d_position[reflection] +=
806 source.d_gaussian_variance_d_position[reflection];
807 arrays.d_lorentzian_fwhm_d_position[reflection] +=
808 source.d_lorentzian_fwhm_d_position[reflection];
809 arrays.d_intensity_multiplier_d_position[reflection] =
810 arrays.d_intensity_multiplier_d_position[reflection] * child_multiplier
811 + old_multiplier * source.d_intensity_multiplier_d_position[reflection];
812 arrays.intensity_multiplier[reflection] *= child_multiplier;
813 for prior in 0..row_offset {
814 arrays.d_intensity_multiplier_d_parameters[prior * count + reflection] *=
815 child_multiplier;
816 }
817 }
818 for row in 0..item.parameter_names.len() {
819 for (reflection, previous_multiplier) in
820 previous_multipliers.iter().copied().enumerate()
821 {
822 let source_index = row * count + reflection;
823 let target_index = (row_offset + row) * count + reflection;
824 arrays.d_gaussian_variance_d_parameters[target_index] =
825 source.d_gaussian_variance_d_parameters[source_index];
826 arrays.d_lorentzian_fwhm_d_parameters[target_index] =
827 source.d_lorentzian_fwhm_d_parameters[source_index];
828 arrays.d_intensity_multiplier_d_parameters[target_index] =
829 source.d_intensity_multiplier_d_parameters[source_index] * previous_multiplier;
830 }
831 }
832 row_offset += item.parameter_names.len();
833 }
834 Ok(EvaluatedSamplePhysics {
835 contributions: OwnedCwContributions::new(count, parameter_count, arrays)?,
836 parameter_names: names,
837 })
838}
839
840#[derive(Debug)]
842pub enum SamplePhysicsError {
843 InvalidModel,
845 InvalidInput,
847 EmptyComposite,
849 DuplicateParameterName,
851 ParameterSetMismatch,
853 Contributions(CwContributionsError),
855}
856
857impl Display for SamplePhysicsError {
858 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
859 match self {
860 Self::InvalidModel => formatter.write_str("native sample-physics model is invalid"),
861 Self::InvalidInput => formatter.write_str("native sample-physics input is invalid"),
862 Self::EmptyComposite => formatter.write_str("sample-physics composite is empty"),
863 Self::DuplicateParameterName => {
864 formatter.write_str("sample-physics parameter names are duplicated")
865 }
866 Self::ParameterSetMismatch => {
867 formatter.write_str("sample-physics replacement parameters do not match")
868 }
869 Self::Contributions(error) => Display::fmt(error, formatter),
870 }
871 }
872}
873
874impl Error for SamplePhysicsError {
875 fn source(&self) -> Option<&(dyn Error + 'static)> {
876 match self {
877 Self::Contributions(error) => Some(error),
878 Self::InvalidModel
879 | Self::InvalidInput
880 | Self::EmptyComposite
881 | Self::DuplicateParameterName
882 | Self::ParameterSetMismatch => None,
883 }
884 }
885}
886
887impl From<CwContributionsError> for SamplePhysicsError {
888 fn from(value: CwContributionsError) -> Self {
889 Self::Contributions(value)
890 }
891}