use itertools::iproduct;
use log::warn;
use nalgebra::{Matrix2, Matrix3, Vector2};
use once_cell::sync::Lazy;
use crate::base::{EPS, Lattice, Operations, UnimodularLinear, UnimodularTransformation};
use crate::data::Centering;
use crate::identify::match_origin_shift;
use crate::math::minkowski_reduce_2d;
pub(super) static AXIS_PERMUTATIONS3: Lazy<Vec<UnimodularLinear>> = Lazy::new(|| {
vec![
Matrix3::new(1, 0, 0, 0, 1, 0, 0, 0, 1), Matrix3::new(0, 1, 0, 0, 0, 1, 1, 0, 0), Matrix3::new(0, 0, 1, 1, 0, 0, 0, 1, 0), Matrix3::new(0, 1, 0, -1, 0, 0, 0, 0, 1), Matrix3::new(-1, 0, 0, 0, 0, 1, 0, 1, 0), Matrix3::new(0, 0, 1, 0, 1, 0, -1, 0, 0), ]
});
pub(super) fn monoclinic_candidate_corrections(conv_lattice: &Lattice) -> Vec<UnimodularLinear> {
let unique_axis = monoclinic_unique_axis(conv_lattice);
let i = (unique_axis + 1) % 3;
let j = (unique_axis + 2) % 3;
let basis = conv_lattice.basis; let vi = basis.column(i).into_owned();
let vj = basis.column(j).into_owned();
let e1 = vi.normalize();
let e2 = (vj - vj.dot(&e1) * e1).normalize();
let basis_2d = Matrix2::new(vi.dot(&e1), vj.dot(&e1), vi.dot(&e2), vj.dot(&e2));
let (reduced_2d, trans_2d) = minkowski_reduce_2d(&basis_2d);
let c1 = Vector2::new(trans_2d[(0, 0)], trans_2d[(1, 0)]);
let mut c2 = Vector2::new(trans_2d[(0, 1)], trans_2d[(1, 1)]);
if reduced_2d.column(0).dot(&reduced_2d.column(1)) > 0.0 {
c2 = -c2;
}
let c3 = -(c1 + c2);
let triple = [c1, c2, c3];
let mut candidates = vec![];
for (x, y) in iproduct!(0..3, 0..3).filter(|(x, y)| x != y) {
for (sign_x, sign_y, sign_unique) in iproduct!([1, -1], [1, -1], [1, -1]) {
let mut corr = UnimodularLinear::zeros();
corr[(i, i)] = sign_x * triple[x][0];
corr[(j, i)] = sign_x * triple[x][1];
corr[(i, j)] = sign_y * triple[y][0];
corr[(j, j)] = sign_y * triple[y][1];
corr[(unique_axis, unique_axis)] = sign_unique;
candidates.push(corr);
}
}
candidates
}
fn monoclinic_unique_axis(conv_lattice: &Lattice) -> usize {
let lc = conv_lattice.lattice_constant();
(0..3)
.max_by(|&i, &j| {
(lc[3 + i] - 90.0)
.abs()
.partial_cmp(&(lc[3 + j] - 90.0).abs())
.unwrap()
})
.unwrap()
}
pub(super) fn monoclinic_rank_key(lattice: &Lattice) -> Vec<f64> {
let lc = lattice.lattice_constant();
let cos_angles = lc[3..]
.iter()
.map(|angle_deg| angle_deg.to_radians().cos())
.collect::<Vec<_>>();
let skewness = cos_angles.iter().map(|cos| cos.abs()).sum::<f64>();
let signed_cos_sum = cos_angles.iter().sum::<f64>();
vec![skewness, signed_cos_sum, lc[0], lc[1], lc[2]]
}
pub(super) fn orthorhombic_rank_key(lattice: &Lattice) -> Vec<f64> {
let lc = lattice.lattice_constant();
vec![lc[0], lc[1], lc[2]]
}
pub(super) fn select_conventional_correction<F>(
conv_lattice: &Lattice,
centering: Centering,
prim_std_operations: &Operations,
db_prim_generators: &Operations,
candidates: &[UnimodularLinear],
rank_key: F,
epsilon: f64,
) -> UnimodularTransformation
where
F: Fn(&Lattice) -> Vec<f64>,
{
let q = centering.linear().map(|e| e as f64);
let q_inv = q.try_inverse().unwrap();
let mut best: Option<(Vec<f64>, UnimodularTransformation)> = None;
for corr in candidates {
let corr_f64 = corr.map(|e| e as f64);
if corr_f64.determinant().round() as i32 != 1 {
continue;
}
if !preserves_centering(corr, centering, epsilon) {
continue;
}
let prim_corr_f64 = q * corr_f64 * q_inv;
let prim_corr = prim_corr_f64.map(|e| e.round() as i32);
if (prim_corr_f64 - prim_corr.map(|e| e as f64)).abs().max() > epsilon {
continue;
}
let Some(origin_shift) =
match_origin_shift(prim_std_operations, &prim_corr, db_prim_generators, epsilon)
else {
continue;
};
let key =
rank_key(&UnimodularTransformation::from_linear(*corr).transform_lattice(conv_lattice));
let is_better = match &best {
Some((best_key, _)) => lexicographic_less(&key, best_key),
None => true,
};
if is_better {
best = Some((key, UnimodularTransformation::new(prim_corr, origin_shift)));
}
}
match best {
Some((_, correction)) => correction,
None => {
warn!("No admissible correction of the conventional cell; keep the identified one");
UnimodularTransformation::from_linear(UnimodularLinear::identity())
}
}
}
fn preserves_centering(linear: &UnimodularLinear, centering: Centering, epsilon: f64) -> bool {
let lattice_points = centering.lattice_points();
let linear_f64 = linear.map(|e| e as f64);
lattice_points.iter().all(|translation| {
let mapped = linear_f64 * translation;
lattice_points.iter().any(|other| {
let mut diff = mapped - other;
diff -= diff.map(|e| e.round()); diff.iter().all(|e| e.abs() <= epsilon)
})
})
}
fn lexicographic_less(lhs: &[f64], rhs: &[f64]) -> bool {
for (l, r) in lhs.iter().zip(rhs.iter()) {
if (l - r).abs() < EPS {
continue;
}
return l < r;
}
false
}