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