1#![allow(clippy::indexing_slicing)] use crate::{Error, Site, Structure, StructureBuilder};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct Plane {
10 pub normal: [f64; 3],
12 pub point: [f64; 3],
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub struct Ellipsoid {
19 pub axes: [[f64; 3]; 3],
21 pub lengths: [f64; 3],
23 pub probability: f64,
25}
26
27#[must_use]
29pub fn translate(points: &[[f64; 3]], delta: [f64; 3]) -> Vec<[f64; 3]> {
30 points
31 .iter()
32 .map(|point| {
33 [
34 point[0] + delta[0],
35 point[1] + delta[1],
36 point[2] + delta[2],
37 ]
38 })
39 .collect()
40}
41
42pub fn rotate(
48 points: &[[f64; 3]],
49 axis: [f64; 3],
50 angle_degrees: f64,
51 center: [f64; 3],
52) -> Result<Vec<[f64; 3]>, Error> {
53 if !axis.iter().all(|value| value.is_finite()) || !angle_degrees.is_finite() {
54 return Err(Error::InvalidInput(
55 "rotation axis and angle must be finite".to_owned(),
56 ));
57 }
58 let axis = normalize(axis)?;
59 let angle = angle_degrees.to_radians();
60 let (sine, cosine) = angle.sin_cos();
61 Ok(points
62 .iter()
63 .map(|point| {
64 let relative = subtract(*point, center);
65 let rotated = add(
66 add(scale(relative, cosine), scale(cross(axis, relative), sine)),
67 scale(axis, dot(axis, relative) * (1.0 - cosine)),
68 );
69 add(rotated, center)
70 })
71 .collect())
72}
73
74#[must_use]
76pub fn mirror(points: &[[f64; 3]], plane: Plane) -> Vec<[f64; 3]> {
77 points
78 .iter()
79 .map(|point| {
80 let distance = dot(subtract(*point, plane.point), plane.normal);
81 subtract(*point, scale(plane.normal, 2.0 * distance))
82 })
83 .collect()
84}
85
86#[must_use]
88pub fn invert(points: &[[f64; 3]], center: [f64; 3]) -> Vec<[f64; 3]> {
89 points
90 .iter()
91 .map(|point| {
92 [
93 2.0 * center[0] - point[0],
94 2.0 * center[1] - point[1],
95 2.0 * center[2] - point[2],
96 ]
97 })
98 .collect()
99}
100
101pub fn align_vector(
107 points: &[[f64; 3]],
108 from: [f64; 3],
109 to: [f64; 3],
110 center: [f64; 3],
111) -> Result<Vec<[f64; 3]>, Error> {
112 let from = normalize(from)?;
113 let to = normalize(to)?;
114 let axis = cross(from, to);
115 let sine = norm(axis);
116 if sine < 1e-12 {
117 if dot(from, to) > 0.0 {
118 return Ok(points.to_vec());
119 }
120 let perpendicular = if from[0].abs() < 0.9 {
121 normalize(cross(from, [1.0, 0.0, 0.0]))?
122 } else {
123 normalize(cross(from, [0.0, 1.0, 0.0]))?
124 };
125 return rotate(points, perpendicular, 180.0, center);
126 }
127 let angle = sine.atan2(dot(from, to)).to_degrees();
128 rotate(points, axis, angle, center)
129}
130
131pub fn fit_plane(points: &[[f64; 3]]) -> Result<Plane, Error> {
138 if points.len() < 3 {
139 return Err(Error::InvalidInput(
140 "a plane needs at least three points".to_owned(),
141 ));
142 }
143 let centroid = centroid(points);
144 let mut first = [0.0; 3];
145 for point in points {
146 let candidate = subtract(*point, centroid);
147 if norm(candidate) > 1e-9 {
148 first = candidate;
149 break;
150 }
151 }
152 let mut second = [0.0; 3];
153 for point in points {
154 let candidate = subtract(*point, centroid);
155 let normal = cross(first, candidate);
156 if norm(normal) > 1e-9 {
157 second = candidate;
158 break;
159 }
160 }
161 let normal = normalize(cross(first, second))?;
162 Ok(Plane {
163 normal,
164 point: centroid,
165 })
166}
167
168pub fn align_planes(
174 points: &[[f64; 3]],
175 from_normal: [f64; 3],
176 to_normal: [f64; 3],
177 center: [f64; 3],
178) -> Result<Vec<[f64; 3]>, Error> {
179 align_vector(points, from_normal, to_normal, center)
180}
181
182pub fn displacement_ellipsoid(uij: [f64; 6], probability: f64) -> Result<Ellipsoid, Error> {
192 if !uij.iter().all(|value| value.is_finite()) || !probability.is_finite() {
193 return Err(Error::InvalidInput(
194 "displacement parameters must be finite".to_owned(),
195 ));
196 }
197 if !(0.0 < probability && probability < 1.0) {
198 return Err(Error::InvalidInput(
199 "ellipsoid probability must be in (0, 1)".to_owned(),
200 ));
201 }
202 let matrix = [
203 [uij[0], uij[3], uij[4]],
204 [uij[3], uij[1], uij[5]],
205 [uij[4], uij[5], uij[2]],
206 ];
207 let (values, vectors) = jacobi_eigen(matrix)?;
208 let mut lengths = [0.0; 3];
209 for index in 0..3 {
210 if values[index] < -1e-12 {
211 return Err(Error::InvalidInput(
212 "displacement tensor is not positive semidefinite".to_owned(),
213 ));
214 }
215 lengths[index] = values[index].max(0.0).sqrt() * chi2_scale(probability);
216 }
217 Ok(Ellipsoid {
218 axes: vectors,
219 lengths,
220 probability,
221 })
222}
223
224impl Structure {
225 pub fn finite_cell(&self, tolerance: f64) -> Result<Self, Error> {
231 if !tolerance.is_finite() || tolerance < 0.0 {
232 return Err(Error::InvalidInput(
233 "finite-cell tolerance must be non-negative and finite".to_owned(),
234 ));
235 }
236 let p1 = self.p1()?;
237 if p1.atoms.is_empty() {
238 return Ok(self.clone());
239 }
240 let lattice = p1.lattice;
241 let mut sites: Vec<Site> = Vec::new();
242 for atom in &p1.atoms {
243 let shifts = atom.frac.map(|value| {
244 let mut out = vec![0];
245 if value <= tolerance {
246 out.push(1);
247 }
248 if value >= 1.0 - tolerance {
249 out.push(-1);
250 }
251 out
252 });
253 for x in &shifts[0] {
254 for y in &shifts[1] {
255 for z in &shifts[2] {
256 let frac = [
257 atom.frac[0] + f64::from(*x),
258 atom.frac[1] + f64::from(*y),
259 atom.frac[2] + f64::from(*z),
260 ];
261 if frac
262 .iter()
263 .all(|value| (-tolerance..=1.0 + tolerance).contains(value))
264 {
265 let duplicate = sites.iter().any(|existing| {
266 existing.element() == atom.element
267 && existing.frac().iter().zip(frac.iter()).all(
268 |(first, second)| {
269 (first - second).abs() <= tolerance.max(1e-9)
270 },
271 )
272 });
273 if !duplicate {
274 sites.push(Site::new(
275 format!("{}{}", atom.element, sites.len() + 1),
276 &atom.element,
277 frac,
278 )?);
279 }
280 }
281 }
282 }
283 }
284 }
285 let mut builder = StructureBuilder::new(self.name(), lattice);
286 for site in sites {
287 builder = builder.site(site);
288 }
289 builder.build()
290 }
291}
292
293fn centroid(points: &[[f64; 3]]) -> [f64; 3] {
294 let mut sum = [0.0; 3];
295 for point in points {
296 sum[0] += point[0];
297 sum[1] += point[1];
298 sum[2] += point[2];
299 }
300 let count = points.len() as f64;
301 [sum[0] / count, sum[1] / count, sum[2] / count]
302}
303
304fn chi2_scale(probability: f64) -> f64 {
305 let table = [(0.5, 1.538), (0.9, 2.500), (0.95, 2.796), (0.99, 3.368)];
306 if probability <= table[0].0 {
307 return table[0].1;
308 }
309 let mut previous = table[0];
310 for (level, value) in table {
311 if probability <= level {
312 let fraction = (probability - previous.0) / (level - previous.0);
313 return previous.1 + fraction * (value - previous.1);
314 }
315 previous = (level, value);
316 }
317 table[table.len() - 1].1
318}
319
320fn jacobi_eigen(matrix: [[f64; 3]; 3]) -> Result<([f64; 3], [[f64; 3]; 3]), Error> {
321 let mut a = matrix;
322 let mut v = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
323 for _ in 0..32 {
324 let (p, q) = largest_off_diagonal(a);
325 if a[p][q].abs() < 1e-12 {
326 break;
327 }
328 let angle = 0.5 * (2.0 * a[p][q]).atan2(a[q][q] - a[p][p]);
329 let (sine, cosine) = angle.sin_cos();
330 let app = a[p][p];
331 let aqq = a[q][q];
332 let apq = a[p][q];
333 a[p][p] = cosine * cosine * app - 2.0 * sine * cosine * apq + sine * sine * aqq;
334 a[q][q] = sine * sine * app + 2.0 * sine * cosine * apq + cosine * cosine * aqq;
335 a[p][q] = 0.0;
336 a[q][p] = 0.0;
337 for index in 0..3 {
338 if index != p && index != q {
339 let aip = a[index][p];
340 let aiq = a[index][q];
341 a[index][p] = cosine * aip - sine * aiq;
342 a[p][index] = a[index][p];
343 a[index][q] = sine * aip + cosine * aiq;
344 a[q][index] = a[index][q];
345 }
346 let vpi = v[index][p];
347 let vqi = v[index][q];
348 v[index][p] = cosine * vpi - sine * vqi;
349 v[index][q] = sine * vpi + cosine * vqi;
350 }
351 }
352 let mut values = [a[0][0], a[1][1], a[2][2]];
353 let mut axes = [
354 [v[0][0], v[1][0], v[2][0]],
355 [v[0][1], v[1][1], v[2][1]],
356 [v[0][2], v[1][2], v[2][2]],
357 ];
358 for index in 0..3 {
359 if values[index] < -1e-9 {
360 return Err(Error::InvalidInput(
361 "displacement tensor is not positive semidefinite".to_owned(),
362 ));
363 }
364 values[index] = values[index].max(0.0);
365 axes[index] = normalize(axes[index]).unwrap_or([0.0, 0.0, 0.0]);
366 }
367 let mut order = [0usize, 1, 2];
368 order.sort_by(|first, second| {
369 values[*first]
370 .partial_cmp(&values[*second])
371 .unwrap_or(std::cmp::Ordering::Equal)
372 });
373 Ok((
374 [values[order[0]], values[order[1]], values[order[2]]],
375 [axes[order[0]], axes[order[1]], axes[order[2]]],
376 ))
377}
378
379fn largest_off_diagonal(matrix: [[f64; 3]; 3]) -> (usize, usize) {
380 let candidates = [(0, 1), (0, 2), (1, 2)];
381 let mut best = candidates[0];
382 for candidate in candidates {
383 if matrix[candidate.0][candidate.1].abs() > matrix[best.0][best.1].abs() {
384 best = candidate;
385 }
386 }
387 best
388}
389
390fn add(first: [f64; 3], second: [f64; 3]) -> [f64; 3] {
391 [
392 first[0] + second[0],
393 first[1] + second[1],
394 first[2] + second[2],
395 ]
396}
397
398fn subtract(first: [f64; 3], second: [f64; 3]) -> [f64; 3] {
399 [
400 first[0] - second[0],
401 first[1] - second[1],
402 first[2] - second[2],
403 ]
404}
405
406fn scale(vector: [f64; 3], factor: f64) -> [f64; 3] {
407 [vector[0] * factor, vector[1] * factor, vector[2] * factor]
408}
409
410fn cross(first: [f64; 3], second: [f64; 3]) -> [f64; 3] {
411 [
412 first[1] * second[2] - first[2] * second[1],
413 first[2] * second[0] - first[0] * second[2],
414 first[0] * second[1] - first[1] * second[0],
415 ]
416}
417
418fn dot(first: [f64; 3], second: [f64; 3]) -> f64 {
419 first[0] * second[0] + first[1] * second[1] + first[2] * second[2]
420}
421
422fn norm(vector: [f64; 3]) -> f64 {
423 dot(vector, vector).sqrt()
424}
425
426fn normalize(vector: [f64; 3]) -> Result<[f64; 3], Error> {
427 let length = norm(vector);
428 if length < 1e-12 {
429 return Err(Error::InvalidInput("the vector has zero length".to_owned()));
430 }
431 Ok(scale(vector, 1.0 / length))
432}