use log::debug;
use serde::Serialize;
use super::layer_point_group::LayerPointGroup;
use super::normalizer::integral_normalizer_2_1;
use crate::base::{
Lattice2D, LayerLattice, MoyoError, Operations, UnimodularTransformation, project_rotations,
};
use crate::data::{
LayerHallNumber, LayerHallSymbol, LayerNumber, LayerSetting, layer_hall_symbol_entry,
};
#[derive(Debug, Clone, Serialize)]
pub struct LayerGroup {
pub number: LayerNumber,
pub hall_number: LayerHallNumber,
pub transformation: UnimodularTransformation,
}
impl LayerGroup {
pub fn new(
prim_layer_operations: &Operations,
setting: LayerSetting,
epsilon: f64,
) -> Result<Self, MoyoError> {
let prim_rotations = project_rotations(prim_layer_operations);
let layer_point_group = LayerPointGroup::new(&prim_rotations)?;
debug!(
"Layer point group: arithmetic_number={}",
layer_point_group.arithmetic_number
);
for hall_number in setting.hall_numbers() {
let entry = layer_hall_symbol_entry(hall_number).unwrap();
if entry.arithmetic_number != layer_point_group.arithmetic_number {
continue;
}
let lh_symbol = LayerHallSymbol::from_hall_number(hall_number)
.ok_or(MoyoError::LayerGroupTypeIdentificationError)?;
let db_prim_generators = lh_symbol.primitive_generators();
let conjugators =
integral_normalizer_2_1(prim_layer_operations, &db_prim_generators, epsilon);
let Some(transformation) = conjugators.into_iter().next() else {
continue;
};
debug!(
"Matched layer Hall number {} (LG {}) via prim_trans_mat {:?}",
hall_number, entry.number, transformation.linear
);
return Ok(Self {
number: entry.number,
hall_number,
transformation,
});
}
Err(MoyoError::LayerGroupTypeIdentificationError)
}
pub fn from_lattice(
lattice: &LayerLattice,
prim_layer_operations: &Operations,
setting: LayerSetting,
epsilon: f64,
) -> Result<Self, MoyoError> {
let reduced_trans_mat = Lattice2D::lift_inplane_minkowski_reduce(lattice.basis())?;
let to_reduced = UnimodularTransformation::from_linear(reduced_trans_mat);
let reduced_prim_operations = to_reduced.transform_operations(prim_layer_operations);
let reduced = Self::new(&reduced_prim_operations, setting, epsilon)?;
Ok(LayerGroup {
number: reduced.number,
hall_number: reduced.hall_number,
transformation: reduced.transformation * to_reduced,
})
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use nalgebra::{Matrix3, matrix, vector};
use super::*;
use crate::base::{AngleTolerance, Lattice};
use crate::data::{LayerHallSymbol, iter_layer_hall_symbol_entry};
#[test]
fn test_round_trip_all_layer_hall_numbers() {
for entry in iter_layer_hall_symbol_entry() {
let lh_symbol = LayerHallSymbol::from_hall_number(entry.hall_number).unwrap();
let prim_operations = lh_symbol.primitive_traverse();
let identified = LayerGroup::new(
&prim_operations,
LayerSetting::HallNumber(entry.hall_number),
1e-8,
)
.unwrap_or_else(|_| {
panic!(
"failed to identify layer hall_number {} (LG {})",
entry.hall_number, entry.number
)
});
assert_eq!(
identified.hall_number, entry.hall_number,
"Hall number mismatch for LG {}",
entry.number
);
assert_eq!(
identified.number, entry.number,
"LG number mismatch for hall_number {}",
entry.hall_number
);
assert_eq!(
identified
.transformation
.linear_as_f64()
.determinant()
.round() as i32,
1,
"non-unimodular transformation for hall_number {}",
entry.hall_number
);
let matched_lh = LayerHallSymbol::from_hall_number(identified.hall_number).unwrap();
let matched_prim_operations = matched_lh.primitive_traverse();
let mut hm_translations = HashMap::new();
for op in matched_prim_operations.iter() {
hm_translations.insert(op.rotation, op.translation);
}
let transformed = identified
.transformation
.transform_operations(&prim_operations);
assert_eq!(
matched_prim_operations.len(),
transformed.len(),
"operation count mismatch for hall_number {}",
entry.hall_number
);
for op in transformed.iter() {
let target = hm_translations.get(&op.rotation).unwrap_or_else(|| {
panic!(
"rotation missing in DB for hall_number {}",
entry.hall_number
)
});
let mut diff = *target - op.translation;
diff -= diff.map(|e| e.round());
assert_relative_eq!(diff, vector![0.0, 0.0, 0.0], epsilon = 1e-8);
}
}
}
#[test]
fn test_round_trip_default_hall_numbers_via_standard_setting() {
let standard_halls = LayerSetting::Standard.hall_numbers();
for &hall_number in &standard_halls {
let lh_symbol = LayerHallSymbol::from_hall_number(hall_number).unwrap();
let prim_operations = lh_symbol.primitive_traverse();
let identified =
LayerGroup::new(&prim_operations, LayerSetting::Standard, 1e-8).unwrap();
assert_eq!(identified.hall_number, hall_number);
}
}
#[test]
fn test_normalizer_aligns_non_canonical_basis() {
let cases = [
(13, 8, 12), (15, 9, 14), (19, 11, 18), (21, 12, 20), (25, 14, 24), (27, 15, 26), (29, 16, 28), (31, 17, 30), (36, 20, 35), (41, 24, 40), (45, 27, 44), (66, 38, 65), ];
for (input_hall, expected_lg, expected_standard_hall) in cases {
let lh_symbol = LayerHallSymbol::from_hall_number(input_hall).unwrap();
let prim_operations = lh_symbol.primitive_traverse();
let identified = LayerGroup::new(&prim_operations, LayerSetting::Standard, 1e-8)
.unwrap_or_else(|_| panic!("non-canonical-basis case hall={} failed", input_hall));
assert_eq!(
identified.number, expected_lg,
"input hall {} (LG {}): wrong LG number",
input_hall, expected_lg
);
assert_eq!(
identified.hall_number, expected_standard_hall,
"input hall {} (LG {}): expected to land on Standard hall {} after correction",
input_hall, expected_lg, expected_standard_hall
);
assert_eq!(
identified
.transformation
.linear_as_f64()
.determinant()
.round() as i32,
1,
"non-unimodular transformation for input hall {}",
input_hall
);
}
}
#[test]
fn test_from_lattice_handles_inplane_skew() {
let canonical_hall: LayerHallNumber = 12;
let lh = LayerHallSymbol::from_hall_number(canonical_hall).unwrap();
let canonical_ops = lh.primitive_traverse();
let shear: Matrix3<i32> = matrix![
1, 4, 0;
0, 1, 0;
0, 0, 1;
];
let to_skewed = UnimodularTransformation::from_linear(shear);
let skewed_ops = to_skewed.transform_operations(&canonical_ops);
let skewed_basis = Matrix3::<f64>::identity() * shear.map(|e| e as f64);
let skewed_lattice = LayerLattice::new(
Lattice {
basis: skewed_basis,
},
1e-4,
AngleTolerance::Default,
)
.unwrap();
let identified =
LayerGroup::from_lattice(&skewed_lattice, &skewed_ops, LayerSetting::Standard, 1e-8)
.unwrap();
assert_eq!(identified.number, 8);
assert_eq!(identified.hall_number, canonical_hall);
assert_eq!(
identified
.transformation
.linear_as_f64()
.determinant()
.round() as i32,
1,
);
}
#[test]
fn test_from_lattice_identity_on_reduced_basis() {
let canonical_hall: LayerHallNumber = 12;
let lh = LayerHallSymbol::from_hall_number(canonical_hall).unwrap();
let canonical_ops = lh.primitive_traverse();
let identity_lattice = LayerLattice::new(
Lattice {
basis: Matrix3::<f64>::identity(),
},
1e-4,
AngleTolerance::Default,
)
.unwrap();
let via_lattice = LayerGroup::from_lattice(
&identity_lattice,
&canonical_ops,
LayerSetting::Standard,
1e-8,
)
.unwrap();
let via_new = LayerGroup::new(&canonical_ops, LayerSetting::Standard, 1e-8).unwrap();
assert_eq!(via_lattice.number, via_new.number);
assert_eq!(via_lattice.hall_number, via_new.hall_number);
}
}