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