1use 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#[derive(Debug, Clone)]
95pub enum Lattice {
96 Cart(LatticeCart),
100 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#[derive(Debug, Clone)]
149pub enum Positions {
150 Frac(PositionsFrac),
152 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#[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 pub lattice: Lattice,
232 pub positions: Positions,
236 #[builder(default)]
240 pub kpoints: KpointsParams,
241 #[builder(default)]
245 pub spectral: SpectralParams,
246 #[builder(default)]
250 pub optics_magres: OpticsMagresParams,
251 #[builder(default)]
255 pub symmetry: SymmetryParams,
256 #[builder(default)]
261 pub constraints: ConstraintsParams,
262 #[builder(default)]
266 pub external_fields: ExternalFieldParams,
267 #[builder(default)]
272 pub species: SpeciesParams,
273 #[builder(default)]
277 pub phonon: PhononParams,
278 #[builder(default)]
282 pub phonon_fine: PhononFineParams,
283 #[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 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 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}