1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6pub const CELL_PARAMETER_COUNT: usize = 6;
9
10pub type Matrix3 = [[f64; 3]; 3];
12
13#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct UnitCell {
16 pub a_angstrom: f64,
18 pub b_angstrom: f64,
20 pub c_angstrom: f64,
22 pub alpha_deg: f64,
24 pub beta_deg: f64,
26 pub gamma_deg: f64,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct CellGeometry {
33 pub direct_basis: Matrix3,
35 pub reciprocal_basis: Matrix3,
38 pub direct_metric: Matrix3,
40 pub reciprocal_metric: Matrix3,
42 pub volume_angstrom3: f64,
44 direct_metric_derivatives: [Matrix3; CELL_PARAMETER_COUNT],
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum CellError {
50 NonFiniteParameter,
52 NonPositiveLength,
54 InvalidAngle,
56 DegenerateCell,
58 ZeroReflection,
60}
61
62impl Display for CellError {
63 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
64 formatter.write_str(match self {
65 Self::NonFiniteParameter => "unit-cell parameters must be finite",
66 Self::NonPositiveLength => "unit-cell lengths must be positive",
67 Self::InvalidAngle => "unit-cell angles must lie strictly within (0, 180) degrees",
68 Self::DegenerateCell => "unit-cell parameters must define a positive finite volume",
69 Self::ZeroReflection => "hkl = (0, 0, 0) has no finite d-spacing",
70 })
71 }
72}
73
74impl Error for CellError {}
75
76impl UnitCell {
77 pub fn geometry(self) -> Result<CellGeometry, CellError> {
84 let parameters = [
85 self.a_angstrom,
86 self.b_angstrom,
87 self.c_angstrom,
88 self.alpha_deg,
89 self.beta_deg,
90 self.gamma_deg,
91 ];
92 if parameters.iter().any(|value| !value.is_finite()) {
93 return Err(CellError::NonFiniteParameter);
94 }
95 if parameters[..3].iter().any(|value| *value <= 0.0) {
96 return Err(CellError::NonPositiveLength);
97 }
98 if parameters[3..]
99 .iter()
100 .any(|value| !(*value > 0.0 && *value < 180.0))
101 {
102 return Err(CellError::InvalidAngle);
103 }
104
105 let radians_per_degree = std::f64::consts::PI / 180.0;
106 let alpha = self.alpha_deg * radians_per_degree;
107 let beta = self.beta_deg * radians_per_degree;
108 let gamma = self.gamma_deg * radians_per_degree;
109 let (cos_alpha, cos_beta, cos_gamma) = (alpha.cos(), beta.cos(), gamma.cos());
110 let sin_gamma = gamma.sin();
111 let direct_metric = [
112 [
113 self.a_angstrom * self.a_angstrom,
114 self.a_angstrom * self.b_angstrom * cos_gamma,
115 self.a_angstrom * self.c_angstrom * cos_beta,
116 ],
117 [
118 self.a_angstrom * self.b_angstrom * cos_gamma,
119 self.b_angstrom * self.b_angstrom,
120 self.b_angstrom * self.c_angstrom * cos_alpha,
121 ],
122 [
123 self.a_angstrom * self.c_angstrom * cos_beta,
124 self.b_angstrom * self.c_angstrom * cos_alpha,
125 self.c_angstrom * self.c_angstrom,
126 ],
127 ];
128 let determinant = determinant(direct_metric);
129 if !determinant.is_finite() || determinant <= 0.0 || sin_gamma <= 0.0 {
130 return Err(CellError::DegenerateCell);
131 }
132 let volume = determinant.sqrt();
133 let c_x = self.c_angstrom * cos_beta;
134 let c_y = self.c_angstrom * (cos_alpha - cos_beta * cos_gamma) / sin_gamma;
135 let c_z = volume / (self.a_angstrom * self.b_angstrom * sin_gamma);
136 if !c_y.is_finite() || !c_z.is_finite() || c_z <= 0.0 {
137 return Err(CellError::DegenerateCell);
138 }
139 let direct_basis = [
140 [self.a_angstrom, self.b_angstrom * cos_gamma, c_x],
141 [0.0, self.b_angstrom * sin_gamma, c_y],
142 [0.0, 0.0, c_z],
143 ];
144 let reciprocal_metric = inverse(direct_metric).ok_or(CellError::DegenerateCell)?;
145 let reciprocal_basis = transpose(inverse(direct_basis).ok_or(CellError::DegenerateCell)?);
146 let direct_metric_derivatives = direct_metric_derivatives(self, alpha, beta, gamma);
147 Ok(CellGeometry {
148 direct_basis,
149 reciprocal_basis,
150 direct_metric,
151 reciprocal_metric,
152 volume_angstrom3: volume,
153 direct_metric_derivatives,
154 })
155 }
156}
157
158impl CellGeometry {
159 #[must_use]
165 pub fn reciprocal_axis_lengths_and_derivatives(
166 &self,
167 ) -> ([f64; 3], [[f64; CELL_PARAMETER_COUNT]; 3]) {
168 let lengths = [
169 self.reciprocal_metric[0][0].sqrt(),
170 self.reciprocal_metric[1][1].sqrt(),
171 self.reciprocal_metric[2][2].sqrt(),
172 ];
173 let mut derivatives = [[0.0; CELL_PARAMETER_COUNT]; 3];
174 for axis in 0..3 {
175 let reciprocal_column = [
176 self.reciprocal_metric[0][axis],
177 self.reciprocal_metric[1][axis],
178 self.reciprocal_metric[2][axis],
179 ];
180 for (parameter, derivative) in derivatives[axis].iter_mut().enumerate() {
181 let product =
182 matrix_vector(self.direct_metric_derivatives[parameter], reciprocal_column);
183 let d_reciprocal_diagonal = -dot(reciprocal_column, product);
184 *derivative = 0.5 * d_reciprocal_diagonal / lengths[axis];
185 }
186 }
187 (lengths, derivatives)
188 }
189
190 #[must_use]
192 pub fn q_squared(&self, hkl: [i32; 3]) -> f64 {
193 let h = [f64::from(hkl[0]), f64::from(hkl[1]), f64::from(hkl[2])];
194 dot(h, matrix_vector(self.reciprocal_metric, h))
195 }
196
197 #[must_use]
200 pub fn q_squared_and_derivatives(&self, hkl: [i32; 3]) -> (f64, [f64; CELL_PARAMETER_COUNT]) {
201 let h = [f64::from(hkl[0]), f64::from(hkl[1]), f64::from(hkl[2])];
202 let reciprocal_h = matrix_vector(self.reciprocal_metric, h);
203 let q_squared = dot(h, reciprocal_h);
204 let mut derivatives = [0.0; CELL_PARAMETER_COUNT];
205 for (parameter, derivative) in derivatives.iter_mut().enumerate() {
206 let d_metric_h = matrix_vector(self.direct_metric_derivatives[parameter], reciprocal_h);
207 *derivative = -dot(reciprocal_h, d_metric_h);
208 }
209 (q_squared, derivatives)
210 }
211
212 pub fn d_spacing_and_derivatives(
218 &self,
219 hkl: [i32; 3],
220 ) -> Result<(f64, [f64; CELL_PARAMETER_COUNT]), CellError> {
221 let (q_squared, q_derivatives) = self.q_squared_and_derivatives(hkl);
222 if q_squared <= 0.0 || !q_squared.is_finite() {
223 return Err(CellError::ZeroReflection);
224 }
225 let d_spacing = q_squared.sqrt().recip();
226 let factor = -0.5 * d_spacing.powi(3);
227 Ok((d_spacing, q_derivatives.map(|value| factor * value)))
228 }
229
230 #[must_use]
232 pub fn volume_derivatives(&self) -> [f64; CELL_PARAMETER_COUNT] {
233 let mut derivatives = [0.0; CELL_PARAMETER_COUNT];
234 for (parameter, derivative) in derivatives.iter_mut().enumerate() {
235 let product = multiply(
236 self.reciprocal_metric,
237 self.direct_metric_derivatives[parameter],
238 );
239 *derivative =
240 0.5 * self.volume_angstrom3 * (product[0][0] + product[1][1] + product[2][2]);
241 }
242 derivatives
243 }
244}
245
246fn direct_metric_derivatives(
247 cell: UnitCell,
248 alpha: f64,
249 beta: f64,
250 gamma: f64,
251) -> [Matrix3; CELL_PARAMETER_COUNT] {
252 let zero = [[0.0; 3]; 3];
253 let mut derivatives = [zero; CELL_PARAMETER_COUNT];
254 let (ca, cb, cg) = (alpha.cos(), beta.cos(), gamma.cos());
255 derivatives[0] = [
256 [
257 2.0 * cell.a_angstrom,
258 cell.b_angstrom * cg,
259 cell.c_angstrom * cb,
260 ],
261 [cell.b_angstrom * cg, 0.0, 0.0],
262 [cell.c_angstrom * cb, 0.0, 0.0],
263 ];
264 derivatives[1] = [
265 [0.0, cell.a_angstrom * cg, 0.0],
266 [
267 cell.a_angstrom * cg,
268 2.0 * cell.b_angstrom,
269 cell.c_angstrom * ca,
270 ],
271 [0.0, cell.c_angstrom * ca, 0.0],
272 ];
273 derivatives[2] = [
274 [0.0, 0.0, cell.a_angstrom * cb],
275 [0.0, 0.0, cell.b_angstrom * ca],
276 [
277 cell.a_angstrom * cb,
278 cell.b_angstrom * ca,
279 2.0 * cell.c_angstrom,
280 ],
281 ];
282 let radians_per_degree = std::f64::consts::PI / 180.0;
283 let d_alpha = -cell.b_angstrom * cell.c_angstrom * alpha.sin() * radians_per_degree;
284 derivatives[3][1][2] = d_alpha;
285 derivatives[3][2][1] = d_alpha;
286 let d_beta = -cell.a_angstrom * cell.c_angstrom * beta.sin() * radians_per_degree;
287 derivatives[4][0][2] = d_beta;
288 derivatives[4][2][0] = d_beta;
289 let d_gamma = -cell.a_angstrom * cell.b_angstrom * gamma.sin() * radians_per_degree;
290 derivatives[5][0][1] = d_gamma;
291 derivatives[5][1][0] = d_gamma;
292 derivatives
293}
294
295fn determinant(matrix: Matrix3) -> f64 {
296 matrix[0][0] * (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1])
297 - matrix[0][1] * (matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0])
298 + matrix[0][2] * (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0])
299}
300
301fn inverse(matrix: Matrix3) -> Option<Matrix3> {
302 let det = determinant(matrix);
303 if !det.is_finite() || det == 0.0 {
304 return None;
305 }
306 let inverse_det = det.recip();
307 Some([
308 [
309 (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1]) * inverse_det,
310 (matrix[0][2] * matrix[2][1] - matrix[0][1] * matrix[2][2]) * inverse_det,
311 (matrix[0][1] * matrix[1][2] - matrix[0][2] * matrix[1][1]) * inverse_det,
312 ],
313 [
314 (matrix[1][2] * matrix[2][0] - matrix[1][0] * matrix[2][2]) * inverse_det,
315 (matrix[0][0] * matrix[2][2] - matrix[0][2] * matrix[2][0]) * inverse_det,
316 (matrix[0][2] * matrix[1][0] - matrix[0][0] * matrix[1][2]) * inverse_det,
317 ],
318 [
319 (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]) * inverse_det,
320 (matrix[0][1] * matrix[2][0] - matrix[0][0] * matrix[2][1]) * inverse_det,
321 (matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]) * inverse_det,
322 ],
323 ])
324}
325
326fn transpose(matrix: Matrix3) -> Matrix3 {
327 [
328 [matrix[0][0], matrix[1][0], matrix[2][0]],
329 [matrix[0][1], matrix[1][1], matrix[2][1]],
330 [matrix[0][2], matrix[1][2], matrix[2][2]],
331 ]
332}
333
334fn multiply(left: Matrix3, right: Matrix3) -> Matrix3 {
335 let mut result = [[0.0; 3]; 3];
336 for (row, result_row) in result.iter_mut().enumerate() {
337 for (column, value) in result_row.iter_mut().enumerate() {
338 *value = (0..3)
339 .map(|inner| left[row][inner] * right[inner][column])
340 .sum();
341 }
342 }
343 result
344}
345
346fn matrix_vector(matrix: Matrix3, vector: [f64; 3]) -> [f64; 3] {
347 matrix.map(|row| dot(row, vector))
348}
349
350fn dot(left: [f64; 3], right: [f64; 3]) -> f64 {
351 left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 fn triclinic() -> UnitCell {
359 UnitCell {
360 a_angstrom: 4.3,
361 b_angstrom: 5.1,
362 c_angstrom: 6.2,
363 alpha_deg: 78.0,
364 beta_deg: 83.0,
365 gamma_deg: 71.0,
366 }
367 }
368
369 fn changed(mut cell: UnitCell, parameter: usize, delta: f64) -> UnitCell {
370 match parameter {
371 0 => cell.a_angstrom += delta,
372 1 => cell.b_angstrom += delta,
373 2 => cell.c_angstrom += delta,
374 3 => cell.alpha_deg += delta,
375 4 => cell.beta_deg += delta,
376 5 => cell.gamma_deg += delta,
377 _ => unreachable!(),
378 }
379 cell
380 }
381
382 #[test]
383 fn cubic_geometry_is_exact() {
384 let geometry = UnitCell {
385 a_angstrom: 4.0,
386 b_angstrom: 4.0,
387 c_angstrom: 4.0,
388 alpha_deg: 90.0,
389 beta_deg: 90.0,
390 gamma_deg: 90.0,
391 }
392 .geometry()
393 .expect("cubic cell");
394 assert!((geometry.volume_angstrom3 - 64.0).abs() < 1.0e-12);
395 assert!((geometry.reciprocal_metric[0][0] - 1.0 / 16.0).abs() < 1.0e-15);
396 let (d, _) = geometry
397 .d_spacing_and_derivatives([1, 1, 0])
398 .expect("nonzero reflection");
399 assert!((d - 4.0 / 2.0_f64.sqrt()).abs() < 1.0e-14);
400 }
401
402 #[test]
403 fn cell_derivatives_match_centered_differences() {
404 let cell = triclinic();
405 let geometry = cell.geometry().expect("triclinic cell");
406 let (d, derivatives) = geometry
407 .d_spacing_and_derivatives([2, -1, 3])
408 .expect("reflection");
409 let volume_derivatives = geometry.volume_derivatives();
410 let (reciprocal_lengths, reciprocal_derivatives) =
411 geometry.reciprocal_axis_lengths_and_derivatives();
412 for parameter in 0..CELL_PARAMETER_COUNT {
413 let step = if parameter < 3 { 1.0e-6 } else { 1.0e-5 };
414 let plus = changed(cell, parameter, step)
415 .geometry()
416 .expect("plus cell");
417 let minus = changed(cell, parameter, -step)
418 .geometry()
419 .expect("minus cell");
420 let plus_d = plus
421 .d_spacing_and_derivatives([2, -1, 3])
422 .expect("plus reflection")
423 .0;
424 let minus_d = minus
425 .d_spacing_and_derivatives([2, -1, 3])
426 .expect("minus reflection")
427 .0;
428 let finite_d = (plus_d - minus_d) / (2.0 * step);
429 let finite_volume = (plus.volume_angstrom3 - minus.volume_angstrom3) / (2.0 * step);
430 assert!((derivatives[parameter] - finite_d).abs() < 2.0e-8 * d.max(1.0));
431 assert!(
432 (volume_derivatives[parameter] - finite_volume).abs()
433 < 2.0e-8 * geometry.volume_angstrom3
434 );
435 let plus_lengths = plus.reciprocal_axis_lengths_and_derivatives().0;
436 let minus_lengths = minus.reciprocal_axis_lengths_and_derivatives().0;
437 for axis in 0..3 {
438 let finite_reciprocal = (plus_lengths[axis] - minus_lengths[axis]) / (2.0 * step);
439 assert!(
440 (reciprocal_derivatives[axis][parameter] - finite_reciprocal).abs()
441 < 2.0e-8 * reciprocal_lengths[axis].max(1.0)
442 );
443 }
444 }
445 }
446
447 #[test]
448 fn invalid_cells_and_zero_reflection_are_rejected() {
449 let mut cell = triclinic();
450 cell.a_angstrom = 0.0;
451 assert_eq!(cell.geometry(), Err(CellError::NonPositiveLength));
452 let geometry = triclinic().geometry().expect("cell");
453 assert_eq!(
454 geometry.d_spacing_and_derivatives([0, 0, 0]),
455 Err(CellError::ZeroReflection)
456 );
457 }
458}