use crate::geom::Point2;
const STEP: f64 = std::f64::consts::FRAC_PI_6;
const QUANT: f64 = 1e6;
pub(crate) fn canonicalise(coords: &mut [Point2], ranks: &[u32]) {
if coords.len() < 2 {
return;
}
let mut order: Vec<usize> = (0..coords.len()).collect();
order.sort_by_key(|i| (ranks[*i], *i));
let n = coords.len() as f64;
let c = order.iter().fold(Point2::ORIGIN, |s, i| s + coords[*i]) * (1.0 / n);
for p in coords.iter_mut() {
*p = *p - c;
}
let mut best: Option<(Key, Vec<Point2>)> = None;
for mirror in [false, true] {
for k in 0..12 {
let cand: Vec<Point2> = coords
.iter()
.map(|p| {
let q = if mirror { Point2::new(p.x, -p.y) } else { *p };
q.rotated(STEP * f64::from(k))
})
.collect();
let key = key_of(&cand, &order);
let take = match &best {
None => true,
Some((b, _)) => key < *b,
};
if take {
best = Some((key, cand));
}
}
}
let (_, chosen) = best.expect("12×2 个候选里必有一个");
coords.copy_from_slice(&chosen);
}
type Key = (i64, Vec<(i64, i64)>);
fn key_of(pts: &[Point2], order: &[usize]) -> Key {
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for p in pts {
x0 = x0.min(p.x);
y0 = y0.min(p.y);
x1 = x1.max(p.x);
y1 = y1.max(p.y);
}
let (w, h) = (x1 - x0, y1 - y0);
let flat = ((h / w.max(1e-9)) * QUANT).round() as i64;
let seq: Vec<(i64, i64)> = order
.iter()
.map(|i| {
(
(pts[*i].x * QUANT).round() as i64,
(pts[*i].y * QUANT).round() as i64,
)
})
.collect();
(flat, seq)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{generate, style::Style};
use omgkit_core::MolBuilder;
fn prep(smi: &str) -> MolBuilder {
let mut m = omgkit_io::smiles::parse(smi).unwrap();
omgkit_chem::pipeline::sanitize(&mut m).unwrap();
m
}
fn canonical_coords(smi: &str, style: &Style) -> Vec<(i64, i64)> {
let m = prep(smi);
let ranks = omgkit_io::canon::canonical_ranks(&m);
let d = generate(&m, style);
let mut order: Vec<usize> = (0..d.coords.len()).collect();
order.sort_by_key(|i| (ranks[*i], *i));
order
.iter()
.map(|i| {
(
(d.coords[*i].x * 1e4).round() as i64,
(d.coords[*i].y * 1e4).round() as i64,
)
})
.collect()
}
#[test]
fn different_writings_give_literally_the_same_coordinates() {
let groups = [
vec![
"CC(=O)Oc1ccccc1C(=O)O",
"O=C(C)Oc1ccccc1C(O)=O",
"OC(=O)c1ccccc1OC(C)=O",
],
vec!["c1ccc2ccccc2c1", "c1ccc2c(c1)cccc2", "c1cc2ccccc2cc1"],
vec!["CC(C)(C)c1ccccc1", "c1ccccc1C(C)(C)C"],
vec!["CCCCO", "OCCCC"],
vec!["CN1C=NC2=C1C(=O)N(C)C(=O)N2C", "Cn1cnc2c1c(=O)n(C)c(=O)n2C"],
];
for style in &Style::ALL {
for ws in &groups {
let a = canonical_coords(ws[0], style);
for w in &ws[1..] {
assert_eq!(
a,
canonical_coords(w, style),
"[{}] {w} 与 {} 坐标不同",
style.name,
ws[0]
);
}
}
}
}
#[test]
fn shuffling_the_storage_order_does_not_move_the_picture() {
let n = 24usize;
let pts: Vec<Point2> = (0..n)
.map(|i| {
#[allow(clippy::suboptimal_flops)]
let t = 0.1 * i as f64 + 0.3;
Point2::new(
t.cos() * (1.0 + 0.1 * i as f64),
t.sin() * (1.0 + 0.7 * i as f64),
)
})
.collect();
let perm: Vec<usize> = (0..n).map(|i| (i * 7 + 3) % n).collect();
let mut a: Vec<Point2> = pts.clone();
let ranks_a: Vec<u32> = (0..n).map(|i| u32::try_from(i).unwrap()).collect();
canonicalise(&mut a, &ranks_a);
let mut b: Vec<Point2> = perm.iter().map(|i| pts[*i]).collect();
let ranks_b: Vec<u32> = perm.iter().map(|i| u32::try_from(*i).unwrap()).collect();
canonicalise(&mut b, &ranks_b);
for (i, &j) in perm.iter().enumerate() {
assert!(
a[j].x.to_bits() == b[i].x.to_bits() && a[j].y.to_bits() == b[i].y.to_bits(),
"换个存储序之后点 {j} 从 {:?} 挪到了 {:?}",
a[j],
b[i]
);
}
}
#[test]
fn the_picture_comes_out_wider_than_tall() {
for smi in ["CCCCCCCCCC", "c1ccc2ccccc2c1", "CC(=O)Oc1ccccc1C(=O)O"] {
let d = generate(&prep(smi), &Style::ACS_1996);
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for p in &d.coords {
x0 = x0.min(p.x);
y0 = y0.min(p.y);
x1 = x1.max(p.x);
y1 = y1.max(p.y);
}
assert!(
x1 - x0 >= y1 - y0 - 1e-9,
"{smi} 摆成了竖的:{:.2} 宽 × {:.2} 高",
x1 - x0,
y1 - y0
);
}
}
#[test]
fn rings_keep_their_tidy_angles() {
let m = prep("c1ccccc1");
let d = generate(&m, &Style::ACS_1996);
for b in m.bonds() {
let v = d.coords[b.end as usize] - d.coords[b.begin as usize];
let deg = v.angle().to_degrees().rem_euclid(30.0);
let off = deg.min(30.0 - deg);
assert!(
off < 1e-6,
"键 {}–{} 偏离 30° 的整数倍 {off:.4}°",
b.begin,
b.end
);
}
}
#[test]
fn orienting_does_not_move_atoms_relative_to_each_other() {
let m = prep("CC(=O)Oc1ccccc1C(=O)O");
let ranks = omgkit_io::canon::canonical_ranks(&m);
let d = generate(&m, &Style::ACS_1996);
let mut moved = d.coords.clone();
canonicalise(&mut moved, &ranks);
for (a, b) in d.coords.iter().zip(&moved) {
assert!(a.dist(*b) < 1e-9, "已经摆正的图再摆一次又动了");
}
}
}