1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use crate::cell::{CELL_PARAMETER_COUNT, CellError, CellGeometry, UnitCell};
7use crate::symmetry::{SpaceGroup, SymmetryError};
8
9const TWO_PI: f64 = 2.0 * std::f64::consts::PI;
10const DEFAULT_METRIC_TOLERANCE: f64 = 1.0e-10;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
14pub enum ReflectionRange {
15 DSpacing {
17 min_angstrom: f64,
19 max_angstrom: f64,
21 },
22 ScatteringVector {
24 min_inverse_angstrom: f64,
26 max_inverse_angstrom: f64,
28 },
29 CwTwoTheta {
31 min_deg: f64,
33 max_deg: f64,
35 wavelength_angstrom: f64,
37 },
38 Tof {
40 min_us: f64,
42 max_us: f64,
44 search_min_d_angstrom: f64,
46 search_max_d_angstrom: f64,
48 zero_us: f64,
50 difc_us_per_angstrom: f64,
52 difa_us_per_angstrom2: f64,
54 difb_us_angstrom: f64,
56 },
57}
58
59impl ReflectionRange {
60 fn reciprocal_bounds(self) -> Result<(f64, f64), ReflectionGenerationError> {
61 self.validate()?;
62 Ok(match self {
63 Self::DSpacing {
64 min_angstrom,
65 max_angstrom,
66 } => (max_angstrom.recip(), min_angstrom.recip()),
67 Self::ScatteringVector {
68 min_inverse_angstrom,
69 max_inverse_angstrom,
70 } => (min_inverse_angstrom / TWO_PI, max_inverse_angstrom / TWO_PI),
71 Self::CwTwoTheta {
72 min_deg,
73 max_deg,
74 wavelength_angstrom,
75 } => {
76 let min_theta = 0.5 * min_deg.to_radians();
77 let max_theta = 0.5 * max_deg.to_radians();
78 (
79 2.0 * min_theta.sin() / wavelength_angstrom,
80 2.0 * max_theta.sin() / wavelength_angstrom,
81 )
82 }
83 Self::Tof {
84 search_min_d_angstrom,
85 search_max_d_angstrom,
86 ..
87 } => (search_max_d_angstrom.recip(), search_min_d_angstrom.recip()),
88 })
89 }
90
91 fn validate(self) -> Result<(), ReflectionGenerationError> {
92 let finite = match self {
93 Self::DSpacing {
94 min_angstrom,
95 max_angstrom,
96 } => {
97 min_angstrom.is_finite()
98 && max_angstrom.is_finite()
99 && min_angstrom > 0.0
100 && max_angstrom >= min_angstrom
101 }
102 Self::ScatteringVector {
103 min_inverse_angstrom,
104 max_inverse_angstrom,
105 } => {
106 min_inverse_angstrom.is_finite()
107 && max_inverse_angstrom.is_finite()
108 && min_inverse_angstrom >= 0.0
109 && max_inverse_angstrom > 0.0
110 && max_inverse_angstrom >= min_inverse_angstrom
111 }
112 Self::CwTwoTheta {
113 min_deg,
114 max_deg,
115 wavelength_angstrom,
116 } => {
117 min_deg.is_finite()
118 && max_deg.is_finite()
119 && wavelength_angstrom.is_finite()
120 && min_deg >= 0.0
121 && max_deg < 180.0
122 && max_deg >= min_deg
123 && wavelength_angstrom > 0.0
124 }
125 Self::Tof {
126 min_us,
127 max_us,
128 search_min_d_angstrom,
129 search_max_d_angstrom,
130 zero_us,
131 difc_us_per_angstrom,
132 difa_us_per_angstrom2,
133 difb_us_angstrom,
134 } => {
135 [
136 min_us,
137 max_us,
138 search_min_d_angstrom,
139 search_max_d_angstrom,
140 zero_us,
141 difc_us_per_angstrom,
142 difa_us_per_angstrom2,
143 difb_us_angstrom,
144 ]
145 .into_iter()
146 .all(f64::is_finite)
147 && max_us >= min_us
148 && search_min_d_angstrom > 0.0
149 && search_max_d_angstrom >= search_min_d_angstrom
150 }
151 };
152 if finite {
153 Ok(())
154 } else {
155 Err(ReflectionGenerationError::InvalidRange)
156 }
157 }
158
159 fn contains(self, reciprocal_length: f64, d_spacing: f64) -> bool {
160 match self {
161 Self::DSpacing {
162 min_angstrom,
163 max_angstrom,
164 } => inclusive_contains(d_spacing, min_angstrom, max_angstrom),
165 Self::ScatteringVector {
166 min_inverse_angstrom,
167 max_inverse_angstrom,
168 } => inclusive_contains(
169 TWO_PI * reciprocal_length,
170 min_inverse_angstrom,
171 max_inverse_angstrom,
172 ),
173 Self::CwTwoTheta {
174 min_deg,
175 max_deg,
176 wavelength_angstrom,
177 } => {
178 let argument = 0.5 * wavelength_angstrom * reciprocal_length;
179 if argument > 1.0 {
180 return false;
181 }
182 let two_theta = 2.0 * argument.asin().to_degrees();
183 inclusive_contains(two_theta, min_deg, max_deg)
184 }
185 Self::Tof {
186 min_us,
187 max_us,
188 zero_us,
189 difc_us_per_angstrom,
190 difa_us_per_angstrom2,
191 difb_us_angstrom,
192 ..
193 } => {
194 let tof = zero_us
195 + difc_us_per_angstrom * d_spacing
196 + difa_us_per_angstrom2 * d_spacing * d_spacing
197 + difb_us_angstrom / d_spacing;
198 inclusive_contains(tof, min_us, max_us)
199 }
200 }
201 }
202}
203
204#[derive(Clone, Debug, PartialEq)]
206pub struct GeneratedReflection {
207 pub reflection_id: String,
209 pub hkl: [i32; 3],
211 pub conventional_hkl: [i32; 3],
213 pub multiplicity: usize,
215 pub d_spacing_angstrom: f64,
217 pub reciprocal_length_inverse_angstrom: f64,
219 pub d_spacing_derivatives: [f64; CELL_PARAMETER_COUNT],
221}
222
223#[derive(Clone, Debug)]
225pub struct PreparedReflectionGenerator {
226 space_group: SpaceGroup,
227 merge_friedel: bool,
228 max_candidates: usize,
229 metric_tolerance: f64,
230}
231
232impl PreparedReflectionGenerator {
233 pub fn new(
242 space_group: SpaceGroup,
243 merge_friedel: bool,
244 max_candidates: usize,
245 ) -> Result<Self, ReflectionGenerationError> {
246 if max_candidates == 0 {
247 return Err(ReflectionGenerationError::InvalidCandidateLimit);
248 }
249 Ok(Self {
250 space_group,
251 merge_friedel,
252 max_candidates,
253 metric_tolerance: DEFAULT_METRIC_TOLERANCE,
254 })
255 }
256
257 #[must_use]
259 pub const fn space_group(&self) -> &SpaceGroup {
260 &self.space_group
261 }
262
263 #[must_use]
265 pub const fn merge_friedel(&self) -> bool {
266 self.merge_friedel
267 }
268
269 pub fn generate(
282 &self,
283 cell: UnitCell,
284 range: ReflectionRange,
285 ) -> Result<Vec<GeneratedReflection>, ReflectionGenerationError> {
286 let geometry = cell.geometry()?;
287 validate_metric_compatibility(
288 &geometry,
289 self.space_group.metric_constraints().equations.as_slice(),
290 self.metric_tolerance,
291 )?;
292 let (min_reciprocal, max_reciprocal) = range.reciprocal_bounds()?;
293 let bounds = safe_index_bounds(&geometry, max_reciprocal)?;
294 let mut sides = [0_usize; 3];
295 for (index, side) in sides.iter_mut().enumerate() {
296 *side = usize::try_from(2_i64 * i64::from(bounds[index]) + 1)
297 .map_err(|_| ReflectionGenerationError::CandidateLimitExceeded)?;
298 }
299 let candidate_count = sides[0]
300 .checked_mul(sides[1])
301 .and_then(|value| value.checked_mul(sides[2]))
302 .ok_or(ReflectionGenerationError::CandidateLimitExceeded)?;
303 if candidate_count > self.max_candidates {
304 return Err(ReflectionGenerationError::CandidateLimitExceeded);
305 }
306
307 let min_squared = min_reciprocal * min_reciprocal;
308 let max_squared = max_reciprocal * max_reciprocal;
309 let boundary_tolerance = 64.0 * f64::EPSILON * max_squared.max(1.0);
310 let mut reflections = Vec::new();
311 for h in -bounds[0]..=bounds[0] {
312 for k in -bounds[1]..=bounds[1] {
313 for l in -bounds[2]..=bounds[2] {
314 let hkl = [h, k, l];
315 if hkl == [0, 0, 0] {
316 continue;
317 }
318 let reciprocal_squared = geometry.q_squared(hkl);
319 if reciprocal_squared + boundary_tolerance < min_squared
320 || reciprocal_squared - boundary_tolerance > max_squared
321 {
322 continue;
323 }
324 let family = self
325 .space_group
326 .reflection_family(hkl, self.merge_friedel)?;
327 if family.canonical_hkl != hkl {
328 continue;
329 }
330 if self.space_group.is_systematically_absent(hkl)? {
331 continue;
332 }
333 let (d_spacing, derivatives) = geometry.d_spacing_and_derivatives(hkl)?;
334 let reciprocal_length = reciprocal_squared.sqrt();
335 if !range.contains(reciprocal_length, d_spacing) {
336 continue;
337 }
338 reflections.push(GeneratedReflection {
339 reflection_id: family.reflection_id,
340 hkl,
341 conventional_hkl: family.conventional_hkl,
342 multiplicity: family.multiplicity,
343 d_spacing_angstrom: d_spacing,
344 reciprocal_length_inverse_angstrom: reciprocal_length,
345 d_spacing_derivatives: derivatives,
346 });
347 }
348 }
349 }
350 reflections.sort_by(|left, right| {
351 left.reciprocal_length_inverse_angstrom
352 .total_cmp(&right.reciprocal_length_inverse_angstrom)
353 .then_with(|| left.hkl.cmp(&right.hkl))
354 });
355 Ok(reflections)
356 }
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361pub enum ReflectionGenerationError {
362 Cell(CellError),
364 Symmetry(SymmetryError),
366 InvalidRange,
368 InvalidCandidateLimit,
370 CandidateLimitExceeded,
372 CellSymmetryMismatch,
374 DegenerateReciprocalMetric,
376}
377
378impl Display for ReflectionGenerationError {
379 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
380 match self {
381 Self::Cell(error) => Display::fmt(error, formatter),
382 Self::Symmetry(error) => Display::fmt(error, formatter),
383 Self::InvalidRange => formatter.write_str("reflection range is invalid"),
384 Self::InvalidCandidateLimit => {
385 formatter.write_str("reflection candidate limit must be positive")
386 }
387 Self::CandidateLimitExceeded => {
388 formatter.write_str("safe reflection candidate box exceeds the configured limit")
389 }
390 Self::CellSymmetryMismatch => {
391 formatter.write_str("unit-cell metric is incompatible with the symmetry rotations")
392 }
393 Self::DegenerateReciprocalMetric => {
394 formatter.write_str("reciprocal metric must be finite and positive definite")
395 }
396 }
397 }
398}
399
400impl Error for ReflectionGenerationError {}
401
402impl From<CellError> for ReflectionGenerationError {
403 fn from(value: CellError) -> Self {
404 Self::Cell(value)
405 }
406}
407
408impl From<SymmetryError> for ReflectionGenerationError {
409 fn from(value: SymmetryError) -> Self {
410 Self::Symmetry(value)
411 }
412}
413
414#[allow(clippy::cast_precision_loss)]
415fn validate_metric_compatibility(
416 geometry: &CellGeometry,
417 equations: &[[i64; 6]],
418 tolerance: f64,
419) -> Result<(), ReflectionGenerationError> {
420 let metric = geometry.direct_metric;
421 let components = [
422 metric[0][0],
423 metric[1][1],
424 metric[2][2],
425 metric[1][2],
426 metric[0][2],
427 metric[0][1],
428 ];
429 let scale = components
430 .iter()
431 .copied()
432 .map(f64::abs)
433 .fold(1.0_f64, f64::max);
434 for equation in equations {
435 let residual = equation
436 .iter()
437 .zip(components)
438 .map(|(coefficient, value)| *coefficient as f64 * value)
439 .sum::<f64>();
440 let coefficient_scale = equation.iter().copied().map(i64::unsigned_abs).sum::<u64>() as f64;
441 if residual.abs() > tolerance * scale * coefficient_scale.max(1.0) {
442 return Err(ReflectionGenerationError::CellSymmetryMismatch);
443 }
444 }
445 Ok(())
446}
447
448fn inclusive_contains(value: f64, minimum: f64, maximum: f64) -> bool {
449 let tolerance =
450 64.0 * f64::EPSILON * value.abs().max(minimum.abs()).max(maximum.abs()).max(1.0);
451 value + tolerance >= minimum && value - tolerance <= maximum
452}
453
454fn safe_index_bounds(
455 geometry: &CellGeometry,
456 max_reciprocal: f64,
457) -> Result<[i32; 3], ReflectionGenerationError> {
458 let direct_diagonal = [
459 geometry.direct_metric[0][0],
460 geometry.direct_metric[1][1],
461 geometry.direct_metric[2][2],
462 ];
463 let reciprocal_eigenvalue_lower_bound = direct_diagonal.iter().sum::<f64>().recip();
464 if !reciprocal_eigenvalue_lower_bound.is_finite() || reciprocal_eigenvalue_lower_bound <= 0.0 {
465 return Err(ReflectionGenerationError::DegenerateReciprocalMetric);
466 }
467 let common_bound = max_reciprocal / reciprocal_eigenvalue_lower_bound.sqrt();
468 let safety_factor = 1.0 + 64.0 * f64::EPSILON;
469 let mut bounds = [0; 3];
470 for (index, bound) in bounds.iter_mut().enumerate() {
471 let projected = max_reciprocal * direct_diagonal[index].sqrt();
474 let value = (projected.min(common_bound) * safety_factor).ceil() + 1.0;
475 if !value.is_finite() || value > f64::from(i32::MAX - 1) {
476 return Err(ReflectionGenerationError::CandidateLimitExceeded);
477 }
478 #[allow(clippy::cast_possible_truncation)]
479 {
480 *bound = value as i32;
481 }
482 }
483 Ok(bounds)
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use crate::symmetry::{Rational, SymmetryOperation};
490
491 fn cubic_cell() -> UnitCell {
492 UnitCell {
493 a_angstrom: 1.0,
494 b_angstrom: 1.0,
495 c_angstrom: 1.0,
496 alpha_deg: 90.0,
497 beta_deg: 90.0,
498 gamma_deg: 90.0,
499 }
500 }
501
502 fn identity_group() -> SpaceGroup {
503 SpaceGroup::new(vec![SymmetryOperation::identity()]).expect("P1")
504 }
505
506 fn body_centred_group() -> SpaceGroup {
507 let half = Rational::new(1, 2).expect("half");
508 SpaceGroup::new(vec![
509 SymmetryOperation::identity(),
510 SymmetryOperation::new(SymmetryOperation::identity().rotation(), [half, half, half])
511 .expect("centring operation"),
512 ])
513 .expect("I centring group")
514 }
515
516 #[test]
517 fn p1_cubic_generation_has_expected_families_and_multiplicity() {
518 let generator =
519 PreparedReflectionGenerator::new(identity_group(), true, 1_000_000).expect("generator");
520 let reflections = generator
521 .generate(
522 cubic_cell(),
523 ReflectionRange::DSpacing {
524 min_angstrom: 0.7,
525 max_angstrom: 1.0,
526 },
527 )
528 .expect("reflections");
529 assert_eq!(reflections.len(), 9);
530 assert!(
531 reflections
532 .iter()
533 .all(|reflection| reflection.multiplicity == 2)
534 );
535 assert!(reflections.windows(2).all(|pair| {
536 pair[0].reciprocal_length_inverse_angstrom <= pair[1].reciprocal_length_inverse_angstrom
537 }));
538 }
539
540 #[test]
541 fn body_centring_removes_odd_index_sum() {
542 let generator = PreparedReflectionGenerator::new(body_centred_group(), true, 1_000_000)
543 .expect("generator");
544 let reflections = generator
545 .generate(
546 cubic_cell(),
547 ReflectionRange::DSpacing {
548 min_angstrom: 0.7,
549 max_angstrom: 1.0,
550 },
551 )
552 .expect("reflections");
553 assert_eq!(reflections.len(), 6);
554 assert!(
555 reflections
556 .iter()
557 .all(|reflection| reflection.hkl.into_iter().sum::<i32>() % 2 == 0)
558 );
559 }
560
561 #[test]
562 fn physical_range_forms_select_the_same_cubic_shell() {
563 let generator =
564 PreparedReflectionGenerator::new(identity_group(), true, 1_000_000).expect("generator");
565 let d_range = generator
566 .generate(
567 cubic_cell(),
568 ReflectionRange::DSpacing {
569 min_angstrom: 0.7,
570 max_angstrom: 1.0,
571 },
572 )
573 .expect("d range");
574 let q_range = generator
575 .generate(
576 cubic_cell(),
577 ReflectionRange::ScatteringVector {
578 min_inverse_angstrom: TWO_PI,
579 max_inverse_angstrom: TWO_PI * 2.0_f64.sqrt(),
580 },
581 )
582 .expect("Q range");
583 let cw_range = generator
584 .generate(
585 cubic_cell(),
586 ReflectionRange::CwTwoTheta {
587 min_deg: 2.0 * 0.5_f64.asin().to_degrees(),
588 max_deg: 2.0 * (0.5 * 2.0_f64.sqrt()).asin().to_degrees(),
589 wavelength_angstrom: 1.0,
590 },
591 )
592 .expect("CW range");
593 let expected = d_range.iter().map(|item| item.hkl).collect::<Vec<_>>();
594 assert_eq!(
595 q_range.iter().map(|item| item.hkl).collect::<Vec<_>>(),
596 expected
597 );
598 assert_eq!(
599 cw_range.iter().map(|item| item.hkl).collect::<Vec<_>>(),
600 expected
601 );
602 }
603
604 #[test]
605 fn tof_filter_and_candidate_limit_are_explicit() {
606 let generator =
607 PreparedReflectionGenerator::new(identity_group(), true, 1_000_000).expect("generator");
608 let reflections = generator
609 .generate(
610 cubic_cell(),
611 ReflectionRange::Tof {
612 min_us: 900.0,
613 max_us: 1100.0,
614 search_min_d_angstrom: 0.5,
615 search_max_d_angstrom: 1.5,
616 zero_us: 0.0,
617 difc_us_per_angstrom: 1000.0,
618 difa_us_per_angstrom2: 0.0,
619 difb_us_angstrom: 0.0,
620 },
621 )
622 .expect("TOF range");
623 assert!(
624 reflections
625 .iter()
626 .all(|reflection| (reflection.d_spacing_angstrom - 1.0).abs() < 1e-14)
627 );
628
629 let limited = PreparedReflectionGenerator::new(identity_group(), true, 10)
630 .expect("limited generator");
631 assert_eq!(
632 limited.generate(
633 cubic_cell(),
634 ReflectionRange::DSpacing {
635 min_angstrom: 0.1,
636 max_angstrom: 1.0,
637 }
638 ),
639 Err(ReflectionGenerationError::CandidateLimitExceeded)
640 );
641 }
642
643 #[test]
644 fn incompatible_cell_and_point_group_is_rejected() {
645 let quarter_turn =
646 SymmetryOperation::new([[0, -1, 0], [1, 0, 0], [0, 0, 1]], [Rational::zero(); 3])
647 .expect("quarter turn");
648 let half_turn = quarter_turn.compose(quarter_turn).expect("half turn");
649 let three_quarters = quarter_turn.compose(half_turn).expect("three-quarter turn");
650 let tetragonal = SpaceGroup::new(vec![
651 SymmetryOperation::identity(),
652 quarter_turn,
653 half_turn,
654 three_quarters,
655 ])
656 .expect("four-fold group");
657 let generator =
658 PreparedReflectionGenerator::new(tetragonal, true, 1_000_000).expect("generator");
659 let incompatible = UnitCell {
660 b_angstrom: 1.1,
661 ..cubic_cell()
662 };
663 assert_eq!(
664 generator.generate(
665 incompatible,
666 ReflectionRange::DSpacing {
667 min_angstrom: 0.5,
668 max_angstrom: 2.0,
669 }
670 ),
671 Err(ReflectionGenerationError::CellSymmetryMismatch)
672 );
673 }
674}