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