use crate::bounds;
use crate::chiral::{self, Center};
use crate::embed::{self, reference_distances};
use crate::field::Field;
use crate::optimize::{minimize, Options};
use crate::smooth::{triangle_smooth, Bounds, SmoothError};
use omgkit_core::MolBuilder;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConformerError {
Infeasible {
pair: (usize, usize),
},
Embed(crate::embed::EmbedError),
Sanitize(omgkit_chem::SanitizeError),
}
impl core::fmt::Display for ConformerError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Infeasible { pair } => write!(
f,
"界矩阵自相矛盾:原子 {} 与 {} 之间的上下界交空",
pair.0, pair.1
),
Self::Embed(e) => write!(f, "嵌入失败:{e:?}"),
Self::Sanitize(e) => write!(f, "净化失败:{e}"),
}
}
}
#[derive(Debug, Clone)]
pub struct Conformer {
pub coords: Vec<[f64; 3]>,
pub energy_before: f64,
pub energy: f64,
pub iterations: usize,
pub converged: bool,
pub grad_norm: f64,
pub reflected: bool,
pub spread: usize,
pub chiral_total: usize,
pub chiral_ok: usize,
}
const RETRY_RESIDUAL: f64 = 1e-6;
const RETRY_STEPS: usize = 4;
const RETRY_AMPLITUDE: f64 = 0.1;
fn jitter(step: usize, idx: usize) -> f64 {
let mut z = (step as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
^ (idx as u64).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
#[allow(clippy::cast_precision_loss)]
let unit = (z >> 11) as f64 / (1u64 << 52) as f64;
unit.mul_add(2.0, -1.0)
}
fn chiral_ok_of(x: &[f64], centers: &[Center]) -> usize {
let coords: Vec<[f64; 3]> = x.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect();
chiral::correct_count(&coords, centers)
}
const BROKEN_BOND_TOL: f64 = 0.1;
fn broken_bonds_of(x: &[f64], mol: &MolBuilder, b: &Bounds) -> usize {
mol.bonds()
.iter()
.filter(|bd| {
let (i, j) = (bd.begin as usize, bd.end as usize);
let d = (0..3)
.map(|k| (x[3 * i + k] - x[3 * j + k]).powi(2))
.sum::<f64>()
.sqrt();
(b.lower(i, j) - d).max(d - b.upper(i, j)) > BROKEN_BOND_TOL
})
.count()
}
pub const MAX_REFINE_ITER: usize = 400;
pub fn conformer_for(mol: &mut MolBuilder) -> Result<Conformer, ConformerError> {
let centers = prepare(mol)?;
conformer(mol, ¢ers)
}
pub fn prepare(mol: &mut MolBuilder) -> Result<Vec<Center>, ConformerError> {
omgkit_chem::pipeline::sanitize(mol).map_err(ConformerError::Sanitize)?;
omgkit_io::stereo::perceive_bond_stereo(mol);
let ranks = omgkit_io::canon::classed_ranks(mol);
omgkit_chem::add_explicit_hs(mol, &ranks);
Ok(chiral::centers(mol))
}
pub fn conformer(mol: &MolBuilder, centers: &[Center]) -> Result<Conformer, ConformerError> {
let n = mol.num_atoms();
let (mut b, _) = bounds::build(mol);
if let Err(SmoothError::Infeasible { pair }) = triangle_smooth(&mut b) {
return Err(ConformerError::Infeasible { pair });
}
let e = embed::embed(&reference_distances(&b), n).map_err(ConformerError::Embed)?;
let mut coords = e.coords;
let spread = crate::spread::break_coincidence(&mut coords);
let reflected = chiral::needs_reflection(&coords, centers);
if reflected {
chiral::reflect(&mut coords);
}
let field = Field::new(&b, centers);
let mut x: Vec<f64> = coords.iter().flat_map(|p| p.iter().copied()).collect();
let mut g = vec![0.0; x.len()];
let energy_before = {
use crate::optimize::Objective;
field.value_and_grad(&x, &mut g)
};
let opts = Options {
max_iter: MAX_REFINE_ITER,
grad_tol: 1e-6,
memory: 8,
};
let report = minimize(&field, &mut x, &opts);
let mut best_report = report;
let mut best_score = (
chiral_ok_of(&x, centers),
-isize::try_from(broken_bonds_of(&x, mol, &b)).unwrap_or(isize::MIN),
-best_report.value,
);
let base = x.clone();
let mut step = 1;
while best_report.value > RETRY_RESIDUAL && step <= RETRY_STEPS {
let mut xk = base.clone();
let amp = RETRY_AMPLITUDE * f64::from(u32::try_from(step).unwrap_or(u32::MAX));
for (idx, v) in xk.iter_mut().enumerate() {
*v += amp * jitter(step, idx);
}
let r = minimize(&field, &mut xk, &opts);
let score = (
chiral_ok_of(&xk, centers),
-isize::try_from(broken_bonds_of(&xk, mol, &b)).unwrap_or(isize::MIN),
-r.value,
);
if score > best_score {
best_score = score;
best_report = r;
x.copy_from_slice(&xk);
}
step += 1;
}
let report = best_report;
for (i, p) in coords.iter_mut().enumerate() {
*p = [x[3 * i], x[3 * i + 1], x[3 * i + 2]];
}
Ok(Conformer {
chiral_total: centers.len(),
chiral_ok: chiral::correct_count(&coords, centers),
coords,
energy_before,
spread,
energy: report.value,
iterations: report.iterations,
converged: report.converged,
grad_norm: report.grad_norm,
reflected,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn prep(smi: &str) -> MolBuilder {
let mut m = omgkit_io::smiles::parse(smi).expect("SMILES 该解析得了");
omgkit_chem::pipeline::sanitize(&mut m).expect("该 sanitize 得了");
omgkit_io::stereo::perceive_bond_stereo(&mut m);
let r = omgkit_io::canon::classed_ranks(&m);
omgkit_chem::add_explicit_hs(&mut m, &r);
m
}
fn torsion(p: &[[f64; 3]], a: usize, b: usize, c: usize, d: usize) -> f64 {
let sub = |u: [f64; 3], v: [f64; 3]| [u[0] - v[0], u[1] - v[1], u[2] - v[2]];
let dot = |u: [f64; 3], v: [f64; 3]| u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
let cross = |u: [f64; 3], v: [f64; 3]| {
[
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
]
};
let (b0, b1, b2) = (sub(p[a], p[b]), sub(p[c], p[b]), sub(p[d], p[c]));
let n = dot(b1, b1).sqrt();
let u = [b1[0] / n, b1[1] / n, b1[2] / n];
let proj = |v: [f64; 3]| {
let t = dot(v, u);
[v[0] - t * u[0], v[1] - t * u[1], v[2] - t * u[2]]
};
let (v, w) = (proj(b0), proj(b2));
dot(cross(u, v), w).atan2(dot(v, w)).to_degrees()
}
#[test]
fn 双键顺反必须落到正确的一侧() {
for (smi, cis) in [
("F/C=C/F", false),
("F/C=C\\F", true),
("C/C=C/C", false),
("C/C=C\\C", true),
("Cl/C(C)=C(C)/Br", false),
("Cl/C(C)=C(C)\\Br", true),
("[H]/N=C/1\\N[C@]2(CSC(=[NH+]2)N)CS1", true),
("[H]/N=C/1\\N=C([C@H](S1)CC(=O)[O-])O", true),
("CCOC(=O)[C@@H]1C(=N/C(=N/CC=C)/S1)C", false),
] {
let m = prep(smi);
assert!(
!omgkit_io::stereo::directions_not_perceived(&m),
"{smi}:有方向键没折算,`prep` 漏了 perceive_bond_stereo"
);
let marked: Vec<_> = m
.bonds()
.iter()
.filter(|b| b.stereo != omgkit_core::BondStereo::None)
.copied()
.collect();
assert!(!marked.is_empty(), "{smi}:一根带立体标记的双键都没有");
let centers = chiral::centers(&m);
let c = conformer(&m, ¢ers).unwrap_or_else(|e| panic!("{smi} 失败:{e:?}"));
let bd = marked[0];
let (i, j) = (bd.stereo_atoms[0] as usize, bd.stereo_atoms[1] as usize);
let t = torsion(&c.coords, i, bd.begin as usize, bd.end as usize, j);
assert_eq!(
t.abs() < 90.0,
cis,
"{smi} 键 {}={}({:?}):参照 {i}/{j} 的扭转 {t:.1}°,应当在 {} 一侧",
bd.begin,
bd.end,
bd.stereo,
if cis {
"顺式(|τ|<90°)"
} else {
"反式(|τ|>90°)"
}
);
}
}
#[test]
fn 三配位立体中心抽得出来且两个对映体互为镜像() {
for (a, b) in [
("C[S@](=O)CC", "C[S@@](=O)CC"),
("C[S@](=O)c1ccccc1", "C[S@@](=O)c1ccccc1"),
(
"C[C@@H]1CO[S@@](=O)N1c2ccccc2",
"C[C@@H]1CO[S@](=O)N1c2ccccc2",
),
("C[P@H]CC", "C[P@@H]CC"),
("CC[P@](C)c1ccccc1", "CC[P@@](C)c1ccccc1"),
("C[C@@H]1CC[P@](c2ccccc2)C1", "C[C@@H]1CC[P@@](c2ccccc2)C1"),
] {
let (ma, mb) = (prep(a), prep(b));
let (ca, cb) = (chiral::centers(&ma), chiral::centers(&mb));
let three_a = ca.iter().filter(|c| c.is_three_coordinate()).count();
assert!(three_a > 0, "{a}:一个三配位中心都没抽出来");
assert_eq!(
three_a,
cb.iter().filter(|c| c.is_three_coordinate()).count()
);
for (x, y) in ca.iter().zip(cb.iter()) {
assert_eq!(x.atom, y.atom);
if !x.is_three_coordinate() {
continue;
}
assert_eq!(
x.sign, -y.sign,
"{a} / {b}:对映体在 {} 号上应当相反,实得 {} vs {}",
x.atom, x.sign, y.sign
);
}
let (fa, fb) = (
conformer(&ma, &ca).unwrap_or_else(|e| panic!("{a}:{e:?}")),
conformer(&mb, &cb).unwrap_or_else(|e| panic!("{b}:{e:?}")),
);
assert_eq!(fa.chiral_ok, fa.chiral_total, "{a}:交付的号不对");
assert_eq!(fb.chiral_ok, fb.chiral_total, "{b}:交付的号不对");
for (x, y) in ca.iter().zip(cb.iter()) {
if !x.is_three_coordinate() {
continue;
}
let (va, vb) = (
chiral::center_volume(&fa.coords, x),
chiral::center_volume(&fb.coords, y),
);
assert!(
va * vb < 0.0,
"{a} / {b}:中心 {} 的体积没反号({va:+.3} vs {vb:+.3})",
x.atom
);
}
}
}
#[test]
fn 三配位但没有孤对的不算立体中心() {
for smi in ["C[N+](C)C", "C[B](C)C"] {
let mut m = omgkit_io::smiles::parse(smi).expect("解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("净化");
for i in 0..m.num_atoms() as u32 {
let z = m.atoms()[i as usize].atomic_num;
if matches!(z, 5 | 7) {
if let Some(a) = m.atom_mut(i) {
a.chiral_tag = omgkit_core::ChiralTag::Ccw;
}
}
}
let r = omgkit_io::canon::classed_ranks(&m);
omgkit_chem::add_explicit_hs(&mut m, &r);
for c in chiral::centers(&m) {
assert!(
!c.is_three_coordinate(),
"{smi}:原子 {} 不该被当成三配位立体中心",
c.atom
);
}
}
for smi in ["C[S@](=O)CC", "C[P@H]CC", "C[S@+](CC)CCC", "C[Se@+](CC)CCC"] {
let mut m = omgkit_io::smiles::parse(smi).expect("解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("净化");
let r = omgkit_io::canon::classed_ranks(&m);
omgkit_chem::add_explicit_hs(&mut m, &r);
assert!(
chiral::centers(&m).iter().any(Center::is_three_coordinate),
"{smi}:三配位的立体中心一个都没抽出来"
);
}
}
#[test]
fn 漏了顺反折算会被谓词看见() {
let mut m = omgkit_io::smiles::parse("F/C=C/F").expect("解析");
omgkit_chem::pipeline::sanitize(&mut m).expect("净化");
let r = omgkit_io::canon::classed_ranks(&m);
omgkit_chem::add_explicit_hs(&mut m, &r);
assert!(
omgkit_io::stereo::directions_not_perceived(&m),
"漏了折算却没被谓词看见 —— 那道前置条件闸是瞎的"
);
}
#[test]
fn 常见分子都给得出构型() {
for smi in [
"CCO",
"c1ccccc1",
"C1CCCCC1",
"CC(=O)Nc1ccc(O)cc1",
"C1CC2CCC1CC2",
"CC(C)(C)OC(=O)N1CCC(CC1)N",
"FS(F)(F)(F)(F)F",
"C=C=C",
] {
let m = prep(smi);
let c = conformer(&m, &[]).unwrap_or_else(|e| panic!("{smi} 失败:{e:?}"));
assert_eq!(c.coords.len(), m.num_atoms(), "{smi} 坐标数不对");
for (i, p) in c.coords.iter().enumerate() {
assert!(
p.iter().all(|v| v.is_finite()),
"{smi} 第 {i} 个原子坐标不是有限数:{p:?}"
);
}
assert!(
c.energy <= c.energy_before,
"{smi} 精修之后反而更差:{} → {}",
c.energy_before,
c.energy
);
}
}
#[test]
fn 精修确实在干活() {
let m = prep("CC(=O)Nc1ccc(O)cc1");
let c = conformer(&m, &[]).unwrap();
assert!(
c.energy_before > 0.1,
"起点残差 {} 太小,测不到东西",
c.energy_before
);
assert!(
c.energy < c.energy_before * 0.2,
"只压掉了 {:.1}%:{} → {}",
100.0 * (1.0 - c.energy / c.energy_before),
c.energy_before,
c.energy
);
}
#[test]
fn 同一个分子两次给逐位相同的坐标() {
let m = prep("CC(C)(C)OC(=O)N1CCC(CC1)N");
let a = conformer(&m, &[]).unwrap();
let b = conformer(&m, &[]).unwrap();
assert_eq!(a.coords, b.coords, "两次跑出来的坐标不逐位相同");
assert_eq!(a.energy, b.energy);
assert_eq!(a.iterations, b.iterations);
}
#[test]
fn 界不可行时如实报失败() {
use crate::smooth::Bounds;
let mut b = Bounds::new(3, 0.0, 10.0);
b.set_lower(0, 1, 5.0);
b.set_upper(0, 1, 5.0);
b.set_lower(1, 2, 5.0);
b.set_upper(1, 2, 5.0);
b.set_lower(0, 2, 50.0);
b.set_upper(0, 2, 50.0);
assert!(triangle_smooth(&mut b).is_err(), "这组界本来就该判不可行");
}
}