ccmat/
lib.rs

1/// notes:
2/// Operation safety is guranteed by the type.
3/// Use Angstrom as the major internal and default API unit to be consistent with xx/xx.
4/// Internally use ``FracCoord`` to represent the position to make sure fractional
5/// coords don’t change if lattice changes shape or scale.
6///
7/// - Use 'lattice'
8
9// TODO: naming convention for vars, check IUCr or cif specification
10// Give a table to compare in between different popular tools.
11//
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct Angstrom(pub f64);
14
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct Bohr(pub f64);
17
18// // FracCoord [0.0, 1.0) and only used internally for site position.
19// // TODO: internally I need to check the Frac is valid in between 0.0~1.0
20// #[derive(Debug, Clone, Copy, PartialEq)]
21// struct FracCoord(f64);
22
23/// Lattice
24/// inner data structure of the struct are private.
25#[derive(Debug)]
26pub struct Lattice {
27    a: [Angstrom; 3],
28    b: [Angstrom; 3],
29    c: [Angstrom; 3],
30}
31
32impl Lattice {
33    // TODO: how to use type system to validate the row/column definition or unit?
34    #[must_use]
35    pub fn new(a: [Angstrom; 3], b: [Angstrom; 3], c: [Angstrom; 3]) -> Self {
36        Lattice { a, b, c }
37    }
38
39    #[must_use]
40    pub fn a(&self) -> [Angstrom; 3] {
41        self.a
42    }
43
44    #[must_use]
45    pub fn b(&self) -> [Angstrom; 3] {
46        self.b
47    }
48
49    #[must_use]
50    pub fn c(&self) -> [Angstrom; 3] {
51        self.c
52    }
53}
54
55// TODO: add lattice_bohr!()
56// TODO: impl Display to Lattice for pretty print.
57
58#[macro_export]
59macro_rules! __vec3_angstrom {
60    ([$x:expr, $y:expr, $z:expr]) => {
61        [Angstrom($x), Angstrom($y), Angstrom($z)]
62    };
63    (($x:expr, $y:expr, $z:expr)) => {
64        [
65            $crate::Angstrom($x),
66            $crate::Angstrom($y),
67            $crate::Angstrom($z),
68        ]
69    };
70}
71
72/// Create a [`Lattice`] using vectors expressed in **Ångström** units.
73///
74/// This macro constructs a [`Lattice`] from three lattice vectors (`a`, `b`, and `c`)
75/// expressed as tuples or arrays of three floating-point numbers.
76///
77/// - Each component is converted to [`Angstrom`] automatically.
78/// - Both `(x, y, z)` and `[x, y, z]` tuple/array syntax are supported.
79/// - Trailing commas are optional.
80///
81/// It supports both **named** and **positional** forms:
82///
83/// - **Named form** (explicit `a=`, `b=`, `c=`):
84///   ```
85///   use ccmat::lattice_angstrom;
86///
87///   let latt = lattice_angstrom!(
88///       a = (1.0, 0.0, 0.0),
89///       b = (0.0, 1.0, 0.0),
90///       c = (0.0, 0.0, 1.0),
91///   );
92///   ```
93///
94/// - **Positional form** (omit names, ordered as `a`, `b`, `c`):
95///   ```
96///   use ccmat::{lattice_angstrom, Lattice, Angstrom};
97///
98///   let latt = lattice_angstrom!(
99///       (1.0, 0.0, 0.0),
100///       (0.0, 1.0, 0.0),
101///       (0.0, 0.0, 1.0),
102///   );
103///   ```
104///
105/// # Errors
106/// - None at compile time; this macro expands directly to constructor calls.
107///
108/// # Example
109/// ```
110/// use ccmat::{lattice_angstrom, Lattice, Angstrom};
111///
112/// let latt = lattice_angstrom!(
113///     a = [2.5, 0.0, 0.0],
114///     b = [0.0, 2.5, 0.0],
115///     c = [0.0, 0.0, 2.5],
116/// );
117/// println!("{:?}", latt);
118/// ```
119#[macro_export]
120macro_rules! lattice_angstrom {
121    (
122        a = $a:tt,
123        b = $b:tt,
124        c = $c:tt $(,)?
125    ) => {{
126        let lattice = $crate::Lattice::new(
127            $crate::__vec3_angstrom!($a),
128            $crate::__vec3_angstrom!($b),
129            $crate::__vec3_angstrom!($c),
130        );
131        lattice
132    }};
133    (
134        $a:tt,
135        $b:tt,
136        $c:tt $(,)?
137    ) => {{
138        let lattice = Lattice::new(
139            $crate::__vec3_angstrom!($a),
140            $crate::__vec3_angstrom!($b),
141            $crate::__vec3_angstrom!($c),
142        );
143        lattice
144    }};
145}
146
147#[derive(Debug)]
148pub struct CrystalValidateError {
149    message: String,
150}
151
152impl std::fmt::Display for CrystalValidateError {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        write!(f, "{}", self.message)
155    }
156}
157
158impl std::error::Error for CrystalValidateError {}
159
160pub struct LatticeSet;
161pub struct LatticeNotSet;
162pub struct AtomsSet;
163pub struct AtomsNotSet;
164
165/// use builder pattern so the validation is runtime
166/// To make it a compile time check, I can use the proc macro (`build`/`build_unchcek` API).
167///
168/// # Example
169/// ```
170/// use ccmat::*;
171///
172/// let lattice = lattice_angstrom![
173///     a = (1.0, 0.0, 0.0),
174///     b = (0.0, 1.0, 0.0),
175///     c = (0.0, 0.0, 1.0),
176/// ];
177/// let atoms = vec![];
178/// let crystal = CrystalBuilder::new()
179///     .with_lattice(lattice)
180///     .with_atoms(&atoms)
181///     .build()
182///     .unwrap();
183/// ```
184#[derive(Debug)]
185pub struct CrystalBuilder<LatticeState, AtomsState> {
186    crystal: Crystal,
187    _lattice: std::marker::PhantomData<LatticeState>,
188    _atoms: std::marker::PhantomData<AtomsState>,
189}
190
191impl Default for CrystalBuilder<LatticeNotSet, AtomsNotSet> {
192    fn default() -> Self {
193        Self {
194            crystal: Crystal {
195                lattice: lattice_angstrom!([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0],),
196                positions: vec![],
197                kinds: vec![],
198            },
199            _lattice: std::marker::PhantomData,
200            _atoms: std::marker::PhantomData,
201        }
202    }
203}
204
205impl CrystalBuilder<LatticeNotSet, AtomsNotSet> {
206    #[must_use]
207    pub fn new() -> Self {
208        Self::default()
209    }
210}
211
212impl<A> CrystalBuilder<LatticeNotSet, A> {
213    // TODO: should Lattice pass as ref?
214    #[must_use]
215    pub fn with_lattice(self, lattice: Lattice) -> CrystalBuilder<LatticeSet, A> {
216        CrystalBuilder {
217            crystal: Crystal {
218                lattice,
219                ..self.crystal
220            },
221            _lattice: std::marker::PhantomData,
222            _atoms: std::marker::PhantomData,
223        }
224    }
225}
226
227impl<L> CrystalBuilder<L, AtomsNotSet> {
228    #[must_use]
229    pub fn with_atoms(self, atoms: &[Atom]) -> CrystalBuilder<L, AtomsSet> {
230        let (positions, kinds) = atoms
231            .iter()
232            .map(|atom| (atom.position, atom.kind))
233            .collect();
234
235        CrystalBuilder {
236            crystal: Crystal {
237                positions,
238                kinds,
239                ..self.crystal
240            },
241            _lattice: std::marker::PhantomData,
242            _atoms: std::marker::PhantomData,
243        }
244    }
245}
246
247impl CrystalBuilder<LatticeSet, AtomsSet> {
248    /// build and validate the it is a valid crystal.
249    /// At the moment only validate the size(positions) == size(numbers)
250    pub fn build(self) -> Result<Crystal, CrystalValidateError> {
251        // TODO: call validate
252        if !self.crystal.positions.len() == self.crystal.kinds.len() {
253            return Err(CrystalValidateError {
254                message: "crystal valid failed".to_string(),
255            });
256        }
257
258        Ok(self.crystal)
259    }
260
261    // build without runtime validation this is for proc macro which valid in compile time.
262    pub(crate) fn build_uncheck(self) -> Crystal {
263        self.crystal
264    }
265}
266
267#[derive(Debug)]
268pub struct Atom {
269    position: [f64; 3],
270    kind: i32,
271}
272
273impl Atom {
274    // TODO: kind can be more complex, need a type to hold it.
275    pub fn new(position: [f64; 3], kind: i32) -> Self {
276        Atom { position, kind }
277    }
278}
279
280// Crystal is the public API so should be align with the real world convention.
281// I did not expose the data structure for crystal directly but the builder.
282// Internally fileds data structures are private to keep API stable.
283// Now I try to align it with moyo's Cell data structure.
284//
285//
286// TODO: not yet generic, but for 3D only at the moment, generalize when doing 2D and 1D
287// in prototype, this struct include as much information as possible. may need to be generic.
288// use rust's type system to check the problem of frac coordinates and direct coordinates.
289#[derive(Debug)]
290pub struct Crystal {
291    lattice: Lattice,
292    positions: Vec<[f64; 3]>,
293    kinds: Vec<i32>,
294}
295
296impl Crystal {
297    #[must_use]
298    pub fn builder() -> CrystalBuilder<LatticeNotSet, AtomsNotSet> {
299        CrystalBuilder::new()
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn macro_lattice_angstrom() {
309        let _ = lattice_angstrom![
310            a = (1.0, 0.0, 0.0),
311            b = (0.0, 1.0, 0.0),
312            c = (0.0, 0.0, 1.0),
313        ];
314        let _ = lattice_angstrom![(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0),];
315        let _ = lattice_angstrom![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],];
316        // trailing comma ','
317        let _ = lattice_angstrom![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
318    }
319
320    #[test]
321    fn build_crystal_compile_error() {
322        let t = trybuild::TestCases::new();
323        t.compile_fail("tests/build_crystal/fail_*.rs");
324    }
325}