Skip to main content

castep_cell_io/
cell_document.rs

1//! Top-level document structure for CASTEP `.cell` files.
2//!
3//! This module provides [`CellDocument`], the primary type for representing a complete
4//! CASTEP cell file in memory. It handles both parsing from text format and serialization
5//! back to the CASTEP format.
6//!
7//! # Structure
8//!
9//! A cell document consists of:
10//! - **Required fields**: lattice vectors and atomic positions
11//! - **Optional blocks**: k-point sampling, constraints, external fields, species properties,
12//!   phonon calculations, and more
13//!
14//! # Usage
15//!
16//! ## Parsing from text
17//!
18//! ```no_run
19//! use castep_cell_io::CellDocument;
20//!
21//! let input = std::fs::read_to_string("structure.cell")?;
22//! let doc = castep_cell_fmt::parse::<CellDocument>(&input)?;
23//!
24//! // Access required fields
25//! println!("Lattice: {:?}", doc.lattice);
26//! println!("Positions: {:?}", doc.positions);
27//!
28//! // Check optional blocks
29//! if let Some(kpoints) = &doc.kpoints.kpoints_list {
30//!     println!("K-points defined: {:?}", kpoints);
31//! }
32//! # Ok::<(), Box<dyn std::error::Error>>(())
33//! ```
34//!
35//! ## Building programmatically
36//!
37//! ```ignore
38//! use castep_cell_io::CellDocument;
39//!
40//! let doc = CellDocument::builder()
41//!     .lattice(todo!()) // LatticeCart instance
42//!     .positions(todo!()) // PositionsFrac or PositionsAbs instance
43//!     .build();
44//! ```
45//!
46//! ## Serializing to text
47//!
48//! ```ignore
49//! use castep_cell_io::CellDocument;
50//! use castep_cell_fmt::{ToCellFile, format::to_string_many_spaced};
51//!
52//! // Assuming you have a CellDocument instance
53//! let doc = todo!(); // Your CellDocument instance
54//! let cells = doc.to_cell_file();
55//! let output = to_string_many_spaced(&cells);
56//! ```
57
58use bon::Builder;
59use castep_cell_fmt::{
60    CResult, Cell, CellValue, Error, ToCell, ToCellFile,
61    parse::{FromBlock, FromCellFile},
62    query::find_block,
63};
64
65use crate::cell::{
66    constraints_params::ConstraintsParams,
67    dynamics_params::DynamicsParams,
68    external_field_params::ExternalFieldParams,
69    kpoints_params::KpointsParams,
70    lattice_param::{LatticeABC, LatticeCart},
71    optics_magres_params::OpticsMagresParams,
72    phonon_params::PhononParams,
73    phonon_fine_params::PhononFineParams,
74    positions::{PositionsAbs, PositionsFrac},
75    species_params::SpeciesParams,
76    spectral_params::SpectralParams,
77    symmetry_params::SymmetryParams,
78};
79use cell_document_builder::IsComplete;
80
81/// Lattice vector specification for the simulation cell.
82///
83/// Defines the periodic boundary conditions of the crystal structure.
84/// Supports both Cartesian and ABC+angles representations.
85///
86/// # Example
87///
88/// ```no_run
89/// use castep_cell_io::Lattice;
90///
91/// // Use builder to construct LatticeCart, then wrap in enum
92/// let lattice = Lattice::Cart(todo!());
93/// ```
94#[derive(Debug, Clone)]
95pub enum Lattice {
96    /// Lattice vectors in Cartesian coordinates (Angstroms).
97    ///
98    /// Corresponds to the `%BLOCK LATTICE_CART` section in CASTEP input files.
99    Cart(LatticeCart),
100    /// Lattice vectors in ABC+angles format.
101    ///
102    /// Corresponds to the `%BLOCK LATTICE_ABC` section in CASTEP input files.
103    Abc(LatticeABC),
104}
105
106impl ToCell for Lattice {
107    fn to_cell(&self) -> Cell<'_> {
108        match self {
109            Lattice::Cart(cart) => cart.to_cell(),
110            Lattice::Abc(abc) => abc.to_cell(),
111        }
112    }
113}
114
115impl From<LatticeCart> for Lattice {
116    fn from(v: LatticeCart) -> Self {
117        Lattice::Cart(v)
118    }
119}
120
121impl From<LatticeABC> for Lattice {
122    fn from(v: LatticeABC) -> Self {
123        Lattice::Abc(v)
124    }
125}
126
127/// Atomic positions within the simulation cell.
128///
129/// Positions can be specified in either fractional (relative to lattice vectors)
130/// or absolute Cartesian coordinates. CASTEP requires exactly one position block
131/// per cell file.
132///
133/// # Coordinate Systems
134///
135/// - **Fractional**: Coordinates relative to lattice vectors (0.0 to 1.0 range).
136///   Most common for periodic systems. Corresponds to `%BLOCK POSITIONS_FRAC`.
137/// - **Absolute**: Cartesian coordinates in Angstroms. Useful for non-periodic
138///   or mixed systems. Corresponds to `%BLOCK POSITIONS_ABS`.
139///
140/// # Example
141///
142/// ```no_run
143/// use castep_cell_io::Positions;
144///
145/// // Use builder to construct PositionsFrac, then wrap in enum
146/// let positions = Positions::Frac(todo!());
147/// ```
148#[derive(Debug, Clone)]
149pub enum Positions {
150    /// Fractional coordinates relative to lattice vectors.
151    Frac(PositionsFrac),
152    /// Absolute Cartesian coordinates in Angstroms.
153    Abs(PositionsAbs),
154}
155
156impl ToCell for Positions {
157    fn to_cell(&self) -> Cell<'_> {
158        match self {
159            Positions::Frac(frac) => frac.to_cell(),
160            Positions::Abs(abs) => abs.to_cell(),
161        }
162    }
163}
164
165/// Complete representation of a CASTEP `.cell` file.
166///
167/// This is the primary type for working with CASTEP cell files. It contains all
168/// structural information, calculation parameters, and optional blocks that can
169/// appear in a cell file.
170///
171/// # Required Fields
172///
173/// - [`lattice`](Self::lattice): Periodic boundary conditions
174/// - [`positions`](Self::positions): Atomic coordinates
175///
176/// All other fields are optional and correspond to specific CASTEP features.
177///
178/// # Construction
179///
180/// Use the builder pattern (via [`bon`](https://docs.rs/bon)) for ergonomic construction:
181///
182/// ```ignore
183/// use castep_cell_io::CellDocument;
184///
185/// let doc = CellDocument::builder()
186///     .lattice(todo!())  // LatticeCart - automatically wrapped in Lattice::Cart
187///     .positions(todo!())  // PositionsFrac/PositionsAbs - automatically wrapped
188///     .build();
189/// ```
190///
191/// # Parsing and Serialization
192///
193/// Implements [`FromCellFile`] for parsing and [`ToCellFile`] for serialization:
194///
195/// ```no_run
196/// use castep_cell_io::CellDocument;
197/// use castep_cell_fmt::{ToCellFile, format::to_string_many_spaced};
198///
199/// // Parse from string
200/// let input = std::fs::read_to_string("input.cell")?;
201/// let doc = castep_cell_fmt::parse::<CellDocument>(&input)?;
202///
203/// // Serialize back to CASTEP format
204/// let cells = doc.to_cell_file();
205/// let output = to_string_many_spaced(&cells);
206/// # Ok::<(), Box<dyn std::error::Error>>(())
207/// ```
208///
209/// # Optional Blocks (via Cell Document Groups)
210///
211/// The document organizes optional blocks into logical sub-groups,
212/// mirroring the `ParamDocument` pattern:
213///
214/// - **K-points**: [`kpoints`](Self::kpoints) — SCF k-point sampling
215/// - **Spectral**: [`spectral`](Self::spectral) — BS_ and SPECTRAL_ k-point paths
216/// - **Optics/Magres**: [`optics_magres`](Self::optics_magres) — optics and magnetic resonance k-points
217/// - **Symmetry**: [`symmetry`](Self::symmetry) — symmetry operations and generation
218/// - **Constraints**: [`constraints`](Self::constraints) — ionic and cell constraints
219/// - **External fields**: [`external_fields`](Self::external_fields) — electric field and pressure
220/// - **Species**: [`species`](Self::species) — masses, pseudopotentials, Hubbard U
221/// - **Phonon**: [`phonon`](Self::phonon) — coarse phonon k-point settings
222/// - **Phonon fine**: [`phonon_fine`](Self::phonon_fine) — fine phonon k-point settings
223/// - **Dynamics**: [`dynamics`](Self::dynamics) — ionic velocities for MD
224#[allow(clippy::duplicated_attributes)]
225#[derive(Debug, Clone, Builder)]
226#[builder(on(Lattice, into), on(Positions, into), finish_fn(vis = "", name = build_internal))]
227pub struct CellDocument {
228    /// Lattice vectors defining the simulation cell.
229    ///
230    /// Required field. Defines the periodic boundary conditions.
231    pub lattice: Lattice,
232    /// Atomic positions within the cell.
233    ///
234    /// Required field. Can be fractional or absolute coordinates.
235    pub positions: Positions,
236    /// SCF k-point sampling parameters.
237    ///
238    /// Contains KPOINTS_LIST, KPOINTS_MP_GRID, KPOINTS_MP_SPACING, and KPOINTS_MP_OFFSET.
239    #[builder(default)]
240    pub kpoints: KpointsParams,
241    /// Spectral/BS k-point parameters.
242    ///
243    /// Contains BS_ and SPECTRAL_ prefixed k-point types for band structure calculations.
244    #[builder(default)]
245    pub spectral: SpectralParams,
246    /// Optics and magnetic resonance k-point lists.
247    ///
248    /// Contains OPTICS_KPOINTS_LIST and MAGRES_KPOINTS_LIST.
249    #[builder(default)]
250    pub optics_magres: OpticsMagresParams,
251    /// Symmetry parameters.
252    ///
253    /// Contains SYMMETRY_OPS, SYMMETRY_GENERATE, and SYMMETRY_TOL.
254    #[builder(default)]
255    pub symmetry: SymmetryParams,
256    /// Movement constraints for ions and cell.
257    ///
258    /// Contains FIX_COM, IONIC_CONSTRAINTS, NONLINEAR_CONSTRAINTS,
259    /// FIX_ALL_IONS, FIX_ALL_CELL, CELL_CONSTRAINTS, and FIX_VOL.
260    #[builder(default)]
261    pub constraints: ConstraintsParams,
262    /// External field parameters.
263    ///
264    /// Contains EXTERNAL_EFIELD and EXTERNAL_PRESSURE.
265    #[builder(default)]
266    pub external_fields: ExternalFieldParams,
267    /// Species properties.
268    ///
269    /// Contains SPECIES_MASS, SPECIES_POT, SPECIES_LCAO_STATES,
270    /// SPECIES_Q, HUBBARD_U, and SEDC_CUSTOM_PARAMS.
271    #[builder(default)]
272    pub species: SpeciesParams,
273    /// Phonon (coarse) k-point parameters.
274    ///
275    /// Contains phonon k-point lists, paths, MP grids, and related settings.
276    #[builder(default)]
277    pub phonon: PhononParams,
278    /// Phonon fine k-point parameters.
279    ///
280    /// Contains fine phonon k-point paths, lists, and MP grids.
281    #[builder(default)]
282    pub phonon_fine: PhononFineParams,
283    /// Molecular dynamics dynamics parameters.
284    ///
285    /// Contains IONIC_VELOCITIES for MD restart.
286    #[builder(default)]
287    pub dynamics: DynamicsParams,
288}
289
290impl<S: cell_document_builder::IsComplete> CellDocumentBuilder<S> {
291    pub fn build(self) -> CResult<CellDocument> {
292        let mut doc = self.build_internal();
293        doc.kpoints = doc.kpoints.validate().map_err(|e| Error::Message(e.to_string()))?;
294        doc.spectral = doc.spectral.validate().map_err(|e| Error::Message(e.to_string()))?;
295        doc.symmetry = doc.symmetry.validate().map_err(|e| Error::Message(e.to_string()))?;
296        doc.constraints = doc.constraints.validate().map_err(|e| Error::Message(e.to_string()))?;
297        doc.phonon = doc.phonon.validate().map_err(|e| Error::Message(e.to_string()))?;
298        doc.phonon_fine = doc.phonon_fine.validate().map_err(|e| Error::Message(e.to_string()))?;
299        doc.optics_magres = doc.optics_magres.validate().map_err(|e| Error::Message(e.to_string()))?;
300        doc.external_fields = doc.external_fields.validate().map_err(|e| Error::Message(e.to_string()))?;
301        doc.species = doc.species.validate().map_err(|e| Error::Message(e.to_string()))?;
302        doc.dynamics = doc.dynamics.validate().map_err(|e| Error::Message(e.to_string()))?;
303        Ok(doc)
304    }
305}
306
307impl FromCellFile for CellDocument {
308    /// Parse a [`CellDocument`] from a slice of parsed [`Cell`] tokens.
309    ///
310    /// This method is called by [`castep_cell_fmt::parse`] after tokenizing the input text.
311    /// It extracts all recognized blocks and keywords from the token stream.
312    ///
313    /// # Required Blocks
314    ///
315    /// - `%BLOCK LATTICE_CART` — must be present
316    /// - Either `%BLOCK POSITIONS_FRAC` or `%BLOCK POSITIONS_ABS` — must have exactly one
317    ///
318    /// # Errors
319    ///
320    /// Returns [`Error`] if:
321    /// - Required blocks are missing
322    /// - Block content is malformed
323    /// - Multiple position blocks are present
324    /// - Any block fails to parse according to its schema
325    ///
326    /// # Example
327    ///
328    /// ```no_run
329    /// use castep_cell_io::CellDocument;
330    ///
331    /// let input = r#"
332    /// %BLOCK LATTICE_CART
333    ///   10.0  0.0  0.0
334    ///    0.0 10.0  0.0
335    ///    0.0  0.0 10.0
336    /// %ENDBLOCK LATTICE_CART
337    ///
338    /// %BLOCK POSITIONS_FRAC
339    /// Si  0.0  0.0  0.0
340    /// Si  0.25 0.25 0.25
341    /// %ENDBLOCK POSITIONS_FRAC
342    /// "#;
343    ///
344    /// let doc = castep_cell_fmt::parse::<CellDocument>(input)?;
345    /// # Ok::<(), castep_cell_fmt::Error>(())
346    /// ```
347    fn from_cell_file(cells: &[Cell<'_>]) -> CResult<Self> {
348        let has_lattice_cart = find_block(cells, "LATTICE_CART").is_ok();
349        let has_lattice_abc = find_block(cells, "LATTICE_ABC").is_ok();
350        if has_lattice_cart && has_lattice_abc {
351            return Err(Error::Message(
352                "Both LATTICE_CART and LATTICE_ABC are specified. Only one lattice specification is allowed."
353                    .into(),
354            ));
355        }
356        let lattice = if has_lattice_cart {
357            Lattice::Cart(LatticeCart::from_block_rows(find_block(cells, "LATTICE_CART")?)?)
358        } else {
359            Lattice::Abc(LatticeABC::from_block_rows(find_block(cells, "LATTICE_ABC")?)?)
360        };
361
362        let positions = if find_block(cells, "POSITIONS_FRAC").is_ok() {
363            Positions::Frac(PositionsFrac::from_block_rows(find_block(cells, "POSITIONS_FRAC")?)?)
364        } else {
365            Positions::Abs(PositionsAbs::from_block_rows(find_block(cells, "POSITIONS_ABS")?)?)
366        };
367
368        Self::builder()
369            .lattice(lattice)
370            .positions(positions)
371            .kpoints(KpointsParams::from_cell_file(cells)?)
372            .spectral(SpectralParams::from_cell_file(cells)?)
373            .optics_magres(OpticsMagresParams::from_cell_file(cells)?)
374            .symmetry(SymmetryParams::from_cell_file(cells)?)
375            .constraints(ConstraintsParams::from_cell_file(cells)?)
376            .external_fields(ExternalFieldParams::from_cell_file(cells)?)
377            .species(SpeciesParams::from_cell_file(cells)?)
378            .phonon(PhononParams::from_cell_file(cells)?)
379            .phonon_fine(PhononFineParams::from_cell_file(cells)?)
380            .dynamics(DynamicsParams::from_cell_file(cells)?)
381            .build()
382    }
383}
384
385impl ToCellFile for CellDocument {
386    /// Serialize this document to a vector of [`Cell`] tokens.
387    ///
388    /// Converts the structured document back to the token representation used by
389    /// [`castep_cell_fmt`]. The tokens can then be formatted to text with
390    /// [`castep_cell_fmt::format`].
391    ///
392    /// # Block Order
393    ///
394    /// Blocks are emitted in a standard order:
395    /// 1. Lattice and positions (required)
396    /// 2. K-point sampling blocks
397    /// 3. Constraints and flags
398    /// 4. External fields
399    /// 5. Species properties
400    /// 6. Phonon calculation blocks
401    /// 7. Dynamics (ionic velocities)
402    ///
403    /// Optional blocks that are `None` are omitted from the output.
404    ///
405    /// # Example
406    ///
407    /// ```ignore
408    /// use castep_cell_io::CellDocument;
409    /// use castep_cell_fmt::{ToCellFile, format::to_string_many_spaced};
410    ///
411    /// // Assuming you have a CellDocument instance
412    /// let doc = todo!(); // Your CellDocument instance
413    /// let cells = doc.to_cell_file();
414    /// let output = to_string_many_spaced(&cells);
415    /// ```
416    fn to_cell_file(&self) -> Vec<Cell<'_>> {
417        let mut cells = vec![self.lattice.to_cell(), self.positions.to_cell()];
418        cells.extend(self.kpoints.to_cell_file());
419        cells.extend(self.spectral.to_cell_file());
420        cells.extend(self.optics_magres.to_cell_file());
421        cells.extend(self.symmetry.to_cell_file());
422        cells.extend(self.constraints.to_cell_file());
423        cells.extend(self.external_fields.to_cell_file());
424        cells.extend(self.species.to_cell_file());
425        cells.extend(self.phonon.to_cell_file());
426        cells.extend(self.phonon_fine.to_cell_file());
427        cells.extend(self.dynamics.to_cell_file());
428        cells
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::cell::bz_sampling_kpoints::{
436        BsKpointPath, BsKpointPathEntry, Kpoint, KpointsList, KpointsMpGrid, KpointsMpOffset,
437        KpointsMpSpacing, SpectralKpointPath, SpectralKpointPathEntry, SpectralKpointsMpGrid,
438        SpectralKpointsMpOffset,
439    };
440    use crate::cell::phonon::{PhononKpointList, PhononKpointListEntry, PhononKpointPath, PhononKpointPathEntry};
441    use crate::cell::positions::PositionFracEntry;
442    use crate::cell::species::Species;
443    use crate::cell::symmetry::{SymmetryGenerate, SymmetryOp, SymmetryOps};
444
445    #[test]
446    fn test_parse_mg2sio4_forsterite_cell() {
447        let input = std::fs::read_to_string("tests/fixtures/Mg2SiO4_Cr_1.cell").unwrap();
448        let doc = castep_cell_fmt::parse::<CellDocument>(&input).expect("Failed to parse Mg2SiO4_Cr_1.cell");
449        assert!(matches!(doc.lattice, Lattice::Cart(ref c)
450            if (c.a[0] - 10.183).abs() < 0.001
451            && (c.b[1] - 5.970).abs() < 0.001
452            && (c.c[2] - 4.751).abs() < 0.001));
453        assert!(doc.kpoints.kpoints_list.is_some());
454        assert_eq!(doc.kpoints.kpoints_list.as_ref().unwrap().kpts.len(), 3);
455        assert!(doc.symmetry.symmetry_ops.is_some());
456        assert_eq!(doc.symmetry.symmetry_ops.as_ref().unwrap().ops.len(), 2);
457        assert!(doc.constraints.fix_com.is_some());
458        assert_eq!(doc.constraints.fix_com.as_ref().unwrap().0, false);
459        assert!(doc.species.species_mass.is_some());
460        assert_eq!(doc.species.species_mass.as_ref().unwrap().masses.len(), 4);
461    }
462
463    #[test]
464    fn test_parse_fe2o3_cell() {
465        let input = std::fs::read_to_string("tests/fixtures/Fe2O3.cell").unwrap();
466        let doc = castep_cell_fmt::parse::<CellDocument>(&input).expect("Failed to parse Fe2O3.cell");
467        assert!(matches!(doc.lattice, Lattice::Cart(ref c)
468            if (c.a[0] - 4.360).abs() < 0.001
469            && (c.b[1] - 5.035).abs() < 0.001
470            && (c.c[2] - 13.72).abs() < 0.01));
471        assert!(doc.kpoints.kpoints_list.is_some());
472        assert_eq!(doc.kpoints.kpoints_list.as_ref().unwrap().kpts.len(), 5);
473        assert!(doc.constraints.fix_all_cell.is_some());
474        assert_eq!(doc.constraints.fix_all_cell.as_ref().unwrap().0, true);
475        assert!(doc.external_fields.external_pressure.is_some());
476        assert!(doc.species.hubbard_u.is_some());
477        assert_eq!(doc.species.hubbard_u.as_ref().unwrap().atom_u_values.len(), 12);
478        assert!(doc.species.species_mass.is_some());
479        assert_eq!(doc.species.species_mass.as_ref().unwrap().masses.len(), 2);
480    }
481
482    #[test]
483    fn test_parse_zno_lr_cell() {
484        let input = std::fs::read_to_string("tests/fixtures/ZnO_LR.cell").unwrap();
485        let doc = castep_cell_fmt::parse::<CellDocument>(&input).expect("Failed to parse ZnO_LR.cell");
486        assert!(matches!(doc.lattice, Lattice::Cart(_)));
487        assert!(doc.kpoints.kpoints_list.is_some());
488        assert_eq!(doc.kpoints.kpoints_list.as_ref().unwrap().kpts.len(), 10);
489        assert!(doc.symmetry.symmetry_ops.is_some());
490        assert_eq!(doc.symmetry.symmetry_ops.as_ref().unwrap().ops.len(), 12);
491        assert!(doc.constraints.cell_constraints.is_some());
492        let cc = doc.constraints.cell_constraints.as_ref().unwrap();
493        assert_eq!(cc.lengths, [1, 1, 3]);
494        assert_eq!(cc.angles, [0, 0, 0]);
495        assert!(matches!(doc.positions, Positions::Frac(_)));
496        if let Positions::Frac(ref pos) = doc.positions {
497            assert_eq!(pos.positions.len(), 4);
498        }
499    }
500
501    fn minimal_lattice() -> Lattice {
502        Lattice::Cart(LatticeCart {
503            unit: None,
504            a: [10.0, 0.0, 0.0],
505            b: [0.0, 10.0, 0.0],
506            c: [0.0, 0.0, 10.0],
507        })
508    }
509
510    fn minimal_positions() -> Positions {
511        Positions::Frac(PositionsFrac {
512            positions: vec![PositionFracEntry {
513                species: Species::Symbol("Si".to_string()),
514                coord: [0.0, 0.0, 0.0],
515                spin: None,
516                mixture: None,
517            }],
518        })
519    }
520
521    #[test]
522    fn build_rejects_multiple_kpoint_specs() {
523        let result = CellDocument::builder()
524            .lattice(minimal_lattice())
525            .positions(minimal_positions())
526            .kpoints(KpointsParams {
527                kpoints_list: Some(KpointsList::builder()
528                    .kpts(vec![Kpoint::builder().coord([0.0, 0.0, 0.0]).weight(1.0).build()])
529                    .build()),
530                kpoints_mp_grid: Some(KpointsMpGrid([2, 2, 2])),
531                ..Default::default()
532            })
533            .build();
534        assert!(result.is_err());
535    }
536
537    #[test]
538    fn build_rejects_spectral_and_bs_duplication() {
539        let result = CellDocument::builder()
540            .lattice(minimal_lattice())
541            .positions(minimal_positions())
542            .spectral(SpectralParams {
543                spectral_kpoint_path: Some(SpectralKpointPath::builder()
544                    .points(vec![SpectralKpointPathEntry { coord: [0.0, 0.0, 0.0] }])
545                    .build()),
546                bs_kpoint_path: Some(BsKpointPath::builder()
547                    .points(vec![BsKpointPathEntry { coord: [0.0, 0.0, 0.0] }])
548                    .build()),
549                ..Default::default()
550            })
551            .build();
552        assert!(result.is_err());
553    }
554
555    #[test]
556    fn build_rejects_multiple_phonon_specs() {
557        let result = CellDocument::builder()
558            .lattice(minimal_lattice())
559            .positions(minimal_positions())
560            .phonon(PhononParams {
561                phonon_kpoint_path: Some(PhononKpointPath {
562                    points: vec![PhononKpointPathEntry { coord: [0.0, 0.0, 0.0] }],
563                }),
564                phonon_kpoint_list: Some(PhononKpointList::builder()
565                    .kpoints(vec![PhononKpointListEntry { coord: [0.0, 0.0, 0.0], weight: 1.0 }])
566                    .build()),
567                ..Default::default()
568            })
569            .build();
570        assert!(result.is_err());
571    }
572
573    #[test]
574    fn build_rejects_symmetry_generate_and_ops() {
575        let result = CellDocument::builder()
576            .lattice(minimal_lattice())
577            .positions(minimal_positions())
578            .symmetry(SymmetryParams {
579                symmetry_generate: Some(SymmetryGenerate),
580                symmetry_ops: Some(SymmetryOps::builder()
581                    .ops(vec![SymmetryOp::builder()
582                        .rotation([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
583                        .translation([0.0, 0.0, 0.0])
584                        .build()])
585                    .build()),
586                ..Default::default()
587            })
588            .build();
589        assert!(result.is_err());
590    }
591
592    #[test]
593    fn build_allows_mp_offset_with_grid() {
594        let result = CellDocument::builder()
595            .lattice(minimal_lattice())
596            .positions(minimal_positions())
597            .kpoints(KpointsParams {
598                kpoints_mp_grid: Some(KpointsMpGrid([2, 2, 2])),
599                kpoints_mp_offset: Some(KpointsMpOffset([0.0, 0.0, 0.0])),
600                ..Default::default()
601            })
602            .build();
603        assert!(result.is_ok());
604    }
605
606    #[test]
607    fn build_allows_spectral_mp_offset_with_grid() {
608        let result = CellDocument::builder()
609            .lattice(minimal_lattice())
610            .positions(minimal_positions())
611            .spectral(SpectralParams {
612                spectral_kpoints_mp_grid: Some(SpectralKpointsMpGrid([2, 2, 2])),
613                spectral_kpoints_mp_offset: Some(SpectralKpointsMpOffset([0.0, 0.0, 0.0])),
614                ..Default::default()
615            })
616            .build();
617        assert!(result.is_ok());
618    }
619
620    #[test]
621    fn build_allows_single_spec_each_category() {
622        let r1 = CellDocument::builder()
623            .lattice(minimal_lattice())
624            .positions(minimal_positions())
625            .kpoints(KpointsParams {
626                kpoints_mp_grid: Some(KpointsMpGrid([2, 2, 2])),
627                ..Default::default()
628            })
629            .build();
630        assert!(r1.is_ok());
631        let r2 = CellDocument::builder()
632            .lattice(minimal_lattice())
633            .positions(minimal_positions())
634            .spectral(SpectralParams {
635                spectral_kpoint_path: Some(SpectralKpointPath::builder()
636                    .points(vec![SpectralKpointPathEntry { coord: [0.0, 0.0, 0.0] }])
637                    .build()),
638                ..Default::default()
639            })
640            .build();
641        assert!(r2.is_ok());
642        let r3 = CellDocument::builder()
643            .lattice(minimal_lattice())
644            .positions(minimal_positions())
645            .phonon(PhononParams {
646                phonon_kpoint_path: Some(PhononKpointPath {
647                    points: vec![PhononKpointPathEntry { coord: [0.0, 0.0, 0.0] }],
648                }),
649                ..Default::default()
650            })
651            .build();
652        assert!(r3.is_ok());
653        let r4 = CellDocument::builder()
654            .lattice(minimal_lattice())
655            .positions(minimal_positions())
656            .symmetry(SymmetryParams {
657                symmetry_ops: Some(SymmetryOps::builder()
658                    .ops(vec![SymmetryOp::builder()
659                        .rotation([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
660                        .translation([0.0, 0.0, 0.0])
661                        .build()])
662                    .build()),
663                ..Default::default()
664            })
665            .build();
666        assert!(r4.is_ok());
667    }
668
669    #[test]
670    fn build_rejects_all_three_kpoint_specs() {
671        let result = CellDocument::builder()
672            .lattice(minimal_lattice())
673            .positions(minimal_positions())
674            .kpoints(KpointsParams {
675                kpoints_list: Some(KpointsList::builder()
676                    .kpts(vec![Kpoint::builder().coord([0.0, 0.0, 0.0]).weight(1.0).build()])
677                    .build()),
678                kpoints_mp_grid: Some(KpointsMpGrid([2, 2, 2])),
679                kpoints_mp_spacing: Some(KpointsMpSpacing { value: 0.05, unit: None }),
680                ..Default::default()
681            })
682            .build();
683        assert!(result.is_err());
684    }
685
686    #[test]
687    fn build_allows_empty_document() {
688        let result = CellDocument::builder()
689            .lattice(minimal_lattice())
690            .positions(minimal_positions())
691            .build();
692        assert!(result.is_ok());
693    }
694}