Skip to main content

readcon_core/
chemfiles_selection.rs

1//! Chemfiles selection grammar on CON frames.
2//!
3//! Real evaluation requires the `chemfiles` Cargo feature. Without it, APIs
4//! return [`ChemfilesImportError::FeatureDisabled`](crate::chemfiles_import::ChemfilesImportError::FeatureDisabled).
5
6#[cfg(not(feature = "chemfiles"))]
7use crate::chemfiles_import::ChemfilesImportError;
8#[cfg(not(feature = "chemfiles"))]
9use crate::types::ConFrame;
10
11/// One selection match (up to 4 atom indices, chemfiles-style).
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SelectionMatch {
14    /// Number of valid entries in [`Self::atoms`] (1–4).
15    pub size: usize,
16    /// Atom indices in CON `atom_data` order.
17    pub atoms: [usize; 4],
18}
19
20impl SelectionMatch {
21    /// Slice of valid atom indices for this match.
22    pub fn indices(&self) -> &[usize] {
23        &self.atoms[..self.size.min(4)]
24    }
25}
26
27/// Result of evaluating a chemfiles selection string on a [`ConFrame`].
28#[derive(Debug, Clone)]
29pub struct SelectionResult {
30    /// Selection string that was evaluated.
31    pub selection: String,
32    /// 1 = atom, 2 = pair/bond, 3 = angle, 4 = dihedral.
33    pub context_size: usize,
34    /// Matches in CON `atom_data` index space.
35    pub matches: Vec<SelectionMatch>,
36}
37
38impl SelectionResult {
39    /// First atom index of each match (chemfiles “primary” index).
40    pub fn primary_indices(&self) -> Vec<usize> {
41        self.matches
42            .iter()
43            .filter_map(|m| m.indices().first().copied())
44            .collect()
45    }
46}
47
48/// One frame’s contribution to a multi-frame selection.
49#[derive(Debug, Clone)]
50pub struct FrameSelectionSlice {
51    /// Index of this frame in the input slice (0-based).
52    pub frame_index: usize,
53    /// Full selection result on this frame.
54    pub result: SelectionResult,
55    /// For atom-context selections (`context_size == 1`): xyz of each selected
56    /// atom in `atom_data` order for that frame (same length as sorted unique
57    /// primary indices). Empty for pair/angle/dihedral contexts (use `result.matches`).
58    pub positions: Vec<[f64; 3]>,
59    /// Atom-context indices that produced [`Self::positions`] (sorted unique).
60    pub atom_indices: Vec<usize>,
61}
62
63/// Multi-frame chemfiles selection: evaluate the same string on each frame.
64#[derive(Debug, Clone)]
65pub struct MultiFrameSelectionResult {
66    /// Selection string evaluated on every frame.
67    pub selection: String,
68    /// Per-frame slices (same length as the input frame list).
69    pub frames: Vec<FrameSelectionSlice>,
70}
71
72impl MultiFrameSelectionResult {
73    /// Atom-context trajectory positions: for each frame, the `positions` vec
74    /// (empty if that frame’s selection was not atom context or had no matches).
75    pub fn positions_per_frame(&self) -> impl Iterator<Item = &Vec<[f64; 3]>> {
76        self.frames.iter().map(|f| &f.positions)
77    }
78}
79
80#[cfg(feature = "chemfiles")]
81#[path = "chemfiles_selection_imp.rs"]
82mod imp;
83
84#[cfg(feature = "chemfiles")]
85pub use imp::{
86    apply_con_bonds_to_chemfiles_frame, chemfiles_frame_from_con_frame,
87    evaluate_selection_on_chemfiles_frame, evaluate_selection_on_con_frame,
88    evaluate_selection_on_frames, parse_selection_string, select_atom_indices,
89    select_atom_positions_on_frames,
90};
91
92#[cfg(not(feature = "chemfiles"))]
93/// Evaluate a chemfiles selection-language string on a [`ConFrame`].
94///
95/// Always returns [`ChemfilesImportError::FeatureDisabled`] without the feature.
96pub fn evaluate_selection_on_con_frame(
97    _selection: &str,
98    _frame: &ConFrame,
99) -> Result<SelectionResult, ChemfilesImportError> {
100    Err(ChemfilesImportError::FeatureDisabled)
101}
102
103#[cfg(not(feature = "chemfiles"))]
104/// Atom-context selection: sorted unique atom indices.
105///
106/// Always returns [`ChemfilesImportError::FeatureDisabled`] without the feature.
107pub fn select_atom_indices(
108    _selection: &str,
109    _frame: &ConFrame,
110) -> Result<Vec<usize>, ChemfilesImportError> {
111    Err(ChemfilesImportError::FeatureDisabled)
112}
113
114#[cfg(not(feature = "chemfiles"))]
115/// Parse-only check for a selection string (returns context size).
116///
117/// Always returns [`ChemfilesImportError::FeatureDisabled`] without the feature.
118pub fn parse_selection_string(_selection: &str) -> Result<usize, ChemfilesImportError> {
119    Err(ChemfilesImportError::FeatureDisabled)
120}
121
122#[cfg(not(feature = "chemfiles"))]
123/// Multi-frame selection (stub without `chemfiles` feature).
124pub fn evaluate_selection_on_frames(
125    _selection: &str,
126    _frames: &[ConFrame],
127) -> Result<MultiFrameSelectionResult, ChemfilesImportError> {
128    Err(ChemfilesImportError::FeatureDisabled)
129}
130
131#[cfg(not(feature = "chemfiles"))]
132/// Atom-context multi-frame positions (stub without `chemfiles` feature).
133pub fn select_atom_positions_on_frames(
134    _selection: &str,
135    _frames: &[ConFrame],
136) -> Result<MultiFrameSelectionResult, ChemfilesImportError> {
137    Err(ChemfilesImportError::FeatureDisabled)
138}
139
140#[cfg(all(test, not(feature = "chemfiles")))]
141mod stub_tests {
142    use super::*;
143    use crate::chemfiles_import::ChemfilesImportError;
144    use crate::types::ConFrameBuilder;
145
146    #[test]
147    fn selection_stub_is_feature_disabled() {
148        let frame = ConFrameBuilder::new([10.0; 3], [90.0; 3]).build();
149        let err = evaluate_selection_on_con_frame("name O", &frame).unwrap_err();
150        assert!(matches!(err, ChemfilesImportError::FeatureDisabled));
151    }
152
153    #[test]
154    fn multi_frame_selection_stub_is_feature_disabled() {
155        let frame = ConFrameBuilder::new([10.0; 3], [90.0; 3]).build();
156        let err = evaluate_selection_on_frames("name H", &[frame]).unwrap_err();
157        assert!(matches!(err, ChemfilesImportError::FeatureDisabled));
158        let frame2 = ConFrameBuilder::new([10.0; 3], [90.0; 3]).build();
159        let err2 = select_atom_positions_on_frames("name H", &[frame2]).unwrap_err();
160        assert!(matches!(err2, ChemfilesImportError::FeatureDisabled));
161    }
162}