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}