use crate::error::{InchiError, Result};
use crate::options::Options;
use crate::output::InchiOutput;
const MAX_ATOMS: usize = 1024;
const MAX_BONDS_PER_ATOM: usize = inchi_sys::MAXVAL as usize;
const ELNAME_CAP: usize = inchi_sys::ATOM_EL_LEN as usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Radical {
#[default]
None,
Singlet,
Doublet,
Triplet,
}
impl Radical {
fn code(self) -> i8 {
let v = match self {
Radical::None => inchi_sys::INCHI_RADICAL_NONE,
Radical::Singlet => inchi_sys::INCHI_RADICAL_SINGLET,
Radical::Doublet => inchi_sys::INCHI_RADICAL_DOUBLET,
Radical::Triplet => inchi_sys::INCHI_RADICAL_TRIPLET,
};
v as i8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum BondOrder {
#[default]
Single,
Double,
Triple,
Alternating,
}
impl BondOrder {
fn code(self) -> i8 {
let v = match self {
BondOrder::Single => inchi_sys::INCHI_BOND_TYPE_SINGLE,
BondOrder::Double => inchi_sys::INCHI_BOND_TYPE_DOUBLE,
BondOrder::Triple => inchi_sys::INCHI_BOND_TYPE_TRIPLE,
BondOrder::Alternating => inchi_sys::INCHI_BOND_TYPE_ALTERN,
};
v as i8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ImplicitH {
#[default]
Auto,
Exactly(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Parity {
Odd,
Even,
Unknown,
}
impl Parity {
fn code(self) -> i8 {
let v = match self {
Parity::Odd => inchi_sys::INCHI_PARITY_ODD,
Parity::Even => inchi_sys::INCHI_PARITY_EVEN,
Parity::Unknown => inchi_sys::INCHI_PARITY_UNKNOWN,
};
v as i8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Stereo {
Tetrahedral {
center: usize,
neighbors: [usize; 4],
parity: Parity,
},
DoubleBond {
ends: [usize; 4],
parity: Parity,
},
Allene {
center: usize,
ends: [usize; 4],
parity: Parity,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Atom {
element: String,
x: f64,
y: f64,
z: f64,
charge: i8,
isotope: Option<u16>,
radical: Radical,
implicit_h: ImplicitH,
}
impl Atom {
#[must_use]
pub fn new(element: impl Into<String>) -> Self {
Atom {
element: element.into(),
x: 0.0,
y: 0.0,
z: 0.0,
charge: 0,
isotope: None,
radical: Radical::None,
implicit_h: ImplicitH::Auto,
}
}
#[must_use]
pub fn position(mut self, x: f64, y: f64, z: f64) -> Self {
self.x = x;
self.y = y;
self.z = z;
self
}
#[must_use]
pub fn charge(mut self, charge: i8) -> Self {
self.charge = charge;
self
}
#[must_use]
pub fn isotope(mut self, mass: u16) -> Self {
self.isotope = Some(mass);
self
}
#[must_use]
pub fn radical(mut self, radical: Radical) -> Self {
self.radical = radical;
self
}
#[must_use]
pub fn implicit_hydrogens(mut self, h: ImplicitH) -> Self {
self.implicit_h = h;
self
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Molecule {
atoms: Vec<Atom>,
bonds: Vec<(usize, usize, BondOrder)>,
stereo: Vec<Stereo>,
polymer_units: Vec<crate::polymer::PolymerUnit>,
}
impl Molecule {
#[must_use]
pub fn new() -> Self {
Molecule::default()
}
pub fn add_atom(&mut self, atom: Atom) -> usize {
self.atoms.push(atom);
self.atoms.len() - 1
}
pub fn add_bond(&mut self, a: usize, b: usize, order: BondOrder) -> Result<()> {
if a == b {
return Err(InchiError::InvalidStructure {
reason: format!("bond connects atom {a} to itself"),
});
}
let n = self.atoms.len();
if a >= n || b >= n {
return Err(InchiError::InvalidStructure {
reason: format!("bond ({a}, {b}) references a nonexistent atom (have {n})"),
});
}
self.bonds.push((a, b, order));
Ok(())
}
pub fn add_stereo(&mut self, stereo: Stereo) {
self.stereo.push(stereo);
}
pub fn add_polymer_unit(&mut self, unit: crate::polymer::PolymerUnit) {
self.polymer_units.push(unit);
}
#[must_use]
pub fn atom_count(&self) -> usize {
self.atoms.len()
}
#[must_use]
pub fn bond_count(&self) -> usize {
self.bonds.len()
}
#[must_use]
pub fn stereo_count(&self) -> usize {
self.stereo.len()
}
pub fn to_inchi(&self, options: impl Into<Options>) -> Result<InchiOutput> {
let options = options.into();
let mut atoms = self.build_atoms()?;
let mut stereo = self.build_stereo()?;
let opts = crate::raw::to_cstring(&options.to_arg_string())?;
let num_atoms = i16::try_from(atoms.len()).map_err(|_| InchiError::InvalidStructure {
reason: format!("too many atoms ({})", atoms.len()),
})?;
let num_stereo = i16::try_from(stereo.len()).map_err(|_| InchiError::InvalidStructure {
reason: format!("too many stereo elements ({})", stereo.len()),
})?;
let mut input: inchi_sys::inchi_Input = unsafe { std::mem::zeroed() };
input.atom = atoms.as_mut_ptr();
input.num_atoms = num_atoms;
input.stereo0D = if stereo.is_empty() {
std::ptr::null_mut()
} else {
stereo.as_mut_ptr()
};
input.num_stereo0D = num_stereo;
input.szOptions = opts.as_ptr() as *mut std::os::raw::c_char;
if self.polymer_units.is_empty() {
let _guard = crate::raw::lock();
let mut out = crate::raw::OutputGuard::new();
let rc = unsafe { inchi_sys::GetINCHI(&mut input, out.as_mut_ptr()) };
drop(atoms);
drop(stereo);
drop(opts);
return crate::build_output(rc, &out);
}
let mut backing = PolymerBacking::build(&self.polymer_units, self.atoms.len())?;
let mut input_ex: inchi_sys::inchi_InputEx = unsafe { std::mem::zeroed() };
input_ex.atom = atoms.as_mut_ptr();
input_ex.num_atoms = num_atoms;
input_ex.stereo0D = if stereo.is_empty() {
std::ptr::null_mut()
} else {
stereo.as_mut_ptr()
};
input_ex.num_stereo0D = num_stereo;
input_ex.szOptions = opts.as_ptr() as *mut std::os::raw::c_char;
input_ex.polymer = backing.as_mut_ptr();
let _guard = crate::raw::lock();
let mut out = crate::raw::OutputGuard::new();
let rc = unsafe { inchi_sys::GetINCHIEx(&mut input_ex, out.as_mut_ptr()) };
drop(atoms);
drop(stereo);
drop(opts);
drop(backing);
crate::build_output(rc, &out)
}
fn build_atoms(&self) -> Result<Vec<inchi_sys::inchi_Atom>> {
if self.atoms.is_empty() {
return Err(InchiError::InvalidStructure {
reason: "molecule has no atoms".to_string(),
});
}
if self.atoms.len() > MAX_ATOMS {
return Err(InchiError::InvalidStructure {
reason: format!("too many atoms ({} > {MAX_ATOMS})", self.atoms.len()),
});
}
let mut raw: Vec<inchi_sys::inchi_Atom> = Vec::with_capacity(self.atoms.len());
for atom in &self.atoms {
let mut a: inchi_sys::inchi_Atom = unsafe { std::mem::zeroed() };
a.x = atom.x;
a.y = atom.y;
a.z = atom.z;
write_elname(&mut a.elname, &atom.element)?;
a.charge = atom.charge;
a.radical = atom.radical.code();
if let Some(mass) = atom.isotope {
a.isotopic_mass =
i16::try_from(mass).map_err(|_| InchiError::InvalidStructure {
reason: format!("isotopic mass {mass} out of range"),
})?;
}
a.num_iso_H = match atom.implicit_h {
ImplicitH::Auto => [-1, 0, 0, 0],
ImplicitH::Exactly(n) => [
i8::try_from(n).map_err(|_| InchiError::InvalidStructure {
reason: format!("implicit H count {n} out of range"),
})?,
0,
0,
0,
],
};
raw.push(a);
}
for &(a, b, order) in &self.bonds {
push_neighbor(&mut raw, a, b, order)?;
push_neighbor(&mut raw, b, a, order)?;
}
Ok(raw)
}
fn build_stereo(&self) -> Result<Vec<inchi_sys::inchi_Stereo0D>> {
let n = self.atoms.len();
let check = |idx: usize| -> Result<i16> {
if idx >= n {
return Err(InchiError::InvalidStructure {
reason: format!("stereo references nonexistent atom {idx} (have {n})"),
});
}
i16::try_from(idx).map_err(|_| InchiError::InvalidStructure {
reason: format!("atom index {idx} out of range"),
})
};
let mut raw = Vec::with_capacity(self.stereo.len());
for stereo in &self.stereo {
let mut s: inchi_sys::inchi_Stereo0D = unsafe { std::mem::zeroed() };
match *stereo {
Stereo::Tetrahedral { center, neighbors, parity } => {
s.central_atom = check(center)?;
s.neighbor = [
check(neighbors[0])?,
check(neighbors[1])?,
check(neighbors[2])?,
check(neighbors[3])?,
];
s.type_ = inchi_sys::INCHI_StereoType_Tetrahedral as i8;
s.parity = parity.code();
}
Stereo::DoubleBond { ends, parity } => {
s.central_atom = inchi_sys::NO_ATOM as i16;
s.neighbor = [
check(ends[0])?,
check(ends[1])?,
check(ends[2])?,
check(ends[3])?,
];
s.type_ = inchi_sys::INCHI_StereoType_DoubleBond as i8;
s.parity = parity.code();
}
Stereo::Allene { center, ends, parity } => {
s.central_atom = check(center)?;
s.neighbor = [
check(ends[0])?,
check(ends[1])?,
check(ends[2])?,
check(ends[3])?,
];
s.type_ = inchi_sys::INCHI_StereoType_Allene as i8;
s.parity = parity.code();
}
}
raw.push(s);
}
Ok(raw)
}
}
struct PolymerBacking {
alists: Vec<Vec<std::os::raw::c_int>>,
blists: Vec<Vec<std::os::raw::c_int>>,
units: Vec<inchi_sys::inchi_Input_PolymerUnit>,
unit_ptrs: Vec<*mut inchi_sys::inchi_Input_PolymerUnit>,
polymer: inchi_sys::inchi_Input_Polymer,
}
impl PolymerBacking {
fn build(units_in: &[crate::polymer::PolymerUnit], num_atoms: usize) -> Result<Box<Self>> {
let one_based = |idx: usize| -> Result<std::os::raw::c_int> {
if idx >= num_atoms {
return Err(InchiError::InvalidStructure {
reason: format!("polymer unit references nonexistent atom {idx} (have {num_atoms})"),
});
}
i32::try_from(idx + 1).map_err(|_| InchiError::InvalidStructure {
reason: format!("atom index {idx} out of range"),
})
};
let mut alists = Vec::with_capacity(units_in.len());
let mut blists = Vec::with_capacity(units_in.len());
for unit in units_in {
let mut alist = Vec::with_capacity(unit.atoms.len());
for &a in &unit.atoms {
alist.push(one_based(a)?);
}
let mut blist = Vec::with_capacity(unit.crossing_bonds.len() * 2);
for &[a, b] in &unit.crossing_bonds {
blist.push(one_based(a)?);
blist.push(one_based(b)?);
}
alists.push(alist);
blists.push(blist);
}
let mut me = Box::new(PolymerBacking {
alists,
blists,
units: Vec::with_capacity(units_in.len()),
unit_ptrs: Vec::with_capacity(units_in.len()),
polymer: unsafe { std::mem::zeroed() },
});
let mut built = Vec::with_capacity(units_in.len());
let lists = me.alists.iter().zip(me.blists.iter());
for (unit, (alist, blist)) in units_in.iter().zip(lists) {
let mut raw: inchi_sys::inchi_Input_PolymerUnit = unsafe { std::mem::zeroed() };
raw.id = unit.id;
raw.label = unit.label;
raw.type_ = unit.kind.code();
raw.subtype = unit.subtype.code();
raw.conn = unit.connection.code();
raw.na = i32::try_from(unit.atoms.len()).unwrap_or(0);
raw.nb = i32::try_from(unit.crossing_bonds.len()).unwrap_or(0);
write_subscript(&mut raw.smt, &unit.subscript);
raw.alist = if alist.is_empty() {
std::ptr::null_mut()
} else {
alist.as_ptr() as *mut std::os::raw::c_int
};
raw.blist = if blist.is_empty() {
std::ptr::null_mut()
} else {
blist.as_ptr() as *mut std::os::raw::c_int
};
built.push(raw);
}
me.units = built;
let mut ptrs = Vec::with_capacity(me.units.len());
for u in me.units.iter_mut() {
ptrs.push(u as *mut inchi_sys::inchi_Input_PolymerUnit);
}
me.unit_ptrs = ptrs;
me.polymer.n = i32::try_from(me.unit_ptrs.len()).unwrap_or(0);
me.polymer.units = me.unit_ptrs.as_mut_ptr();
Ok(me)
}
fn as_mut_ptr(&mut self) -> *mut inchi_sys::inchi_Input_Polymer {
&mut self.polymer
}
}
fn write_subscript(dst: &mut [std::os::raw::c_char; 80], subscript: &str) {
let max = dst.len().saturating_sub(1);
for (slot, &b) in dst.iter_mut().zip(subscript.as_bytes().iter().take(max)) {
*slot = b as std::os::raw::c_char;
}
}
fn push_neighbor(
atoms: &mut [inchi_sys::inchi_Atom],
from: usize,
to: usize,
order: BondOrder,
) -> Result<()> {
let to_idx = i16::try_from(to).map_err(|_| InchiError::InvalidStructure {
reason: format!("atom index {to} out of range"),
})?;
let atom = atoms.get_mut(from).ok_or_else(|| InchiError::InvalidStructure {
reason: format!("bond references nonexistent atom {from}"),
})?;
let slot = atom.num_bonds as usize;
if slot >= MAX_BONDS_PER_ATOM {
return Err(InchiError::InvalidStructure {
reason: format!("atom {from} exceeds the maximum of {MAX_BONDS_PER_ATOM} bonds"),
});
}
if let (Some(nbr), Some(bt)) = (atom.neighbor.get_mut(slot), atom.bond_type.get_mut(slot)) {
*nbr = to_idx;
*bt = order.code();
atom.num_bonds += 1;
Ok(())
} else {
Err(InchiError::InvalidStructure {
reason: format!("atom {from} bond slot {slot} out of range"),
})
}
}
fn write_elname(dst: &mut [std::os::raw::c_char; ELNAME_CAP], symbol: &str) -> Result<()> {
let bytes = symbol.as_bytes();
if bytes.is_empty() {
return Err(InchiError::InvalidStructure {
reason: "empty element symbol".to_string(),
});
}
if !symbol.is_ascii() {
return Err(InchiError::InvalidStructure {
reason: format!("element symbol {symbol:?} is not ASCII"),
});
}
if bytes.len() >= ELNAME_CAP {
return Err(InchiError::InvalidStructure {
reason: format!("element symbol {symbol:?} is too long (max {} chars)", ELNAME_CAP - 1),
});
}
for (slot, &b) in dst.iter_mut().zip(bytes) {
*slot = b as std::os::raw::c_char;
}
Ok(())
}