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 multiplicity: usize,
213 pub d_spacing_angstrom: f64,
215 pub reciprocal_length_inverse_angstrom: f64,
217 pub d_spacing_derivatives: [f64; CELL_PARAMETER_COUNT],
219}
220
221#[derive(Clone, Debug)]
223pub struct PreparedReflectionGenerator {
224 space_group: SpaceGroup,
225 merge_friedel: bool,
226 max_candidates: usize,
227 metric_tolerance: f64,
228}
229
230impl PreparedReflectionGenerator {
231 pub fn new(
240 space_group: SpaceGroup,
241 merge_friedel: bool,
242 max_candidates: usize,
243 ) -> Result<Self, ReflectionGenerationError> {
244 if max_candidates == 0 {
245 return Err(ReflectionGenerationError::InvalidCandidateLimit);
246 }
247 Ok(Self {
248 space_group,
249 merge_friedel,
250 max_candidates,
251 metric_tolerance: DEFAULT_METRIC_TOLERANCE,
252 })
253 }
254
255 #[must_use]
257 pub const fn space_group(&self) -> &SpaceGroup {
258 &self.space_group
259 }
260
261 #[must_use]
263 pub const fn merge_friedel(&self) -> bool {
264 self.merge_friedel
265 }
266
267 pub fn generate(
280 &self,
281 cell: UnitCell,
282 range: ReflectionRange,
283 ) -> Result<Vec<GeneratedReflection>, ReflectionGenerationError> {
284 let geometry = cell.geometry()?;
285 validate_metric_compatibility(
286 &geometry,
287 self.space_group.metric_constraints().equations.as_slice(),
288 self.metric_tolerance,
289 )?;
290 let (min_reciprocal, max_reciprocal) = range.reciprocal_bounds()?;
291 let bounds = safe_index_bounds(&geometry, max_reciprocal)?;
292 let mut sides = [0_usize; 3];
293 for (index, side) in sides.iter_mut().enumerate() {
294 *side = usize::try_from(2_i64 * i64::from(bounds[index]) + 1)
295 .map_err(|_| ReflectionGenerationError::CandidateLimitExceeded)?;
296 }
297 let candidate_count = sides[0]
298 .checked_mul(sides[1])
299 .and_then(|value| value.checked_mul(sides[2]))
300 .ok_or(ReflectionGenerationError::CandidateLimitExceeded)?;
301 if candidate_count > self.max_candidates {
302 return Err(ReflectionGenerationError::CandidateLimitExceeded);
303 }
304
305 let min_squared = min_reciprocal * min_reciprocal;
306 let max_squared = max_reciprocal * max_reciprocal;
307 let boundary_tolerance = 64.0 * f64::EPSILON * max_squared.max(1.0);
308 let mut reflections = Vec::new();
309 for h in -bounds[0]..=bounds[0] {
310 for k in -bounds[1]..=bounds[1] {
311 for l in -bounds[2]..=bounds[2] {
312 let hkl = [h, k, l];
313 if hkl == [0, 0, 0] {
314 continue;
315 }
316 let reciprocal_squared = geometry.q_squared(hkl);
317 if reciprocal_squared + boundary_tolerance < min_squared
318 || reciprocal_squared - boundary_tolerance > max_squared
319 {
320 continue;
321 }
322 let family = self
323 .space_group
324 .reflection_family(hkl, self.merge_friedel)?;
325 if family.canonical_hkl != hkl {
326 continue;
327 }
328 if self.space_group.is_systematically_absent(hkl)? {
329 continue;
330 }
331 let (d_spacing, derivatives) = geometry.d_spacing_and_derivatives(hkl)?;
332 let reciprocal_length = reciprocal_squared.sqrt();
333 if !range.contains(reciprocal_length, d_spacing) {
334 continue;
335 }
336 reflections.push(GeneratedReflection {
337 reflection_id: family.reflection_id,
338 hkl,
339 multiplicity: family.multiplicity,
340 d_spacing_angstrom: d_spacing,
341 reciprocal_length_inverse_angstrom: reciprocal_length,
342 d_spacing_derivatives: derivatives,
343 });
344 }
345 }
346 }
347 reflections.sort_by(|left, right| {
348 left.reciprocal_length_inverse_angstrom
349 .total_cmp(&right.reciprocal_length_inverse_angstrom)
350 .then_with(|| left.hkl.cmp(&right.hkl))
351 });
352 Ok(reflections)
353 }
354}
355
356#[derive(Clone, Copy, Debug, PartialEq, Eq)]
358pub enum ReflectionGenerationError {
359 Cell(CellError),
361 Symmetry(SymmetryError),
363 InvalidRange,
365 InvalidCandidateLimit,
367 CandidateLimitExceeded,
369 CellSymmetryMismatch,
371 DegenerateReciprocalMetric,
373}
374
375impl Display for ReflectionGenerationError {
376 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
377 match self {
378 Self::Cell(error) => Display::fmt(error, formatter),
379 Self::Symmetry(error) => Display::fmt(error, formatter),
380 Self::InvalidRange => formatter.write_str("reflection range is invalid"),
381 Self::InvalidCandidateLimit => {
382 formatter.write_str("reflection candidate limit must be positive")
383 }
384 Self::CandidateLimitExceeded => {
385 formatter.write_str("safe reflection candidate box exceeds the configured limit")
386 }
387 Self::CellSymmetryMismatch => {
388 formatter.write_str("unit-cell metric is incompatible with the symmetry rotations")
389 }
390 Self::DegenerateReciprocalMetric => {
391 formatter.write_str("reciprocal metric must be finite and positive definite")
392 }
393 }
394 }
395}
396
397impl Error for ReflectionGenerationError {}
398
399impl From<CellError> for ReflectionGenerationError {
400 fn from(value: CellError) -> Self {
401 Self::Cell(value)
402 }
403}
404
405impl From<SymmetryError> for ReflectionGenerationError {
406 fn from(value: SymmetryError) -> Self {
407 Self::Symmetry(value)
408 }
409}
410
411#[allow(clippy::cast_precision_loss)]
412fn validate_metric_compatibility(
413 geometry: &CellGeometry,
414 equations: &[[i64; 6]],
415 tolerance: f64,
416) -> Result<(), ReflectionGenerationError> {
417 let metric = geometry.direct_metric;
418 let components = [
419 metric[0][0],
420 metric[1][1],
421 metric[2][2],
422 metric[1][2],
423 metric[0][2],
424 metric[0][1],
425 ];
426 let scale = components
427 .iter()
428 .copied()
429 .map(f64::abs)
430 .fold(1.0_f64, f64::max);
431 for equation in equations {
432 let residual = equation
433 .iter()
434 .zip(components)
435 .map(|(coefficient, value)| *coefficient as f64 * value)
436 .sum::<f64>();
437 let coefficient_scale = equation.iter().copied().map(i64::unsigned_abs).sum::<u64>() as f64;
438 if residual.abs() > tolerance * scale * coefficient_scale.max(1.0) {
439 return Err(ReflectionGenerationError::CellSymmetryMismatch);
440 }
441 }
442 Ok(())
443}
444
445fn inclusive_contains(value: f64, minimum: f64, maximum: f64) -> bool {
446 let tolerance =
447 64.0 * f64::EPSILON * value.abs().max(minimum.abs()).max(maximum.abs()).max(1.0);
448 value + tolerance >= minimum && value - tolerance <= maximum
449}
450
451fn safe_index_bounds(
452 geometry: &CellGeometry,
453 max_reciprocal: f64,
454) -> Result<[i32; 3], ReflectionGenerationError> {
455 let direct_diagonal = [
456 geometry.direct_metric[0][0],
457 geometry.direct_metric[1][1],
458 geometry.direct_metric[2][2],
459 ];
460 let reciprocal_eigenvalue_lower_bound = direct_diagonal.iter().sum::<f64>().recip();
461 if !reciprocal_eigenvalue_lower_bound.is_finite() || reciprocal_eigenvalue_lower_bound <= 0.0 {
462 return Err(ReflectionGenerationError::DegenerateReciprocalMetric);
463 }
464 let common_bound = max_reciprocal / reciprocal_eigenvalue_lower_bound.sqrt();
465 let safety_factor = 1.0 + 64.0 * f64::EPSILON;
466 let mut bounds = [0; 3];
467 for (index, bound) in bounds.iter_mut().enumerate() {
468 let projected = max_reciprocal * direct_diagonal[index].sqrt();
471 let value = (projected.min(common_bound) * safety_factor).ceil() + 1.0;
472 if !value.is_finite() || value > f64::from(i32::MAX - 1) {
473 return Err(ReflectionGenerationError::CandidateLimitExceeded);
474 }
475 #[allow(clippy::cast_possible_truncation)]
476 {
477 *bound = value as i32;
478 }
479 }
480 Ok(bounds)
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486 use crate::symmetry::{Rational, SymmetryOperation};
487
488 fn cubic_cell() -> UnitCell {
489 UnitCell {
490 a_angstrom: 1.0,
491 b_angstrom: 1.0,
492 c_angstrom: 1.0,
493 alpha_deg: 90.0,
494 beta_deg: 90.0,
495 gamma_deg: 90.0,
496 }
497 }
498
499 fn identity_group() -> SpaceGroup {
500 SpaceGroup::new(vec![SymmetryOperation::identity()]).expect("P1")
501 }
502
503 fn body_centred_group() -> SpaceGroup {
504 let half = Rational::new(1, 2).expect("half");
505 SpaceGroup::new(vec![
506 SymmetryOperation::identity(),
507 SymmetryOperation::new(SymmetryOperation::identity().rotation(), [half, half, half])
508 .expect("centring operation"),
509 ])
510 .expect("I centring group")
511 }
512
513 #[test]
514 fn p1_cubic_generation_has_expected_families_and_multiplicity() {
515 let generator =
516 PreparedReflectionGenerator::new(identity_group(), true, 1_000_000).expect("generator");
517 let reflections = generator
518 .generate(
519 cubic_cell(),
520 ReflectionRange::DSpacing {
521 min_angstrom: 0.7,
522 max_angstrom: 1.0,
523 },
524 )
525 .expect("reflections");
526 assert_eq!(reflections.len(), 9);
527 assert!(
528 reflections
529 .iter()
530 .all(|reflection| reflection.multiplicity == 2)
531 );
532 assert!(reflections.windows(2).all(|pair| {
533 pair[0].reciprocal_length_inverse_angstrom <= pair[1].reciprocal_length_inverse_angstrom
534 }));
535 }
536
537 #[test]
538 fn body_centring_removes_odd_index_sum() {
539 let generator = PreparedReflectionGenerator::new(body_centred_group(), true, 1_000_000)
540 .expect("generator");
541 let reflections = generator
542 .generate(
543 cubic_cell(),
544 ReflectionRange::DSpacing {
545 min_angstrom: 0.7,
546 max_angstrom: 1.0,
547 },
548 )
549 .expect("reflections");
550 assert_eq!(reflections.len(), 6);
551 assert!(
552 reflections
553 .iter()
554 .all(|reflection| reflection.hkl.into_iter().sum::<i32>() % 2 == 0)
555 );
556 }
557
558 #[test]
559 fn physical_range_forms_select_the_same_cubic_shell() {
560 let generator =
561 PreparedReflectionGenerator::new(identity_group(), true, 1_000_000).expect("generator");
562 let d_range = generator
563 .generate(
564 cubic_cell(),
565 ReflectionRange::DSpacing {
566 min_angstrom: 0.7,
567 max_angstrom: 1.0,
568 },
569 )
570 .expect("d range");
571 let q_range = generator
572 .generate(
573 cubic_cell(),
574 ReflectionRange::ScatteringVector {
575 min_inverse_angstrom: TWO_PI,
576 max_inverse_angstrom: TWO_PI * 2.0_f64.sqrt(),
577 },
578 )
579 .expect("Q range");
580 let cw_range = generator
581 .generate(
582 cubic_cell(),
583 ReflectionRange::CwTwoTheta {
584 min_deg: 2.0 * 0.5_f64.asin().to_degrees(),
585 max_deg: 2.0 * (0.5 * 2.0_f64.sqrt()).asin().to_degrees(),
586 wavelength_angstrom: 1.0,
587 },
588 )
589 .expect("CW range");
590 let expected = d_range.iter().map(|item| item.hkl).collect::<Vec<_>>();
591 assert_eq!(
592 q_range.iter().map(|item| item.hkl).collect::<Vec<_>>(),
593 expected
594 );
595 assert_eq!(
596 cw_range.iter().map(|item| item.hkl).collect::<Vec<_>>(),
597 expected
598 );
599 }
600
601 #[test]
602 fn tof_filter_and_candidate_limit_are_explicit() {
603 let generator =
604 PreparedReflectionGenerator::new(identity_group(), true, 1_000_000).expect("generator");
605 let reflections = generator
606 .generate(
607 cubic_cell(),
608 ReflectionRange::Tof {
609 min_us: 900.0,
610 max_us: 1100.0,
611 search_min_d_angstrom: 0.5,
612 search_max_d_angstrom: 1.5,
613 zero_us: 0.0,
614 difc_us_per_angstrom: 1000.0,
615 difa_us_per_angstrom2: 0.0,
616 difb_us_angstrom: 0.0,
617 },
618 )
619 .expect("TOF range");
620 assert!(
621 reflections
622 .iter()
623 .all(|reflection| (reflection.d_spacing_angstrom - 1.0).abs() < 1e-14)
624 );
625
626 let limited = PreparedReflectionGenerator::new(identity_group(), true, 10)
627 .expect("limited generator");
628 assert_eq!(
629 limited.generate(
630 cubic_cell(),
631 ReflectionRange::DSpacing {
632 min_angstrom: 0.1,
633 max_angstrom: 1.0,
634 }
635 ),
636 Err(ReflectionGenerationError::CandidateLimitExceeded)
637 );
638 }
639
640 #[test]
641 fn incompatible_cell_and_point_group_is_rejected() {
642 let quarter_turn =
643 SymmetryOperation::new([[0, -1, 0], [1, 0, 0], [0, 0, 1]], [Rational::zero(); 3])
644 .expect("quarter turn");
645 let half_turn = quarter_turn.compose(quarter_turn).expect("half turn");
646 let three_quarters = quarter_turn.compose(half_turn).expect("three-quarter turn");
647 let tetragonal = SpaceGroup::new(vec![
648 SymmetryOperation::identity(),
649 quarter_turn,
650 half_turn,
651 three_quarters,
652 ])
653 .expect("four-fold group");
654 let generator =
655 PreparedReflectionGenerator::new(tetragonal, true, 1_000_000).expect("generator");
656 let incompatible = UnitCell {
657 b_angstrom: 1.1,
658 ..cubic_cell()
659 };
660 assert_eq!(
661 generator.generate(
662 incompatible,
663 ReflectionRange::DSpacing {
664 min_angstrom: 0.5,
665 max_angstrom: 2.0,
666 }
667 ),
668 Err(ReflectionGenerationError::CellSymmetryMismatch)
669 );
670 }
671}