1use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7use nalgebra::{Matrix3, SymmetricEigen};
8use phasesmith_core::{TofError, TofInstrument};
9use phasesmith_crystallography::{
10 CellError, CrystalSystem, PreparedReflectionGenerator, ReflectionGenerationError,
11 ReflectionRange, SpaceGroup, UnitCell,
12};
13
14const CELL_NAMES: [&str; 6] = [
15 "a_angstrom",
16 "b_angstrom",
17 "c_angstrom",
18 "alpha_deg",
19 "beta_deg",
20 "gamma_deg",
21];
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24enum LatticeKind {
25 Triclinic,
26 Monoclinic { angle: usize },
27 Orthorhombic,
28 PlaneUnique { plane: [usize; 2], unique: usize },
29 Rhombohedral,
30 Cubic,
31}
32
33#[derive(Clone, Debug, PartialEq)]
35pub struct LatticeParameterization {
36 space_group: SpaceGroup,
37 reference_cell: UnitCell,
38 kind: LatticeKind,
39 parameter_names: Vec<String>,
40}
41
42impl LatticeParameterization {
43 pub fn new(space_group: SpaceGroup, cell: UnitCell) -> Result<Self, LatticeError> {
50 validate_metric_compatibility(&space_group, cell)?;
51 let basis = &space_group.metric_constraints().parameterization_basis;
52 let kind = match space_group.crystal_system() {
53 CrystalSystem::Triclinic => LatticeKind::Triclinic,
54 CrystalSystem::Monoclinic => {
55 let free = (0..3)
56 .filter(|angle| basis.iter().any(|row| row[3 + angle] != 0))
57 .collect::<Vec<_>>();
58 if free.len() != 1 {
59 return Err(LatticeError::UnsupportedSetting);
60 }
61 LatticeKind::Monoclinic { angle: free[0] }
62 }
63 CrystalSystem::Orthorhombic => LatticeKind::Orthorhombic,
64 CrystalSystem::Tetragonal | CrystalSystem::Hexagonal => plane_unique_kind(basis)?,
65 CrystalSystem::Trigonal => {
66 let diagonal_equal = (1..3).all(|right| metric_columns_equal(basis, 0, right));
67 let off_diagonal_equal = (4..6).all(|right| metric_columns_equal(basis, 3, right));
68 if diagonal_equal && off_diagonal_equal && basis.iter().any(|row| row[3] != 0) {
69 LatticeKind::Rhombohedral
70 } else {
71 plane_unique_kind(basis)?
72 }
73 }
74 CrystalSystem::Cubic => LatticeKind::Cubic,
75 };
76 let parameter_names = names_for_kind(&kind);
77 let result = Self {
78 space_group,
79 reference_cell: cell,
80 kind,
81 parameter_names,
82 };
83 let values = result.values_from_cell(cell)?;
84 let rebuilt = result.to_cell(&values)?;
85 if !cells_close(rebuilt, cell, 2.0e-10) {
86 return Err(LatticeError::UnsupportedSetting);
87 }
88 Ok(result)
89 }
90
91 #[must_use]
93 pub const fn space_group(&self) -> &SpaceGroup {
94 &self.space_group
95 }
96
97 #[must_use]
99 pub fn parameter_names(&self) -> &[String] {
100 &self.parameter_names
101 }
102
103 #[must_use]
105 pub const fn reference_cell(&self) -> UnitCell {
106 self.reference_cell
107 }
108
109 pub fn values_from_cell(&self, cell: UnitCell) -> Result<Vec<f64>, LatticeError> {
115 cell.geometry().map_err(LatticeError::Cell)?;
116 let full = cell_values(cell);
117 let values = match self.kind {
118 LatticeKind::Triclinic => full.to_vec(),
119 LatticeKind::Monoclinic { angle } => {
120 vec![full[0], full[1], full[2], full[3 + angle]]
121 }
122 LatticeKind::Orthorhombic => full[..3].to_vec(),
123 LatticeKind::PlaneUnique { plane, unique } => {
124 let first = plane[0].min(unique);
125 let second = plane[0].max(unique);
126 vec![full[first], full[second]]
127 }
128 LatticeKind::Rhombohedral => vec![full[0], full[3]],
129 LatticeKind::Cubic => vec![full[0]],
130 };
131 if !cells_close(self.to_cell(&values)?, cell, 2.0e-9) {
132 return Err(LatticeError::IncompatibleCell);
133 }
134 Ok(values)
135 }
136
137 pub fn to_cell(&self, values: &[f64]) -> Result<UnitCell, LatticeError> {
143 if values.len() != self.parameter_names.len() || values.iter().any(|v| !v.is_finite()) {
144 return Err(LatticeError::ValueShape);
145 }
146 let mut full = cell_values(self.reference_cell);
147 match self.kind {
148 LatticeKind::Triclinic => full.copy_from_slice(values),
149 LatticeKind::Monoclinic { angle } => {
150 full[..3].copy_from_slice(&values[..3]);
151 full[3 + angle] = values[3];
152 }
153 LatticeKind::Orthorhombic => full[..3].copy_from_slice(values),
154 LatticeKind::PlaneUnique { plane, unique } => {
155 let representatives = [plane[0].min(unique), plane[0].max(unique)];
156 let plane_value = values[usize::from(representatives[1] == plane[0])];
157 let unique_value = values[usize::from(representatives[1] == unique)];
158 full[plane[0]] = plane_value;
159 full[plane[1]] = plane_value;
160 full[unique] = unique_value;
161 }
162 LatticeKind::Rhombohedral => {
163 full[..3].fill(values[0]);
164 full[3..].fill(values[1]);
165 }
166 LatticeKind::Cubic => full[..3].fill(values[0]),
167 }
168 let cell = UnitCell {
169 a_angstrom: full[0],
170 b_angstrom: full[1],
171 c_angstrom: full[2],
172 alpha_deg: full[3],
173 beta_deg: full[4],
174 gamma_deg: full[5],
175 };
176 cell.geometry().map_err(LatticeError::Cell)?;
177 Ok(cell)
178 }
179
180 pub fn cell_jacobian(&self, values: &[f64]) -> Result<Vec<f64>, LatticeError> {
186 self.to_cell(values)?;
187 let columns = values.len();
188 let mut matrix = vec![0.0; 6 * columns];
189 let mut set = |row: usize, column: usize| matrix[row * columns + column] = 1.0;
190 match self.kind {
191 LatticeKind::Triclinic => (0..6).for_each(|index| set(index, index)),
192 LatticeKind::Monoclinic { angle } => {
193 (0..3).for_each(|index| set(index, index));
194 set(3 + angle, 3);
195 }
196 LatticeKind::Orthorhombic => (0..3).for_each(|index| set(index, index)),
197 LatticeKind::PlaneUnique { plane, unique } => {
198 let representatives = [plane[0].min(unique), plane[0].max(unique)];
199 let plane_column = usize::from(representatives[1] == plane[0]);
200 let unique_column = usize::from(representatives[1] == unique);
201 set(plane[0], plane_column);
202 set(plane[1], plane_column);
203 set(unique, unique_column);
204 }
205 LatticeKind::Rhombohedral => {
206 (0..3).for_each(|row| set(row, 0));
207 (3..6).for_each(|row| set(row, 1));
208 }
209 LatticeKind::Cubic => (0..3).for_each(|row| set(row, 0)),
210 }
211 Ok(matrix)
212 }
213}
214
215#[derive(Clone, Debug, PartialEq)]
217pub struct LatticeBounds {
218 parameter_names: Vec<String>,
219 lower: Vec<f64>,
220 upper: Vec<f64>,
221}
222
223impl LatticeBounds {
224 pub fn new(
230 parameterization: &LatticeParameterization,
231 lower: Vec<f64>,
232 upper: Vec<f64>,
233 ) -> Result<Self, LatticeError> {
234 let count = parameterization.parameter_names.len();
235 if lower.len() != count
236 || upper.len() != count
237 || lower.iter().chain(&upper).any(|value| !value.is_finite())
238 || lower.iter().zip(&upper).any(|(low, high)| low >= high)
239 {
240 return Err(LatticeError::InvalidBounds);
241 }
242 let bounds = Self {
243 parameter_names: parameterization.parameter_names.clone(),
244 lower,
245 upper,
246 };
247 bounds.validate_for(parameterization)?;
248 Ok(bounds)
249 }
250
251 pub fn around(
257 parameterization: &LatticeParameterization,
258 relative_length: f64,
259 angle_delta_deg: f64,
260 ) -> Result<Self, LatticeError> {
261 if !relative_length.is_finite()
262 || !(0.0..1.0).contains(&relative_length)
263 || relative_length == 0.0
264 || !angle_delta_deg.is_finite()
265 || angle_delta_deg <= 0.0
266 {
267 return Err(LatticeError::InvalidBounds);
268 }
269 let values = parameterization.values_from_cell(parameterization.reference_cell)?;
270 let mut lower = Vec::with_capacity(values.len());
271 let mut upper = Vec::with_capacity(values.len());
272 for (name, value) in parameterization.parameter_names.iter().zip(values) {
273 if name.ends_with("_angstrom") {
274 lower.push(value * (1.0 - relative_length));
275 upper.push(value * (1.0 + relative_length));
276 } else {
277 lower.push((value - angle_delta_deg).max(f64::from_bits(1)));
278 upper.push((value + angle_delta_deg).min(f64::from_bits(180_f64.to_bits() - 1)));
279 }
280 }
281 Self::new(parameterization, lower, upper)
282 }
283
284 #[must_use]
286 pub fn lower(&self) -> &[f64] {
287 &self.lower
288 }
289
290 #[must_use]
292 pub fn upper(&self) -> &[f64] {
293 &self.upper
294 }
295
296 #[must_use]
298 pub fn parameter_names(&self) -> &[String] {
299 &self.parameter_names
300 }
301
302 #[must_use]
304 pub fn corner_values(&self) -> Vec<Vec<f64>> {
305 let count = 1_usize << self.lower.len();
306 (0..count)
307 .map(|mask| {
308 (0..self.lower.len())
309 .map(|index| {
310 if mask & (1 << index) == 0 {
311 self.lower[index]
312 } else {
313 self.upper[index]
314 }
315 })
316 .collect()
317 })
318 .collect()
319 }
320
321 fn contains(&self, values: &[f64]) -> bool {
322 values.len() == self.lower.len()
323 && values
324 .iter()
325 .zip(self.lower.iter().zip(&self.upper))
326 .all(|(value, (low, high))| low <= value && value <= high)
327 }
328
329 fn validate_for(&self, parameterization: &LatticeParameterization) -> Result<(), LatticeError> {
330 if self.parameter_names != parameterization.parameter_names
331 || !self.contains(¶meterization.values_from_cell(parameterization.reference_cell)?)
332 {
333 return Err(LatticeError::InvalidBounds);
334 }
335 for corner in self.corner_values() {
336 parameterization.to_cell(&corner)?;
337 }
338 Ok(())
339 }
340}
341
342#[derive(Clone, Debug, PartialEq)]
344pub struct CwLatticeGeometry {
345 pub d_spacing_angstrom: Vec<f64>,
347 pub two_theta_deg: Vec<f64>,
349 pub d_d_spacing_d_parameters: Vec<f64>,
351 pub d_two_theta_d_parameters: Vec<f64>,
353 pub parameter_names: Vec<String>,
355}
356
357#[derive(Clone, Debug, PartialEq)]
359pub struct TofLatticeGeometry {
360 pub d_spacing_angstrom: Vec<f64>,
362 pub tof_us: Vec<f64>,
364 pub d_d_spacing_d_parameters: Vec<f64>,
366 pub d_tof_d_parameters: Vec<f64>,
368 pub parameter_names: Vec<String>,
370}
371
372pub fn cw_lattice_geometry(
378 parameterization: &LatticeParameterization,
379 cell: UnitCell,
380 hkl: &[[i32; 3]],
381 wavelength_angstrom: f64,
382) -> Result<CwLatticeGeometry, LatticeError> {
383 if !wavelength_angstrom.is_finite() || wavelength_angstrom <= 0.0 {
384 return Err(LatticeError::InvalidWavelength);
385 }
386 let values = parameterization.values_from_cell(cell)?;
387 let chain = parameterization.cell_jacobian(&values)?;
388 let columns = values.len();
389 let geometry = cell.geometry().map_err(LatticeError::Cell)?;
390 let mut spacing = Vec::with_capacity(hkl.len());
391 let mut positions = Vec::with_capacity(hkl.len());
392 let mut derivatives = vec![0.0; hkl.len() * columns];
393 let mut spacing_derivatives = vec![0.0; hkl.len() * columns];
394 for (reflection, hkl) in hkl.iter().copied().enumerate() {
395 let (d, d_cell) = geometry
396 .d_spacing_and_derivatives(hkl)
397 .map_err(LatticeError::Cell)?;
398 let argument = wavelength_angstrom / (2.0 * d);
399 if argument >= 1.0 {
400 return Err(LatticeError::InaccessibleReflection);
401 }
402 spacing.push(d);
403 positions.push(2.0 * argument.asin().to_degrees());
404 let per_d = -180.0 / std::f64::consts::PI * wavelength_angstrom
405 / (d * d * (1.0 - argument * argument).sqrt());
406 for parameter in 0..columns {
407 let d_parameter = (0..6)
408 .map(|cell_parameter| {
409 d_cell[cell_parameter] * chain[cell_parameter * columns + parameter]
410 })
411 .sum::<f64>();
412 spacing_derivatives[reflection * columns + parameter] = d_parameter;
413 derivatives[reflection * columns + parameter] = per_d * d_parameter;
414 }
415 }
416 Ok(CwLatticeGeometry {
417 d_spacing_angstrom: spacing,
418 two_theta_deg: positions,
419 d_d_spacing_d_parameters: spacing_derivatives,
420 d_two_theta_d_parameters: derivatives,
421 parameter_names: parameterization.parameter_names.clone(),
422 })
423}
424
425pub fn tof_lattice_geometry(
437 parameterization: &LatticeParameterization,
438 cell: UnitCell,
439 hkl: &[[i32; 3]],
440 instrument: TofInstrument,
441) -> Result<TofLatticeGeometry, LatticeError> {
442 instrument.validate().map_err(LatticeError::Tof)?;
443 let values = parameterization.values_from_cell(cell)?;
444 let chain = parameterization.cell_jacobian(&values)?;
445 let columns = values.len();
446 let geometry = cell.geometry().map_err(LatticeError::Cell)?;
447 let mut spacing = Vec::with_capacity(hkl.len());
448 let mut positions = Vec::with_capacity(hkl.len());
449 let mut spacing_derivatives = vec![0.0; hkl.len() * columns];
450 let mut position_derivatives = vec![0.0; hkl.len() * columns];
451 for (reflection, hkl) in hkl.iter().copied().enumerate() {
452 let (d, d_cell) = geometry
453 .d_spacing_and_derivatives(hkl)
454 .map_err(LatticeError::Cell)?;
455 let inverse_d = d.recip();
456 let position = instrument.zero_us
457 + instrument.difc_us_per_angstrom * d
458 + instrument.difa_us_per_angstrom2 * d * d
459 + instrument.difb_us_angstrom * inverse_d;
460 if !position.is_finite() {
461 return Err(LatticeError::Tof(TofError::InvalidPosition));
462 }
463 spacing.push(d);
464 positions.push(position);
465 let per_d = instrument.difc_us_per_angstrom + 2.0 * instrument.difa_us_per_angstrom2 * d
466 - instrument.difb_us_angstrom * inverse_d * inverse_d;
467 for parameter in 0..columns {
468 let d_parameter = (0..6)
469 .map(|cell_parameter| {
470 d_cell[cell_parameter] * chain[cell_parameter * columns + parameter]
471 })
472 .sum::<f64>();
473 spacing_derivatives[reflection * columns + parameter] = d_parameter;
474 position_derivatives[reflection * columns + parameter] = per_d * d_parameter;
475 }
476 }
477 Ok(TofLatticeGeometry {
478 d_spacing_angstrom: spacing,
479 tof_us: positions,
480 d_d_spacing_d_parameters: spacing_derivatives,
481 d_tof_d_parameters: position_derivatives,
482 parameter_names: parameterization.parameter_names.clone(),
483 })
484}
485
486#[derive(Clone, Debug, PartialEq)]
488pub struct LatticeReflectionDomain {
489 parameterization: LatticeParameterization,
490 bounds: LatticeBounds,
491 wavelength_angstrom: f64,
492 visible_two_theta_deg: [f64; 2],
493 initial_intensity: f64,
494 merge_friedel: bool,
495 max_candidates: usize,
496 guard_scale: f64,
497}
498
499impl LatticeReflectionDomain {
500 #[allow(clippy::too_many_arguments)]
506 pub fn new(
507 parameterization: LatticeParameterization,
508 bounds: LatticeBounds,
509 wavelength_angstrom: f64,
510 visible_two_theta_deg: [f64; 2],
511 initial_intensity: f64,
512 merge_friedel: bool,
513 max_candidates: usize,
514 guard_scale: f64,
515 ) -> Result<Self, LatticeError> {
516 if !wavelength_angstrom.is_finite()
517 || wavelength_angstrom <= 0.0
518 || !initial_intensity.is_finite()
519 || initial_intensity < 0.0
520 || !visible_two_theta_deg.iter().all(|value| value.is_finite())
521 || !(0.0 < visible_two_theta_deg[0]
522 && visible_two_theta_deg[0] < visible_two_theta_deg[1]
523 && visible_two_theta_deg[1] < 180.0)
524 || max_candidates == 0
525 || !guard_scale.is_finite()
526 || guard_scale < 1.0
527 {
528 return Err(LatticeError::InvalidDomain);
529 }
530 bounds.validate_for(¶meterization)?;
531 let domain = Self {
532 parameterization,
533 bounds,
534 wavelength_angstrom,
535 visible_two_theta_deg,
536 initial_intensity,
537 merge_friedel,
538 max_candidates,
539 guard_scale,
540 };
541 domain.guarded_d_range(domain.parameterization.reference_cell)?;
542 Ok(domain)
543 }
544
545 #[must_use]
547 pub const fn parameterization(&self) -> &LatticeParameterization {
548 &self.parameterization
549 }
550
551 #[must_use]
553 pub const fn bounds(&self) -> &LatticeBounds {
554 &self.bounds
555 }
556
557 #[must_use]
559 pub const fn wavelength_angstrom(&self) -> f64 {
560 self.wavelength_angstrom
561 }
562
563 #[must_use]
565 pub const fn visible_two_theta_deg(&self) -> [f64; 2] {
566 self.visible_two_theta_deg
567 }
568
569 #[must_use]
571 pub const fn initial_intensity(&self) -> f64 {
572 self.initial_intensity
573 }
574
575 #[must_use]
577 pub const fn merge_friedel(&self) -> bool {
578 self.merge_friedel
579 }
580
581 #[must_use]
583 pub const fn max_candidates(&self) -> usize {
584 self.max_candidates
585 }
586
587 #[must_use]
589 pub const fn guard_scale(&self) -> f64 {
590 self.guard_scale
591 }
592
593 pub fn with_wavelength(&self, wavelength_angstrom: f64) -> Result<Self, LatticeError> {
600 Self::new(
601 self.parameterization.clone(),
602 self.bounds.clone(),
603 wavelength_angstrom,
604 self.visible_two_theta_deg,
605 self.initial_intensity,
606 self.merge_friedel,
607 self.max_candidates,
608 self.guard_scale,
609 )
610 }
611
612 pub fn validate_cell(&self, cell: UnitCell) -> Result<Vec<f64>, LatticeError> {
618 let values = self.parameterization.values_from_cell(cell)?;
619 if !self.bounds.contains(&values) {
620 return Err(LatticeError::OutsideBounds);
621 }
622 Ok(values)
623 }
624
625 pub fn generate(
631 &self,
632 cell: UnitCell,
633 previous: Option<&BTreeMap<String, f64>>,
634 ) -> Result<GeneratedLatticeDomain, LatticeError> {
635 self.validate_cell(cell)?;
636 if previous.is_some_and(|items| {
637 items
638 .values()
639 .any(|value| !value.is_finite() || *value < 0.0)
640 }) {
641 return Err(LatticeError::InvalidIntensity);
642 }
643 let (min_d, max_d) = self.guarded_d_range(cell)?;
644 let generator = PreparedReflectionGenerator::new(
645 self.parameterization.space_group.clone(),
646 self.merge_friedel,
647 self.max_candidates,
648 )
649 .map_err(LatticeError::Generation)?;
650 let generated = generator
651 .generate(
652 cell,
653 ReflectionRange::DSpacing {
654 min_angstrom: min_d,
655 max_angstrom: max_d,
656 },
657 )
658 .map_err(LatticeError::Generation)?;
659 let physical = generated
660 .into_iter()
661 .filter(|item| self.wavelength_angstrom < 2.0 * item.d_spacing_angstrom)
662 .collect::<Vec<_>>();
663 if physical.is_empty() {
664 return Err(LatticeError::NoPhysicalReflections);
665 }
666 let hkl = physical.iter().map(|item| item.hkl).collect::<Vec<_>>();
667 let geometry =
668 cw_lattice_geometry(&self.parameterization, cell, &hkl, self.wavelength_angstrom)?;
669 let reflection_ids = physical
670 .iter()
671 .map(|item| item.reflection_id.clone())
672 .collect::<Vec<_>>();
673 let previous_ids = previous
674 .map(|values| values.keys().cloned().collect::<BTreeSet<_>>())
675 .unwrap_or_default();
676 let current_ids = reflection_ids.iter().cloned().collect::<BTreeSet<_>>();
677 let preserved_reflection_count = current_ids.intersection(&previous_ids).count();
678 let intensities = reflection_ids
679 .iter()
680 .map(|id| {
681 previous
682 .and_then(|values| values.get(id))
683 .copied()
684 .unwrap_or(self.initial_intensity)
685 })
686 .collect::<Vec<_>>();
687 let visible = geometry
688 .two_theta_deg
689 .iter()
690 .map(|value| {
691 self.visible_two_theta_deg[0] <= *value && *value <= self.visible_two_theta_deg[1]
692 })
693 .collect();
694 let added_reflection_ids = reflection_ids
695 .iter()
696 .filter(|id| !previous_ids.contains(*id))
697 .cloned()
698 .collect();
699 let removed_reflection_ids = previous
700 .map(|items| {
701 items
702 .keys()
703 .filter(|id| !current_ids.contains(*id))
704 .cloned()
705 .collect()
706 })
707 .unwrap_or_default();
708 Ok(GeneratedLatticeDomain {
709 reflection_ids,
710 hkl,
711 multiplicity: physical.iter().map(|item| item.multiplicity).collect(),
712 d_spacing_angstrom: geometry.d_spacing_angstrom,
713 two_theta_deg: geometry.two_theta_deg,
714 integrated_intensity: intensities,
715 visible,
716 guarded_d_min_angstrom: min_d,
717 guarded_d_max_angstrom: max_d,
718 added_reflection_ids,
719 removed_reflection_ids,
720 preserved_reflection_count,
721 })
722 }
723
724 fn guarded_d_range(&self, reference_cell: UnitCell) -> Result<(f64, f64), LatticeError> {
725 let cells = self
726 .bounds
727 .corner_values()
728 .into_iter()
729 .map(|values| self.parameterization.to_cell(&values).map(cell_values))
730 .collect::<Result<Vec<_>, _>>()?;
731 let mut physical_lower = [f64::INFINITY; 6];
732 let mut physical_upper = [f64::NEG_INFINITY; 6];
733 for cell in cells {
734 for index in 0..6 {
735 physical_lower[index] = physical_lower[index].min(cell[index]);
736 physical_upper[index] = physical_upper[index].max(cell[index]);
737 }
738 }
739 let cos_lower = [physical_upper[3], physical_upper[4], physical_upper[5]]
740 .map(|value| value.to_radians().cos());
741 let cos_upper = [physical_lower[3], physical_lower[4], physical_lower[5]]
742 .map(|value| value.to_radians().cos());
743 let mut product_lower = f64::INFINITY;
744 for mask in 0..8 {
745 let product = (0..3)
746 .map(|index| {
747 if mask & (1 << index) == 0 {
748 cos_lower[index]
749 } else {
750 cos_upper[index]
751 }
752 })
753 .product::<f64>();
754 product_lower = product_lower.min(product);
755 }
756 let maximum_squares = (0..3)
757 .map(|index| cos_lower[index].powi(2).max(cos_upper[index].powi(2)))
758 .sum::<f64>();
759 let angular_lower = 1.0 + 2.0 * product_lower - maximum_squares;
760 if angular_lower <= 0.0 {
761 return Err(LatticeError::UnboundedGuard);
762 }
763 let length_product = physical_lower[..3].iter().product::<f64>();
764 let determinant_lower = length_product * length_product * angular_lower;
765 let trace_upper = physical_upper[..3]
766 .iter()
767 .map(|value| value * value)
768 .sum::<f64>();
769 let direct_eigenvalue_lower = 4.0 * determinant_lower / trace_upper.powi(2);
770 let reciprocal_lower = trace_upper.recip();
771 let reciprocal_upper = direct_eigenvalue_lower.recip();
772 let reciprocal = reference_cell
773 .geometry()
774 .map_err(LatticeError::Cell)?
775 .reciprocal_metric;
776 let matrix = Matrix3::from_row_slice(&[
777 reciprocal[0][0],
778 reciprocal[0][1],
779 reciprocal[0][2],
780 reciprocal[1][0],
781 reciprocal[1][1],
782 reciprocal[1][2],
783 reciprocal[2][0],
784 reciprocal[2][1],
785 reciprocal[2][2],
786 ]);
787 let eigenvalues = SymmetricEigen::new(matrix).eigenvalues;
788 let minimum_ratio = (eigenvalues.min() / reciprocal_upper).sqrt() / self.guard_scale;
789 let maximum_ratio = (eigenvalues.max() / reciprocal_lower).sqrt() * self.guard_scale;
790 let theta_min = 0.5 * self.visible_two_theta_deg[0].to_radians();
791 let theta_max = 0.5 * self.visible_two_theta_deg[1].to_radians();
792 let visible_max = self.wavelength_angstrom / (2.0 * theta_min.sin());
793 let visible_min = self.wavelength_angstrom / (2.0 * theta_max.sin());
794 Ok((visible_min / maximum_ratio, visible_max / minimum_ratio))
795 }
796}
797
798#[derive(Clone, Debug, PartialEq)]
800pub struct GeneratedLatticeDomain {
801 pub reflection_ids: Vec<String>,
803 pub hkl: Vec<[i32; 3]>,
805 pub multiplicity: Vec<usize>,
807 pub d_spacing_angstrom: Vec<f64>,
809 pub two_theta_deg: Vec<f64>,
811 pub integrated_intensity: Vec<f64>,
813 pub visible: Vec<bool>,
815 pub guarded_d_min_angstrom: f64,
817 pub guarded_d_max_angstrom: f64,
819 pub added_reflection_ids: Vec<String>,
821 pub removed_reflection_ids: Vec<String>,
823 pub preserved_reflection_count: usize,
825}
826
827fn plane_unique_kind(basis: &[[i64; 6]]) -> Result<LatticeKind, LatticeError> {
828 let pairs = (0..3)
829 .flat_map(|left| (left + 1..3).map(move |right| [left, right]))
830 .filter(|pair| metric_columns_equal(basis, pair[0], pair[1]))
831 .collect::<Vec<_>>();
832 if pairs.len() != 1 {
833 return Err(LatticeError::UnsupportedSetting);
834 }
835 let unique = (0..3)
836 .find(|axis| !pairs[0].contains(axis))
837 .ok_or(LatticeError::UnsupportedSetting)?;
838 Ok(LatticeKind::PlaneUnique {
839 plane: pairs[0],
840 unique,
841 })
842}
843
844fn metric_columns_equal(basis: &[[i64; 6]], left: usize, right: usize) -> bool {
845 basis.iter().all(|row| row[left] == row[right])
846}
847
848fn names_for_kind(kind: &LatticeKind) -> Vec<String> {
849 match kind {
850 LatticeKind::Triclinic => CELL_NAMES.iter().map(ToString::to_string).collect(),
851 LatticeKind::Monoclinic { angle } => [
852 CELL_NAMES[0],
853 CELL_NAMES[1],
854 CELL_NAMES[2],
855 CELL_NAMES[3 + angle],
856 ]
857 .into_iter()
858 .map(ToString::to_string)
859 .collect(),
860 LatticeKind::Orthorhombic => CELL_NAMES[..3].iter().map(ToString::to_string).collect(),
861 LatticeKind::PlaneUnique { plane, unique } => {
862 let mut axes = [plane[0], *unique];
863 axes.sort_unstable();
864 axes.into_iter()
865 .map(|axis| CELL_NAMES[axis].to_owned())
866 .collect()
867 }
868 LatticeKind::Rhombohedral => vec![CELL_NAMES[0].to_owned(), CELL_NAMES[3].to_owned()],
869 LatticeKind::Cubic => vec![CELL_NAMES[0].to_owned()],
870 }
871}
872
873fn cell_values(cell: UnitCell) -> [f64; 6] {
874 [
875 cell.a_angstrom,
876 cell.b_angstrom,
877 cell.c_angstrom,
878 cell.alpha_deg,
879 cell.beta_deg,
880 cell.gamma_deg,
881 ]
882}
883
884fn cells_close(left: UnitCell, right: UnitCell, tolerance: f64) -> bool {
885 cell_values(left)
886 .iter()
887 .zip(cell_values(right))
888 .all(|(left, right)| (left - right).abs() <= tolerance)
889}
890
891#[allow(clippy::cast_precision_loss)]
892fn validate_metric_compatibility(
893 space_group: &SpaceGroup,
894 cell: UnitCell,
895) -> Result<(), LatticeError> {
896 let metric = cell.geometry().map_err(LatticeError::Cell)?.direct_metric;
897 let components = [
898 metric[0][0],
899 metric[1][1],
900 metric[2][2],
901 metric[1][2],
902 metric[0][2],
903 metric[0][1],
904 ];
905 let scale = components
906 .iter()
907 .copied()
908 .map(f64::abs)
909 .fold(1.0_f64, f64::max);
910 for equation in &space_group.metric_constraints().equations {
911 let residual = equation
912 .iter()
913 .zip(components)
914 .map(|(coefficient, value)| *coefficient as f64 * value)
915 .sum::<f64>();
916 let coefficient_scale = equation.iter().copied().map(i64::unsigned_abs).sum::<u64>() as f64;
917 if residual.abs() > 1.0e-10 * scale * coefficient_scale.max(1.0) {
918 return Err(LatticeError::IncompatibleCell);
919 }
920 }
921 Ok(())
922}
923
924#[derive(Debug)]
926pub enum LatticeError {
927 Cell(CellError),
929 Generation(ReflectionGenerationError),
931 Tof(TofError),
933 UnsupportedSetting,
935 IncompatibleCell,
937 ValueShape,
939 InvalidBounds,
941 OutsideBounds,
943 InvalidDomain,
945 InvalidWavelength,
947 InaccessibleReflection,
949 NoPhysicalReflections,
951 InvalidIntensity,
953 UnboundedGuard,
955}
956
957impl Display for LatticeError {
958 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
959 match self {
960 Self::Cell(error) => Display::fmt(error, formatter),
961 Self::Generation(error) => Display::fmt(error, formatter),
962 Self::Tof(error) => Display::fmt(error, formatter),
963 Self::UnsupportedSetting => formatter.write_str("unsupported lattice setting"),
964 Self::IncompatibleCell => {
965 formatter.write_str("cell is incompatible with lattice setting")
966 }
967 Self::ValueShape => formatter
968 .write_str("lattice values must match the independent variables and be finite"),
969 Self::InvalidBounds => formatter.write_str(
970 "lattice bounds must be finite, ordered, physical, and contain the reference",
971 ),
972 Self::OutsideBounds => {
973 formatter.write_str("lattice cell lies outside the reflection-domain bounds")
974 }
975 Self::InvalidDomain => formatter.write_str("lattice reflection domain is invalid"),
976 Self::InvalidWavelength => {
977 formatter.write_str("wavelength must be positive and finite")
978 }
979 Self::InaccessibleReflection => {
980 formatter.write_str("reflection lies outside the monochromatic Bragg domain")
981 }
982 Self::NoPhysicalReflections => {
983 formatter.write_str("no physical reflections lie in the guarded CW domain")
984 }
985 Self::InvalidIntensity => formatter
986 .write_str("transferred reflection intensities must be finite and non-negative"),
987 Self::UnboundedGuard => formatter
988 .write_str("lattice angle bounds are too broad for a finite guarded domain"),
989 }
990 }
991}
992
993impl Error for LatticeError {
994 fn source(&self) -> Option<&(dyn Error + 'static)> {
995 match self {
996 Self::Cell(error) => Some(error),
997 Self::Generation(error) => Some(error),
998 Self::Tof(error) => Some(error),
999 _ => None,
1000 }
1001 }
1002}