use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use crate::cyclotomic::IsRing;
use crate::cyclotomic::geometry::cmp_xy;
use crate::cyclotomic::geometry::float::{cross_f, norm2_f};
use crate::geom::iso::Iso;
pub(crate) const PARALLEL_EPS: f64 = 1e-9;
pub(crate) fn independent_pair<T: IsRing>(vs: impl IntoIterator<Item = T>) -> Option<(T, T)> {
let mut first: Option<T> = None;
for v in vs {
if v.xy() == (0.0, 0.0) {
continue;
}
match first {
None => first = Some(v),
Some(a) => {
if cross_f(&a, &v).abs() > PARALLEL_EPS {
return Some((a, v));
}
}
}
}
None
}
pub(crate) fn lattice_from_orbit<T: IsRing>(placements: &[Iso<T>]) -> Option<(T, T)> {
let mut trans: Vec<(f64, T)> = placements
.iter()
.filter(|p| p.rot == 0)
.map(|p| (norm2_f(&p.shift), p.shift))
.filter(|(d, _)| *d > PARALLEL_EPS)
.collect();
trans.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
independent_pair(trans.into_iter().map(|(_, v)| v))
}
pub(crate) fn basis_inverse<T: IsRing>(v1: &T, v2: &T) -> Option<[[f64; 2]; 2]> {
let (v1x, v1y) = v1.xy();
let (v2x, v2y) = v2.xy();
let det = v1x * v2y - v1y * v2x;
if det.abs() < PARALLEL_EPS {
return None;
}
Some([[v2y / det, -v2x / det], [-v1y / det, v1x / det]])
}
pub(crate) fn in_lattice<T: IsRing>(d: T, v1: T, v2: T, inv: &[[f64; 2]; 2]) -> bool {
let (dx, dy) = d.xy();
let m1 = (inv[0][0] * dx + inv[0][1] * dy).round() as i64;
let m2 = (inv[1][0] * dx + inv[1][1] * dy).round() as i64;
d == v1.scale(m1) + v2.scale(m2)
}
pub(crate) fn gauss_reduce<T: IsRing>(mut a: T, mut b: T) -> Option<(T, T)> {
if cross_f(&a, &b).abs() < PARALLEL_EPS {
return None;
}
let dot = |u: &T, w: &T| {
let (ux, uy) = u.xy();
let (wx, wy) = w.xy();
ux * wx + uy * wy
};
for _ in 0..64 {
if norm2_f(&a) > norm2_f(&b) {
std::mem::swap(&mut a, &mut b);
}
let mu = (dot(&a, &b) / norm2_f(&a)).round() as i64;
if mu == 0 {
break;
}
b = b - a.scale(mu);
}
Some((a, b))
}
pub(crate) fn signature<T: IsRing>(tile_verts: &[T]) -> (Vec<T>, T) {
let anchor = tile_verts
.iter()
.copied()
.min_by(|a, b| cmp_xy(a, b))
.expect("tile has vertices");
let mut norm: Vec<T> = tile_verts.iter().map(|&x| x - anchor).collect();
norm.sort_by(|a, b| cmp_xy(a, b));
(norm, anchor)
}
pub(crate) fn signature_groups<T: IsRing>(
placements: &[Iso<T>],
verts: &[T],
) -> HashMap<Vec<T>, Vec<(T, Iso<T>)>> {
let mut groups: HashMap<Vec<T>, Vec<(T, Iso<T>)>> = HashMap::default();
for iso in placements {
let (sig, anchor) = signature(&iso.tile(verts));
groups.entry(sig).or_default().push((anchor, *iso));
}
groups
}
pub(crate) fn lattice_classes<T: IsRing>(
groups: &HashMap<Vec<T>, Vec<(T, Iso<T>)>>,
v1: T,
v2: T,
inv: &[[f64; 2]; 2],
) -> Vec<(usize, Iso<T>)> {
let mut classes: Vec<(usize, Iso<T>)> = Vec::new();
for members in groups.values() {
let mut group: Vec<(T, usize, Iso<T>)> = Vec::new();
for &(m, iso) in members {
if let Some(slot) = group
.iter_mut()
.find(|(a, _, _)| in_lattice(m - *a, v1, v2, inv))
{
slot.1 += 1;
} else {
group.push((m, 1, iso));
}
}
classes.extend(group.into_iter().map(|(_, c, iso)| (c, iso)));
}
classes
}
pub(crate) fn lay_lattice_block<T: IsRing>(
domain: &[Iso<T>],
v1: T,
v2: T,
reach1: i64,
reach2: i64,
) -> Vec<Iso<T>> {
let mut seen: HashSet<Iso<T>> = HashSet::default();
let mut orbit: Vec<Iso<T>> = Vec::new();
for d in domain {
for i in -reach1..=reach1 {
for j in -reach2..=reach2 {
let iso = Iso {
rot: d.rot,
shift: d.shift + v1.scale(i) + v2.scale(j),
};
if seen.insert(iso) {
orbit.push(iso);
}
}
}
}
orbit
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cyclotomic::ZZ12;
use crate::cyclotomic::traits::{SymNum, Units};
#[test]
fn gauss_reduce_shrinks_and_preserves_lattice() {
let u0 = ZZ12::unit(0);
let u3 = ZZ12::unit(3);
let (a, b) = (u0, u0.scale(1000) + u3);
let covol_before = cross_f(&a, &b).abs();
let (ra, rb) = gauss_reduce(a, b).expect("independent pair reduces");
assert!(
(cross_f(&ra, &rb).abs() - covol_before).abs() < 1e-9,
"same covolume"
);
assert!(
norm2_f(&ra).max(norm2_f(&rb)) < 2.0,
"skew removed: both vectors short"
);
assert!(gauss_reduce(u0, u0.scale(7)).is_none());
}
}