use nalgebra::Matrix3;
use nalgebra::linalg::QR;
use super::standardize::{
align_primitive_permutations, assign_wyckoffs_by_orbit, group_sites_by_orbit,
match_wyckoff_coordinates, symmetrize_positions,
};
use crate::base::{
EPS, Lattice, Lattice2D, LayerCell, LayerLattice, Linear, MoyoError, Operations, Permutation,
Rotations, Transformation, UnimodularTransformation, project_rotations,
};
use crate::data::{
LayerHallNumber, LayerHallSymbol, LayerLatticeSystem, LayerWyckoffPosition,
iter_layer_wyckoff_positions, layer_arithmetic_crystal_class_entry, layer_hall_symbol_entry,
};
use crate::identify::LayerGroup;
pub(crate) struct StandardizedLayerCell {
pub prim_layer_cell: LayerCell,
pub prim_transformation: UnimodularTransformation,
pub layer_cell: LayerCell,
pub wyckoffs: Vec<LayerWyckoffPosition>,
pub transformation: Transformation,
pub rotation_matrix: Matrix3<f64>,
pub site_mapping: Vec<usize>,
}
impl StandardizedLayerCell {
pub fn new(
prim_layer_cell: &LayerCell,
prim_layer_operations: &Operations,
prim_layer_permutations: &[Permutation],
layer_group: &LayerGroup,
symprec: f64,
epsilon: f64,
rotate_basis: bool,
) -> Result<Self, MoyoError> {
let (
prim_std_layer_cell,
prim_std_permutations,
prim_transformation,
std_layer_cell,
transformation,
rotation_matrix,
site_mapping,
) = Self::standardize_and_symmetrize_cell(
prim_layer_cell,
prim_layer_operations,
prim_layer_permutations,
layer_group,
epsilon,
rotate_basis,
)?;
let wyckoffs = Self::assign_wyckoffs(
&prim_std_layer_cell,
&prim_std_permutations,
&std_layer_cell,
&site_mapping,
layer_group.hall_number,
symprec,
)?;
Ok(StandardizedLayerCell {
prim_layer_cell: prim_std_layer_cell,
prim_transformation,
layer_cell: std_layer_cell,
wyckoffs,
transformation,
rotation_matrix,
site_mapping,
})
}
#[allow(clippy::type_complexity)]
fn standardize_and_symmetrize_cell(
prim_layer_cell: &LayerCell,
prim_layer_operations: &Operations,
prim_layer_permutations: &[Permutation],
layer_group: &LayerGroup,
epsilon: f64,
rotate_basis: bool,
) -> Result<
(
LayerCell,
Vec<Permutation>,
UnimodularTransformation,
LayerCell,
Transformation,
Matrix3<f64>,
Vec<usize>,
),
MoyoError,
> {
let entry = layer_hall_symbol_entry(layer_group.hall_number)
.ok_or(MoyoError::StandardizationError)?;
let lh_symbol = LayerHallSymbol::from_hall_number(layer_group.hall_number)
.ok_or(MoyoError::StandardizationError)?;
let (conv_std_operations, prim_std_operations) =
lh_symbol.traverse_and_primitive_traverse();
let lattice_system = layer_arithmetic_crystal_class_entry(entry.arithmetic_number)
.ok_or(MoyoError::StandardizationError)?
.layer_lattice_system();
let prim_transformation = match lattice_system {
LayerLatticeSystem::Oblique => {
standardize_oblique_layer_cell(prim_layer_cell, &layer_group.transformation)
}
_ => layer_group.transformation.clone(),
};
let prim_std_cell_tmp = prim_transformation.transform_layer_cell(prim_layer_cell);
let prim_std_permutations = align_primitive_permutations(
&prim_transformation,
prim_layer_operations,
prim_layer_permutations,
&prim_std_operations,
)?;
let new_prim_std_positions = symmetrize_positions(
prim_std_cell_tmp.positions(),
&prim_std_operations,
&prim_std_permutations,
epsilon,
);
let prim_std_cell = LayerCell::new_unchecked(
prim_std_cell_tmp.lattice().clone(),
new_prim_std_positions,
prim_std_cell_tmp.numbers().to_vec(),
);
let conv_trans_linear: Linear = entry.centering.linear();
let (std_cell, site_mapping) =
Transformation::from_linear(conv_trans_linear).transform_layer_cell(&prim_std_cell);
let transformation = Transformation::new(
prim_transformation.linear * conv_trans_linear,
prim_transformation.origin_shift,
);
if rotate_basis {
let layer_rotations = project_rotations(&conv_std_operations);
let (new_inner_lattice, rotation_matrix) =
symmetrize_layer_lattice(&std_cell.lattice().as_lattice(), &layer_rotations);
let new_prim_inner_lattice = prim_std_cell
.lattice()
.as_lattice()
.rotate(&rotation_matrix);
let prim_rotated = LayerCell::new_unchecked(
LayerLattice::new_unchecked(new_prim_inner_lattice),
prim_std_cell.positions().to_vec(),
prim_std_cell.numbers().to_vec(),
);
let std_rotated = LayerCell::new_unchecked(
LayerLattice::new_unchecked(new_inner_lattice),
std_cell.positions().to_vec(),
std_cell.numbers().to_vec(),
);
Ok((
prim_rotated,
prim_std_permutations,
prim_transformation,
std_rotated,
transformation,
rotation_matrix,
site_mapping,
))
} else {
Ok((
prim_std_cell,
prim_std_permutations,
prim_transformation,
std_cell,
transformation,
Matrix3::identity(),
site_mapping,
))
}
}
fn assign_wyckoffs(
prim_std_cell: &LayerCell,
prim_std_permutations: &[Permutation],
std_cell: &LayerCell,
site_mapping: &[usize],
hall_number: LayerHallNumber,
symprec: f64,
) -> Result<Vec<LayerWyckoffPosition>, MoyoError> {
let group = group_sites_by_orbit(
prim_std_cell.num_atoms(),
prim_std_permutations,
site_mapping,
std_cell.num_atoms(),
);
let lattice = std_cell.lattice().as_lattice();
assign_wyckoffs_by_orbit(&group, std_cell.positions(), |position, multiplicity| {
iter_layer_wyckoff_positions(hall_number, multiplicity)
.find(|w| match_wyckoff_coordinates(position, w.coordinates, &lattice, symprec))
.cloned()
})
}
}
fn standardize_oblique_layer_cell(
prim_layer_cell: &LayerCell,
transformation_to_prim_std: &UnimodularTransformation,
) -> UnimodularTransformation {
let lattice_after =
transformation_to_prim_std.transform_lattice(&prim_layer_cell.lattice().as_lattice());
let lifted: Linear = match Lattice2D::lift_inplane_minkowski_reduce(&lattice_after.basis) {
Ok(t) => t,
Err(_) => return transformation_to_prim_std.clone(),
};
UnimodularTransformation::new(
lifted * transformation_to_prim_std.linear,
transformation_to_prim_std.origin_shift,
)
}
fn symmetrize_layer_lattice(lattice: &Lattice, rotations: &Rotations) -> (Lattice, Matrix3<f64>) {
let metric_tensor = lattice.metric_tensor();
let mut sym_metric: Matrix3<f64> = rotations
.iter()
.map(|rotation| {
rotation.transpose().map(|e| e as f64) * metric_tensor * rotation.map(|e| e as f64)
})
.sum();
sym_metric /= rotations.len() as f64;
sym_metric[(0, 2)] = 0.0;
sym_metric[(2, 0)] = 0.0;
sym_metric[(1, 2)] = 0.0;
sym_metric[(2, 1)] = 0.0;
let g11 = sym_metric[(0, 0)];
let g22 = sym_metric[(1, 1)];
let g33 = sym_metric[(2, 2)];
let g12 = sym_metric[(0, 1)];
let ax = g11.sqrt();
let bx = if ax.abs() > EPS { g12 / ax } else { 0.0 };
let by = (g22 - bx * bx).max(0.0).sqrt();
let cz_mag = g33.sqrt();
let cz = if lattice.basis.determinant() < 0.0 {
-cz_mag
} else {
cz_mag
};
let new_basis = Matrix3::new(
ax, bx, 0.0, 0.0, by, 0.0, 0.0, 0.0, cz,
);
let mut rotation_matrix = QR::new(new_basis * lattice.basis.try_inverse().unwrap()).q();
if rotation_matrix.determinant() < 0.0 {
rotation_matrix *= -1.0;
}
(Lattice { basis: new_basis }, rotation_matrix)
}
#[cfg(test)]
mod tests {
use nalgebra::{Vector3, matrix};
use super::*;
use crate::base::{AngleTolerance, Cell, Lattice, traverse};
use crate::data::{GeometricCrystalClass, LayerSetting, PointGroupRepresentative};
use crate::search::{LayerPrimitiveCell, LayerPrimitiveSymmetrySearch};
const SYMPREC: f64 = 1e-4;
fn run_layer_pipeline(
cell: Cell,
setting: LayerSetting,
) -> (StandardizedLayerCell, LayerGroup) {
let layer = LayerCell::new(cell, SYMPREC, AngleTolerance::Default).unwrap();
let primitive = LayerPrimitiveCell::new(&layer, SYMPREC).unwrap();
let symmetry = LayerPrimitiveSymmetrySearch::new(
&primitive.layer_cell,
SYMPREC,
AngleTolerance::Default,
)
.unwrap();
let volume = primitive.layer_cell.lattice().basis().determinant().abs();
let epsilon = SYMPREC / volume.powf(1.0 / 3.0);
let layer_group =
LayerGroup::new(&symmetry.operations, setting, epsilon).expect("identification failed");
let standardized = StandardizedLayerCell::new(
&primitive.layer_cell,
&symmetry.operations,
&symmetry.permutations,
&layer_group,
SYMPREC,
epsilon,
true,
)
.expect("standardization failed");
(standardized, layer_group)
}
#[test]
fn test_layer_p_minus_1_standardize_round_trip() {
let gamma = 80.0_f64.to_radians();
let a = 1.0;
let b = 1.5;
let cell = Cell::new(
Lattice::new(matrix![
a, b * gamma.cos(), 0.0;
0.0, b * gamma.sin(), 0.0;
0.0, 0.0, 5.0;
]),
vec![Vector3::new(0.1, 0.2, 0.1), Vector3::new(0.3, 0.5, 0.2)],
vec![1, 1],
);
let (std, lg) = run_layer_pipeline(cell, LayerSetting::Standard);
assert_eq!(lg.number, 2);
assert_relative_eq!(
std.layer_cell.lattice().basis().column(2).norm(),
5.0,
epsilon = 1e-10
);
assert_relative_eq!(
std.layer_cell.lattice().basis()[(2, 2)],
5.0,
epsilon = 1e-10
);
assert_relative_eq!(
std.layer_cell.lattice().basis()[(0, 2)],
0.0,
epsilon = 1e-10
);
assert_relative_eq!(
std.layer_cell.lattice().basis()[(1, 2)],
0.0,
epsilon = 1e-10
);
assert_eq!(std.wyckoffs.len(), 2);
for w in std.wyckoffs.iter() {
assert_eq!(w.letter, 'e');
assert_eq!(w.multiplicity, 2);
assert_eq!(w.site_symmetry, "1");
}
}
#[test]
fn test_layer_p2_per_m_standardize_origin_atom() {
let gamma = 80.0_f64.to_radians();
let a = 1.0;
let b = 1.5;
let cell = Cell::new(
Lattice::new(matrix![
a, b * gamma.cos(), 0.0;
0.0, b * gamma.sin(), 0.0;
0.0, 0.0, 5.0;
]),
vec![Vector3::new(0.0, 0.0, 0.13)],
vec![1],
);
let (std, lg) = run_layer_pipeline(cell, LayerSetting::Standard);
assert_eq!(lg.number, 6);
assert_eq!(std.wyckoffs.len(), 1);
assert_eq!(std.wyckoffs[0].letter, 'a');
assert_eq!(std.wyckoffs[0].site_symmetry, "2/m");
}
#[test]
fn test_layer_p4_per_mmm_standardize_high_symmetry_site() {
let cell = Cell::new(
Lattice::new(matrix![
1.0, 0.0, 0.0;
0.0, 1.0, 0.0;
0.0, 0.0, 5.0;
]),
vec![Vector3::new(0.0, 0.0, 0.1)],
vec![1],
);
let (std, lg) = run_layer_pipeline(cell, LayerSetting::Standard);
assert_eq!(lg.number, 61);
let a = std.layer_cell.lattice().basis().column(0).norm();
let b = std.layer_cell.lattice().basis().column(1).norm();
assert_relative_eq!(a, b, epsilon = 1e-10);
assert_relative_eq!(
std.layer_cell.lattice().basis().column(2).norm(),
5.0,
epsilon = 1e-10
);
assert_eq!(std.wyckoffs.len(), 1);
assert_eq!(std.wyckoffs[0].letter, 'a');
assert_eq!(std.wyckoffs[0].site_symmetry, "4/mmm");
}
#[test]
fn test_standardize_orthogonalizes_inplane_block_against_c() {
let cell = Cell::new(
Lattice::new(matrix![
1.0, 0.3, 0.0;
0.0, 1.05, 0.0;
0.0, 0.0, 5.0;
]),
vec![Vector3::new(0.2, 0.3, 0.1)],
vec![1],
);
let (std, _) = run_layer_pipeline(cell, LayerSetting::Standard);
let basis = std.layer_cell.lattice().basis();
assert_relative_eq!(basis[(2, 0)], 0.0, epsilon = 1e-10);
assert_relative_eq!(basis[(2, 1)], 0.0, epsilon = 1e-10);
assert_relative_eq!(basis[(0, 2)], 0.0, epsilon = 1e-10);
assert_relative_eq!(basis[(1, 2)], 0.0, epsilon = 1e-10);
}
#[test]
fn test_layer_p2_per_m_two_atoms_share_wyckoff() {
let cell = Cell::new(
Lattice::new(matrix![
1.0, 0.2, 0.0;
0.0, 1.3, 0.0;
0.0, 0.0, 5.0;
]),
vec![
Vector3::new(0.31, 0.42, 0.07),
Vector3::new(-0.31, -0.42, 0.07),
],
vec![1, 1],
);
let (std, lg) = run_layer_pipeline(cell, LayerSetting::Standard);
assert_eq!(lg.number, 6);
assert_eq!(std.layer_cell.num_atoms(), 2);
assert_eq!(std.wyckoffs.len(), 2);
assert_eq!(std.wyckoffs[0].letter, std.wyckoffs[1].letter);
assert_eq!(std.wyckoffs[0].multiplicity, 2);
}
#[test]
fn test_symmetrize_layer_lattice_preserves_handedness() {
let lattice_rh = Lattice::new(matrix![
1.0, 0.0, 0.0;
0.0, 1.0, 0.0;
0.0, 0.0, 5.0;
]);
assert!(lattice_rh.basis.determinant() > 0.0);
let rotations = vec![nalgebra::Matrix3::<i32>::identity()];
let (std_rh, rot_rh) = symmetrize_layer_lattice(&lattice_rh, &rotations);
assert!(std_rh.basis[(2, 2)] > 0.0);
assert_relative_eq!(rot_rh.determinant(), 1.0, epsilon = 1e-10);
let lattice_lh = Lattice::new(matrix![
1.0, 0.0, 0.0;
0.0, 1.0, 0.0;
0.0, 0.0, -5.0;
]);
assert!(lattice_lh.basis.determinant() < 0.0);
let (std_lh, rot_lh) = symmetrize_layer_lattice(&lattice_lh, &rotations);
assert!(std_lh.basis[(2, 2)] < 0.0);
assert_relative_eq!(std_lh.basis[(2, 2)].abs(), 5.0, epsilon = 1e-10);
assert_relative_eq!(rot_lh.determinant(), 1.0, epsilon = 1e-10);
}
#[test]
fn test_symmetrize_layer_lattice_square() {
let lattice = Lattice::new(matrix![
1.001, 0.0, 0.0;
0.0, 0.999, 0.0;
0.0, 0.0, 7.0;
]);
let rep =
PointGroupRepresentative::from_geometric_crystal_class(GeometricCrystalClass::C4v);
let rotations = traverse(&rep.generators);
let (new_lattice, rotation_matrix) = symmetrize_layer_lattice(&lattice, &rotations);
assert_relative_eq!(new_lattice.basis[(0, 0)], new_lattice.basis[(1, 1)]);
assert_relative_eq!(new_lattice.basis[(2, 2)], 7.0, epsilon = 1e-12);
assert_relative_eq!(new_lattice.basis[(0, 2)], 0.0);
assert_relative_eq!(new_lattice.basis[(1, 2)], 0.0);
assert_relative_eq!(new_lattice.basis[(2, 0)], 0.0);
assert_relative_eq!(new_lattice.basis[(2, 1)], 0.0);
assert_relative_eq!(new_lattice.basis[(1, 0)], 0.0, epsilon = 1e-12);
assert_relative_eq!(rotation_matrix.determinant(), 1.0, epsilon = 1e-8);
}
#[test]
fn test_symmetrize_layer_lattice_hexagonal_preserves_c() {
let lattice = Lattice::new(matrix![
1.0, 0.0, 0.0;
-0.5, (3.0_f64).sqrt() / 2.0, 0.0;
0.0, 0.0, 5.0;
]);
let rep =
PointGroupRepresentative::from_geometric_crystal_class(GeometricCrystalClass::C6v);
let rotations = traverse(&rep.generators);
let (new_lattice, _) = symmetrize_layer_lattice(&lattice, &rotations);
assert_relative_eq!(new_lattice.basis[(2, 2)], 5.0, epsilon = 1e-12);
let a = new_lattice.basis[(0, 0)];
assert!(a > 0.0);
let bx = new_lattice.basis[(0, 1)];
let by = new_lattice.basis[(1, 1)];
assert_relative_eq!((bx * bx + by * by).sqrt(), a, epsilon = 1e-10);
assert_relative_eq!(bx / a, -0.5, epsilon = 1e-10);
}
}