use chematic_core::{AtomIdx, BondOrder, Chirality, Molecule, STEREO_H_SENTINEL};
use crate::stereo2d::{P3, signed_volume, wedge_z};
const VOLUME_EPS: f64 = 1e-6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StereoRejectionReason {
ContradictoryWedges,
MissingCoordinate,
DegenerateGeometry,
UnsupportedCoordination,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StereoDiagnostic {
pub atom: AtomIdx,
pub reason: StereoRejectionReason,
}
enum ParityOutcome {
NotRequested,
Assigned(Chirality, Vec<u32>),
Rejected(StereoRejectionReason),
}
fn has_wedge_or_hash(mol: &Molecule, center: AtomIdx) -> bool {
mol.neighbors(center)
.any(|(_, bidx)| matches!(mol.bond(bidx).order, BondOrder::Up | BondOrder::Down))
}
fn classify_local_parity(mol: &Molecule, coords: &[(f64, f64)], center: AtomIdx) -> ParityOutcome {
let nbs: Vec<AtomIdx> = mol.neighbors(center).map(|(nb, _)| nb).collect();
if !(3..=4).contains(&nbs.len()) || !has_wedge_or_hash(mol, center) {
return ParityOutcome::NotRequested;
}
let result = match nbs.len() {
4 => tetrahedral_4(mol, coords, center, &nbs),
3 if chematic_core::implicit_hcount(mol, center) == 1 => {
tetrahedral_3_implicit_h(mol, coords, center, &nbs)
}
_ => Err(StereoRejectionReason::UnsupportedCoordination),
};
match result {
Ok((chirality, order)) => ParityOutcome::Assigned(chirality, order),
Err(reason) => ParityOutcome::Rejected(reason),
}
}
pub fn local_parity_from_wedges(
mol: &Molecule,
coords: &[(f64, f64)],
center: AtomIdx,
) -> Option<(Chirality, Vec<u32>)> {
match classify_local_parity(mol, coords, center) {
ParityOutcome::Assigned(chirality, order) => Some((chirality, order)),
ParityOutcome::NotRequested | ParityOutcome::Rejected(_) => None,
}
}
fn point_for(coords: &[(f64, f64)], mol: &Molecule, center: AtomIdx, nb: AtomIdx) -> Option<P3> {
let (x, y) = coords.get(nb.0 as usize).copied()?;
Some(P3 {
x,
y,
z: wedge_z(mol, center, nb),
})
}
fn volume_sign(volume: f64) -> Option<bool> {
if volume.abs() < VOLUME_EPS {
None
} else {
Some(volume < 0.0)
}
}
fn wedges_agree_4(pts: &[P3]) -> bool {
let wedged: Vec<usize> = (0..4).filter(|&i| pts[i].z != 0.0).collect();
if wedged.len() <= 1 {
return true;
}
let isolated_sign = |i: usize| -> Option<bool> {
let iso: Vec<P3> = (0..4)
.map(|j| {
if j == i {
pts[j]
} else {
P3 { z: 0.0, ..pts[j] }
}
})
.collect();
volume_sign(signed_volume(iso[1], iso[2], iso[3], iso[0]))
};
let Some(first) = isolated_sign(wedged[0]) else {
return false;
};
wedged[1..].iter().all(|&i| isolated_sign(i) == Some(first))
}
fn wedges_agree_3(pts: &[P3], center_pt: P3) -> bool {
let wedged: Vec<usize> = (0..3).filter(|&i| pts[i].z != 0.0).collect();
if wedged.len() <= 1 {
return true;
}
let isolated_sign = |i: usize| -> Option<bool> {
let iso: Vec<P3> = (0..3)
.map(|j| {
if j == i {
pts[j]
} else {
P3 { z: 0.0, ..pts[j] }
}
})
.collect();
volume_sign(signed_volume(iso[0], iso[1], iso[2], center_pt))
};
let Some(first) = isolated_sign(wedged[0]) else {
return false;
};
wedged[1..].iter().all(|&i| isolated_sign(i) == Some(first))
}
fn tetrahedral_4(
mol: &Molecule,
coords: &[(f64, f64)],
center: AtomIdx,
nbs: &[AtomIdx],
) -> Result<(Chirality, Vec<u32>), StereoRejectionReason> {
let pts: Vec<P3> = nbs
.iter()
.map(|&nb| point_for(coords, mol, center, nb))
.collect::<Option<_>>()
.ok_or(StereoRejectionReason::MissingCoordinate)?;
if !wedges_agree_4(&pts) {
return Err(StereoRejectionReason::ContradictoryWedges);
}
let vol = signed_volume(pts[1], pts[2], pts[3], pts[0]);
if vol.abs() < VOLUME_EPS {
return Err(StereoRejectionReason::DegenerateGeometry);
}
let chirality = if vol < 0.0 {
Chirality::CounterClockwise
} else {
Chirality::Clockwise
};
let order = nbs.iter().map(|a| a.0).collect();
Ok((chirality, order))
}
fn tetrahedral_3_implicit_h(
mol: &Molecule,
coords: &[(f64, f64)],
center: AtomIdx,
nbs: &[AtomIdx],
) -> Result<(Chirality, Vec<u32>), StereoRejectionReason> {
let pts: Vec<P3> = nbs
.iter()
.map(|&nb| point_for(coords, mol, center, nb))
.collect::<Option<_>>()
.ok_or(StereoRejectionReason::MissingCoordinate)?;
let (cx, cy) = coords
.get(center.0 as usize)
.copied()
.ok_or(StereoRejectionReason::MissingCoordinate)?;
let center_pt = P3 {
x: cx,
y: cy,
z: 0.0,
};
if !wedges_agree_3(&pts, center_pt) {
return Err(StereoRejectionReason::ContradictoryWedges);
}
let vol = signed_volume(pts[0], pts[1], pts[2], center_pt);
if vol.abs() < VOLUME_EPS {
return Err(StereoRejectionReason::DegenerateGeometry);
}
let chirality = if vol < 0.0 {
Chirality::Clockwise
} else {
Chirality::CounterClockwise
};
let mut order: Vec<u32> = nbs.iter().map(|a| a.0).collect();
order.push(STEREO_H_SENTINEL);
Ok((chirality, order))
}
pub fn apply_local_parity_from_wedges(mol: &mut Molecule, coords: &[(f64, f64)]) {
let atom_indices: Vec<AtomIdx> = mol.atoms().map(|(idx, _)| idx).collect();
for idx in atom_indices {
if let Some((chirality, order)) = local_parity_from_wedges(mol, coords, idx) {
mol.set_chirality(idx, chirality);
mol.set_stereo_neighbor_order(idx, order);
}
}
}
pub fn apply_local_parity_from_wedges_with_diagnostics(
mol: &mut Molecule,
coords: &[(f64, f64)],
) -> Vec<StereoDiagnostic> {
let atom_indices: Vec<AtomIdx> = mol.atoms().map(|(idx, _)| idx).collect();
let mut diagnostics = Vec::new();
for idx in atom_indices {
match classify_local_parity(mol, coords, idx) {
ParityOutcome::Assigned(chirality, order) => {
mol.set_chirality(idx, chirality);
mol.set_stereo_neighbor_order(idx, order);
}
ParityOutcome::Rejected(reason) => {
diagnostics.push(StereoDiagnostic { atom: idx, reason });
}
ParityOutcome::NotRequested => {}
}
}
diagnostics
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_core::{Atom, BondOrder, Element, MoleculeBuilder};
fn quad_positions() -> [(f64, f64); 4] {
[(-1.0, 0.4), (0.9, 0.7), (-0.5, -1.1), (0.8, -0.6)]
}
fn chfclbr(wedge_on_first: bool) -> (Molecule, Vec<(f64, f64)>, AtomIdx) {
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 i = b.add_atom(Atom::new(Element::I));
let order = if wedge_on_first {
BondOrder::Up
} else {
BondOrder::Single
};
b.add_bond(c, f, order).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
b.add_bond(c, i, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2], quad[3]];
(b.build(), coords, c)
}
#[test]
fn tetrahedral_4heavy_wedge_gives_counterclockwise() {
let (mol, coords, c) = chfclbr(true);
let (chirality, order) = local_parity_from_wedges(&mol, &coords, c).unwrap();
assert_eq!(chirality, Chirality::CounterClockwise);
assert_eq!(order, vec![1, 2, 3, 4]);
}
#[test]
fn tetrahedral_4heavy_no_h_all_explicit() {
let (mol, coords, c) = chfclbr(true);
assert!(local_parity_from_wedges(&mol, &coords, c).is_some());
}
#[test]
fn tetrahedral_4neighbors_explicit_h() {
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, BondOrder::Up).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
b.add_bond(c, h, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2], quad[3]];
let mol = b.build();
let (chirality, order) = local_parity_from_wedges(&mol, &coords, c).unwrap();
assert_eq!(chirality, Chirality::CounterClockwise);
assert_eq!(order, vec![1, 2, 3, 4]);
}
#[test]
fn wedge_hash_inversion_flips_chirality() {
let (mol_wedge, coords, c) = chfclbr(true);
let (wedge_chirality, _) = local_parity_from_wedges(&mol_wedge, &coords, c).unwrap();
let mut b = MoleculeBuilder::new();
let c2 = 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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c2, f, BondOrder::Down).unwrap();
b.add_bond(c2, cl, BondOrder::Single).unwrap();
b.add_bond(c2, br, BondOrder::Single).unwrap();
b.add_bond(c2, i, BondOrder::Single).unwrap();
let mol_hash = b.build();
let (hash_chirality, _) = local_parity_from_wedges(&mol_hash, &coords, c2).unwrap();
assert_ne!(wedge_chirality, hash_chirality);
}
#[test]
fn bond_atom_order_inversion_flips_chirality() {
let (mol1, coords, c1) = chfclbr(true);
let (chirality1, _) = local_parity_from_wedges(&mol1, &coords, c1).unwrap();
let mut b = MoleculeBuilder::new();
let c2 = 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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c2, i, BondOrder::Single).unwrap();
b.add_bond(c2, br, BondOrder::Single).unwrap();
b.add_bond(c2, cl, BondOrder::Single).unwrap();
b.add_bond(c2, f, BondOrder::Up).unwrap();
let mol2 = b.build();
let (chirality2, order2) = local_parity_from_wedges(&mol2, &coords, c2).unwrap();
assert_eq!(chirality1, chirality2);
assert_eq!(order2, vec![4, 3, 2, 1]);
}
#[test]
fn multiple_stereocenters_both_assigned() {
let mut b = MoleculeBuilder::new();
let c1 = b.add_atom(Atom::new(Element::C));
let f1 = b.add_atom(Atom::new(Element::F));
let cl1 = b.add_atom(Atom::new(Element::CL));
let br1 = b.add_atom(Atom::new(Element::BR));
let c2 = b.add_atom(Atom::new(Element::C));
let f2 = b.add_atom(Atom::new(Element::F));
let cl2 = b.add_atom(Atom::new(Element::CL));
let br2 = b.add_atom(Atom::new(Element::BR));
b.add_bond(c1, f1, BondOrder::Up).unwrap();
b.add_bond(c1, cl1, BondOrder::Single).unwrap();
b.add_bond(c1, br1, BondOrder::Single).unwrap();
b.add_bond(c1, c2, BondOrder::Single).unwrap();
b.add_bond(c2, f2, BondOrder::Down).unwrap();
b.add_bond(c2, cl2, BondOrder::Single).unwrap();
b.add_bond(c2, br2, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![
(0.0, 0.0),
quad[0],
quad[1],
quad[2],
(3.0, 0.0),
(3.0 + quad[0].0, quad[0].1),
(3.0 + quad[1].0, quad[1].1),
(3.0 + quad[2].0, quad[2].1),
];
let mut mol = b.build();
apply_local_parity_from_wedges(&mut mol, &coords);
assert_eq!(mol.atom(c1).chirality, Chirality::CounterClockwise);
assert_eq!(mol.atom(c2).chirality, Chirality::Clockwise);
assert_eq!(mol.atom(c1).cip_code, None);
assert_eq!(mol.atom(c2).cip_code, None);
}
#[test]
fn cip_priority_tie_still_gets_chirality() {
let mut b = MoleculeBuilder::new();
let c = b.add_atom(Atom::new(Element::C));
let et1 = b.add_atom(Atom::new(Element::C));
let et1b = b.add_atom(Atom::new(Element::C));
let et2 = b.add_atom(Atom::new(Element::C));
let et2b = b.add_atom(Atom::new(Element::C));
let f = b.add_atom(Atom::new(Element::F));
b.add_bond(c, et1, BondOrder::Single).unwrap();
b.add_bond(et1, et1b, BondOrder::Single).unwrap();
b.add_bond(c, et2, BondOrder::Single).unwrap();
b.add_bond(et2, et2b, BondOrder::Single).unwrap();
b.add_bond(c, f, BondOrder::Up).unwrap();
let h = b.add_atom(Atom::new(Element::H));
b.add_bond(c, h, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![
(0.0, 0.0),
quad[0],
(quad[0].0 + 0.3, quad[0].1 + 1.0),
quad[1],
(quad[1].0 + 0.3, quad[1].1 - 1.0),
quad[2],
quad[3],
];
let mol = b.build();
let cip_result = crate::stereo2d::assign_stereo_from_2d(&mol, &coords);
assert!(cip_result.get(c).is_none(), "CIP-based path should tie");
let (chirality, _) = local_parity_from_wedges(&mol, &coords, c).unwrap();
assert_ne!(chirality, Chirality::None);
}
#[test]
fn missing_coordinates_no_assignment() {
let (mol, mut coords, c) = chfclbr(true);
coords.truncate(3); assert!(local_parity_from_wedges(&mol, &coords, c).is_none());
}
#[test]
fn degenerate_coplanar_no_assignment() {
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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c, f, BondOrder::Single).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
b.add_bond(c, i, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2], quad[3]];
let mol = b.build();
assert!(local_parity_from_wedges(&mol, &coords, c).is_none());
}
#[test]
fn contradictory_wedges_no_assignment() {
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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c, f, BondOrder::Up).unwrap();
b.add_bond(c, cl, BondOrder::Up).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
b.add_bond(c, i, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2], quad[3]];
let mol = b.build();
assert!(local_parity_from_wedges(&mol, &coords, c).is_none());
}
#[test]
fn dual_wedge_disagreeing_parity_rejected() {
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));
b.add_bond(c, f, BondOrder::Up).unwrap();
b.add_bond(c, cl, BondOrder::Down).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2]];
let mol = b.build();
assert!(local_parity_from_wedges(&mol, &coords, c).is_none());
}
#[test]
fn valid_dual_wedge_solid_and_hash_on_different_bonds_accepted() {
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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c, f, BondOrder::Single).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Up).unwrap();
b.add_bond(c, i, BondOrder::Down).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2], quad[3]];
let mol = b.build();
let (chirality, order) = local_parity_from_wedges(&mol, &coords, c).unwrap();
assert_eq!(chirality, Chirality::Clockwise);
assert_eq!(order, vec![1, 2, 3, 4]);
}
#[test]
fn valid_dual_wedge_3heavy_same_direction_accepted() {
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));
b.add_bond(c, f, BondOrder::Up).unwrap();
b.add_bond(c, cl, BondOrder::Up).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2]];
let mol = b.build();
let (chirality, order) = local_parity_from_wedges(&mol, &coords, c).unwrap();
assert_eq!(chirality, Chirality::Clockwise);
assert_eq!(order, vec![1, 2, 3, STEREO_H_SENTINEL]);
}
#[test]
fn dual_wedge_one_isolated_volume_degenerate_rejected() {
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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c, f, BondOrder::Up).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Up).unwrap();
b.add_bond(c, i, BondOrder::Single).unwrap();
let coords = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0), (0.0, 1.0), (3.0, 0.0)];
let mol = b.build();
assert!(local_parity_from_wedges(&mol, &coords, c).is_none());
}
#[test]
fn dual_wedge_both_isolated_volumes_degenerate_rejected() {
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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c, f, BondOrder::Single).unwrap();
b.add_bond(c, cl, BondOrder::Up).unwrap();
b.add_bond(c, br, BondOrder::Up).unwrap();
b.add_bond(c, i, BondOrder::Single).unwrap();
let coords = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)];
let mol = b.build();
assert!(local_parity_from_wedges(&mol, &coords, c).is_none());
}
#[test]
fn tetrahedral_3heavy_implicit_h_wedge() {
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));
b.add_bond(c, f, BondOrder::Up).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2]];
let mol = b.build();
let (chirality, order) = local_parity_from_wedges(&mol, &coords, c).unwrap();
assert_eq!(chirality, Chirality::Clockwise);
assert_eq!(order, vec![1, 2, 3, STEREO_H_SENTINEL]);
}
#[test]
fn tetrahedral_3heavy_implicit_h_hash_inverts() {
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));
b.add_bond(c, f, BondOrder::Down).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2]];
let mol = b.build();
let (chirality, _) = local_parity_from_wedges(&mol, &coords, c).unwrap();
assert_eq!(chirality, Chirality::CounterClockwise);
}
#[test]
fn tetrahedral_3heavy_bond_order_reversed_inverts() {
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));
b.add_bond(c, br, BondOrder::Single).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, f, BondOrder::Up).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2]];
let mol = b.build();
let (chirality, order) = local_parity_from_wedges(&mol, &coords, c).unwrap();
assert_eq!(order, vec![3, 2, 1, STEREO_H_SENTINEL]);
assert_eq!(chirality, Chirality::CounterClockwise);
}
#[test]
fn only_three_heavy_no_implicit_h_no_assignment() {
let mut b = MoleculeBuilder::new();
let mut n_atom = Atom::new(Element::N);
n_atom.charge = 1;
let n = b.add_atom(n_atom);
let c1 = b.add_atom(Atom::new(Element::C));
let c2 = b.add_atom(Atom::new(Element::C));
let c3 = b.add_atom(Atom::new(Element::C));
b.add_bond(n, c1, BondOrder::Double).unwrap();
b.add_bond(n, c2, BondOrder::Single).unwrap();
b.add_bond(n, c3, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2]];
let mol = b.build();
assert!(local_parity_from_wedges(&mol, &coords, n).is_none());
}
#[test]
fn no_wedge_produces_no_diagnostic() {
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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c, f, BondOrder::Single).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
b.add_bond(c, i, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2], quad[3]];
let mut mol = b.build();
let diagnostics = apply_local_parity_from_wedges_with_diagnostics(&mut mol, &coords);
assert!(diagnostics.is_empty());
}
#[test]
fn contradictory_wedges_produce_diagnostic() {
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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c, f, BondOrder::Up).unwrap();
b.add_bond(c, cl, BondOrder::Up).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
b.add_bond(c, i, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2], quad[3]];
let mut mol = b.build();
let diagnostics = apply_local_parity_from_wedges_with_diagnostics(&mut mol, &coords);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].atom, c);
assert_eq!(
diagnostics[0].reason,
StereoRejectionReason::ContradictoryWedges
);
}
#[test]
fn missing_coordinate_produces_diagnostic() {
let (mol_base, mut coords, c) = chfclbr(true);
coords.truncate(3);
let mut mol = mol_base;
let diagnostics = apply_local_parity_from_wedges_with_diagnostics(&mut mol, &coords);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].atom, c);
assert_eq!(
diagnostics[0].reason,
StereoRejectionReason::MissingCoordinate
);
}
#[test]
fn degenerate_geometry_produces_diagnostic() {
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 i = b.add_atom(Atom::new(Element::I));
b.add_bond(c, f, BondOrder::Up).unwrap();
b.add_bond(c, cl, BondOrder::Single).unwrap();
b.add_bond(c, br, BondOrder::Single).unwrap();
b.add_bond(c, i, BondOrder::Single).unwrap();
let coords = vec![(0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (2.0, 0.0), (3.0, 0.0)];
let mut mol = b.build();
let diagnostics = apply_local_parity_from_wedges_with_diagnostics(&mut mol, &coords);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].atom, c);
assert_eq!(
diagnostics[0].reason,
StereoRejectionReason::DegenerateGeometry
);
}
#[test]
fn unsupported_coordination_with_wedge_produces_diagnostic() {
let mut b = MoleculeBuilder::new();
let mut n_atom = Atom::new(Element::N);
n_atom.charge = 1;
let n = b.add_atom(n_atom);
let c1 = b.add_atom(Atom::new(Element::C));
let c2 = b.add_atom(Atom::new(Element::C));
let c3 = b.add_atom(Atom::new(Element::C));
b.add_bond(n, c1, BondOrder::Double).unwrap();
b.add_bond(n, c2, BondOrder::Up).unwrap();
b.add_bond(n, c3, BondOrder::Single).unwrap();
let quad = quad_positions();
let coords = vec![(0.0, 0.0), quad[0], quad[1], quad[2]];
let mut mol = b.build();
let diagnostics = apply_local_parity_from_wedges_with_diagnostics(&mut mol, &coords);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].atom, n);
assert_eq!(
diagnostics[0].reason,
StereoRejectionReason::UnsupportedCoordination
);
}
#[test]
fn five_coordinate_wedged_center_is_not_a_tetrahedral_request() {
let mut b = MoleculeBuilder::new();
let center = b.add_atom(Atom::new(Element::P));
let f1 = b.add_atom(Atom::new(Element::F));
let f2 = b.add_atom(Atom::new(Element::F));
let f3 = b.add_atom(Atom::new(Element::F));
let f4 = b.add_atom(Atom::new(Element::F));
let f5 = b.add_atom(Atom::new(Element::F));
b.add_bond(center, f1, BondOrder::Up).unwrap();
b.add_bond(center, f2, BondOrder::Single).unwrap();
b.add_bond(center, f3, BondOrder::Single).unwrap();
b.add_bond(center, f4, BondOrder::Single).unwrap();
b.add_bond(center, f5, BondOrder::Single).unwrap();
let coords = vec![
(0.0, 0.0),
(1.0, 0.0),
(0.5, 1.0),
(-0.5, 1.0),
(-1.0, 0.0),
(0.0, -1.0),
];
let mut mol = b.build();
let diagnostics = apply_local_parity_from_wedges_with_diagnostics(&mut mol, &coords);
assert!(diagnostics.is_empty());
assert_eq!(mol.atom(center).chirality, Chirality::None);
assert!(mol.stereo_neighbor_order(center).is_none());
}
#[test]
fn valid_stereo_produces_no_rejection_diagnostic() {
let (mol_base, coords, c) = chfclbr(true);
let mut mol = mol_base;
let diagnostics = apply_local_parity_from_wedges_with_diagnostics(&mut mol, &coords);
assert!(diagnostics.is_empty());
assert_eq!(mol.atom(c).chirality, Chirality::CounterClockwise);
}
}