Skip to main content

inchi/
structure.rs

1//! Reverse direction: parse an InChI string back into an atom/bond/stereo
2//! [`Structure`] via the native `GetStructFromINCHI` entry point.
3
4use crate::error::{InchiError, Result, Status};
5use crate::molecule::{BondOrder, Parity, Stereo};
6
7/// One atom of a [`Structure`] recovered from an InChI.
8#[derive(Debug, Clone, PartialEq)]
9#[non_exhaustive]
10pub struct StructureAtom {
11    /// The element symbol (e.g. `"C"`).
12    pub element: String,
13    /// Cartesian coordinates (zero for InChIs without coordinates).
14    pub position: [f64; 3],
15    /// The formal charge.
16    pub charge: i8,
17    /// The absolute isotopic mass, or `None` for the natural composition.
18    pub isotope: Option<u16>,
19    /// Total number of implicit (non-isotopic) hydrogens attached.
20    pub implicit_hydrogens: u8,
21}
22
23/// One bond of a [`Structure`], referencing atoms by index.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub struct StructureBond {
27    /// Index of the first atom.
28    pub from: usize,
29    /// Index of the second atom.
30    pub to: usize,
31    /// The bond order.
32    pub order: BondOrder,
33}
34
35/// A molecular structure recovered from an InChI string.
36///
37/// ```
38/// use inchi::struct_from_inchi;
39///
40/// let s = struct_from_inchi("InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3")?;
41/// assert_eq!(s.atoms().len(), 3); // two carbons and an oxygen
42/// assert_eq!(s.bonds().len(), 2);
43/// # Ok::<(), inchi::InchiError>(())
44/// ```
45#[derive(Debug, Clone, PartialEq)]
46pub struct Structure {
47    atoms: Vec<StructureAtom>,
48    bonds: Vec<StructureBond>,
49    stereo: Vec<Stereo>,
50}
51
52impl Structure {
53    /// The atoms, in InChI canonical order.
54    ///
55    /// ```
56    /// # use inchi::struct_from_inchi;
57    /// let s = struct_from_inchi("InChI=1S/H2O/h1H2")?;
58    /// assert_eq!(s.atoms()[0].element, "O");
59    /// assert_eq!(s.atoms()[0].implicit_hydrogens, 2);
60    /// # Ok::<(), inchi::InchiError>(())
61    /// ```
62    #[must_use]
63    pub fn atoms(&self) -> &[StructureAtom] {
64        &self.atoms
65    }
66
67    /// The bonds, each listed once.
68    ///
69    /// ```
70    /// # use inchi::struct_from_inchi;
71    /// let s = struct_from_inchi("InChI=1S/CH4/h1H4")?;
72    /// assert!(s.bonds().is_empty()); // lone carbon, hydrogens are implicit
73    /// # Ok::<(), inchi::InchiError>(())
74    /// ```
75    #[must_use]
76    pub fn bonds(&self) -> &[StructureBond] {
77        &self.bonds
78    }
79
80    /// The 0D stereo descriptors recovered from the InChI.
81    ///
82    /// ```
83    /// # use inchi::struct_from_inchi;
84    /// let s = struct_from_inchi("InChI=1S/C3H7NO2/c1-2(4)3(5)6/h2H,4H2,1H3,(H,5,6)/t2-/m0/s1")?;
85    /// assert_eq!(s.stereo().len(), 1);
86    /// # Ok::<(), inchi::InchiError>(())
87    /// ```
88    #[must_use]
89    pub fn stereo(&self) -> &[Stereo] {
90        &self.stereo
91    }
92}
93
94/// A [`Structure`] together with any polymer data recovered from an InChI by
95/// [`struct_from_inchi_ex`].
96#[derive(Debug, Clone, PartialEq)]
97#[non_exhaustive]
98pub struct ExtendedStructure {
99    /// The atom/bond/stereo structure (identical to [`struct_from_inchi`]).
100    pub structure: Structure,
101    /// The polymer structural repeating units, empty for non-polymer InChIs.
102    pub polymer_units: Vec<crate::polymer::PolymerUnit>,
103}
104
105/// RAII guard ensuring `FreeStructFromINCHI` runs for the output struct.
106struct StructGuard {
107    raw: inchi_sys::inchi_OutputStruct,
108}
109
110impl StructGuard {
111    fn new() -> Self {
112        StructGuard {
113            raw: unsafe { std::mem::zeroed() },
114        }
115    }
116}
117
118impl Drop for StructGuard {
119    fn drop(&mut self) {
120        unsafe { inchi_sys::FreeStructFromINCHI(&mut self.raw) }
121    }
122}
123
124/// Parses an InChI string into its atom/bond/stereo [`Structure`].
125///
126/// This reverses [`from_molfile`](crate::from_molfile) / [`Molecule::to_inchi`](crate::Molecule::to_inchi)
127/// (modulo information InChI does not retain, such as exact coordinates).
128///
129/// # Errors
130///
131/// Returns [`InchiError::Failed`] if the InChI is invalid or cannot be expanded,
132/// and [`InchiError::InteriorNul`] if the input contains a NUL byte.
133///
134/// ```
135/// use inchi::struct_from_inchi;
136///
137/// let s = struct_from_inchi("InChI=1S/CH4/h1H4")?;
138/// assert_eq!(s.atoms().len(), 1);
139/// assert_eq!(s.atoms()[0].element, "C");
140/// assert_eq!(s.atoms()[0].implicit_hydrogens, 4);
141/// # Ok::<(), inchi::InchiError>(())
142/// ```
143pub fn struct_from_inchi(inchi: impl AsRef<str>) -> Result<Structure> {
144    let src = crate::raw::to_cstring(inchi.as_ref())?;
145    // No options are required for the reverse direction.
146    let empty = crate::raw::to_cstring("")?;
147
148    let mut input = inchi_sys::inchi_InputINCHI {
149        szInChI: src.as_ptr() as *mut std::os::raw::c_char,
150        szOptions: empty.as_ptr() as *mut std::os::raw::c_char,
151    };
152
153    let _guard = crate::raw::lock();
154    let mut out = StructGuard::new();
155    let rc = unsafe { inchi_sys::GetStructFromINCHI(&mut input, &mut out.raw) };
156    drop(src);
157    drop(empty);
158
159    let status = Status::from_code(rc);
160    if !status.is_success() {
161        let message = unsafe { crate::raw::cstr_to_string(out.raw.szMessage) };
162        return Err(InchiError::Failed { status, message });
163    }
164
165    // SAFETY: on success the library populated `num_atoms` atoms at `atom` and
166    // `num_stereo0D` stereo elements at `stereo0D` (either may be null/zero).
167    unsafe {
168        convert_raw(
169            out.raw.atom,
170            out.raw.num_atoms,
171            out.raw.stereo0D,
172            out.raw.num_stereo0D,
173        )
174    }
175}
176
177/// RAII guard ensuring `FreeStructFromINCHIEx` runs for the extended output.
178struct StructExGuard {
179    raw: inchi_sys::inchi_OutputStructEx,
180}
181
182impl StructExGuard {
183    fn new() -> Self {
184        StructExGuard {
185            raw: unsafe { std::mem::zeroed() },
186        }
187    }
188}
189
190impl Drop for StructExGuard {
191    fn drop(&mut self) {
192        unsafe { inchi_sys::FreeStructFromINCHIEx(&mut self.raw) }
193    }
194}
195
196/// Parses an InChI into its structure *and* polymer data via the extended
197/// `GetStructFromINCHIEx` entry point.
198///
199/// For an ordinary (non-polymer) InChI this returns the same atoms, bonds, and
200/// stereo as [`struct_from_inchi`], with an empty
201/// [`polymer_units`](ExtendedStructure::polymer_units). For a polymer InChI
202/// (produced with [`Options::polymers`](crate::Options::polymers)) it
203/// additionally recovers the structural repeating units.
204///
205/// # Errors
206///
207/// Returns [`InchiError::Failed`] if the InChI is invalid or cannot be expanded,
208/// and [`InchiError::InteriorNul`] if the input contains a NUL byte.
209///
210/// ```
211/// use inchi::struct_from_inchi_ex;
212///
213/// // An ordinary InChI yields no polymer units.
214/// let ext = struct_from_inchi_ex("InChI=1S/CH4/h1H4")?;
215/// assert_eq!(ext.structure.atoms().len(), 1);
216/// assert!(ext.polymer_units.is_empty());
217/// # Ok::<(), inchi::InchiError>(())
218/// ```
219pub fn struct_from_inchi_ex(inchi: impl AsRef<str>) -> Result<ExtendedStructure> {
220    let src = crate::raw::to_cstring(inchi.as_ref())?;
221    let empty = crate::raw::to_cstring("")?;
222
223    let mut input = inchi_sys::inchi_InputINCHI {
224        szInChI: src.as_ptr() as *mut std::os::raw::c_char,
225        szOptions: empty.as_ptr() as *mut std::os::raw::c_char,
226    };
227
228    let _guard = crate::raw::lock();
229    let mut out = StructExGuard::new();
230    let rc = unsafe { inchi_sys::GetStructFromINCHIEx(&mut input, &mut out.raw) };
231    drop(src);
232    drop(empty);
233
234    let status = Status::from_code(rc);
235    if !status.is_success() {
236        let message = unsafe { crate::raw::cstr_to_string(out.raw.szMessage) };
237        return Err(InchiError::Failed { status, message });
238    }
239
240    // SAFETY: on success the atom/stereo arrays are populated as in the plain
241    // struct output; the field layout matches `convert_raw`'s expectations.
242    let structure = unsafe {
243        convert_raw(
244            out.raw.atom,
245            out.raw.num_atoms,
246            out.raw.stereo0D,
247            out.raw.num_stereo0D,
248        )
249    }?;
250
251    // SAFETY: `polymer`, when non-null, points to a populated polymer block.
252    let polymer_units = unsafe { read_polymer(out.raw.polymer) };
253
254    Ok(ExtendedStructure {
255        structure,
256        polymer_units,
257    })
258}
259
260/// Reads the polymer block of an extended output into safe [`PolymerUnit`]s.
261///
262/// # Safety
263///
264/// `polymer` must be null or a valid pointer to a populated `inchi_Output_Polymer`.
265unsafe fn read_polymer(
266    polymer: *const inchi_sys::inchi_Input_Polymer,
267) -> Vec<crate::polymer::PolymerUnit> {
268    if polymer.is_null() {
269        return Vec::new();
270    }
271    // SAFETY: caller guarantees `polymer` is valid when non-null.
272    let p = unsafe { &*polymer };
273    let n = usize::try_from(p.n).unwrap_or(0);
274    if n == 0 || p.units.is_null() {
275        return Vec::new();
276    }
277    // SAFETY: `units` is an array of `n` unit pointers.
278    let units = unsafe { std::slice::from_raw_parts(p.units, n) };
279    units
280        .iter()
281        .filter_map(|&u| unsafe { crate::polymer::read_unit(u) })
282        .collect()
283}
284
285/// Reconstructs a [`Structure`] from the AuxInfo emitted alongside an InChI.
286///
287/// The auxiliary-information string ([`InchiOutput::aux_info`](crate::InchiOutput::aux_info))
288/// records the original atom numbering, so it recovers a structure closer to
289/// the input than [`struct_from_inchi`] does. The input may be either a full
290/// InChI output block or just the `AuxInfo=` line.
291///
292/// When `add_hydrogens` is `true` the library is allowed to add implicit
293/// hydrogens to complete normal valences (the usual choice); pass `false` to
294/// keep only the hydrogens explicitly recorded.
295///
296/// # Errors
297///
298/// Returns [`InchiError::Failed`] if the AuxInfo cannot be parsed,
299/// [`InchiError::EmptyResult`] if it yields no atoms, and
300/// [`InchiError::InteriorNul`] if the input contains a NUL byte.
301///
302/// ```
303/// use inchi::{from_molfile, struct_from_aux_info};
304///
305/// let methane = "\n  ex\n\n  1  0  0  0  0  0  0  0  0  0999 V2000\n\
306///     \x20   0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0\nM  END\n";
307/// let out = from_molfile(methane, ())?;
308/// let s = struct_from_aux_info(out.aux_info(), true)?;
309/// assert_eq!(s.atoms()[0].element, "C");
310/// # Ok::<(), inchi::InchiError>(())
311/// ```
312pub fn struct_from_aux_info(aux_info: impl AsRef<str>, add_hydrogens: bool) -> Result<Structure> {
313    let aux = crate::raw::to_cstring(aux_info.as_ref())?;
314
315    let _guard = crate::raw::lock();
316    let mut guard = InputGuard::new();
317    // `bDoNotAddH` is the inverse of `add_hydrogens`; `bDiffUnkUndfStereo = 0`
318    // keeps the default of merging unknown and undefined stereo labels.
319    let b_do_not_add_h = i32::from(!add_hydrogens);
320    // SAFETY: `aux` is a valid NUL-terminated string for the call; the library
321    // treats it as read-only despite the `*mut` signature. `guard.data.pInp`
322    // points at the zeroed `inchi_Input` that `guard` owns and frees on drop.
323    let rc = unsafe {
324        inchi_sys::Get_inchi_Input_FromAuxInfo(
325            aux.as_ptr() as *mut std::os::raw::c_char,
326            b_do_not_add_h,
327            0,
328            &mut guard.data,
329        )
330    };
331    drop(aux);
332
333    let status = Status::from_code(rc);
334    if !status.is_success() {
335        let message = unsafe { read_err_msg(&guard.data.szErrMsg) };
336        return Err(InchiError::Failed { status, message });
337    }
338
339    // SAFETY: on success `pInp` points to a populated `inchi_Input`.
340    let inp = &guard.input;
341    let s = unsafe { convert_raw(inp.atom, inp.num_atoms, inp.stereo0D, inp.num_stereo0D) }?;
342    if s.atoms.is_empty() {
343        return Err(InchiError::EmptyResult);
344    }
345    Ok(s)
346}
347
348/// RAII guard owning the `inchi_Input` that `Get_inchi_Input_FromAuxInfo`
349/// populates, ensuring `Free_inchi_Input` runs for whatever it allocated.
350struct InputGuard {
351    input: Box<inchi_sys::inchi_Input>,
352    data: inchi_sys::InchiInpData,
353}
354
355impl InputGuard {
356    fn new() -> Self {
357        // `input` is boxed so its address is stable while `data.pInp` borrows it.
358        let mut input = Box::new(unsafe { std::mem::zeroed::<inchi_sys::inchi_Input>() });
359        let mut data: inchi_sys::InchiInpData = unsafe { std::mem::zeroed() };
360        data.pInp = input.as_mut();
361        InputGuard { input, data }
362    }
363}
364
365impl Drop for InputGuard {
366    fn drop(&mut self) {
367        // Frees the atom/stereo arrays the library allocated and zeroes the
368        // changed members; safe to call even when nothing was populated.
369        unsafe { inchi_sys::Free_inchi_Input(self.input.as_mut()) }
370    }
371}
372
373/// Reads the fixed-size `szErrMsg` buffer into an owned `String`.
374///
375/// # Safety
376///
377/// `buf` must be a valid array holding a NUL-terminated C string.
378unsafe fn read_err_msg(buf: &[std::os::raw::c_char]) -> String {
379    crate::raw::cstr_to_string(buf.as_ptr())
380}
381
382/// Converts a populated atom/stereo array (shared by `inchi_Input` and
383/// `inchi_OutputStruct`) into a safe [`Structure`].
384///
385/// # Safety
386///
387/// When `num_atoms > 0`, `atom` must point to that many valid [`inchi_Atom`](inchi_sys::inchi_Atom)
388/// values; likewise `stereo0D` must hold `num_stereo0D` valid elements.
389unsafe fn convert_raw(
390    atom: *mut inchi_sys::inchi_Atom,
391    num_atoms: inchi_sys::AT_NUM,
392    stereo0d: *mut inchi_sys::inchi_Stereo0D,
393    num_stereo0d: inchi_sys::AT_NUM,
394) -> Result<Structure> {
395    let num_atoms = usize::try_from(num_atoms).unwrap_or(0);
396    if num_atoms == 0 || atom.is_null() {
397        return Ok(Structure {
398            atoms: Vec::new(),
399            bonds: Vec::new(),
400            stereo: Vec::new(),
401        });
402    }
403
404    // SAFETY: the caller guarantees `num_atoms` valid atoms at `atom`.
405    let c_atoms = unsafe { std::slice::from_raw_parts(atom, num_atoms) };
406
407    let mut atoms = Vec::with_capacity(num_atoms);
408    let mut bonds = Vec::new();
409    for (i, ca) in c_atoms.iter().enumerate() {
410        atoms.push(StructureAtom {
411            element: read_elname(&ca.elname),
412            position: [ca.x, ca.y, ca.z],
413            charge: ca.charge,
414            isotope: read_isotope(ca.isotopic_mass),
415            implicit_hydrogens: read_implicit_h(&ca.num_iso_H),
416        });
417
418        // Each bond appears in both atoms' adjacency lists; keep it once.
419        let degree = (ca.num_bonds.max(0) as usize).min(ca.neighbor.len());
420        for slot in 0..degree {
421            let (Some(&nbr), Some(&bt)) = (ca.neighbor.get(slot), ca.bond_type.get(slot)) else {
422                continue;
423            };
424            let j = usize::try_from(nbr).unwrap_or(usize::MAX);
425            if j < num_atoms && i < j {
426                bonds.push(StructureBond {
427                    from: i,
428                    to: j,
429                    order: decode_order(bt),
430                });
431            }
432        }
433    }
434
435    // SAFETY: forwarded from the caller's guarantee on `stereo0d`/`num_stereo0d`.
436    let stereo = unsafe { read_stereo(stereo0d, num_stereo0d, num_atoms) };
437
438    Ok(Structure {
439        atoms,
440        bonds,
441        stereo,
442    })
443}
444
445/// # Safety
446///
447/// When `num_stereo0d > 0`, `stereo0d` must point to that many valid elements.
448unsafe fn read_stereo(
449    stereo0d: *mut inchi_sys::inchi_Stereo0D,
450    num_stereo0d: inchi_sys::AT_NUM,
451    num_atoms: usize,
452) -> Vec<Stereo> {
453    let count = usize::try_from(num_stereo0d).unwrap_or(0);
454    if count == 0 || stereo0d.is_null() {
455        return Vec::new();
456    }
457    // SAFETY: the caller guarantees `count` valid stereo elements.
458    let c_stereo = unsafe { std::slice::from_raw_parts(stereo0d, count) };
459
460    let idx = |v: inchi_sys::AT_NUM| usize::try_from(v).ok().filter(|&i| i < num_atoms);
461    let mut out = Vec::with_capacity(count);
462    for cs in c_stereo {
463        let Some(parity) = decode_parity(cs.parity) else {
464            continue;
465        };
466        let Some(ends) = (|| {
467            Some([
468                idx(cs.neighbor[0])?,
469                idx(cs.neighbor[1])?,
470                idx(cs.neighbor[2])?,
471                idx(cs.neighbor[3])?,
472            ])
473        })() else {
474            continue;
475        };
476        let ty = cs.type_ as u32;
477        if ty == inchi_sys::INCHI_StereoType_Tetrahedral {
478            if let Some(center) = idx(cs.central_atom) {
479                out.push(Stereo::Tetrahedral {
480                    center,
481                    neighbors: ends,
482                    parity,
483                });
484            }
485        } else if ty == inchi_sys::INCHI_StereoType_DoubleBond {
486            out.push(Stereo::DoubleBond { ends, parity });
487        } else if ty == inchi_sys::INCHI_StereoType_Allene {
488            if let Some(center) = idx(cs.central_atom) {
489                out.push(Stereo::Allene {
490                    center,
491                    ends,
492                    parity,
493                });
494            }
495        }
496    }
497    out
498}
499
500fn read_elname(raw: &[std::os::raw::c_char]) -> String {
501    let mut s = String::new();
502    for &c in raw {
503        if c == 0 {
504            break;
505        }
506        s.push(c as u8 as char);
507    }
508    s
509}
510
511fn read_isotope(mass: inchi_sys::AT_NUM) -> Option<u16> {
512    if mass == 0 {
513        None
514    } else {
515        u16::try_from(mass).ok()
516    }
517}
518
519fn read_implicit_h(num_iso_h: &[i8]) -> u8 {
520    // num_iso_H[0] is the count of non-isotopic implicit H (or -1 for "auto",
521    // which never appears in library output). Sum all isotopes for the total.
522    num_iso_h
523        .iter()
524        .map(|&n| if n < 0 { 0 } else { n as u16 })
525        .sum::<u16>()
526        .min(u8::MAX as u16) as u8
527}
528
529fn decode_order(code: i8) -> BondOrder {
530    let c = code as u32;
531    if c == inchi_sys::INCHI_BOND_TYPE_DOUBLE {
532        BondOrder::Double
533    } else if c == inchi_sys::INCHI_BOND_TYPE_TRIPLE {
534        BondOrder::Triple
535    } else if c == inchi_sys::INCHI_BOND_TYPE_ALTERN {
536        BondOrder::Alternating
537    } else {
538        BondOrder::Single
539    }
540}
541
542fn decode_parity(code: i8) -> Option<Parity> {
543    // The low 3 bits hold the parity of the connected structure.
544    match (code & 0x07) as u32 {
545        inchi_sys::INCHI_PARITY_ODD => Some(Parity::Odd),
546        inchi_sys::INCHI_PARITY_EVEN => Some(Parity::Even),
547        inchi_sys::INCHI_PARITY_UNKNOWN => Some(Parity::Unknown),
548        _ => None,
549    }
550}