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}