1#![allow(dead_code)]
8
9use scirs2_core::ndarray::Array2;
10use std::collections::HashMap;
11use std::f64::consts::PI;
12
13pub struct CrystalStructurePredictor {
15 composition: ChemicalComposition,
17 method: PredictionMethod,
19 constraints: StructureConstraints,
21 energy_model: EnergyModel,
23 search_strategy: SearchStrategy,
25}
26
27#[derive(Debug, Clone)]
28pub struct ChemicalComposition {
29 pub elements: HashMap<String, usize>,
31 pub total_atoms: usize,
33 pub stoichiometry: Option<Vec<f64>>,
35 pub oxidation_states: Option<HashMap<String, i32>>,
37}
38
39#[derive(Debug, Clone)]
40pub enum PredictionMethod {
41 GlobalOptimization {
43 algorithm: GlobalOptAlgorithm,
44 max_iterations: usize,
45 },
46 DataMining {
48 database: StructureDatabase,
49 similarity_threshold: f64,
50 },
51 Evolutionary {
53 population_size: usize,
54 generations: usize,
55 mutation_rate: f64,
56 },
57 MachineLearning {
59 model: MLModel,
60 confidence_threshold: f64,
61 },
62 AIRSS {
64 num_searches: usize,
65 symmetry_constraints: bool,
66 },
67}
68
69#[derive(Debug, Clone)]
70pub enum GlobalOptAlgorithm {
71 SimulatedAnnealing,
72 BasinHopping,
73 ParticleSwarm,
74 GeneticAlgorithm,
75 MinimumHopping,
76}
77
78#[derive(Debug, Clone)]
79pub struct StructureDatabase {
80 pub source: DatabaseSource,
82 pub size: usize,
84 pub filters: Vec<DatabaseFilter>,
86}
87
88#[derive(Debug, Clone)]
89pub enum DatabaseSource {
90 MaterialsProject,
92 ICSD,
94 COD,
96 Custom { path: String },
98}
99
100#[derive(Debug, Clone)]
101pub enum DatabaseFilter {
102 Elements {
104 required: Vec<String>,
105 forbidden: Vec<String>,
106 },
107 SpaceGroup { allowed: Vec<u32> },
109 Property { name: String, min: f64, max: f64 },
111 Stability { max_above_hull: f64 },
113}
114
115#[derive(Debug, Clone)]
116pub struct MLModel {
117 pub model_type: MLModelType,
119 pub features: FeatureRepresentation,
121 pub training_size: usize,
123 pub accuracy: f64,
125}
126
127#[derive(Debug, Clone)]
128pub enum MLModelType {
129 GraphNN,
131 CGCNN,
133 SchNet,
135 MEGNet,
137 GAP,
139}
140
141#[derive(Debug, Clone)]
142pub enum FeatureRepresentation {
143 CoulombMatrix,
145 SineMatrix,
147 ManyBodyTensor { order: usize },
149 SOAP {
151 cutoff: f64,
152 n_max: usize,
153 l_max: usize,
154 },
155 CrystalGraph,
157}
158
159#[derive(Debug, Clone)]
160pub struct StructureConstraints {
161 pub lattice: LatticeConstraints,
163 pub symmetry: SymmetryConstraints,
165 pub distances: DistanceConstraints,
167 pub coordination: CoordinationConstraints,
169 pub pressure: Option<f64>,
171}
172
173#[derive(Debug, Clone)]
174pub struct LatticeConstraints {
175 pub min_lengths: Option<Vec3D>,
177 pub max_lengths: Option<Vec3D>,
179 pub angle_ranges: Option<Vec<(f64, f64)>>,
181 pub volume_range: Option<(f64, f64)>,
183 pub lattice_type: Option<LatticeType>,
185}
186
187#[derive(Debug, Clone)]
188pub enum LatticeType {
189 Cubic,
190 Tetragonal,
191 Orthorhombic,
192 Hexagonal,
193 Rhombohedral,
194 Monoclinic,
195 Triclinic,
196}
197
198#[derive(Debug, Clone)]
199pub struct Vec3D {
200 pub x: f64,
201 pub y: f64,
202 pub z: f64,
203}
204
205#[derive(Debug, Clone)]
206pub struct SymmetryConstraints {
207 pub space_groups: Option<Vec<u32>>,
209 pub point_groups: Option<Vec<String>>,
211 pub min_symmetry: Option<usize>,
213 pub wyckoff_positions: Option<Vec<WyckoffPosition>>,
215}
216
217#[derive(Debug, Clone)]
218pub struct WyckoffPosition {
219 pub letter: char,
221 pub multiplicity: usize,
223 pub site_symmetry: String,
225 pub coordinates: Vec<Vec3D>,
227}
228
229#[derive(Debug, Clone)]
230pub struct DistanceConstraints {
231 pub min_distances: HashMap<(String, String), f64>,
233 pub max_distances: HashMap<(String, String), f64>,
235 pub bond_lengths: Vec<BondConstraint>,
237}
238
239#[derive(Debug, Clone)]
240pub struct BondConstraint {
241 pub atoms: (String, String),
243 pub target_length: f64,
245 pub tolerance: f64,
247 pub bond_order: Option<f64>,
249}
250
251#[derive(Debug, Clone)]
252pub struct CoordinationConstraints {
253 pub coordination_numbers: HashMap<String, (usize, usize)>,
255 pub geometries: HashMap<String, CoordinationGeometry>,
257 pub allowed_ligands: HashMap<String, Vec<String>>,
259}
260
261#[derive(Debug, Clone)]
262pub enum CoordinationGeometry {
263 Linear,
264 Trigonal,
265 Tetrahedral,
266 SquarePlanar,
267 TrigonalBipyramidal,
268 Octahedral,
269 PentagonalBipyramidal,
270 CubicCoordination,
271 Custom { angles: Vec<f64> },
272}
273
274#[derive(Debug, Clone)]
275pub enum EnergyModel {
276 Empirical {
278 potential: EmpiricalPotential,
279 parameters: HashMap<String, f64>,
280 },
281 DFT {
283 functional: String,
284 basis_set: String,
285 k_points: Vec<usize>,
286 },
287 MLPotential {
289 model: MLPotentialModel,
290 uncertainty_quantification: bool,
291 },
292 TightBinding {
294 parameterization: String,
295 k_points: Vec<usize>,
296 },
297}
298
299#[derive(Debug, Clone)]
300pub enum EmpiricalPotential {
301 LennardJones,
303 Buckingham,
305 Morse,
307 EAM,
309 Tersoff,
311 StillingerWeber,
313}
314
315#[derive(Debug, Clone)]
316pub struct MLPotentialModel {
317 pub architecture: String,
319 pub training_rmse: f64,
321 pub validation_rmse: f64,
323 pub elements: Vec<String>,
325}
326
327#[derive(Debug, Clone)]
328pub enum SearchStrategy {
329 Random { num_trials: usize },
331 Grid { resolution: Vec<usize> },
333 Bayesian {
335 acquisition_function: AcquisitionFunction,
336 num_initial: usize,
337 },
338 Metadynamics {
340 collective_variables: Vec<CollectiveVariable>,
341 bias_factor: f64,
342 },
343}
344
345#[derive(Debug, Clone)]
346pub enum AcquisitionFunction {
347 ExpectedImprovement,
349 UCB { kappa: f64 },
351 ProbabilityOfImprovement,
353 ThompsonSampling,
355}
356
357#[derive(Debug, Clone)]
358pub enum CollectiveVariable {
359 LatticeParameter { index: usize },
361 CoordinationNumber { element: String },
363 OrderParameter { definition: String },
365 Density,
367}
368
369impl CrystalStructurePredictor {
370 pub fn new(composition: ChemicalComposition, energy_model: EnergyModel) -> Self {
372 Self {
373 composition,
374 method: PredictionMethod::GlobalOptimization {
375 algorithm: GlobalOptAlgorithm::SimulatedAnnealing,
376 max_iterations: 1000,
377 },
378 constraints: StructureConstraints::default(),
379 energy_model,
380 search_strategy: SearchStrategy::Random { num_trials: 100 },
381 }
382 }
383
384 pub fn with_method(mut self, method: PredictionMethod) -> Self {
386 self.method = method;
387 self
388 }
389
390 pub fn with_constraints(mut self, constraints: StructureConstraints) -> Self {
392 self.constraints = constraints;
393 self
394 }
395
396 pub fn build_qubo(&self) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
398 match &self.method {
399 PredictionMethod::GlobalOptimization { .. } => self.build_global_optimization_qubo(),
400 PredictionMethod::Evolutionary { .. } => self.build_evolutionary_qubo(),
401 _ => Err("QUBO formulation not available for this method".to_string()),
402 }
403 }
404
405 fn build_global_optimization_qubo(
407 &self,
408 ) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
409 let lattice_resolution = 10; let position_resolution = 20; let n_lattice_vars = 6 * lattice_resolution;
418 let n_atoms = self.composition.total_atoms;
419 let n_position_vars = n_atoms * 3 * position_resolution;
420 let n_vars = n_lattice_vars + n_position_vars;
421
422 let mut qubo = Array2::zeros((n_vars, n_vars));
423 let mut var_map = HashMap::new();
424
425 self.create_lattice_variables(&mut var_map, lattice_resolution)?;
427 self.create_position_variables(&mut var_map, n_atoms, position_resolution, n_lattice_vars)?;
428
429 self.add_energy_objective(&mut qubo, &var_map)?;
431
432 self.add_lattice_constraints(&mut qubo, &var_map, lattice_resolution)?;
434 self.add_distance_constraints(&mut qubo, &var_map)?;
435 self.add_symmetry_constraints(&mut qubo, &var_map)?;
436
437 Ok((qubo, var_map))
438 }
439
440 fn create_lattice_variables(
442 &self,
443 var_map: &mut HashMap<String, usize>,
444 resolution: usize,
445 ) -> Result<(), String> {
446 let params = ["a", "b", "c", "alpha", "beta", "gamma"];
447 let mut var_idx = 0;
448
449 for param in ¶ms {
450 for i in 0..resolution {
451 let var_name = format!("lattice_{param}_{i}");
452 var_map.insert(var_name, var_idx);
453 var_idx += 1;
454 }
455 }
456
457 Ok(())
458 }
459
460 fn create_position_variables(
462 &self,
463 var_map: &mut HashMap<String, usize>,
464 n_atoms: usize,
465 resolution: usize,
466 offset: usize,
467 ) -> Result<(), String> {
468 let mut var_idx = offset;
469
470 for atom in 0..n_atoms {
471 for coord in ["x", "y", "z"] {
472 for i in 0..resolution {
473 let var_name = format!("pos_{atom}_{coord}_{i}");
474 var_map.insert(var_name, var_idx);
475 var_idx += 1;
476 }
477 }
478 }
479
480 Ok(())
481 }
482
483 fn add_energy_objective(
485 &self,
486 qubo: &mut Array2<f64>,
487 var_map: &HashMap<String, usize>,
488 ) -> Result<(), String> {
489 match &self.energy_model {
491 EnergyModel::Empirical {
492 potential,
493 parameters,
494 } => self.add_empirical_energy(qubo, var_map, potential, parameters),
495 _ => {
496 self.add_surrogate_energy(qubo, var_map)
498 }
499 }
500 }
501
502 fn add_empirical_energy(
504 &self,
505 qubo: &mut Array2<f64>,
506 var_map: &HashMap<String, usize>,
507 potential: &EmpiricalPotential,
508 parameters: &HashMap<String, f64>,
509 ) -> Result<(), String> {
510 if matches!(potential, EmpiricalPotential::LennardJones) {
512 let epsilon = parameters.get("epsilon").unwrap_or(&1.0);
513 let sigma = parameters.get("sigma").unwrap_or(&3.4);
514
515 for i in 0..self.composition.total_atoms {
517 for j in i + 1..self.composition.total_atoms {
518 self.add_pairwise_energy(qubo, var_map, i, j, *epsilon, *sigma)?;
521 }
522 }
523 }
524
525 Ok(())
526 }
527
528 fn add_pairwise_energy(
530 &self,
531 qubo: &mut Array2<f64>,
532 var_map: &HashMap<String, usize>,
533 atom1: usize,
534 atom2: usize,
535 epsilon: f64,
536 _sigma: f64,
537 ) -> Result<(), String> {
538 for coord in ["x", "y", "z"] {
542 for i in 0..20 {
543 let var1 = format!("pos_{atom1}_{coord}_{i}");
545 let var2 = format!("pos_{atom2}_{coord}_{i}");
546
547 if let (Some(&idx1), Some(&idx2)) = (var_map.get(&var1), var_map.get(&var2)) {
548 qubo[[idx1, idx2]] -= epsilon;
550 }
551 }
552 }
553
554 Ok(())
555 }
556
557 fn add_surrogate_energy(
559 &self,
560 qubo: &mut Array2<f64>,
561 var_map: &HashMap<String, usize>,
562 ) -> Result<(), String> {
563 self.add_coordination_energy(qubo, var_map)?;
570
571 self.add_electrostatic_energy(qubo, var_map)?;
573
574 Ok(())
575 }
576
577 fn add_coordination_energy(
579 &self,
580 qubo: &mut Array2<f64>,
581 var_map: &HashMap<String, usize>,
582 ) -> Result<(), String> {
583 if !self
585 .constraints
586 .coordination
587 .coordination_numbers
588 .is_empty()
589 {
590 let _coord_numbers = &self.constraints.coordination.coordination_numbers;
591 let penalty = 10.0;
593
594 for i in 0..self.composition.total_atoms {
595 for coord in ["x", "y", "z"] {
598 for pos in 0..20 {
599 let var_name = format!("pos_{i}_{coord}_{pos}");
600 if let Some(&idx) = var_map.get(&var_name) {
601 let deviation = (pos as f64 - 10.0).abs();
603 qubo[[idx, idx]] += penalty * deviation / 10.0;
604 }
605 }
606 }
607 }
608 }
609
610 Ok(())
611 }
612
613 fn add_electrostatic_energy(
615 &self,
616 qubo: &mut Array2<f64>,
617 var_map: &HashMap<String, usize>,
618 ) -> Result<(), String> {
619 if let Some(_oxidation_states) = &self.composition.oxidation_states {
620 let _elements: Vec<_> = self.composition.elements.keys().collect();
624
625 for i in 0..self.composition.total_atoms {
626 for j in i + 1..self.composition.total_atoms {
627 let charge1 = 1.0; let charge2 = -1.0;
630
631 let interaction = charge1 * charge2;
633
634 for coord in ["x", "y", "z"] {
636 for pos in 0..20 {
637 let var1 = format!("pos_{i}_{coord}_{pos}");
638 let var2 = format!("pos_{j}_{coord}_{pos}");
639
640 if let (Some(&idx1), Some(&idx2)) =
641 (var_map.get(&var1), var_map.get(&var2))
642 {
643 if idx1 != idx2 {
644 qubo[[idx1, idx2]] += interaction * 0.1;
645 }
646 }
647 }
648 }
649 }
650 }
651 }
652
653 Ok(())
654 }
655
656 fn add_lattice_constraints(
658 &self,
659 qubo: &mut Array2<f64>,
660 var_map: &HashMap<String, usize>,
661 resolution: usize,
662 ) -> Result<(), String> {
663 let penalty = 100.0;
664
665 for param in ["a", "b", "c", "alpha", "beta", "gamma"] {
667 for i in 0..resolution {
668 for j in i + 1..resolution {
669 let var1 = format!("lattice_{param}_{i}");
670 let var2 = format!("lattice_{param}_{j}");
671
672 if let (Some(&idx1), Some(&idx2)) = (var_map.get(&var1), var_map.get(&var2)) {
673 qubo[[idx1, idx2]] += penalty;
674 }
675 }
676 }
677 }
678
679 if let Some(lattice_type) = &self.constraints.lattice.lattice_type {
681 self.add_lattice_type_constraints(qubo, var_map, lattice_type, resolution)?;
682 }
683
684 Ok(())
685 }
686
687 fn add_lattice_type_constraints(
689 &self,
690 qubo: &mut Array2<f64>,
691 var_map: &HashMap<String, usize>,
692 lattice_type: &LatticeType,
693 resolution: usize,
694 ) -> Result<(), String> {
695 let penalty = 200.0;
696
697 match lattice_type {
698 LatticeType::Cubic => {
699 for i in 0..resolution {
701 let var_a = format!("lattice_a_{i}");
702 let var_b = format!("lattice_b_{i}");
703 let var_c = format!("lattice_c_{i}");
704
705 if let (Some(&idx_a), Some(&idx_b), Some(&idx_c)) = (
707 var_map.get(&var_a),
708 var_map.get(&var_b),
709 var_map.get(&var_c),
710 ) {
711 qubo[[idx_a, idx_b]] -= penalty;
713 qubo[[idx_b, idx_c]] -= penalty;
714 qubo[[idx_a, idx_c]] -= penalty;
715 }
716 }
717
718 let angle_90_idx = resolution / 2; for angle in ["alpha", "beta", "gamma"] {
721 let var_name = format!("lattice_{angle}_{angle_90_idx}");
722 if let Some(&idx) = var_map.get(&var_name) {
723 qubo[[idx, idx]] -= penalty * 2.0;
724 }
725 }
726 }
727 LatticeType::Hexagonal => {
728 }
731 _ => {}
732 }
733
734 Ok(())
735 }
736
737 fn add_distance_constraints(
739 &self,
740 qubo: &mut Array2<f64>,
741 var_map: &HashMap<String, usize>,
742 ) -> Result<(), String> {
743 if !self.constraints.distances.min_distances.is_empty() {
744 let min_distances = &self.constraints.distances.min_distances;
745 let penalty = 50.0;
746
747 for ((_elem1, _elem2), &_min_dist) in min_distances {
749 for i in 0..self.composition.total_atoms {
752 for j in i + 1..self.composition.total_atoms {
753 for coord in ["x", "y", "z"] {
754 for pos in 0..20 {
755 let var1 = format!("pos_{i}_{coord}_{pos}");
756 let var2 = format!("pos_{j}_{coord}_{pos}");
757
758 if let (Some(&idx1), Some(&idx2)) =
759 (var_map.get(&var1), var_map.get(&var2))
760 {
761 if idx1 == idx2 {
762 qubo[[idx1, idx2]] += penalty;
764 }
765 }
766 }
767 }
768 }
769 }
770 }
771 }
772
773 Ok(())
774 }
775
776 fn add_symmetry_constraints(
778 &self,
779 qubo: &mut Array2<f64>,
780 var_map: &HashMap<String, usize>,
781 ) -> Result<(), String> {
782 if let Some(_space_groups) = &self.constraints.symmetry.space_groups {
783 let symmetry_bonus = -10.0;
785
786 for i in 0..self.composition.total_atoms {
789 for coord in ["x", "y", "z"] {
791 for special_pos in [0, 10, 19] {
792 let var_name = format!("pos_{i}_{coord}_{special_pos}");
794 if let Some(&idx) = var_map.get(&var_name) {
795 qubo[[idx, idx]] += symmetry_bonus;
796 }
797 }
798 }
799 }
800 }
801
802 Ok(())
803 }
804
805 fn build_evolutionary_qubo(&self) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
807 let genome_length = 100; let mut qubo = Array2::zeros((genome_length, genome_length));
810 let mut var_map = HashMap::new();
811
812 for i in 0..genome_length {
813 var_map.insert(format!("gene_{i}"), i);
814 }
815
816 self.add_fitness_function(&mut qubo, &var_map)?;
818
819 Ok((qubo, var_map))
820 }
821
822 fn add_fitness_function(
824 &self,
825 qubo: &mut Array2<f64>,
826 _var_map: &HashMap<String, usize>,
827 ) -> Result<(), String> {
828 for i in 0..qubo.shape()[0] {
835 qubo[[i, i]] = -1.0; }
837
838 Ok(())
839 }
840
841 pub fn decode_solution(
843 &self,
844 solution: &HashMap<String, bool>,
845 ) -> Result<CrystalStructure, String> {
846 let lattice = self.decode_lattice(solution)?;
848
849 let positions = self.decode_positions(solution)?;
851
852 let space_group = self.determine_space_group(&lattice, &positions)?;
854
855 Ok(CrystalStructure {
856 composition: self.composition.clone(),
857 lattice,
858 positions,
859 space_group,
860 energy: None,
861 properties: HashMap::new(),
862 })
863 }
864
865 fn decode_lattice(&self, solution: &HashMap<String, bool>) -> Result<Lattice, String> {
867 let mut params = HashMap::new();
868
869 for param in ["a", "b", "c", "alpha", "beta", "gamma"] {
870 for i in 0..10 {
871 let var_name = format!("lattice_{param}_{i}");
873 if *solution.get(&var_name).unwrap_or(&false) {
874 let value = match param {
876 "a" | "b" | "c" => (i as f64).mul_add(0.5, 3.0), "alpha" | "beta" | "gamma" => (i as f64).mul_add(6.0, 60.0), _ => 0.0,
879 };
880 params.insert(param.to_string(), value);
881 break;
882 }
883 }
884 }
885
886 Ok(Lattice {
887 a: params.get("a").copied().unwrap_or(5.0),
888 b: params.get("b").copied().unwrap_or(5.0),
889 c: params.get("c").copied().unwrap_or(5.0),
890 alpha: params.get("alpha").copied().unwrap_or(90.0),
891 beta: params.get("beta").copied().unwrap_or(90.0),
892 gamma: params.get("gamma").copied().unwrap_or(90.0),
893 })
894 }
895
896 fn decode_positions(
898 &self,
899 solution: &HashMap<String, bool>,
900 ) -> Result<Vec<AtomicPosition>, String> {
901 let mut positions = Vec::new();
902
903 let elements: Vec<_> = self.composition.elements.keys().cloned().collect();
905
906 for atom in 0..self.composition.total_atoms {
907 let mut coords = [0.0, 0.0, 0.0];
908
909 for (i, coord) in ["x", "y", "z"].iter().enumerate() {
910 for pos in 0..20 {
911 let var_name = format!("pos_{atom}_{coord}_{pos}");
912 if *solution.get(&var_name).unwrap_or(&false) {
913 coords[i] = pos as f64 / 19.0; break;
915 }
916 }
917 }
918
919 positions.push(AtomicPosition {
920 element: elements[atom % elements.len()].clone(),
921 x: coords[0],
922 y: coords[1],
923 z: coords[2],
924 occupancy: 1.0,
925 });
926 }
927
928 Ok(positions)
929 }
930
931 fn determine_space_group(
933 &self,
934 lattice: &Lattice,
935 _positions: &[AtomicPosition],
936 ) -> Result<SpaceGroup, String> {
937 let lattice_type = lattice.determine_type();
939
940 Ok(SpaceGroup {
941 number: 1, symbol: "P1".to_string(),
943 lattice_type,
944 point_group: "1".to_string(),
945 })
946 }
947}
948
949impl Default for StructureConstraints {
950 fn default() -> Self {
951 Self {
952 lattice: LatticeConstraints {
953 min_lengths: None,
954 max_lengths: None,
955 angle_ranges: None,
956 volume_range: None,
957 lattice_type: None,
958 },
959 symmetry: SymmetryConstraints {
960 space_groups: None,
961 point_groups: None,
962 min_symmetry: None,
963 wyckoff_positions: None,
964 },
965 distances: DistanceConstraints {
966 min_distances: HashMap::new(),
967 max_distances: HashMap::new(),
968 bond_lengths: Vec::new(),
969 },
970 coordination: CoordinationConstraints {
971 coordination_numbers: HashMap::new(),
972 geometries: HashMap::new(),
973 allowed_ligands: HashMap::new(),
974 },
975 pressure: None,
976 }
977 }
978}
979
980#[derive(Debug, Clone)]
981pub struct Lattice {
982 pub a: f64,
983 pub b: f64,
984 pub c: f64,
985 pub alpha: f64,
986 pub beta: f64,
987 pub gamma: f64,
988}
989
990impl Lattice {
991 pub fn volume(&self) -> f64 {
993 let alpha_rad = self.alpha.to_radians();
994 let beta_rad = self.beta.to_radians();
995 let gamma_rad = self.gamma.to_radians();
996
997 self.a
998 * self.b
999 * self.c
1000 * (2.0 * alpha_rad.cos() * beta_rad.cos())
1001 .mul_add(
1002 gamma_rad.cos(),
1003 gamma_rad.cos().mul_add(
1004 -gamma_rad.cos(),
1005 beta_rad.cos().mul_add(
1006 -beta_rad.cos(),
1007 alpha_rad.cos().mul_add(-alpha_rad.cos(), 1.0),
1008 ),
1009 ),
1010 )
1011 .sqrt()
1012 }
1013
1014 pub fn determine_type(&self) -> LatticeType {
1016 let tol = 0.01;
1017
1018 if (self.a - self.b).abs() < tol && (self.b - self.c).abs() < tol {
1019 if (self.alpha - 90.0).abs() < tol
1020 && (self.beta - 90.0).abs() < tol
1021 && (self.gamma - 90.0).abs() < tol
1022 {
1023 LatticeType::Cubic
1024 } else if (self.alpha - self.beta).abs() < tol && (self.beta - self.gamma).abs() < tol {
1025 LatticeType::Rhombohedral
1026 } else {
1027 LatticeType::Triclinic
1028 }
1029 } else if (self.a - self.b).abs() < tol {
1030 if (self.alpha - 90.0).abs() < tol
1031 && (self.beta - 90.0).abs() < tol
1032 && (self.gamma - 120.0).abs() < tol
1033 {
1034 LatticeType::Hexagonal
1035 } else if (self.alpha - 90.0).abs() < tol
1036 && (self.beta - 90.0).abs() < tol
1037 && (self.gamma - 90.0).abs() < tol
1038 {
1039 LatticeType::Tetragonal
1040 } else {
1041 LatticeType::Monoclinic
1042 }
1043 } else if (self.alpha - 90.0).abs() < tol
1044 && (self.beta - 90.0).abs() < tol
1045 && (self.gamma - 90.0).abs() < tol
1046 {
1047 LatticeType::Orthorhombic
1048 } else if (self.alpha - 90.0).abs() < tol && (self.gamma - 90.0).abs() < tol {
1049 LatticeType::Monoclinic
1050 } else {
1051 LatticeType::Triclinic
1052 }
1053 }
1054
1055 pub fn transformation_matrix(&self) -> Array2<f64> {
1057 let alpha_rad = self.alpha.to_radians();
1058 let beta_rad = self.beta.to_radians();
1059 let gamma_rad = self.gamma.to_radians();
1060
1061 let mut matrix = Array2::zeros((3, 3));
1062
1063 matrix[[0, 0]] = self.a;
1064 matrix[[0, 1]] = self.b * gamma_rad.cos();
1065 matrix[[0, 2]] = self.c * beta_rad.cos();
1066
1067 matrix[[1, 0]] = 0.0;
1068 matrix[[1, 1]] = self.b * gamma_rad.sin();
1069 matrix[[1, 2]] =
1070 self.c * beta_rad.cos().mul_add(-gamma_rad.cos(), alpha_rad.cos()) / gamma_rad.sin();
1071
1072 matrix[[2, 0]] = 0.0;
1073 matrix[[2, 1]] = 0.0;
1074 matrix[[2, 2]] = self.c
1075 * (2.0 * alpha_rad.cos() * beta_rad.cos())
1076 .mul_add(
1077 gamma_rad.cos(),
1078 gamma_rad.cos().mul_add(
1079 -gamma_rad.cos(),
1080 beta_rad.cos().mul_add(
1081 -beta_rad.cos(),
1082 alpha_rad.cos().mul_add(-alpha_rad.cos(), 1.0),
1083 ),
1084 ),
1085 )
1086 .sqrt()
1087 / gamma_rad.sin();
1088
1089 matrix
1090 }
1091}
1092
1093#[derive(Debug, Clone)]
1094pub struct AtomicPosition {
1095 pub element: String,
1096 pub x: f64,
1097 pub y: f64,
1098 pub z: f64,
1099 pub occupancy: f64,
1100}
1101
1102#[derive(Debug, Clone)]
1103pub struct SpaceGroup {
1104 pub number: u32,
1105 pub symbol: String,
1106 pub lattice_type: LatticeType,
1107 pub point_group: String,
1108}
1109
1110#[derive(Debug, Clone)]
1111pub struct CrystalStructure {
1112 pub composition: ChemicalComposition,
1113 pub lattice: Lattice,
1114 pub positions: Vec<AtomicPosition>,
1115 pub space_group: SpaceGroup,
1116 pub energy: Option<f64>,
1117 pub properties: HashMap<String, f64>,
1118}
1119
1120impl CrystalStructure {
1121 pub fn density(&self) -> f64 {
1123 let volume = self.lattice.volume();
1124 let mass = self.calculate_mass();
1125
1126 mass / volume * 1.66054
1128 }
1129
1130 fn calculate_mass(&self) -> f64 {
1132 let masses: HashMap<&str, f64> = [
1134 ("H", 1.008),
1135 ("C", 12.011),
1136 ("N", 14.007),
1137 ("O", 15.999),
1138 ("Na", 22.990),
1139 ("Mg", 24.305),
1140 ("Al", 26.982),
1141 ("Si", 28.085),
1142 ("Fe", 55.845),
1143 ]
1144 .iter()
1145 .copied()
1146 .collect();
1147
1148 self.composition
1149 .elements
1150 .iter()
1151 .map(|(elem, count)| masses.get(elem.as_str()).unwrap_or(&1.0) * *count as f64)
1152 .sum()
1153 }
1154
1155 pub fn supercell(&self, nx: usize, ny: usize, nz: usize) -> Self {
1157 let mut new_positions = Vec::new();
1158
1159 for i in 0..nx {
1160 for j in 0..ny {
1161 for k in 0..nz {
1162 for pos in &self.positions {
1163 new_positions.push(AtomicPosition {
1164 element: pos.element.clone(),
1165 x: (pos.x + i as f64) / nx as f64,
1166 y: (pos.y + j as f64) / ny as f64,
1167 z: (pos.z + k as f64) / nz as f64,
1168 occupancy: pos.occupancy,
1169 });
1170 }
1171 }
1172 }
1173 }
1174
1175 let mut new_composition = self.composition.clone();
1176 for count in new_composition.elements.values_mut() {
1177 *count *= nx * ny * nz;
1178 }
1179 new_composition.total_atoms *= nx * ny * nz;
1180
1181 Self {
1182 composition: new_composition,
1183 lattice: Lattice {
1184 a: self.lattice.a * nx as f64,
1185 b: self.lattice.b * ny as f64,
1186 c: self.lattice.c * nz as f64,
1187 ..self.lattice.clone()
1188 },
1189 positions: new_positions,
1190 space_group: self.space_group.clone(),
1191 energy: None,
1192 properties: HashMap::new(),
1193 }
1194 }
1195}
1196
1197pub struct PhaseTransitionAnalyzer {
1199 structures: Vec<CrystalStructure>,
1201 method: TransitionMethod,
1203 order_parameters: Vec<OrderParameter>,
1205}
1206
1207#[derive(Debug, Clone)]
1208pub enum TransitionMethod {
1209 NEB { images: usize, spring_constant: f64 },
1211 Metadynamics {
1213 bias_factor: f64,
1214 gaussian_width: f64,
1215 },
1216 TPS { shooting_moves: usize },
1218 ML { model: String },
1220}
1221
1222#[derive(Debug, Clone)]
1223pub struct OrderParameter {
1224 pub name: String,
1226 pub definition: OrderParameterDef,
1228 pub range: (f64, f64),
1230}
1231
1232#[derive(Debug, Clone)]
1233pub enum OrderParameterDef {
1234 Structural { description: String },
1236 Electronic { property: String },
1238 Magnetic { moment_type: String },
1240 Custom,
1242}
1243
1244pub struct DefectModeler {
1246 host: CrystalStructure,
1248 defect_types: Vec<DefectType>,
1250 interactions: DefectInteractions,
1252}
1253
1254#[derive(Debug, Clone)]
1255pub enum DefectType {
1256 Vacancy { site: usize },
1258 Interstitial { element: String, position: Vec3D },
1260 Substitution { site: usize, new_element: String },
1262 Frenkel {
1264 vacancy_site: usize,
1265 interstitial_pos: Vec3D,
1266 },
1267 Schottky { sites: Vec<usize> },
1269 GrainBoundary { plane: (i32, i32, i32), angle: f64 },
1271}
1272
1273#[derive(Debug, Clone)]
1274pub struct DefectInteractions {
1275 pub cutoff: f64,
1277 pub model: InteractionModel,
1279 pub clustering: bool,
1281}
1282
1283#[derive(Debug, Clone)]
1284pub enum InteractionModel {
1285 Coulombic,
1287 Elastic { elastic_constants: Array2<f64> },
1289 Combined,
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295 use super::*;
1296
1297 #[test]
1298 fn test_crystal_structure_predictor() {
1299 let composition = ChemicalComposition {
1300 elements: {
1301 let mut elements = HashMap::new();
1302 elements.insert("Na".to_string(), 1);
1303 elements.insert("Cl".to_string(), 1);
1304 elements
1305 },
1306 total_atoms: 2,
1307 stoichiometry: Some(vec![1.0, 1.0]),
1308 oxidation_states: Some({
1309 let mut states = HashMap::new();
1310 states.insert("Na".to_string(), 1);
1311 states.insert("Cl".to_string(), -1);
1312 states
1313 }),
1314 };
1315
1316 let energy_model = EnergyModel::Empirical {
1317 potential: EmpiricalPotential::LennardJones,
1318 parameters: {
1319 let mut params = HashMap::new();
1320 params.insert("epsilon".to_string(), 1.0);
1321 params.insert("sigma".to_string(), 3.4);
1322 params
1323 },
1324 };
1325
1326 let predictor = CrystalStructurePredictor::new(composition, energy_model);
1327 let mut result = predictor.build_qubo();
1328 assert!(result.is_ok());
1329 }
1330
1331 #[test]
1332 fn test_lattice() {
1333 let lattice = Lattice {
1334 a: 5.0,
1335 b: 5.0,
1336 c: 5.0,
1337 alpha: 90.0,
1338 beta: 90.0,
1339 gamma: 90.0,
1340 };
1341
1342 assert_eq!(lattice.determine_type() as u8, LatticeType::Cubic as u8);
1343 assert!((lattice.volume() - 125.0).abs() < 0.01);
1344 }
1345
1346 #[test]
1347 fn test_supercell() {
1348 let structure = CrystalStructure {
1349 composition: ChemicalComposition {
1350 elements: {
1351 let mut elements = HashMap::new();
1352 elements.insert("Si".to_string(), 1);
1353 elements
1354 },
1355 total_atoms: 1,
1356 stoichiometry: None,
1357 oxidation_states: None,
1358 },
1359 lattice: Lattice {
1360 a: 5.0,
1361 b: 5.0,
1362 c: 5.0,
1363 alpha: 90.0,
1364 beta: 90.0,
1365 gamma: 90.0,
1366 },
1367 positions: vec![AtomicPosition {
1368 element: "Si".to_string(),
1369 x: 0.0,
1370 y: 0.0,
1371 z: 0.0,
1372 occupancy: 1.0,
1373 }],
1374 space_group: SpaceGroup {
1375 number: 225,
1376 symbol: "Fm-3m".to_string(),
1377 lattice_type: LatticeType::Cubic,
1378 point_group: "m-3m".to_string(),
1379 },
1380 energy: None,
1381 properties: HashMap::new(),
1382 };
1383
1384 let supercell = structure.supercell(2, 2, 2);
1385 assert_eq!(supercell.positions.len(), 8);
1386 assert_eq!(supercell.composition.total_atoms, 8);
1387 }
1388}