1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use std::{fs::File, io::Write};

use chemrust_core::data::{lattice::LatticeVectors, Atom};
use nalgebra::{Matrix3, Rotation3, Unit, Vector3};

use crate::{Cell, ModelFormat};

use super::{FileExport, StructureFile};

#[derive(Debug, Clone, Copy, Default)]
/// A unit struct to mark `msi` format
pub struct Msi;

impl ModelFormat for Msi {}

impl FileExport for StructureFile<Msi> {
    fn write_to<P: AsRef<std::path::Path>>(&self, path: &P) -> Result<(), std::io::Error> {
        let text = self.export_msi();
        let mut f = File::create(path)?;
        write!(f, "{}", text)
    }
}
impl StructureFile<Msi> {
    fn atom_export(atom: &Atom) -> String {
        format!(
            r#"  ({item_id} Atom
    (A C ACL "{elm_id} {elm}")
    (A C Label "{elm}")
    (A D XYZ ({x:.12} {y:.12} {z:.12}))
    (A I Id {atom_id})
  )
"#,
            item_id = atom.index() + 2,
            elm_id = atom.atomic_number(),
            elm = atom.symbol(),
            x = atom.cartesian_coord().x,
            y = atom.cartesian_coord().y,
            z = atom.cartesian_coord().z,
            atom_id = atom.index() + 1
        )
    }
    fn rotate_to_standard_direction(&self) -> Option<Matrix3<f64>> {
        if let Some(vectors) = self.lattice_model.lattice_vectors() {
            let vector_b = vectors.data().column(1);
            let angle = Vector3::y_axis().angle(&vector_b);
            let rot_axis = Unit::new_normalize(vector_b.cross(&Vector3::y_axis()));
            let rot_mat = Rotation3::from_axis_angle(&rot_axis, angle);
            Some(*rot_mat.matrix())
        } else {
            None
        }
    }
    fn rotated_lattice_vector(&self) -> Option<LatticeVectors> {
        if let Some(vectors) = self.lattice_model.lattice_vectors() {
            let rotated_vectors = self.rotate_to_standard_direction().unwrap() * vectors.data();
            Some(LatticeVectors::new(rotated_vectors))
        } else {
            None
        }
    }
    fn lattice_vector_export(vec: &LatticeVectors) -> String {
        // Rotate to let B align with Y
        let vec_names = ["A3", "B3", "C3"];
        let lines: Vec<String> = vec
            .data()
            .column_iter()
            .zip(vec_names.iter())
            .map(|(col, name)| {
                format!(
                    "  (A D {} ({:.12} {:.12} {:.12}))\n",
                    name, col.x, col.y, col.z
                )
            })
            .collect();
        lines.concat()
    }
    pub fn export_msi(&self) -> String {
        let headers = if self.lattice_model.lattice_vectors().is_some() {
            let rotated_vectors = self.rotated_lattice_vector().unwrap();

            format!(
                r#"# MSI CERIUS2 DataModel File Version 4 0
(1 Model
  (A I CRY/DISPLAY (192 256))
  (A I PeriodicType 100)
  (A C SpaceGroup "1 1")
{}  (A D CRY/TOLERANCE 0.05)
"#,
                Self::lattice_vector_export(&rotated_vectors)
            )
        } else {
            "# MSI CERIUS2 DataModel File Version 4 0\n(1 Model\n".to_string()
        };
        let atoms: Vec<String> = match self.lattice_model.lattice_vectors() {
            Some(_) => {
                let rotated_matrix = self.rotate_to_standard_direction().unwrap();
                self.lattice_model
                    .atoms()
                    .iter()
                    .map(|atom| {
                        let rotated_coord = rotated_matrix * atom.cartesian_coord();
                        let rotated_atom = Atom::new_builder()
                            .with_coord(&rotated_coord)
                            .with_index(atom.index())
                            .with_atomic_number(atom.atomic_number())
                            .with_symbol(atom.symbol())
                            .ready()
                            .build();
                        Self::atom_export(&rotated_atom)
                    })
                    .collect()
            }
            None => self
                .lattice_model
                .atoms()
                .iter()
                .map(Self::atom_export)
                .collect(),
        };
        format!("{headers}{atoms_text})", atoms_text = atoms.concat())
    }
}

impl From<StructureFile<Cell>> for StructureFile<Msi> {
    fn from(value: StructureFile<Cell>) -> Self {
        StructureFile::<Msi>::new(value.lattice_model)
    }
}