use omgkit_core::{AtomFlags, BondData, BondOrder, MolBuilder};
use crate::react::{align_for_rebase, components, Outcome};
const MAX_BONDS: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Budget {
pub open_valence: u32,
pub fragment_hydrogens: u32,
pub delta_h: i32,
pub need: i32,
pub remaining: i32,
pub delta_charge: i32,
pub fragment_charge: i32,
pub charge_shift: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unresolved {
OddValence,
BudgetExceedsValence,
HydrogenBudgetNegative,
TooManyBonds,
ProductsUnsanitizable,
NoPairing,
FragmentUnsanitizable,
StrainedClosure,
SubstrateUnkekulizable,
BudgetMismatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Nothing,
Capped,
Bonded {
bonds: u32,
},
Unresolved(Unresolved),
}
impl Verdict {
#[must_use]
pub fn is_closed(self) -> bool {
matches!(self, Verdict::Capped | Verdict::Bonded { .. })
}
}
#[derive(Debug, Clone)]
pub struct Byproducts {
pub molecules: Vec<MolBuilder>,
pub verdict: Verdict,
pub budget: Budget,
}
struct Site {
opens: u32,
borrowed_h: u32,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct CloseStats {
pub site_visits: u64,
pub fragment_scans: u64,
pub pair_visits: u64,
}
#[must_use]
pub fn reconstruct_counted(
reactants: &[MolBuilder],
outcome: &Outcome,
) -> (Byproducts, CloseStats) {
let mut stats = CloseStats::default();
let by = reconstruct_inner(reactants, outcome, &mut stats);
(by, stats)
}
#[must_use]
pub fn reconstruct(reactants: &[MolBuilder], outcome: &Outcome) -> Byproducts {
reconstruct_counted(reactants, outcome).0
}
fn reconstruct_inner(
reactants: &[MolBuilder],
outcome: &Outcome,
stats: &mut CloseStats,
) -> Byproducts {
let empty = Budget {
open_valence: 0,
fragment_hydrogens: 0,
delta_h: 0,
need: 0,
remaining: 0,
delta_charge: 0,
fragment_charge: 0,
charge_shift: 0,
};
if outcome.discarded.iter().all(Vec::is_empty) {
return Byproducts {
molecules: Vec::new(),
verdict: Verdict::Nothing,
budget: empty,
};
}
let mut h_products: i32 = 0;
let mut q_products: i32 = 0;
for p in &outcome.products {
let mut copy = p.clone();
if omgkit_chem::sanitize(&mut copy).is_err() {
return Byproducts {
molecules: Vec::new(),
verdict: Verdict::Unresolved(Unresolved::ProductsUnsanitizable),
budget: empty,
};
}
h_products += total_hydrogens(©);
q_products += total_charge(©);
}
let mut kekulized: Vec<MolBuilder> = Vec::with_capacity(reactants.len());
for m in reactants {
let mut k = m.clone();
if omgkit_chem::kekulize(&mut k).is_err() {
return Byproducts {
molecules: Vec::new(),
verdict: Verdict::Unresolved(Unresolved::SubstrateUnkekulizable),
budget: empty,
};
}
kekulized.push(k);
}
let (mut frag, mut sites) = build_fragment(&kekulized, &outcome.discarded);
debug_assert_eq!(
frag.num_atoms(),
outcome.discarded.iter().map(Vec::len).sum::<usize>(),
"build_fragment 丢了原子 —— 那两处 `continue` 触发了"
);
let open_valence: u32 = sites.iter().map(|s| s.opens).sum();
let fragment_hydrogens = u32::try_from(total_hydrogens(&frag)).unwrap_or(0);
let fragment_charge = total_charge(&frag);
let h_inputs: i32 = reactants.iter().map(total_hydrogens).sum();
let q_inputs: i32 = reactants.iter().map(total_charge).sum();
let delta_h = h_inputs - h_products;
let delta_charge = q_inputs - q_products;
let need = delta_h - i32::try_from(fragment_hydrogens).unwrap_or(0);
let charge_shift = delta_charge - fragment_charge;
let charges_ok = charge_shift == 0 || apply_charges(&mut frag, &mut sites, charge_shift, stats);
let opens_after: i32 = sites
.iter()
.map(|s| i32::from(u16::try_from(s.opens).unwrap_or(0)))
.sum();
let remaining = opens_after - need;
let budget = Budget {
open_valence,
fragment_hydrogens,
delta_h,
need,
remaining,
delta_charge,
fragment_charge,
charge_shift,
};
let bail = |why: Unresolved| Byproducts {
molecules: Vec::new(),
verdict: Verdict::Unresolved(why),
budget,
};
if delta_h < 0 {
return bail(Unresolved::HydrogenBudgetNegative);
}
if !charges_ok {
return bail(Unresolved::NoPairing);
}
if remaining < 0 {
return bail(Unresolved::BudgetExceedsValence);
}
if remaining % 2 != 0 {
return bail(Unresolved::OddValence);
}
let to_bond = u32::try_from(remaining).unwrap_or(0) / 2;
if to_bond > MAX_BONDS {
return bail(Unresolved::TooManyBonds);
}
if need < 0 {
let extra = u32::try_from(-need).unwrap_or(0);
if !borrow_hydrogens(&frag, &mut sites, extra, stats) {
return bail(Unresolved::OddValence);
}
}
let mut closed = frag;
let heavy_before = heavy_atoms(&closed);
if !form_bonds(&mut closed, &mut sites, to_bond, stats) {
return bail(Unresolved::NoPairing);
}
settle_hydrogens(&mut closed, &sites, stats);
if omgkit_chem::sanitize(&mut closed).is_err() {
return bail(Unresolved::FragmentUnsanitizable);
}
if let Some(size) = strained_triple_bond(&mut closed) {
let _ = size;
return bail(Unresolved::StrainedClosure);
}
if heavy_atoms(&closed) != heavy_before
|| total_hydrogens(&closed) != delta_h
|| total_charge(&closed) != budget.delta_charge
{
return bail(Unresolved::BudgetMismatch);
}
Byproducts {
molecules: split(&closed),
verdict: if to_bond == 0 {
Verdict::Capped
} else {
Verdict::Bonded { bonds: to_bond }
},
budget,
}
}
fn strained_triple_bond(mol: &mut MolBuilder) -> Option<u8> {
const MIN_RING_FOR_TRIPLE: u8 = 8;
if !mol.bonds().iter().any(|b| b.order == BondOrder::Triple) {
return None; }
let rings = omgkit_chem::perceive_rings(mol);
for b in mol.bonds() {
if b.order != BondOrder::Triple {
continue;
}
let sizes = [
rings.atom_min_ring_size[b.begin as usize],
rings.atom_min_ring_size[b.end as usize],
];
let in_ring = sizes.iter().copied().filter(|&s| s > 0).min();
if let Some(size) = in_ring {
if size < MIN_RING_FOR_TRIPLE {
return Some(size);
}
}
}
None
}
fn total_hydrogens(mol: &MolBuilder) -> i32 {
mol.atoms()
.iter()
.map(|a| {
i32::from(a.num_explicit_hs)
+ i32::from(a.num_implicit_hs)
+ i32::from(a.atomic_num == 1)
})
.sum()
}
fn heavy_atoms(mol: &MolBuilder) -> usize {
mol.atoms().iter().filter(|a| a.atomic_num != 1).count()
}
fn total_charge(mol: &MolBuilder) -> i32 {
mol.atoms().iter().map(|a| i32::from(a.formal_charge)).sum()
}
fn build_fragment(reactants: &[MolBuilder], discarded: &[Vec<u32>]) -> (MolBuilder, Vec<Site>) {
let mut out = MolBuilder::new();
let mut sites: Vec<Site> = Vec::new();
let mut index: Vec<Vec<u32>> = reactants
.iter()
.map(|m| vec![u32::MAX; m.num_atoms()])
.collect();
for (ti, drop_list) in discarded.iter().enumerate() {
let Some(mol) = reactants.get(ti) else {
debug_assert!(false, "丢弃表第 {ti} 项没有对应的反应物");
continue;
};
for &a in drop_list {
let Some(&data) = mol.atoms().get(a as usize) else {
debug_assert!(false, "丢弃表里的原子下标 {a} 越界");
continue;
};
let mut carried = data;
carried.atom_map = 0;
let idx = out.add_atom_data(carried);
index[ti][a as usize] = idx;
debug_assert_eq!(
idx as usize,
sites.len(),
"sites[i] 必须对应片段的第 i 个原子"
);
sites.push(Site {
opens: 0,
borrowed_h: 0,
});
}
}
for (ti, drop_list) in discarded.iter().enumerate() {
let Some(mol) = reactants.get(ti) else {
continue;
};
let gone: Vec<bool> = {
let mut v = vec![false; mol.num_atoms()];
for &a in drop_list {
if let Some(slot) = v.get_mut(a as usize) {
*slot = true;
}
}
v
};
for b in mol.bonds() {
let (i, j) = (b.begin as usize, b.end as usize);
match (gone.get(i), gone.get(j)) {
(Some(true), Some(true)) => {
let mut nb = *b;
nb.begin = index[ti][i];
nb.end = index[ti][j];
nb.stereo_atoms = [BondData::NO_STEREO_ATOM; 2];
let _ = out.add_bond_data(nb);
}
(Some(true), Some(false)) | (Some(false), Some(true)) => {
let inside = if gone[i] { i } else { j };
let owed = if b.order == BondOrder::Dative {
let idx = u32::try_from(inside).unwrap_or(u32::MAX);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let v = b.valence_contribution_to(idx).round() as u32;
v
} else {
order_valence(b.order)
};
let target = index[ti][inside];
if let Some(s) = sites.get_mut(target as usize) {
s.opens += owed;
}
}
_ => {}
}
}
}
rebase_fragment_chirality(reactants, discarded, &index, &mut out);
(out, sites)
}
fn rebase_fragment_chirality(
reactants: &[MolBuilder],
discarded: &[Vec<u32>],
index: &[Vec<u32>],
out: &mut MolBuilder,
) {
for (ti, drop_list) in discarded.iter().enumerate() {
let Some(mol) = reactants.get(ti) else {
continue;
};
for &a in drop_list {
let Some(&dst) = index[ti].get(a as usize) else {
continue;
};
if dst == u32::MAX {
continue;
}
let tag = out.atoms()[dst as usize].chiral_tag;
if !tag.is_tetrahedral() {
continue;
}
let after: Vec<u32> = out.neighbors(dst).map(|(other, _)| other).collect();
let slots: Vec<Option<u32>> = mol
.neighbors(a)
.map(|(other, _)| {
index[ti]
.get(other as usize)
.copied()
.filter(|&p| p != u32::MAX && after.contains(&p))
})
.collect();
let Some((before, aligned)) = align_for_rebase(&slots, &after) else {
continue;
};
if omgkit_core::permutation_is_odd(&before, &aligned) == Some(true) {
if let Some(at) = out.atom_mut(dst) {
at.chiral_tag = tag.inverted();
}
}
}
}
}
fn order_valence(order: BondOrder) -> u32 {
match order {
BondOrder::Double => 2,
BondOrder::Triple => 3,
BondOrder::Quadruple => 4,
_ => 1,
}
}
fn apply_charges(
frag: &mut MolBuilder,
sites: &mut [Site],
shift: i32,
stats: &mut CloseStats,
) -> bool {
let want_negative = shift < 0;
let step: i8 = if want_negative { -1 } else { 1 };
let mut left = shift.unsigned_abs();
while left > 0 {
let mut best: Option<(u8, usize)> = None;
for (k, site) in sites.iter().enumerate() {
stats.site_visits += 1;
let Some(a) = frag.atoms().get(k) else {
continue;
};
let dv = omgkit_chem::valence_shift(
a.atomic_num,
a.formal_charge,
a.formal_charge.saturating_add(step),
);
if dv < 0 && site.opens == 0 {
continue;
}
if dv == 0 {
continue; }
let rank = if want_negative {
match a.atomic_num {
9 | 17 | 35 | 53 => 0,
8 | 16 => 1,
7 => 2,
_ => 3,
}
} else {
match a.atomic_num {
7 => 0,
8 | 16 => 1,
_ => 2,
}
};
let better = match best {
None => true,
Some((r, _)) => rank < r,
};
if better {
best = Some((rank, k));
}
}
let Some((_, k)) = best else {
return false;
};
let Some(a) = frag.atom_mut(u32::try_from(k).unwrap_or(u32::MAX)) else {
return false;
};
let dv = omgkit_chem::valence_shift(
a.atomic_num,
a.formal_charge,
a.formal_charge.saturating_add(step),
);
a.formal_charge = a.formal_charge.saturating_add(step);
if dv < 0 {
sites[k].opens = sites[k].opens.saturating_sub(1);
} else {
sites[k].opens += 1;
}
left -= 1;
}
true
}
fn borrow_hydrogens(
frag: &MolBuilder,
sites: &mut [Site],
extra: u32,
stats: &mut CloseStats,
) -> bool {
let mut left = extra;
for adjacent_first in [true, false] {
for k in 0..sites.len() {
stats.site_visits += 1;
while left > 0 && has_spare_hydrogen(frag, sites, k) {
let near = frag
.neighbors(u32::try_from(k).unwrap_or(u32::MAX))
.any(|(other, _)| sites.get(other as usize).is_some_and(|s| s.opens > 0));
if adjacent_first != near {
break;
}
sites[k].opens += 1;
sites[k].borrowed_h += 1;
left -= 1;
}
}
if left == 0 {
return true;
}
}
left == 0
}
fn has_spare_hydrogen(frag: &MolBuilder, sites: &[Site], k: usize) -> bool {
let Some(a) = frag.atoms().get(k) else {
return false;
};
if a.atomic_num == 1 {
return false;
}
let carried = u32::from(a.num_explicit_hs) + u32::from(a.num_implicit_hs);
carried > sites[k].borrowed_h
}
fn form_bonds(
frag: &mut MolBuilder,
sites: &mut [Site],
to_bond: u32,
stats: &mut CloseStats,
) -> bool {
let mut formed = 0;
let mut open_sites: Vec<usize> = (0..sites.len()).filter(|&k| sites[k].opens > 0).collect();
while formed < to_bond {
stats.fragment_scans += 1;
let comp = components(frag);
let mut best: Option<(u32, usize, usize)> = None;
for (x, &i) in open_sites.iter().enumerate() {
for &j in &open_sites[x + 1..] {
stats.site_visits += 1;
stats.pair_visits += 1;
if sites[i].opens == 0 || sites[j].opens == 0 {
continue;
}
let (a, b) = (
u32::try_from(i).unwrap_or(u32::MAX),
u32::try_from(j).unwrap_or(u32::MAX),
);
if is_halogen(frag, a) && is_halogen(frag, b) {
continue;
}
let score = if frag.bond_between(a, b).is_some() {
0
} else if comp.get(a as usize) != comp.get(b as usize) {
1
} else {
2
};
let better = match best {
None => true,
Some((s, ..)) => score < s,
};
if better {
best = Some((score, i, j));
}
}
}
let Some((_, i, j)) = best else {
return false;
};
let (a, b) = (
u32::try_from(i).unwrap_or(u32::MAX),
u32::try_from(j).unwrap_or(u32::MAX),
);
if let Some(bi) = frag.bond_between(a, b) {
let Some(mut edge) = frag.bond_mut(bi) else {
return false;
};
let Some(up) = raise(edge.get().order) else {
return false;
};
edge.set_order(up);
} else if frag.add_bond(a, b, BondOrder::Single).is_err() {
return false;
}
sites[i].opens -= 1;
sites[j].opens -= 1;
open_sites.retain(|&k| sites[k].opens > 0);
formed += 1;
}
true
}
fn is_halogen(frag: &MolBuilder, idx: u32) -> bool {
frag.atoms()
.get(idx as usize)
.is_some_and(|a| matches!(a.atomic_num, 9 | 17 | 35 | 53 | 85))
}
fn raise(order: BondOrder) -> Option<BondOrder> {
match order {
BondOrder::Single | BondOrder::Aromatic => Some(BondOrder::Double),
BondOrder::Double => Some(BondOrder::Triple),
_ => None,
}
}
fn settle_hydrogens(frag: &mut MolBuilder, sites: &[Site], stats: &mut CloseStats) {
for (i, s) in sites.iter().enumerate() {
stats.site_visits += 1;
let Some(a) = frag.atom_mut(u32::try_from(i).unwrap_or(u32::MAX)) else {
continue;
};
if !a.flags.contains(AtomFlags::NO_IMPLICIT) {
continue;
}
let add = u8::try_from(s.opens).unwrap_or(0);
a.num_explicit_hs = a.num_explicit_hs.saturating_add(add);
let take = u8::try_from(s.borrowed_h).unwrap_or(0);
a.num_explicit_hs = a.num_explicit_hs.saturating_sub(take);
}
}
fn split(mol: &MolBuilder) -> Vec<MolBuilder> {
let comp = components(mol);
let n_comp = comp.iter().copied().max().map_or(0, |m| m as usize + 1);
let mut out: Vec<MolBuilder> = (0..n_comp).map(|_| MolBuilder::new()).collect();
let mut local = vec![u32::MAX; mol.num_atoms()];
for (a, &c) in comp.iter().enumerate() {
local[a] = out[c as usize].add_atom_data(mol.atoms()[a]);
}
for b in mol.bonds() {
let c = comp[b.begin as usize] as usize;
let mut nb = *b;
nb.begin = local[b.begin as usize];
nb.end = local[b.end as usize];
nb.stereo_atoms = [BondData::NO_STEREO_ATOM; 2];
let _ = out[c].add_bond_data(nb);
}
out
}