moyo/identify/
space_group.rs

1use std::collections::HashMap;
2
3use log::debug;
4use nalgebra::{Dyn, Matrix3, OMatrix, OVector, U3, Vector3};
5use serde::Serialize;
6
7use super::point_group::PointGroup;
8use crate::base::{
9    Lattice, MoyoError, Operations, OriginShift, UnimodularLinear, UnimodularTransformation,
10    project_rotations,
11};
12use crate::data::{
13    ArithmeticNumber, GeometricCrystalClass, HallNumber, HallSymbol, Number,
14    PointGroupRepresentative, Setting, arithmetic_crystal_class_entry, hall_symbol_entry,
15};
16use crate::math::SNF;
17
18#[derive(Debug, Clone, Serialize)]
19pub struct SpaceGroup {
20    pub number: Number,
21    pub hall_number: HallNumber,
22    /// Transformation to the representative for `hall_number` in primitive
23    pub transformation: UnimodularTransformation,
24}
25
26impl SpaceGroup {
27    /// Identify the space group from the primitive operations.
28    /// epsilon: tolerance for comparing translation parts
29    pub fn new(
30        prim_operations: &Operations,
31        setting: Setting,
32        epsilon: f64,
33    ) -> Result<Self, MoyoError> {
34        // point_group.trans_mat: self -> primitive
35        let prim_rotations = project_rotations(prim_operations);
36        let point_group = PointGroup::new(&prim_rotations)?;
37        debug!(
38            "Arithmetic crystal class: No. {}",
39            point_group.arithmetic_number
40        );
41
42        for hall_number in setting.hall_numbers() {
43            let entry = hall_symbol_entry(hall_number).unwrap();
44            if entry.arithmetic_number != point_group.arithmetic_number {
45                continue;
46            }
47
48            let hall_symbol = HallSymbol::from_hall_number(hall_number)
49                .ok_or(MoyoError::SpaceGroupTypeIdentificationError)?;
50            let db_prim_generators = hall_symbol.primitive_generators();
51
52            // Try several correction transformation matrices for monoclinic and orthorhombic
53            for trans_mat_corr in correction_transformation_matrices(entry.arithmetic_number) {
54                let trans_mat = point_group.prim_trans_mat * trans_mat_corr;
55                if let Some(origin_shift) =
56                    match_origin_shift(prim_operations, &trans_mat, &db_prim_generators, epsilon)
57                {
58                    debug!(
59                        "Matched with Hall number {} (No. {})",
60                        hall_number, entry.number
61                    );
62                    return Ok(Self {
63                        number: entry.number,
64                        hall_number,
65                        transformation: UnimodularTransformation::new(trans_mat, origin_shift),
66                    });
67                }
68            }
69        }
70
71        Err(MoyoError::SpaceGroupTypeIdentificationError)
72    }
73
74    pub fn from_lattice(
75        lattice: &Lattice,
76        prim_operations: &Operations,
77        setting: Setting,
78        epsilon: f64,
79    ) -> Result<Self, MoyoError> {
80        let (_, reduced_trans_mat) = lattice.minkowski_reduce()?;
81        let to_reduced = UnimodularTransformation::from_linear(reduced_trans_mat);
82        let reduced_prim_operations = to_reduced.transform_operations(prim_operations);
83
84        let reduced_space_group = Self::new(&reduced_prim_operations, setting, epsilon)?;
85        Ok(SpaceGroup {
86            number: reduced_space_group.number,
87            hall_number: reduced_space_group.hall_number,
88            transformation: reduced_space_group.transformation * to_reduced,
89        })
90    }
91
92    pub fn from_hall_number_and_transformation(
93        hall_number: HallNumber,
94        transformation: UnimodularTransformation,
95    ) -> Result<Self, MoyoError> {
96        let entry =
97            hall_symbol_entry(hall_number).ok_or(MoyoError::SpaceGroupTypeIdentificationError)?;
98        Ok(Self {
99            number: entry.number,
100            hall_number,
101            transformation,
102        })
103    }
104}
105
106fn correction_transformation_matrices(
107    arithmetic_number: ArithmeticNumber,
108) -> Vec<UnimodularLinear> {
109    let geometric_crystal_class = arithmetic_crystal_class_entry(arithmetic_number)
110        .unwrap()
111        .geometric_crystal_class;
112
113    // conventional -> conventional(standard)
114    let convs = match geometric_crystal_class {
115        // Monoclinic crystal system
116        GeometricCrystalClass::C2 | GeometricCrystalClass::C1h | GeometricCrystalClass::C2h => {
117            vec![
118                UnimodularLinear::identity(),
119                // b2 to b1
120                UnimodularLinear::new(
121                    0, 0, -1, //
122                    0, 1, 0, //
123                    1, 0, -1, //
124                ),
125                // b3 to b1
126                UnimodularLinear::new(
127                    -1, 0, 1, //
128                    0, 1, 0, //
129                    -1, 0, 0, //
130                ),
131            ]
132        }
133        // Orthorhombic crystal system
134        GeometricCrystalClass::D2 | GeometricCrystalClass::C2v | GeometricCrystalClass::D2h => {
135            vec![
136                // abc
137                UnimodularLinear::identity(),
138                // ba-c
139                UnimodularLinear::new(
140                    0, 1, 0, //
141                    1, 0, 0, //
142                    0, 0, -1, //
143                ),
144                // cab
145                UnimodularLinear::new(
146                    0, 0, 1, //
147                    1, 0, 0, //
148                    0, 1, 0, //
149                ),
150                // -cba
151                UnimodularLinear::new(
152                    0, 0, -1, //
153                    0, 1, 0, //
154                    1, 0, 0, //
155                ),
156                // bca
157                UnimodularLinear::new(
158                    0, 1, 0, //
159                    0, 0, 1, //
160                    1, 0, 0, //
161                ),
162                // a-cb
163                UnimodularLinear::new(
164                    1, 0, 0, //
165                    0, 0, -1, //
166                    0, 1, 0, //
167                ),
168            ]
169        }
170        // m-3
171        GeometricCrystalClass::Th => {
172            vec![
173                UnimodularLinear::identity(),
174                UnimodularLinear::new(
175                    0, 0, 1, //
176                    0, -1, 0, //
177                    1, 0, 0, //
178                ),
179            ]
180        }
181        _ => vec![UnimodularLinear::identity()],
182    };
183
184    // primitive -> conventional -> conventional(standard) -> primitive
185    let point_group = PointGroupRepresentative::from_arithmetic_crystal_class(arithmetic_number);
186    let centering = point_group.centering;
187    let corrections: Vec<UnimodularLinear> = convs
188        .iter()
189        .map(|trans_corr| {
190            let corr = (centering.linear() * trans_corr).map(|e| e as f64) * centering.inverse();
191            corr.map(|e| e.round() as i32)
192        })
193        .filter(|corr| corr.map(|e| e as f64).determinant().round() as i32 == 1)
194        .collect();
195    corrections
196}
197
198/// Search for origin_shift such that (trans_mat, origin_shift) transforms <prim_operations> into <db_prim_generators>
199pub fn match_origin_shift(
200    prim_operations: &Operations,
201    trans_mat: &UnimodularLinear,
202    db_prim_generators: &Operations,
203    epsilon: f64,
204) -> Option<OriginShift> {
205    let new_prim_operations =
206        UnimodularTransformation::from_linear(*trans_mat).transform_operations(prim_operations);
207    let mut hm_translations = HashMap::new();
208    for operation in new_prim_operations.iter() {
209        hm_translations.insert(operation.rotation, operation.translation);
210    }
211
212    // Find origin_shift `c`: (P, c)^-1 G (P, c) = G_db
213    //     (P, c) = (P, 0) (P, 0)^-1 (P, c) = (P, 0) (E, P^-1 c)
214    //     s := P^-1 c
215    //     G' := (P, 0)^-1 G (P, 0)
216    //     (E, s)^-1 G' (E, s) = G_db
217    // Solve (E, s)^-1 (R, t_target) (E, s) = (R, t_db) (mod 1) (for all (R, t_db) in db_prim_generators)
218    //     (R, R * s - s + t_target) = (R, t_db) (mod 1)
219    //     <-> (R - E) * s = t_db - t_target (mod 1)
220    let mut a = OMatrix::<i32, Dyn, U3>::zeros(3 * db_prim_generators.len());
221    let mut b = OVector::<f64, Dyn>::zeros(3 * db_prim_generators.len());
222    for (k, operation) in db_prim_generators.iter().enumerate() {
223        // Correction transformation matrix may not be normalizer of the point group. For example, mm2 -> 2mm
224        let target_translation = hm_translations.get(&operation.rotation)?;
225
226        let ak = operation.rotation - Matrix3::<i32>::identity();
227        let bk = operation.translation - target_translation;
228        for i in 0..3 {
229            for j in 0..3 {
230                a[(3 * k + i, j)] = ak[(i, j)];
231            }
232            b[3 * k + i] = bk[i];
233        }
234    }
235
236    match solve_mod1(&a, &b, epsilon) {
237        Some(s) => {
238            let origin_shift = (trans_mat.map(|e| e as f64) * s).map(|e| e % 1.);
239            Some(origin_shift)
240        }
241        None => None,
242    }
243}
244
245/// Solve a * x = b (mod 1)
246pub fn solve_mod1(
247    a: &OMatrix<i32, Dyn, U3>,
248    b: &OVector<f64, Dyn>,
249    epsilon: f64,
250) -> Option<Vector3<f64>> {
251    // Solve snf.d * y = snf.l * b (x = snf.r * y)
252    let snf = SNF::new(a);
253    let mut y = Vector3::<f64>::zeros();
254    let lb = snf.l.map(|e| e as f64) * b;
255    for i in 0..3 {
256        if snf.d[(i, i)] == 0 {
257            let lbi = lb[i] - lb[i].round();
258            if lbi.abs() > epsilon {
259                return None;
260            }
261        } else {
262            y[i] = lb[i] / (snf.d[(i, i)] as f64);
263        }
264    }
265
266    let x = (snf.r.map(|e| e as f64) * y).map(|e| e % 1.);
267
268    // Check solution
269    let mut residual = a.map(|e| e as f64) * x - b;
270    residual -= residual.map(|e| e.round()); // in [-0.5, 0.5]
271    if residual.iter().any(|e| e.abs() > epsilon) {
272        return None;
273    }
274
275    Some(x)
276}
277
278#[cfg(test)]
279mod tests {
280    use nalgebra::{Dyn, OMatrix, OVector, RowVector3, U3, matrix, vector};
281    use rstest::rstest;
282    use std::collections::HashMap;
283
284    use crate::base::{EPS, UnimodularTransformation};
285    use crate::data::{HallSymbol, Setting, hall_symbol_entry};
286
287    use super::{SpaceGroup, correction_transformation_matrices, solve_mod1};
288
289    #[test]
290    fn test_solve_mod1() {
291        let a = OMatrix::<i32, Dyn, U3>::from_rows(&[
292            RowVector3::new(-2, 0, 0),
293            RowVector3::new(0, -2, 0),
294            RowVector3::new(0, 0, -2),
295            RowVector3::new(-2, 0, 0),
296            RowVector3::new(0, 0, 0),
297            RowVector3::new(0, 0, -2),
298        ]);
299        let b = OVector::<f64, Dyn>::from_row_slice(&[0.0, 0.0, 0.0, 0.0, 0.5, 0.0]);
300        let x = solve_mod1(&a, &b, EPS);
301        assert_eq!(x, None);
302    }
303
304    #[test]
305    fn test_correction_transformation_matrices() {
306        let hall_number = 21; // P 1 c 1
307        let hall_symbol = HallSymbol::from_hall_number(hall_number).unwrap();
308        let prim_operations = hall_symbol.primitive_traverse();
309
310        // The correction transformation matrices should change the group into P1c1, P1a1, and P1n1
311        let entry = hall_symbol_entry(hall_number).unwrap();
312        let corrections = correction_transformation_matrices(entry.arithmetic_number);
313        let expects = vec![
314            vector![0.0, 0.0, 0.5],
315            vector![0.5, 0.0, 0.0],
316            vector![-0.5, 0.0, -0.5],
317        ];
318        for (i, corr) in corrections.iter().enumerate() {
319            let corr_prim_operations =
320                UnimodularTransformation::from_linear(*corr).transform_operations(&prim_operations);
321
322            let mut hm_translations = HashMap::new();
323            for operation in corr_prim_operations.iter() {
324                hm_translations.insert(operation.rotation, operation.translation);
325            }
326            let r = matrix![
327                1, 0, 0;
328                0, -1, 0;
329                0, 0, 1;
330            ];
331            assert_relative_eq!(*hm_translations.get(&r).unwrap(), expects[i]);
332        }
333    }
334
335    #[rstest]
336    #[case(Setting::Spglib)]
337    #[case(Setting::Standard)]
338    fn test_identify_space_group(#[case] setting: Setting) {
339        for hall_number in 1..=530 {
340            let hall_symbol = HallSymbol::from_hall_number(hall_number).unwrap();
341            let prim_operations = hall_symbol.primitive_traverse();
342
343            let space_group = SpaceGroup::new(&prim_operations, setting, 1e-8).unwrap();
344
345            // Check space group type
346            let entry = hall_symbol_entry(hall_number).unwrap();
347            assert_eq!(space_group.number, entry.number);
348
349            // Check transformation matrix
350            assert_eq!(
351                space_group
352                    .transformation
353                    .linear_as_f64()
354                    .determinant()
355                    .round() as i32,
356                1
357            );
358
359            let matched_hall_symbol =
360                HallSymbol::from_hall_number(space_group.hall_number).unwrap();
361            let matched_prim_operations = matched_hall_symbol.primitive_traverse();
362
363            let mut hm_translations = HashMap::new();
364            for operation in matched_prim_operations.iter() {
365                hm_translations.insert(operation.rotation, operation.translation);
366            }
367
368            // Check transformation
369            let transformed_prim_operations = space_group
370                .transformation
371                .transform_operations(&prim_operations);
372            assert_eq!(
373                matched_prim_operations.len(),
374                transformed_prim_operations.len()
375            );
376            for operation in transformed_prim_operations.iter() {
377                assert!(hm_translations.contains_key(&operation.rotation));
378                let mut diff =
379                    *hm_translations.get(&operation.rotation).unwrap() - operation.translation;
380                diff -= diff.map(|e| e.round()); // in [-0.5, 0.5]
381                assert_relative_eq!(diff, vector![0.0, 0.0, 0.0])
382            }
383        }
384    }
385}