Skip to main content

readcon_core/
chemfiles_import.rs

1//! Chemfiles → CON conversion.
2//!
3//! Real implementation requires the `chemfiles` Cargo feature (links libchemfiles).
4//! Without it, path/memory helpers are still present and return
5//! [`ChemfilesImportError::FeatureDisabled`] so call sites compile uniformly.
6//!
7//! After a successful read, numbers are in chemfiles internal units
8//! (Å, Å/ps, amu, degrees). Import stamps those onto CON line-2 `units`
9//! (`time` is `ps`, not the CON-native default `fs`).
10
11use std::path::PathBuf;
12
13/// Options for a chemfiles trajectory read (`read_step`, topology file, stride).
14#[derive(Clone, Debug)]
15pub struct ChemfilesReadOpts {
16    /// First step to keep (inclusive, 0-based). Maps to `Trajectory::read_step`.
17    pub start: usize,
18    /// Stride between kept steps. Must be `>= 1`.
19    pub step: usize,
20    /// Exclusive end step. `None` means `nsteps`.
21    pub stop: Option<usize>,
22    /// Force a chemfiles format name (`"XYZ"`, `"PDB"`, `"GRO"`, …).
23    /// Empty / `None` lets chemfiles guess from the path.
24    pub format: Option<String>,
25    /// Optional topology file (`Trajectory::set_topology_file`).
26    pub topology: Option<PathBuf>,
27    /// Format for [`Self::topology`] (`set_topology_with_format`). Empty = guess.
28    pub topology_format: Option<String>,
29    /// Call chemfiles `guess_bonds` when the frame has no topology bonds.
30    pub guess_bonds: bool,
31}
32
33impl Default for ChemfilesReadOpts {
34    fn default() -> Self {
35        Self {
36            start: 0,
37            step: 1,
38            stop: None,
39            format: None,
40            topology: None,
41            topology_format: None,
42            guess_bonds: false,
43        }
44    }
45}
46
47impl ChemfilesReadOpts {
48    /// Effective stride. Rejects `step == 0`.
49    pub fn stride(&self) -> Result<usize, String> {
50        if self.step == 0 {
51            Err("chemfiles read stride must be >= 1".into())
52        } else {
53            Ok(self.step)
54        }
55    }
56}
57
58/// Chemfiles internal unit system after a successful read
59/// (<https://chemfiles.org/chemfiles/latest/overview.html#units>).
60///
61/// Positions and cell lengths are Ångström, velocities are Å/ps, masses
62/// are amu. Energy is the CON v3 required key; chemfiles does not convert
63/// energies, so this object uses the CON default `eV`.
64pub fn chemfiles_internal_units_json() -> serde_json::Value {
65    serde_json::json!({
66        "length": "angstrom",
67        "energy": "eV",
68        "mass": "amu",
69        "time": "ps"
70    })
71}
72
73#[cfg(feature = "chemfiles")]
74#[path = "chemfiles_import_imp.rs"]
75mod imp;
76
77#[cfg(feature = "chemfiles")]
78pub use imp::*;
79
80#[cfg(not(feature = "chemfiles"))]
81mod stubs {
82    use std::fmt;
83    use std::path::Path;
84
85    use crate::types::ConFrame;
86
87    /// Prefix for unmapped chemfiles frame properties in CON metadata.
88    pub const CHEMFILES_EXTRA_PREFIX: &str = "chemfiles::";
89    /// Per-atom property bag key in frame metadata.
90    pub const CHEMFILES_ATOM_PROPS_KEY: &str = "chemfiles_atom_properties";
91    /// Display names in chemfiles / `atom_id` order.
92    pub const CHEMFILES_ATOM_NAMES_KEY: &str = "chemfiles_atom_names";
93    /// Atomic types in chemfiles / `atom_id` order.
94    pub const CHEMFILES_ATOM_TYPES_KEY: &str = "chemfiles_atom_types";
95    /// Residue list (`name`, optional `id`, remapped `atoms`) from chemfiles topology.
96    pub const CHEMFILES_RESIDUES_KEY: &str = "chemfiles_residues";
97    /// Provenance object for chemfiles internal units (length/velocity/angle/mass).
98    pub const CHEMFILES_UNIT_SYSTEM_KEY: &str = "chemfiles::unit_system";
99
100    /// Errors from chemfiles I/O or conversion (or missing feature).
101    #[derive(Debug)]
102    pub enum ChemfilesImportError {
103        /// Atom / property count mismatch or other structural problem.
104        InvalidFrame(String),
105        /// I/O while reading a trajectory path.
106        Io(std::io::Error),
107        /// This build was compiled without the `chemfiles` Cargo feature.
108        FeatureDisabled,
109    }
110
111    impl fmt::Display for ChemfilesImportError {
112        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113            match self {
114                ChemfilesImportError::InvalidFrame(msg) => {
115                    write!(f, "invalid chemfiles frame: {msg}")
116                }
117                ChemfilesImportError::Io(e) => write!(f, "I/O error: {e}"),
118                ChemfilesImportError::FeatureDisabled => write!(
119                    f,
120                    "chemfiles support is not enabled in this build; rebuild with `--features chemfiles` \
121(Python: `maturin develop --features python,chemfiles` or install the `chemfiles` extra from source — see docs)"
122                ),
123            }
124        }
125    }
126
127    impl std::error::Error for ChemfilesImportError {
128        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
129            match self {
130                ChemfilesImportError::Io(e) => Some(e),
131                ChemfilesImportError::InvalidFrame(_) | ChemfilesImportError::FeatureDisabled => {
132                    None
133                }
134            }
135        }
136    }
137
138    impl From<std::io::Error> for ChemfilesImportError {
139        fn from(e: std::io::Error) -> Self {
140            ChemfilesImportError::Io(e)
141        }
142    }
143
144    fn disabled<T>() -> Result<T, ChemfilesImportError> {
145        Err(ChemfilesImportError::FeatureDisabled)
146    }
147
148    /// Open a trajectory with chemfiles and convert every step to [`ConFrame`].
149    ///
150    /// Stub without the `chemfiles` feature — always returns [`ChemfilesImportError::FeatureDisabled`].
151    pub fn con_frames_from_trajectory_path<P: AsRef<Path>>(
152        _path: P,
153    ) -> Result<Vec<ConFrame>, ChemfilesImportError> {
154        disabled()
155    }
156
157    /// Read the first frame from a trajectory path.
158    ///
159    /// Stub without the `chemfiles` feature — always returns [`ChemfilesImportError::FeatureDisabled`].
160    pub fn con_frame_from_trajectory_path<P: AsRef<Path>>(
161        _path: P,
162    ) -> Result<ConFrame, ChemfilesImportError> {
163        disabled()
164    }
165
166    /// Read a trajectory from an in-memory buffer (chemfiles memory reader).
167    ///
168    /// Stub without the `chemfiles` feature — always returns [`ChemfilesImportError::FeatureDisabled`].
169    pub fn con_frames_from_memory(
170        _data: &str,
171        _format: &str,
172    ) -> Result<Vec<ConFrame>, ChemfilesImportError> {
173        disabled()
174    }
175
176    /// Same as [`con_frames_from_trajectory_path`] with skip / stride / topology.
177    pub fn con_frames_from_trajectory_path_with<P: AsRef<std::path::Path>>(
178        _path: P,
179        _opts: &super::ChemfilesReadOpts,
180    ) -> Result<Vec<ConFrame>, ChemfilesImportError> {
181        disabled()
182    }
183
184    /// Same as [`con_frames_from_memory`] with skip / stride / `guess_bonds`.
185    pub fn con_frames_from_memory_with(
186        _data: &str,
187        _format: &str,
188        _opts: &super::ChemfilesReadOpts,
189    ) -> Result<Vec<ConFrame>, ChemfilesImportError> {
190        disabled()
191    }
192
193    /// Read step `index` via chemfiles `Trajectory::read_step`.
194    pub fn con_frame_from_trajectory_path_nth<P: AsRef<std::path::Path>>(
195        _path: P,
196        _index: usize,
197    ) -> Result<ConFrame, ChemfilesImportError> {
198        disabled()
199    }
200
201    /// Number of steps in a chemfiles trajectory (`Trajectory::nsteps`).
202    pub fn nsteps_from_trajectory_path<P: AsRef<std::path::Path>>(
203        _path: P,
204    ) -> Result<usize, ChemfilesImportError> {
205        disabled()
206    }
207
208    /// Whether this build linked libchemfiles and implements import/selection.
209    pub const fn chemfiles_enabled() -> bool {
210        false
211    }
212}
213
214#[cfg(not(feature = "chemfiles"))]
215pub use stubs::*;
216
217#[cfg(feature = "chemfiles")]
218/// Whether this build linked libchemfiles and implements import/selection.
219pub const fn chemfiles_enabled() -> bool {
220    true
221}
222
223#[cfg(test)]
224mod stub_tests {
225    use super::*;
226
227    #[test]
228    fn chemfiles_enabled_matches_feature() {
229        assert_eq!(chemfiles_enabled(), cfg!(feature = "chemfiles"));
230    }
231
232    #[cfg(not(feature = "chemfiles"))]
233    #[test]
234    fn trajectory_path_stub_is_feature_disabled() {
235        let err = con_frame_from_trajectory_path("nope.xyz").unwrap_err();
236        assert!(matches!(err, ChemfilesImportError::FeatureDisabled));
237        let msg = err.to_string();
238        assert!(msg.contains("chemfiles"), "{msg}");
239        let err = nsteps_from_trajectory_path("nope.xyz").unwrap_err();
240        assert!(matches!(err, ChemfilesImportError::FeatureDisabled));
241        let err = con_frame_from_trajectory_path_nth("nope.xyz", 1).unwrap_err();
242        assert!(matches!(err, ChemfilesImportError::FeatureDisabled));
243    }
244
245    #[test]
246    fn chemfiles_internal_units_are_angstrom_ps() {
247        let u = chemfiles_internal_units_json();
248        assert_eq!(u["length"], "angstrom");
249        assert_eq!(u["time"], "ps");
250        assert_eq!(u["mass"], "amu");
251        assert_eq!(u["energy"], "eV");
252    }
253}