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 pub transformation: UnimodularTransformation,
24}
25
26impl SpaceGroup {
27 pub fn new(
30 prim_operations: &Operations,
31 setting: Setting,
32 epsilon: f64,
33 ) -> Result<Self, MoyoError> {
34 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 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 let convs = match geometric_crystal_class {
115 GeometricCrystalClass::C2 | GeometricCrystalClass::C1h | GeometricCrystalClass::C2h => {
117 vec![
118 UnimodularLinear::identity(),
119 UnimodularLinear::new(
121 0, 0, -1, 0, 1, 0, 1, 0, -1, ),
125 UnimodularLinear::new(
127 -1, 0, 1, 0, 1, 0, -1, 0, 0, ),
131 ]
132 }
133 GeometricCrystalClass::D2 | GeometricCrystalClass::C2v | GeometricCrystalClass::D2h => {
135 vec![
136 UnimodularLinear::identity(),
138 UnimodularLinear::new(
140 0, 1, 0, 1, 0, 0, 0, 0, -1, ),
144 UnimodularLinear::new(
146 0, 0, 1, 1, 0, 0, 0, 1, 0, ),
150 UnimodularLinear::new(
152 0, 0, -1, 0, 1, 0, 1, 0, 0, ),
156 UnimodularLinear::new(
158 0, 1, 0, 0, 0, 1, 1, 0, 0, ),
162 UnimodularLinear::new(
164 1, 0, 0, 0, 0, -1, 0, 1, 0, ),
168 ]
169 }
170 GeometricCrystalClass::Th => {
172 vec![
173 UnimodularLinear::identity(),
174 UnimodularLinear::new(
175 0, 0, 1, 0, -1, 0, 1, 0, 0, ),
179 ]
180 }
181 _ => vec![UnimodularLinear::identity()],
182 };
183
184 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
198pub 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 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 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
245pub fn solve_mod1(
247 a: &OMatrix<i32, Dyn, U3>,
248 b: &OVector<f64, Dyn>,
249 epsilon: f64,
250) -> Option<Vector3<f64>> {
251 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 let mut residual = a.map(|e| e as f64) * x - b;
270 residual -= residual.map(|e| e.round()); 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; let hall_symbol = HallSymbol::from_hall_number(hall_number).unwrap();
308 let prim_operations = hall_symbol.primitive_traverse();
309
310 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 let entry = hall_symbol_entry(hall_number).unwrap();
347 assert_eq!(space_group.number, entry.number);
348
349 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 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()); assert_relative_eq!(diff, vector![0.0, 0.0, 0.0])
382 }
383 }
384 }
385}