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}