Skip to main content

molpack/script/
build.rs

1//! Lower a parsed [`Script`] to either a frame-loader-agnostic
2//! [`ScriptPlan`] (no I/O) or — when the `io` feature is on — a fully
3//! built [`BuildResult`] with templates already read from disk.
4//!
5//! Front-ends pick whichever fits:
6//!
7//! - **Native CLI / examples** — call [`Script::build`] (feature `io`),
8//!   which reads files via molrs-io.
9//! - **PyO3 / WASM / embedding hosts** — call [`Script::lower`], drive
10//!   their own frame loader (e.g. molrs's Python bindings), construct
11//!   [`Target`]s externally, then apply per-structure restraints with
12//!   [`StructurePlan::apply`].
13
14use std::path::{Path, PathBuf};
15
16use crate::{
17    AbovePlaneRestraint, Angle, AtomRestraint, BelowPlaneRestraint, CenteringMode,
18    InsideBoxRestraint, InsideCubeRestraint, InsideCylinderRestraint, InsideEllipsoidRestraint,
19    InsideSphereRestraint, Molpack, OutsideBoxRestraint, OutsideCubeRestraint,
20    OutsideCylinderRestraint, OutsideEllipsoidRestraint, OutsideSphereRestraint, Target,
21};
22
23use super::error::ScriptError;
24use super::parser::{AtomGroup, RestraintSpec, Script, Structure};
25
26/// Loader-agnostic lowering of a [`Script`]: paths resolved, restraints
27/// kept as parsed [`RestraintSpec`]s, no filesystem access yet.
28///
29/// Iterate [`structures`](ScriptPlan::structures), load each
30/// [`StructurePlan::filepath`] with whatever frame loader you have, build
31/// a [`Target`] from the frame, and call [`StructurePlan::apply`] to
32/// stamp on the script's restraints / centering / fixed placement.
33pub struct ScriptPlan {
34    /// Packer pre-configured with `tolerance`, `seed`, and (optional) `pbc`.
35    pub packer: Molpack,
36    /// One entry per `structure … end structure` block, in source order.
37    pub structures: Vec<StructurePlan>,
38    /// Resolved output file path.
39    pub output: PathBuf,
40    /// Outer-loop iteration cap (`nloop` keyword; default `200 * ntype`).
41    pub nloop: usize,
42    /// Global `filetype` override (script-level), if any. Frame loaders
43    /// should fall back to extension-based detection when this is `None`.
44    pub filetype: Option<String>,
45}
46
47/// Per-structure lowering: resolved file path plus the restraints / pose
48/// hints that apply to its [`Target`].
49pub struct StructurePlan {
50    /// Absolute path to the template molecule file (resolved against the
51    /// script's base directory).
52    pub filepath: PathBuf,
53    /// Number of copies to pack.
54    pub number: usize,
55    /// Molecule-wide restraints.
56    pub mol_restraints: Vec<RestraintSpec>,
57    /// Atom-subset restraints (`atoms … end atoms` blocks). Indices are
58    /// **1-based** as written in the script; [`StructurePlan::apply`]
59    /// converts to 0-based when stamping them on a [`Target`].
60    pub atom_groups: Vec<AtomGroup>,
61    /// Whether the `center` keyword was present.
62    pub center: bool,
63    /// Fixed placement: `(position [x,y,z], euler [ex,ey,ez])`.
64    pub fixed: Option<([f64; 3], [f64; 3])>,
65}
66
67impl Script {
68    /// Resolve paths and clone restraints into a [`ScriptPlan`] without
69    /// touching the filesystem.
70    ///
71    /// Use this from front-ends that supply their own frame loader. The
72    /// native counterpart that *does* read files is [`Script::build`]
73    /// (gated behind the `io` feature).
74    pub fn lower(&self, base_dir: &Path) -> Result<ScriptPlan, ScriptError> {
75        if self.structures.is_empty() {
76            return Err(ScriptError::NoStructures);
77        }
78
79        let mut packer = Molpack::new()
80            .with_tolerance(self.tolerance)
81            .with_avoid_overlap(self.avoid_overlap);
82        if let Some(seed) = self.seed {
83            packer = packer.with_seed(seed);
84        }
85        if let Some(pbc) = self.pbc {
86            packer = packer.with_periodic_box(pbc.min, pbc.max);
87        }
88
89        let structures: Vec<StructurePlan> = self
90            .structures
91            .iter()
92            .map(|s| StructurePlan::from_structure(s, base_dir))
93            .collect();
94
95        Ok(ScriptPlan {
96            packer,
97            structures,
98            output: resolve(base_dir, &self.output),
99            nloop: self.nloop,
100            filetype: self.filetype.clone(),
101        })
102    }
103}
104
105impl StructurePlan {
106    fn from_structure(s: &Structure, base_dir: &Path) -> Self {
107        Self {
108            filepath: resolve(base_dir, &s.filepath),
109            number: s.number,
110            mol_restraints: s.mol_restraints.clone(),
111            atom_groups: s.atom_groups.clone(),
112            center: s.center,
113            fixed: s.fixed,
114        }
115    }
116
117    /// Apply this plan's restraints / centering / fixed pose onto a
118    /// [`Target`] the caller built from the template frame.
119    pub fn apply(&self, mut target: Target) -> Target {
120        for r in &self.mol_restraints {
121            target = apply_mol_restraint(target, r);
122        }
123
124        for group in &self.atom_groups {
125            target = apply_atom_group(target, group);
126        }
127
128        if self.center {
129            target = target.with_centering(CenteringMode::Center);
130        }
131
132        if let Some((pos, euler)) = self.fixed {
133            target = target.fixed_at(pos).with_orientation([
134                Angle::from_radians(euler[0]),
135                Angle::from_radians(euler[1]),
136                Angle::from_radians(euler[2]),
137            ]);
138        }
139
140        target
141    }
142}
143
144fn resolve(base: &Path, path: &Path) -> PathBuf {
145    if path.is_absolute() {
146        path.to_path_buf()
147    } else {
148        base.join(path)
149    }
150}
151
152/// Lower one parsed [`RestraintSpec`] to its concrete [`AtomRestraint`].
153///
154/// Single source of truth for the spec → restraint mapping, shared by the
155/// whole-molecule and per-atom-group application paths. Adding a new restraint
156/// kind is one arm here plus one parser arm and one [`RestraintSpec`] variant.
157fn restraint_from_spec(r: &RestraintSpec) -> Box<dyn AtomRestraint> {
158    match *r {
159        RestraintSpec::InsideBox { min, max } => {
160            Box::new(InsideBoxRestraint::new(min, max, [false; 3]))
161        }
162        RestraintSpec::OutsideBox { min, max } => Box::new(OutsideBoxRestraint::new(min, max)),
163        RestraintSpec::InsideCube { origin, side } => {
164            Box::new(InsideCubeRestraint::new(origin, side))
165        }
166        RestraintSpec::OutsideCube { origin, side } => {
167            Box::new(OutsideCubeRestraint::new(origin, side))
168        }
169        RestraintSpec::InsideSphere { center, radius } => {
170            Box::new(InsideSphereRestraint::new(center, radius))
171        }
172        RestraintSpec::OutsideSphere { center, radius } => {
173            Box::new(OutsideSphereRestraint::new(center, radius))
174        }
175        RestraintSpec::InsideEllipsoid {
176            center,
177            axes,
178            exponent,
179        } => Box::new(InsideEllipsoidRestraint::new(center, axes, exponent)),
180        RestraintSpec::OutsideEllipsoid {
181            center,
182            axes,
183            exponent,
184        } => Box::new(OutsideEllipsoidRestraint::new(center, axes, exponent)),
185        RestraintSpec::InsideCylinder {
186            center,
187            axis,
188            radius,
189            length,
190        } => Box::new(InsideCylinderRestraint::new(center, axis, radius, length)),
191        RestraintSpec::OutsideCylinder {
192            center,
193            axis,
194            radius,
195            length,
196        } => Box::new(OutsideCylinderRestraint::new(center, axis, radius, length)),
197        RestraintSpec::AbovePlane { normal, distance } => {
198            Box::new(AbovePlaneRestraint::new(normal, distance))
199        }
200        RestraintSpec::BelowPlane { normal, distance } => {
201            Box::new(BelowPlaneRestraint::new(normal, distance))
202        }
203    }
204}
205
206fn apply_mol_restraint(target: Target, r: &RestraintSpec) -> Target {
207    target.with_restraint(restraint_from_spec(r))
208}
209
210fn apply_atom_group(mut target: Target, group: &AtomGroup) -> Target {
211    // Script indices are 1-based; Target::with_atom_restraint expects 0-based.
212    let zero_indexed: Vec<usize> = group
213        .atom_indices
214        .iter()
215        .map(|&i| i.saturating_sub(1))
216        .collect();
217    let indices = zero_indexed.as_slice();
218    for r in &group.restraints {
219        target = target.with_atom_restraint(indices, restraint_from_spec(r));
220    }
221    target
222}
223
224// ─────────────────────────────────────────────────────────────────────
225// `io`-gated convenience: load every structure file via molrs-io.
226// ─────────────────────────────────────────────────────────────────────
227
228/// Everything a script expanded to: a configured packer, the target
229/// list, the resolved output path, and the outer-loop iteration cap.
230///
231/// The packer is not yet equipped with a handler; callers decide
232/// whether to attach a [`ProgressHandler`](crate::ProgressHandler),
233/// a custom handler, or none.
234#[cfg(feature = "io")]
235pub struct BuildResult {
236    pub packer: Molpack,
237    pub targets: Vec<Target>,
238    pub output: PathBuf,
239    pub nloop: usize,
240}
241
242#[cfg(feature = "io")]
243impl Script {
244    /// Lower the script *and* read each template via molrs-io.
245    ///
246    /// Available when the `io` feature is on; equivalent to calling
247    /// [`Script::lower`] then loading each structure file with
248    /// [`super::io::read_frame`] and applying its [`StructurePlan`].
249    pub fn build(&self, base_dir: &Path) -> Result<BuildResult, ScriptError> {
250        let plan = self.lower(base_dir)?;
251        let filetype = plan.filetype.as_deref();
252        let targets: Vec<Target> = plan
253            .structures
254            .iter()
255            .map(|sp| -> Result<Target, ScriptError> {
256                let frame = super::io::read_frame(&sp.filepath, filetype)?;
257                Ok(sp.apply(Target::new(frame, sp.number)))
258            })
259            .collect::<Result<_, _>>()?;
260        Ok(BuildResult {
261            packer: plan.packer,
262            targets,
263            output: plan.output,
264            nloop: plan.nloop,
265        })
266    }
267}