Skip to main content

castep_cell_io/
param_document.rs

1//! Top-level document structure for CASTEP `.param` files.
2//!
3//! This module provides [`ParamDocument`], the primary type for representing a complete
4//! CASTEP parameter file in memory. It organizes the 100+ CASTEP parameters into 18
5//! logical groups for better maintainability and discoverability.
6//!
7//! # Structure
8//!
9//! Parameters are organized into these groups:
10//! - [`GeneralParams`] — task type, output verbosity, continuation
11//! - [`ElectronicParams`] — charge, spin, band counts, smearing
12//! - [`BasisSetParams`] — cutoff energy, finite basis corrections
13//! - [`ExchangeCorrelationParams`] — XC functional, spin polarization, DFT+U
14//! - [`ElectronicMinimisationParams`] — SCF convergence, mixing schemes
15//! - [`GeometryOptimizationParams`] — optimization method, convergence criteria
16//! - [`PhononParams`] — phonon calculation settings
17//! - [`BandStructureParams`] — band structure calculation parameters
18//! - [`MolecularDynamicsParams`] — MD ensemble, timestep, temperature
19//! - [`ElectricFieldParams`] — finite field calculations
20//! - [`PseudopotentialParams`] — pseudopotential generation and testing
21//! - [`DensityMixingParams`] — charge density mixing parameters
22//! - [`PopulationAnalysisParams`] — Mulliken, Hirshfeld analysis
23//! - [`OpticsParams`] — optical property calculations
24//! - [`NmrParams`] — NMR chemical shift calculations
25//! - [`SolvationParams`] — implicit solvent models
26//! - [`ElectronicExcitationsParams`] — excited state calculations
27//! - [`TransitionStateParams`] — transition state search parameters
28//!
29//! # Usage
30//!
31//! ## Parsing from text
32//!
33//! ```no_run
34//! use castep_cell_io::ParamDocument;
35//!
36//! let input = std::fs::read_to_string("calculation.param")?;
37//! let doc = castep_cell_fmt::parse::<ParamDocument>(&input)?;
38//!
39//! // Access parameter groups
40//! if let Some(task) = &doc.general.task {
41//!     println!("Task: {:?}", task);
42//! }
43//! if let Some(cutoff) = &doc.basis_set.cutoff_energy {
44//!     println!("Cutoff: {} eV", cutoff.value);
45//! }
46//! # Ok::<(), Box<dyn std::error::Error>>(())
47//! ```
48//!
49//! ## Building programmatically
50//!
51//! ```
52//! use castep_cell_io::ParamDocument;
53//!
54//! let doc = ParamDocument::builder()
55//!     .general(Default::default())
56//!     .electronic(Default::default())
57//!     .basis_set(Default::default())
58//!     .exchange_correlation(Default::default())
59//!     .electronic_minimisation(Default::default())
60//!     .geometry_optimization(Default::default())
61//!     .phonon(Default::default())
62//!     .band_structure(Default::default())
63//!     .molecular_dynamics(Default::default())
64//!     .electric_field(Default::default())
65//!     .pseudopotential(Default::default())
66//!     .density_mixing(Default::default())
67//!     .population_analysis(Default::default())
68//!     .optics(Default::default())
69//!     .nmr(Default::default())
70//!     .solvation(Default::default())
71//!     .electronic_excitations(Default::default())
72//!     .transition_state(Default::default())
73//!     .build();
74//! ```
75//!
76//! ## Serializing to text
77//!
78//! ```no_run
79//! use castep_cell_io::ParamDocument;
80//! use castep_cell_fmt::{ToCellFile, format::to_string_many_spaced};
81//!
82//! let doc = ParamDocument::default();
83//! let cells = doc.to_cell_file();
84//! let output = to_string_many_spaced(&cells);
85//! # drop(output);
86//! ```
87
88use bon::Builder;
89use castep_cell_fmt::{Cell, CResult, Error, FromCellFile, ToCellFile};
90use crate::param::{
91    general_params::GeneralParams, electronic_params::ElectronicParams,
92    basis_set_params::BasisSetParams, exchange_correlation_params::ExchangeCorrelationParams,
93    electronic_minimisation_params::ElectronicMinimisationParams,
94    geometry_optimization_params::GeometryOptimizationParams, phonon_params::PhononParams,
95    band_structure_params::BandStructureParams, molecular_dynamics_params::MolecularDynamicsParams,
96    electric_field_params::ElectricFieldParams, pseudopotential_params::PseudopotentialParams,
97    density_mixing_params::DensityMixingParams, population_analysis_params::PopulationAnalysisParams,
98    optics_params::OpticsParams, nmr_params::NmrParams, solvation_params::SolvationParams,
99    electronic_excitations_params::ElectronicExcitationsParams,
100    transition_state_params::TransitionStateParams,
101};
102
103/// Complete representation of a CASTEP `.param` file.
104///
105/// This is the primary type for working with CASTEP parameter files. It organizes
106/// the 100+ CASTEP parameters into 18 logical groups, making it easier to discover
107/// related parameters and maintain the codebase.
108///
109/// # Organization
110///
111/// Each field represents a group of related parameters. All groups default to empty
112/// (all parameters `None`), allowing you to specify only the parameters you need.
113///
114/// # Validation
115///
116/// The document automatically validates inter-group constraints during parsing:
117/// - Band count parameters (`NBANDS`, `NEXTRA_BANDS`, `PERC_EXTRA_BANDS`) are mutually exclusive
118/// - Band structure parameters (`BS_NBANDS`, `BS_NEXTRA_BANDS`, `BS_PERC_EXTRA_BANDS`) are mutually exclusive
119/// - Optics parameters (`OPTICS_NBANDS`, `OPTICS_NEXTRA_BANDS`, `OPTICS_PERC_EXTRA_BANDS`) are mutually exclusive
120///
121/// Each parameter group also validates its own internal constraints.
122///
123/// # Construction
124///
125/// Use the builder pattern (via [`bon`](https://docs.rs/bon)):
126///
127/// ```
128/// use castep_cell_io::ParamDocument;
129///
130/// let doc = ParamDocument::builder()
131///     .general(Default::default())
132///     .electronic(Default::default())
133///     .basis_set(Default::default())
134///     .exchange_correlation(Default::default())
135///     .electronic_minimisation(Default::default())
136///     .geometry_optimization(Default::default())
137///     .phonon(Default::default())
138///     .band_structure(Default::default())
139///     .molecular_dynamics(Default::default())
140///     .electric_field(Default::default())
141///     .pseudopotential(Default::default())
142///     .density_mixing(Default::default())
143///     .population_analysis(Default::default())
144///     .optics(Default::default())
145///     .nmr(Default::default())
146///     .solvation(Default::default())
147///     .electronic_excitations(Default::default())
148///     .transition_state(Default::default())
149///     .build();
150/// ```
151///
152/// Or use [`Default`] for an empty document:
153///
154/// ```
155/// use castep_cell_io::ParamDocument;
156///
157/// let doc = ParamDocument::default();
158/// assert!(doc.general.task.is_none());
159/// ```
160///
161/// # Parsing and Serialization
162///
163/// Implements [`FromCellFile`] for parsing and [`ToCellFile`] for serialization.
164/// Parsing automatically applies validation.
165///
166/// See the module-level documentation for examples.
167#[derive(Debug, Clone, Default, Builder)]
168pub struct ParamDocument {
169    /// General calculation parameters.
170    ///
171    /// Controls task type, output verbosity, continuation, and runtime limits.
172    /// See [`GeneralParams`] for available parameters.
173    pub general: GeneralParams,
174    /// Electronic structure parameters.
175    ///
176    /// Controls charge, spin, band counts, and electronic smearing.
177    /// See [`ElectronicParams`] for available parameters.
178    pub electronic: ElectronicParams,
179    /// Basis set parameters.
180    ///
181    /// Controls plane-wave cutoff energy and finite basis corrections.
182    /// See [`BasisSetParams`] for available parameters.
183    pub basis_set: BasisSetParams,
184    /// Exchange-correlation functional parameters.
185    ///
186    /// Controls XC functional choice, spin polarization, and DFT+U.
187    /// See [`ExchangeCorrelationParams`] for available parameters.
188    pub exchange_correlation: ExchangeCorrelationParams,
189    /// Electronic minimization (SCF) parameters.
190    ///
191    /// Controls SCF convergence criteria and mixing schemes.
192    /// See [`ElectronicMinimisationParams`] for available parameters.
193    pub electronic_minimisation: ElectronicMinimisationParams,
194    /// Geometry optimization parameters.
195    ///
196    /// Controls optimization method and convergence criteria.
197    /// See [`GeometryOptimizationParams`] for available parameters.
198    pub geometry_optimization: GeometryOptimizationParams,
199    /// Phonon calculation parameters.
200    ///
201    /// Controls phonon calculation settings and convergence.
202    /// See [`PhononParams`] for available parameters.
203    pub phonon: PhononParams,
204    /// Band structure calculation parameters.
205    ///
206    /// Controls band structure calculation settings.
207    /// See [`BandStructureParams`] for available parameters.
208    pub band_structure: BandStructureParams,
209    /// Molecular dynamics parameters.
210    ///
211    /// Controls MD ensemble, timestep, temperature, and thermostat.
212    /// See [`MolecularDynamicsParams`] for available parameters.
213    pub molecular_dynamics: MolecularDynamicsParams,
214    /// Electric field parameters.
215    ///
216    /// Controls finite electric field calculations.
217    /// See [`ElectricFieldParams`] for available parameters.
218    pub electric_field: ElectricFieldParams,
219    /// Pseudopotential parameters.
220    ///
221    /// Controls pseudopotential generation and testing.
222    /// See [`PseudopotentialParams`] for available parameters.
223    pub pseudopotential: PseudopotentialParams,
224    /// Density mixing parameters.
225    ///
226    /// Controls charge density mixing during SCF.
227    /// See [`DensityMixingParams`] for available parameters.
228    pub density_mixing: DensityMixingParams,
229    /// Population analysis parameters.
230    ///
231    /// Controls Mulliken and Hirshfeld population analysis.
232    /// See [`PopulationAnalysisParams`] for available parameters.
233    pub population_analysis: PopulationAnalysisParams,
234    /// Optical properties parameters.
235    ///
236    /// Controls optical property calculations.
237    /// See [`OpticsParams`] for available parameters.
238    pub optics: OpticsParams,
239    /// NMR parameters.
240    ///
241    /// Controls NMR chemical shift calculations.
242    /// See [`NmrParams`] for available parameters.
243    pub nmr: NmrParams,
244    /// Solvation parameters.
245    ///
246    /// Controls implicit solvent models.
247    /// See [`SolvationParams`] for available parameters.
248    pub solvation: SolvationParams,
249    /// Electronic excitations parameters.
250    ///
251    /// Controls excited state calculations.
252    /// See [`ElectronicExcitationsParams`] for available parameters.
253    pub electronic_excitations: ElectronicExcitationsParams,
254    /// Transition state search parameters.
255    ///
256    /// Controls transition state search methods.
257    /// See [`TransitionStateParams`] for available parameters.
258    pub transition_state: TransitionStateParams,
259}
260
261impl ParamDocument {
262    /// Validates inter-group and intra-group constraints.
263    ///
264    /// This method is automatically called during parsing via [`FromCellFile`].
265    /// You typically don't need to call it manually unless you're constructing
266    /// a document programmatically and want to verify it's valid.
267    ///
268    /// # Validation Rules
269    ///
270    /// ## Intra-group validation
271    /// Each parameter group validates its own constraints (e.g., value ranges,
272    /// required combinations).
273    ///
274    /// ## Inter-group validation
275    /// - **Electronic band counts**: `NBANDS`, `NEXTRA_BANDS`, and `PERC_EXTRA_BANDS`
276    ///   are mutually exclusive
277    /// - **Band structure band counts**: `BS_NBANDS`, `BS_NEXTRA_BANDS`, and
278    ///   `BS_PERC_EXTRA_BANDS` are mutually exclusive
279    /// - **Optics band counts**: `OPTICS_NBANDS`, `OPTICS_NEXTRA_BANDS`, and
280    ///   `OPTICS_PERC_EXTRA_BANDS` are mutually exclusive
281    ///
282    /// # Errors
283    ///
284    /// Returns `Err` with a descriptive message if any validation constraint is violated.
285    fn validate(mut self) -> Result<Self, String> {
286        // Validate each group by consuming and reassigning
287        self.general = self.general.validate()?;
288        self.electronic = self.electronic.validate()?;
289        self.basis_set = self.basis_set.validate()?;
290        self.exchange_correlation = self.exchange_correlation.validate()?;
291        self.electronic_minimisation = self.electronic_minimisation.validate()?;
292        self.geometry_optimization = self.geometry_optimization.validate()?;
293        self.phonon = self.phonon.validate()?;
294        self.band_structure = self.band_structure.validate()?;
295        self.molecular_dynamics = self.molecular_dynamics.validate()?;
296        self.electric_field = self.electric_field.validate()?;
297        self.pseudopotential = self.pseudopotential.validate()?;
298        self.density_mixing = self.density_mixing.validate()?;
299        self.population_analysis = self.population_analysis.validate()?;
300        self.optics = self.optics.validate()?;
301        self.nmr = self.nmr.validate()?;
302        self.solvation = self.solvation.validate()?;
303        self.electronic_excitations = self.electronic_excitations.validate()?;
304        self.transition_state = self.transition_state.validate()?;
305
306        // Inter-group validation: Band structure mutual exclusivity
307        let bs_count = [
308            self.band_structure.bs_nbands.is_some(),
309            self.band_structure.bs_nextra_bands.is_some(),
310            self.band_structure.bs_perc_extra_bands.is_some(),
311        ]
312        .iter()
313        .filter(|&&x| x)
314        .count();
315        if bs_count > 1 {
316            return Err(
317                "BS_NBANDS, BS_NEXTRA_BANDS, and BS_PERC_EXTRA_BANDS are mutually exclusive. Only one may be specified."
318                    .into(),
319            );
320        }
321
322        // Inter-group validation: Electronic mutual exclusivity
323        let elec_count = [
324            self.electronic.nbands.is_some(),
325            self.electronic.nextra_bands.is_some(),
326            self.electronic.perc_extra_bands.is_some(),
327        ]
328        .iter()
329        .filter(|&&x| x)
330        .count();
331        if elec_count > 1 {
332            return Err(
333                "NBANDS, NEXTRA_BANDS, and PERC_EXTRA_BANDS are mutually exclusive. Only one may be specified."
334                    .into(),
335            );
336        }
337
338        // Inter-group validation: Optics mutual exclusivity
339        let optics_count = [
340            self.optics.optics_nbands.is_some(),
341            self.optics.optics_nextra_bands.is_some(),
342            self.optics.optics_perc_extra_bands.is_some(),
343        ]
344        .iter()
345        .filter(|&&x| x)
346        .count();
347        if optics_count > 1 {
348            return Err(
349                "OPTICS_NBANDS, OPTICS_NEXTRA_BANDS, and OPTICS_PERC_EXTRA_BANDS are mutually exclusive. Only one may be specified."
350                    .into(),
351            );
352        }
353
354        Ok(self)
355    }
356}
357
358impl FromCellFile for ParamDocument {
359    /// Parse a [`ParamDocument`] from a slice of parsed [`Cell`] tokens.
360    ///
361    /// This method is called by [`castep_cell_fmt::parse`] after tokenizing the input text.
362    /// It delegates parsing to each parameter group, then validates the complete document.
363    ///
364    /// # Parsing Strategy
365    ///
366    /// Each parameter group independently scans the token stream for its keywords.
367    /// This allows parameters to appear in any order in the file. Unknown keywords
368    /// are silently ignored (CASTEP's behavior).
369    ///
370    /// # Validation
371    ///
372    /// After parsing all groups, validation is automatically called to check
373    /// inter-group and intra-group constraints.
374    ///
375    /// # Errors
376    ///
377    /// Returns [`Error`] if:
378    /// - Any parameter value is malformed
379    /// - Validation constraints are violated
380    /// - Required parameter combinations are missing
381    ///
382    /// # Example
383    ///
384    /// ```no_run
385    /// use castep_cell_io::ParamDocument;
386    ///
387    /// let input = r#"
388    /// TASK : GeometryOptimization
389    /// XC_FUNCTIONAL : PBE
390    /// CUT_OFF_ENERGY : 500 eV
391    /// "#;
392    ///
393    /// let doc = castep_cell_fmt::parse::<ParamDocument>(input)?;
394    /// # Ok::<(), castep_cell_fmt::Error>(())
395    /// ```
396    fn from_cell_file(tokens: &[Cell<'_>]) -> CResult<Self> {
397        ParamDocument::builder()
398            .general(GeneralParams::from_cell_file(tokens)?)
399            .electronic(ElectronicParams::from_cell_file(tokens)?)
400            .basis_set(BasisSetParams::from_cell_file(tokens)?)
401            .exchange_correlation(ExchangeCorrelationParams::from_cell_file(tokens)?)
402            .electronic_minimisation(ElectronicMinimisationParams::from_cell_file(tokens)?)
403            .geometry_optimization(GeometryOptimizationParams::from_cell_file(tokens)?)
404            .phonon(PhononParams::from_cell_file(tokens)?)
405            .band_structure(BandStructureParams::from_cell_file(tokens)?)
406            .molecular_dynamics(MolecularDynamicsParams::from_cell_file(tokens)?)
407            .electric_field(ElectricFieldParams::from_cell_file(tokens)?)
408            .pseudopotential(PseudopotentialParams::from_cell_file(tokens)?)
409            .density_mixing(DensityMixingParams::from_cell_file(tokens)?)
410            .population_analysis(PopulationAnalysisParams::from_cell_file(tokens)?)
411            .optics(OpticsParams::from_cell_file(tokens)?)
412            .nmr(NmrParams::from_cell_file(tokens)?)
413            .solvation(SolvationParams::from_cell_file(tokens)?)
414            .electronic_excitations(ElectronicExcitationsParams::from_cell_file(tokens)?)
415            .transition_state(TransitionStateParams::from_cell_file(tokens)?)
416            .build()
417            .validate()
418            .map_err(|e| Error::Message(e.to_string()))
419    }
420}
421
422impl ToCellFile for ParamDocument {
423    /// Serialize this document to a vector of [`Cell`] tokens.
424    ///
425    /// Converts the structured document back to the token representation used by
426    /// [`castep_cell_fmt`]. The tokens can then be formatted to text with
427    /// [`castep_cell_fmt::format`].
428    ///
429    /// # Group Order
430    ///
431    /// Parameters are emitted in a standard order matching the field declaration order:
432    /// 1. General parameters
433    /// 2. Electronic structure
434    /// 3. Basis set
435    /// 4. Exchange-correlation
436    /// 5. Electronic minimization
437    /// 6. Geometry optimization
438    /// 7. Phonon calculations
439    /// 8. Band structure
440    /// 9. Molecular dynamics
441    /// 10. Electric field
442    /// 11. Pseudopotentials
443    /// 12. Density mixing
444    /// 13. Population analysis
445    /// 14. Optics
446    /// 15. NMR
447    /// 16. Solvation
448    /// 17. Electronic excitations
449    /// 18. Transition state search
450    ///
451    /// Parameters that are `None` are omitted from the output.
452    ///
453    /// # Example
454    ///
455    /// ```no_run
456    /// use castep_cell_io::ParamDocument;
457    /// use castep_cell_fmt::{ToCellFile, format::to_string_many_spaced};
458    ///
459    /// let doc = ParamDocument::default();
460    /// let cells = doc.to_cell_file();
461    /// let output = to_string_many_spaced(&cells);
462    /// # drop(output);
463    /// ```
464    fn to_cell_file(&self) -> Vec<Cell<'_>> {
465        let mut cells = Vec::new();
466        cells.extend(self.general.to_cell_file());
467        cells.extend(self.electronic.to_cell_file());
468        cells.extend(self.basis_set.to_cell_file());
469        cells.extend(self.exchange_correlation.to_cell_file());
470        cells.extend(self.electronic_minimisation.to_cell_file());
471        cells.extend(self.geometry_optimization.to_cell_file());
472        cells.extend(self.phonon.to_cell_file());
473        cells.extend(self.band_structure.to_cell_file());
474        cells.extend(self.molecular_dynamics.to_cell_file());
475        cells.extend(self.electric_field.to_cell_file());
476        cells.extend(self.pseudopotential.to_cell_file());
477        cells.extend(self.density_mixing.to_cell_file());
478        cells.extend(self.population_analysis.to_cell_file());
479        cells.extend(self.optics.to_cell_file());
480        cells.extend(self.nmr.to_cell_file());
481        cells.extend(self.solvation.to_cell_file());
482        cells.extend(self.electronic_excitations.to_cell_file());
483        cells.extend(self.transition_state.to_cell_file());
484        cells
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use castep_cell_fmt::parse;
492
493    #[test]
494    #[ignore]
495    fn test_parse_co3o4_2_param() {
496        let input = std::fs::read_to_string("tests/fixtures/Co3O4_2.param").unwrap();
497        let doc: ParamDocument = parse(&input).unwrap();
498
499        assert_eq!(
500            doc.general.task.unwrap(),
501            crate::param::general::Task::GeometryOptimization
502        );
503        assert_eq!(
504            doc.exchange_correlation.xc_functional.unwrap(),
505            crate::param::exchange_correlation::XcFunctional::Pbe
506        );
507        assert_eq!(doc.exchange_correlation.spin_polarized.unwrap().0, false);
508        assert_eq!(doc.basis_set.cutoff_energy.unwrap().value, 900.0);
509        assert_eq!(doc.electronic_minimisation.max_scf_cycles.unwrap().0, 400);
510        assert_eq!(
511            doc.geometry_optimization.geom_method.unwrap(),
512            crate::param::geometry_optimization::GeomMethod::Bfgs
513        );
514    }
515
516    #[test]
517    fn test_default_construction() {
518        let doc = ParamDocument::default();
519        assert!(doc.general.task.is_none());
520        assert!(doc.electronic.charge.is_none());
521    }
522
523    #[test]
524    fn test_builder_construction() {
525        let doc = ParamDocument::builder()
526            .general(GeneralParams::default())
527            .electronic(ElectronicParams::default())
528            .basis_set(BasisSetParams::default())
529            .exchange_correlation(ExchangeCorrelationParams::default())
530            .electronic_minimisation(ElectronicMinimisationParams::default())
531            .geometry_optimization(GeometryOptimizationParams::default())
532            .phonon(PhononParams::default())
533            .band_structure(BandStructureParams::default())
534            .molecular_dynamics(MolecularDynamicsParams::default())
535            .electric_field(ElectricFieldParams::default())
536            .pseudopotential(PseudopotentialParams::default())
537            .density_mixing(DensityMixingParams::default())
538            .population_analysis(PopulationAnalysisParams::default())
539            .optics(OpticsParams::default())
540            .nmr(NmrParams::default())
541            .solvation(SolvationParams::default())
542            .electronic_excitations(ElectronicExcitationsParams::default())
543            .transition_state(TransitionStateParams::default())
544            .build();
545        assert!(doc.general.task.is_none());
546    }
547
548    #[test]
549    fn test_validate_empty() {
550        let doc = ParamDocument::default();
551        assert!(doc.validate().is_ok());
552    }
553
554    #[test]
555    fn test_to_cell_file_empty() {
556        let doc = ParamDocument::default();
557        let cells = doc.to_cell_file();
558        assert_eq!(cells.len(), 0);
559    }
560}