ferritin_core/model/
symmetry.rs1pub type Mat4 = [[f32; 4]; 4];
5
6pub const IDENTITY_MAT4: Mat4 = [
8 [1.0, 0.0, 0.0, 0.0],
9 [0.0, 1.0, 0.0, 0.0],
10 [0.0, 0.0, 1.0, 0.0],
11 [0.0, 0.0, 0.0, 1.0],
12];
13
14#[derive(Clone, Debug, PartialEq)]
16pub struct SymmetryOperator {
17 pub label: String,
18 pub matrix: Mat4,
19}
20
21impl SymmetryOperator {
22 pub fn identity() -> Self {
23 Self {
24 label: "identity".to_string(),
25 matrix: IDENTITY_MAT4,
26 }
27 }
28
29 pub fn new(label: impl Into<String>, matrix: Mat4) -> Self {
30 Self {
31 label: label.into(),
32 matrix,
33 }
34 }
35
36 pub fn apply(&self, point: [f32; 3]) -> [f32; 3] {
38 let [x, y, z] = point;
39 [
40 self.matrix[0][0] * x
41 + self.matrix[0][1] * y
42 + self.matrix[0][2] * z
43 + self.matrix[0][3],
44 self.matrix[1][0] * x
45 + self.matrix[1][1] * y
46 + self.matrix[1][2] * z
47 + self.matrix[1][3],
48 self.matrix[2][0] * x
49 + self.matrix[2][1] * y
50 + self.matrix[2][2] * z
51 + self.matrix[2][3],
52 ]
53 }
54
55 pub fn compose(&self, other: &Self, label: impl Into<String>) -> Self {
58 let mut matrix = [[0.0; 4]; 4];
59 for (row, values) in matrix.iter_mut().enumerate() {
60 for (column, value) in values.iter_mut().enumerate() {
61 *value = (0..4)
62 .map(|k| self.matrix[row][k] * other.matrix[k][column])
63 .sum();
64 }
65 }
66 Self::new(label, matrix)
67 }
68}
69
70impl Default for SymmetryOperator {
71 fn default() -> Self {
72 Self::identity()
73 }
74}
75
76#[derive(Clone, Debug, PartialEq)]
78pub struct AssemblyUnit {
79 pub asym_ids: Vec<String>,
81 pub operator: SymmetryOperator,
82}
83
84#[derive(Clone, Debug, PartialEq)]
86pub struct Assembly {
87 pub id: String,
88 pub details: Option<String>,
89 pub units: Vec<AssemblyUnit>,
90}
91
92#[derive(Clone, Debug, Default, PartialEq)]
94pub struct CrystalSymmetry {
95 pub space_group_name: Option<String>,
96 pub cell: Option<[f32; 6]>,
97 pub operators: Vec<SymmetryOperator>,
98}
99
100#[derive(Clone, Debug, Default, PartialEq)]
102pub struct SymmetryData {
103 pub assemblies: Vec<Assembly>,
104 pub crystal: CrystalSymmetry,
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn operator_applies_translation() {
113 let mut matrix = IDENTITY_MAT4;
114 matrix[0][3] = 10.0;
115 matrix[2][3] = -2.0;
116 let operator = SymmetryOperator::new("translated", matrix);
117 assert_eq!(operator.apply([1.0, 2.0, 3.0]), [11.0, 2.0, 1.0]);
118 }
119
120 #[test]
121 fn operator_composition_preserves_application_order() {
122 let mut translate = IDENTITY_MAT4;
123 translate[0][3] = 2.0;
124 let mut scale = IDENTITY_MAT4;
125 scale[0][0] = 3.0;
126 let combined = SymmetryOperator::new("scale", scale)
127 .compose(&SymmetryOperator::new("translate", translate), "combined");
128 assert_eq!(combined.apply([1.0, 0.0, 0.0]), [9.0, 0.0, 0.0]);
129 }
130}