1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::Error;
8
9#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct SymOp {
12 rotation: [[i32; 3]; 3],
13 translation: [f64; 3],
14}
15
16impl SymOp {
17 pub fn new(rotation: [[i32; 3]; 3], translation: [f64; 3]) -> Result<Self, Error> {
24 if !translation.iter().all(|value| value.is_finite()) {
25 return Err(Error::InvalidInput(
26 "symmetry translation must be finite".to_owned(),
27 ));
28 }
29 if determinant(rotation) == 0 {
30 return Err(Error::InvalidInput(
31 "symmetry rotation is singular".to_owned(),
32 ));
33 }
34 Ok(Self {
35 rotation,
36 translation: translation.map(|value| value.rem_euclid(1.0)),
37 })
38 }
39
40 #[must_use]
42 pub const fn identity() -> Self {
43 Self {
44 rotation: [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
45 translation: [0.0, 0.0, 0.0],
46 }
47 }
48
49 #[must_use]
51 pub const fn rotation(&self) -> [[i32; 3]; 3] {
52 self.rotation
53 }
54
55 #[must_use]
57 pub const fn translation(&self) -> [f64; 3] {
58 self.translation
59 }
60
61 #[must_use]
63 pub fn apply(&self, frac: [f64; 3]) -> [f64; 3] {
64 let [x, y, z] = frac;
65 let [[a, b, c], [d, e, f], [g, h, i]] = self.rotation;
66 let [tx, ty, tz] = self.translation;
67 [
68 a as f64 * x + b as f64 * y + c as f64 * z + tx,
69 d as f64 * x + e as f64 * y + f as f64 * z + ty,
70 g as f64 * x + h as f64 * y + i as f64 * z + tz,
71 ]
72 }
73
74 #[must_use]
76 pub fn apply_many(&self, points: &[[f64; 3]]) -> Vec<[f64; 3]> {
77 points.iter().map(|point| self.apply(*point)).collect()
78 }
79
80 pub fn compose(&self, other: &Self) -> Result<Self, Error> {
86 let rotation = multiply_int(self.rotation, other.rotation);
87 let shifted = apply_int(self.rotation, other.translation);
88 let translation = [
89 shifted[0] + self.translation[0],
90 shifted[1] + self.translation[1],
91 shifted[2] + self.translation[2],
92 ];
93 Self::new(rotation, translation)
94 }
95
96 pub fn inverse(&self) -> Result<Self, Error> {
103 let det = determinant(self.rotation);
104 if det.abs() != 1 {
105 return Err(Error::InvalidInput(
106 "symmetry rotation determinant must be ±1".to_owned(),
107 ));
108 }
109 let inverse = inverse_int(self.rotation, det)?;
110 let shifted = apply_int(inverse, self.translation);
111 Self::new(inverse, [-shifted[0], -shifted[1], -shifted[2]])
112 }
113
114 #[must_use]
116 pub fn is_identity(&self) -> bool {
117 self.rotation == [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
118 && self.translation.iter().all(|value| *value == 0.0)
119 }
120
121 pub fn parse(text: &str) -> Result<Self, Error> {
128 let components: Vec<&str> = text.split(',').map(str::trim).collect();
129 if components.len() != 3 {
130 return Err(Error::InvalidInput(format!(
131 "a symmetry triplet needs three comma-separated components: {text:?}"
132 )));
133 }
134 let mut rotation = [[0i32; 3]; 3];
135 let mut translation = [0.0; 3];
136 for (row, component) in components.iter().enumerate() {
137 let (line, constant) = parse_component(component)?;
138 if let (Some(line_slot), Some(constant_slot)) =
139 (rotation.get_mut(row), translation.get_mut(row))
140 {
141 *line_slot = line;
142 *constant_slot = constant;
143 }
144 }
145 Self::new(rotation, translation)
146 }
147
148 #[must_use]
150 pub fn triplet(&self) -> String {
151 (0..3)
152 .map(|row| format_component(&self.rotation, self.translation, row))
153 .collect::<Vec<_>>()
154 .join(",")
155 }
156}
157
158impl fmt::Display for SymOp {
159 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160 formatter.write_str(&self.triplet())
161 }
162}
163
164fn parse_component(text: &str) -> Result<([i32; 3], f64), Error> {
165 let compact: String = text
166 .chars()
167 .filter(|character| !character.is_whitespace())
168 .collect();
169 if compact.is_empty() {
170 return Err(Error::InvalidInput("empty symmetry component".to_owned()));
171 }
172 let mut rotation = [0i32; 3];
173 let mut translation = 0.0;
174 let mut sign = 1.0;
175 let mut term = String::new();
176 for character in compact.chars() {
177 if character == '+' || character == '-' {
178 if !term.is_empty() {
179 add_term(&term, sign, &mut rotation, &mut translation)?;
180 term.clear();
181 }
182 sign = if character == '-' { -1.0 } else { 1.0 };
183 } else {
184 term.push(character);
185 }
186 }
187 if term.is_empty() {
188 return Err(Error::InvalidInput(format!(
189 "malformed symmetry component: {text:?}"
190 )));
191 }
192 add_term(&term, sign, &mut rotation, &mut translation)?;
193 Ok((rotation, translation))
194}
195
196fn add_term(
197 term: &str,
198 sign: f64,
199 rotation: &mut [i32; 3],
200 translation: &mut f64,
201) -> Result<(), Error> {
202 if let Some(index) = term.find(['x', 'y', 'z']) {
203 let axis = match term.get(index..index + 1) {
204 Some("x") => 0,
205 Some("y") => 1,
206 Some("z") => 2,
207 _ => 0,
208 };
209 let coefficient = term.get(..index).unwrap_or("");
210 let after = term.get(index + 1..).unwrap_or("");
211 let mut value = if coefficient.is_empty() || coefficient == "+" {
212 1.0
213 } else if coefficient == "-" {
214 -1.0
215 } else {
216 coefficient.parse::<f64>().map_err(|_| {
217 Error::InvalidInput(format!("malformed symmetry coefficient: {term:?}"))
218 })?
219 };
220 if let Some(denominator) = after.strip_prefix('/') {
221 let denominator: f64 = denominator.parse().map_err(|_| {
222 Error::InvalidInput(format!("malformed symmetry denominator: {term:?}"))
223 })?;
224 if denominator == 0.0 {
225 return Err(Error::InvalidInput(
226 "symmetry denominator must not be zero".to_owned(),
227 ));
228 }
229 value /= denominator;
230 }
231 let value = (sign * value).round() as i32;
232 if let Some(slot) = rotation.get_mut(axis) {
233 *slot += value;
234 }
235 return Ok(());
236 }
237 let value = parse_rational(term)?;
238 *translation += sign * value;
239 Ok(())
240}
241
242fn parse_rational(text: &str) -> Result<f64, Error> {
243 if let Some((numerator, denominator)) = text.split_once('/') {
244 let numerator = numerator
245 .parse::<f64>()
246 .map_err(|_| Error::InvalidInput(format!("malformed rational constant: {text:?}")))?;
247 let denominator = denominator.parse::<f64>().map_err(|_| {
248 Error::InvalidInput(format!("malformed rational denominator: {text:?}"))
249 })?;
250 if denominator == 0.0 {
251 return Err(Error::InvalidInput(
252 "symmetry denominator must not be zero".to_owned(),
253 ));
254 }
255 Ok(numerator / denominator)
256 } else {
257 text.parse::<f64>()
258 .map_err(|_| Error::InvalidInput(format!("malformed constant: {text:?}")))
259 }
260}
261
262fn format_component(rotation: &[[i32; 3]; 3], translation: [f64; 3], row: usize) -> String {
263 let mut out = String::new();
264 let line = rotation.get(row).copied().unwrap_or([0; 3]);
265 for (axis, coefficient) in line.iter().enumerate() {
266 let coefficient = *coefficient;
267 if coefficient == 0 {
268 continue;
269 }
270 let negative = coefficient < 0;
271 if out.is_empty() {
272 if negative {
273 out.push('-');
274 }
275 } else {
276 out.push(if negative { '-' } else { '+' });
277 }
278 let magnitude = coefficient.unsigned_abs();
279 if magnitude != 1 {
280 out.push_str(&magnitude.to_string());
281 }
282 out.push(match axis {
283 0 => 'x',
284 1 => 'y',
285 _ => 'z',
286 });
287 }
288 let constant = translation.get(row).copied().unwrap_or(0.0);
289 if constant != 0.0 {
290 let text = fraction(constant);
291 if out.is_empty() {
292 out.push_str(&text);
293 } else {
294 out.push('+');
295 out.push_str(&text);
296 }
297 }
298 if out.is_empty() {
299 out.push('0');
300 }
301 out
302}
303
304fn fraction(value: f64) -> String {
305 let value = value.rem_euclid(1.0);
306 for denominator in 1..=12u32 {
307 let numerator = (value * f64::from(denominator)).round();
308 let candidate = numerator / f64::from(denominator);
309 if (candidate - value).abs() < 1e-9 {
310 if denominator == 1 {
311 return format!("{}", numerator as i64);
312 }
313 let divisor = gcd(numerator as i64, i64::from(denominator));
314 return format!(
315 "{}/{}",
316 numerator as i64 / divisor,
317 i64::from(denominator) / divisor
318 );
319 }
320 }
321 format!("{value:.6}")
322}
323
324fn gcd(mut first: i64, mut second: i64) -> i64 {
325 while second != 0 {
326 let remainder = first % second;
327 first = second;
328 second = remainder;
329 }
330 first.abs().max(1)
331}
332
333fn determinant(matrix: [[i32; 3]; 3]) -> i32 {
334 let [[a, b, c], [d, e, f], [g, h, i]] = matrix;
335 a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g)
336}
337
338fn multiply_int(first: [[i32; 3]; 3], second: [[i32; 3]; 3]) -> [[i32; 3]; 3] {
339 let [[a, b, c], [d, e, f], [g, h, i]] = first;
340 let [[j, k, l], [m, n, o], [p, q, r]] = second;
341 [
342 [
343 a * j + b * m + c * p,
344 a * k + b * n + c * q,
345 a * l + b * o + c * r,
346 ],
347 [
348 d * j + e * m + f * p,
349 d * k + e * n + f * q,
350 d * l + e * o + f * r,
351 ],
352 [
353 g * j + h * m + i * p,
354 g * k + h * n + i * q,
355 g * l + h * o + i * r,
356 ],
357 ]
358}
359
360fn apply_int(matrix: [[i32; 3]; 3], vector: [f64; 3]) -> [f64; 3] {
361 let [[a, b, c], [d, e, f], [g, h, i]] = matrix;
362 let [x, y, z] = vector;
363 [
364 a as f64 * x + b as f64 * y + c as f64 * z,
365 d as f64 * x + e as f64 * y + f as f64 * z,
366 g as f64 * x + h as f64 * y + i as f64 * z,
367 ]
368}
369
370fn inverse_int(matrix: [[i32; 3]; 3], det: i32) -> Result<[[i32; 3]; 3], Error> {
371 let [[a, b, c], [d, e, f], [g, h, i]] = matrix;
372 let adjugate = [
373 [e * i - f * h, c * h - b * i, b * f - c * e],
374 [f * g - d * i, a * i - c * g, c * d - a * f],
375 [d * h - e * g, b * g - a * h, a * e - b * d],
376 ];
377 let mut inverse = [[0i32; 3]; 3];
378 for (output_row, adjudicate_row) in inverse.iter_mut().zip(adjugate) {
379 for (output, value) in output_row.iter_mut().zip(adjudicate_row) {
380 if value % det != 0 {
381 return Err(Error::InvalidInput(
382 "symmetry rotation is not unimodular".to_owned(),
383 ));
384 }
385 *output = value / det;
386 }
387 }
388 Ok(inverse)
389}