use std::cmp::Ordering;
use chematic_core::{AtomIdx, BondIdx, BondOrder, CipCode, Molecule};
#[derive(Debug, Default, Clone)]
pub struct StereoAssignment2D {
pub assignments: Vec<(AtomIdx, CipCode)>,
}
impl StereoAssignment2D {
pub fn get(&self, idx: AtomIdx) -> Option<CipCode> {
self.assignments
.iter()
.find(|(i, _)| *i == idx)
.map(|(_, c)| *c)
}
}
pub fn assign_stereo_from_2d(mol: &Molecule, coords: &[(f64, f64)]) -> StereoAssignment2D {
let mut assignments = Vec::new();
for (idx, _) in mol.atoms() {
if let Some(code) = assign_rs(mol, coords, idx) {
assignments.push((idx, code));
}
}
StereoAssignment2D { assignments }
}
pub fn assign_ez_from_2d(mol: &mut Molecule, coords: &[(f64, f64)]) {
let bond_indices: Vec<BondIdx> = mol
.bonds()
.filter(|(_, b)| b.order == BondOrder::Double)
.map(|(bidx, _)| bidx)
.collect();
for bidx in bond_indices {
if let Some((atom_idx, code)) = ez_from_coords(mol, bidx, coords) {
mol.set_cip_code(atom_idx, Some(code));
}
}
}
pub fn cip_ez_descriptor(
mol: &Molecule,
bond_idx: BondIdx,
coords: &[(f64, f64)],
) -> Option<CipCode> {
ez_from_coords(mol, bond_idx, coords).map(|(_, code)| code)
}
pub fn apply_stereo_from_2d(mol: &mut Molecule, coords: &[(f64, f64)]) {
let result = assign_stereo_from_2d(mol, coords);
for (idx, code) in result.assignments {
mol.set_cip_code(idx, Some(code));
}
}
fn ez_from_coords(
mol: &Molecule,
bond_idx: BondIdx,
coords: &[(f64, f64)],
) -> Option<(AtomIdx, CipCode)> {
let bond = mol.bond(bond_idx);
if bond.order != BondOrder::Double {
return None;
}
let a1 = bond.atom1;
let a2 = bond.atom2;
let (x1, y1) = coords.get(a1.0 as usize).copied()?;
let (x2, y2) = coords.get(a2.0 as usize).copied()?;
let subs_a1: Vec<AtomIdx> = mol
.neighbors(a1)
.filter(|&(nb, bidx)| nb != a2 && mol.bond(bidx).order != BondOrder::Double)
.map(|(nb, _)| nb)
.collect();
let subs_a2: Vec<AtomIdx> = mol
.neighbors(a2)
.filter(|&(nb, bidx)| nb != a1 && mol.bond(bidx).order != BondOrder::Double)
.map(|(nb, _)| nb)
.collect();
if subs_a1.is_empty() || subs_a2.is_empty() {
return None; }
let ha1 = highest_ez_priority(mol, a1, &subs_a1)?;
let ha2 = highest_ez_priority(mol, a2, &subs_a2)?;
let (hx1, hy1) = coords.get(ha1.0 as usize).copied()?;
let (hx2, hy2) = coords.get(ha2.0 as usize).copied()?;
let vx = x2 - x1;
let vy = y2 - y1;
let side_a1 = cross2d(vx, vy, hx1 - x1, hy1 - y1);
let side_a2 = cross2d(vx, vy, hx2 - x1, hy2 - y1);
if side_a1.abs() < 1e-6 || side_a2.abs() < 1e-6 {
return None; }
let code = if side_a1.signum() == side_a2.signum() {
CipCode::Z
} else {
CipCode::E
};
Some((a1, code))
}
fn highest_ez_priority(mol: &Molecule, center: AtomIdx, subs: &[AtomIdx]) -> Option<AtomIdx> {
if subs.is_empty() {
return None;
}
if subs.len() == 1 {
return Some(subs[0]);
}
let mut sorted = subs.to_vec();
sorted.sort_by(|&a, &b| cmp_priority(mol, center, a, b).reverse());
if cmp_priority(mol, center, sorted[0], sorted[1]) == Ordering::Equal {
return None; }
Some(sorted[0])
}
fn cross2d(vx: f64, vy: f64, ux: f64, uy: f64) -> f64 {
vx * uy - vy * ux
}
#[derive(Clone, Copy)]
struct P3 {
x: f64,
y: f64,
z: f64,
}
fn assign_rs(mol: &Molecule, coords: &[(f64, f64)], center: AtomIdx) -> Option<CipCode> {
let center_pos = coords.get(center.0 as usize)?;
let nbs: Vec<AtomIdx> = mol.neighbors(center).map(|(nb, _)| nb).collect();
if nbs.len() != 4 {
return None; }
let subs: [AtomIdx; 4] = [nbs[0], nbs[1], nbs[2], nbs[3]];
let z_for: [f64; 4] = [
wedge_z(mol, center, subs[0]),
wedge_z(mol, center, subs[1]),
wedge_z(mol, center, subs[2]),
wedge_z(mol, center, subs[3]),
];
let pts: [P3; 4] = [0, 1, 2, 3].map(|i| {
let (x, y) = coords
.get(subs[i].0 as usize)
.copied()
.unwrap_or(*center_pos);
P3 { x, y, z: z_for[i] }
});
let ranks = rank4(mol, center, &subs)?;
let mut order: [usize; 4] = [0, 1, 2, 3];
order.sort_by(|&i, &j| ranks[j].cmp(&ranks[i]));
let s: [P3; 4] = order.map(|i| pts[i]);
let v = signed_volume(s[0], s[1], s[2], s[3]);
if v.abs() < 1e-6 {
return None; }
Some(if v > 0.0 { CipCode::S } else { CipCode::R })
}
fn wedge_z(mol: &Molecule, center: AtomIdx, neighbor: AtomIdx) -> f64 {
if let Some((_, bond)) = mol.bond_between(center, neighbor) {
match bond.order {
BondOrder::Up => {
if bond.atom1 == center {
return 1.0;
} else {
return -1.0;
}
}
BondOrder::Down => {
if bond.atom1 == center {
return -1.0;
} else {
return 1.0;
}
}
_ => {}
}
}
0.0
}
fn priority_key(mol: &Molecule, center: AtomIdx, sub: AtomIdx) -> (u8, Vec<u8>) {
let an = mol.atom(sub).element.atomic_number();
let mut nbs: Vec<u8> = mol
.neighbors(sub)
.filter(|(nb, _)| *nb != center)
.map(|(nb, _)| mol.atom(nb).element.atomic_number())
.collect();
nbs.sort_unstable_by(|a, b| b.cmp(a));
(an, nbs)
}
fn cmp_priority(mol: &Molecule, center: AtomIdx, a: AtomIdx, b: AtomIdx) -> Ordering {
let ka = priority_key(mol, center, a);
let kb = priority_key(mol, center, b);
match ka.0.cmp(&kb.0) {
Ordering::Equal => {
let max_len = ka.1.len().max(kb.1.len());
for i in 0..max_len {
let va = ka.1.get(i).copied().unwrap_or(0);
let vb = kb.1.get(i).copied().unwrap_or(0);
match va.cmp(&vb) {
Ordering::Equal => {}
other => return other,
}
}
Ordering::Equal
}
other => other,
}
}
fn rank4(mol: &Molecule, center: AtomIdx, subs: &[AtomIdx; 4]) -> Option<[u8; 4]> {
let mut order: [usize; 4] = [0, 1, 2, 3];
order.sort_by(|&i, &j| cmp_priority(mol, center, subs[i], subs[j]).reverse());
for k in 0..3 {
if cmp_priority(mol, center, subs[order[k]], subs[order[k + 1]]) == Ordering::Equal {
return None;
}
}
let mut ranks = [0u8; 4];
for (rank_from_top, &idx) in order.iter().enumerate() {
ranks[idx] = (4 - rank_from_top) as u8;
}
Some(ranks)
}
fn signed_volume(p1: P3, p2: P3, p3: P3, p4: P3) -> f64 {
let (ax, ay, az) = (p1.x - p4.x, p1.y - p4.y, p1.z - p4.z);
let (bx, by, bz) = (p2.x - p4.x, p2.y - p4.y, p2.z - p4.z);
let (cx, cy, cz) = (p3.x - p4.x, p3.y - p4.y, p3.z - p4.z);
ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx)
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
#[test]
fn test_ez_terminal_alkene_no_assignment() {
let mol = parse("C=C").unwrap();
let coords = vec![(0.0, 0.0), (1.5, 0.0)];
let bond_idx = mol
.bonds()
.find(|(_, b)| b.order == BondOrder::Double)
.map(|(i, _)| i);
assert!(bond_idx.is_some());
let result = cip_ez_descriptor(&mol, bond_idx.unwrap(), &coords);
assert!(result.is_none(), "terminal alkene should have no E/Z");
}
#[test]
fn test_ez_but2ene_z() {
let mol = parse("CC=CC").unwrap();
let bond_idx = mol
.bonds()
.find(|(_, b)| b.order == BondOrder::Double)
.map(|(i, _)| i)
.unwrap();
let coords = vec![
(-0.866, 0.5), (0.0, 0.0), (1.5, 0.0), (2.366, 0.5), ];
let result = cip_ez_descriptor(&mol, bond_idx, &coords);
assert_eq!(result, Some(CipCode::Z), "(Z)-but-2-ene should be Z");
}
#[test]
fn test_ez_but2ene_e() {
let mol = parse("CC=CC").unwrap();
let bond_idx = mol
.bonds()
.find(|(_, b)| b.order == BondOrder::Double)
.map(|(i, _)| i)
.unwrap();
let coords = vec![
(-0.866, 0.5), (0.0, 0.0), (1.5, 0.0), (2.366, -0.5), ];
let result = cip_ez_descriptor(&mol, bond_idx, &coords);
assert_eq!(result, Some(CipCode::E), "(E)-but-2-ene should be E");
}
#[test]
fn test_assign_ez_from_2d_sets_cip_code() {
let mut mol = parse("CC=CC").unwrap();
let coords = vec![
(-0.866, 0.5),
(0.0, 0.0),
(1.5, 0.0),
(2.366, 0.5), ];
assign_ez_from_2d(&mut mol, &coords);
let has_z = mol
.atoms()
.any(|(_, atom)| atom.cip_code == Some(CipCode::Z));
assert!(has_z, "assign_ez_from_2d should set Z on but-2-ene");
}
#[test]
fn test_no_stereo_ethane() {
let mol = parse("CC").unwrap();
let coords = vec![(0.0, 0.0), (1.0, 0.0)];
let result = assign_stereo_from_2d(&mol, &coords);
assert!(result.assignments.is_empty());
}
#[test]
fn test_r_s_bromochlorofluoromethane() {
use chematic_core::{Atom, BondOrder as BO, Element, MoleculeBuilder};
let mut b = MoleculeBuilder::new();
let c = b.add_atom(Atom::new(Element::C));
let f = b.add_atom(Atom::new(Element::F));
let cl = b.add_atom(Atom::new(Element::CL));
let br = b.add_atom(Atom::new(Element::BR));
let h = b.add_atom(Atom::new(Element::H));
b.add_bond(c, f, BO::Single).unwrap();
b.add_bond(c, cl, BO::Single).unwrap();
b.add_bond(c, br, BO::Up).unwrap(); b.add_bond(c, h, BO::Down).unwrap(); let mol = b.build();
let coords = vec![
(0.0, 0.0), (-1.0, -0.5), (1.0, -0.5), (0.0, 1.0), (0.0, -1.0), ];
let result = assign_stereo_from_2d(&mol, &coords);
assert_eq!(result.assignments.len(), 1);
assert_eq!(result.assignments[0].0, c);
}
}