molrs_compute/environment/
angular_separation.rs1use molrs::spatial::neighbors::NeighborList;
29use molrs::store::frame_access::FrameAccess;
30use molrs::types::F;
31use ndarray::Array2;
32
33use crate::error::ComputeError;
34use crate::result::ComputeResult;
35use crate::traits::Compute;
36
37pub type Quat = [F; 4];
39
40#[inline]
41fn quat_norm(q: Quat) -> F {
42 (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt()
43}
44
45#[inline]
46fn quat_dot(a: Quat, b: Quat) -> F {
47 a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]
48}
49
50pub fn angular_distance(q1: Quat, q2: Quat, equivalent_orientations: bool) -> F {
56 let n1 = quat_norm(q1);
57 let n2 = quat_norm(q2);
58 if n1 == 0.0 || n2 == 0.0 {
59 return 0.0;
60 }
61 let mut d = quat_dot(q1, q2) / (n1 * n2);
62 if equivalent_orientations {
63 d = d.abs();
64 }
65 2.0 * d.clamp(-1.0, 1.0).acos()
66}
67
68#[derive(Debug, Clone, Default)]
73pub struct AngularSeparationGlobalResult {
74 pub angles: Array2<F>,
76}
77
78impl ComputeResult for AngularSeparationGlobalResult {}
79
80#[derive(Debug, Clone)]
81pub struct AngularSeparationGlobal {
82 equivalent_orientations: bool,
83}
84
85impl Default for AngularSeparationGlobal {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl AngularSeparationGlobal {
92 pub fn new() -> Self {
93 Self {
94 equivalent_orientations: true,
95 }
96 }
97
98 pub fn with_equivalent_orientations(mut self, on: bool) -> Self {
99 self.equivalent_orientations = on;
100 self
101 }
102}
103
104pub struct AngularSeparationGlobalArgs<'a> {
109 pub query: &'a [Quat],
110 pub global: &'a [Quat],
111}
112
113impl Compute for AngularSeparationGlobal {
114 type Args<'a> = AngularSeparationGlobalArgs<'a>;
115 type Output = Vec<AngularSeparationGlobalResult>;
116
117 fn compute<'a, FA: FrameAccess + Sync + 'a>(
118 &self,
119 frames: &[&'a FA],
120 args: AngularSeparationGlobalArgs<'a>,
121 ) -> Result<Vec<AngularSeparationGlobalResult>, ComputeError> {
122 if frames.is_empty() {
123 return Err(ComputeError::EmptyInput);
124 }
125 if args.query.is_empty() || args.global.is_empty() {
126 return Err(ComputeError::EmptyInput);
127 }
128 let n_query = args.query.len();
129 let n_global = args.global.len();
130 let mut out = Vec::with_capacity(frames.len());
131 for _ in frames {
132 let mut a = Array2::<F>::zeros((n_query, n_global));
133 for i in 0..n_query {
134 for j in 0..n_global {
135 a[[i, j]] = angular_distance(
136 args.query[i],
137 args.global[j],
138 self.equivalent_orientations,
139 );
140 }
141 }
142 out.push(AngularSeparationGlobalResult { angles: a });
143 }
144 Ok(out)
145 }
146}
147
148#[derive(Debug, Clone, Default)]
153pub struct AngularSeparationNeighborResult {
154 pub angles: Vec<F>,
157}
158
159impl ComputeResult for AngularSeparationNeighborResult {}
160
161#[derive(Debug, Clone)]
162pub struct AngularSeparationNeighbor {
163 equivalent_orientations: bool,
164}
165
166impl Default for AngularSeparationNeighbor {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172impl AngularSeparationNeighbor {
173 pub fn new() -> Self {
174 Self {
175 equivalent_orientations: true,
176 }
177 }
178
179 pub fn with_equivalent_orientations(mut self, on: bool) -> Self {
180 self.equivalent_orientations = on;
181 self
182 }
183}
184
185pub struct AngularSeparationNeighborArgs<'a> {
186 pub nlists: &'a [NeighborList],
187 pub query_orientations: &'a [Vec<Quat>],
189 pub point_orientations: &'a [Vec<Quat>],
191}
192
193impl Compute for AngularSeparationNeighbor {
194 type Args<'a> = AngularSeparationNeighborArgs<'a>;
195 type Output = Vec<AngularSeparationNeighborResult>;
196
197 fn compute<'a, FA: FrameAccess + Sync + 'a>(
198 &self,
199 frames: &[&'a FA],
200 args: AngularSeparationNeighborArgs<'a>,
201 ) -> Result<Vec<AngularSeparationNeighborResult>, ComputeError> {
202 if frames.is_empty() {
203 return Err(ComputeError::EmptyInput);
204 }
205 let nf = frames.len();
206 if args.nlists.len() != nf
207 || args.query_orientations.len() != nf
208 || args.point_orientations.len() != nf
209 {
210 return Err(ComputeError::DimensionMismatch {
211 expected: nf,
212 got: args
213 .nlists
214 .len()
215 .min(args.query_orientations.len())
216 .min(args.point_orientations.len()),
217 what: "AngularSeparationNeighbor frame-aligned inputs",
218 });
219 }
220 let mut out = Vec::with_capacity(nf);
221 for k in 0..nf {
222 let nl = &args.nlists[k];
223 let q = &args.query_orientations[k];
224 let p = &args.point_orientations[k];
225 let i_idx = nl.query_point_indices();
226 let j_idx = nl.point_indices();
227 let mut angles = Vec::with_capacity(nl.n_pairs());
228 for pk in 0..nl.n_pairs() {
229 let i = i_idx[pk] as usize;
230 let j = j_idx[pk] as usize;
231 if i >= q.len() || j >= p.len() {
232 return Err(ComputeError::DimensionMismatch {
233 expected: i.max(j) + 1,
234 got: q.len().min(p.len()),
235 what: "AngularSeparationNeighbor orientations length",
236 });
237 }
238 angles.push(angular_distance(q[i], p[j], self.equivalent_orientations));
239 }
240 out.push(AngularSeparationNeighborResult { angles });
241 }
242 Ok(out)
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use molrs::Frame;
250
251 const TOL: F = 1e-10;
252
253 fn frame() -> Frame {
254 Frame::new()
255 }
256
257 #[test]
258 fn identity_quaternion_zero_distance() {
259 let q = [1.0_f64, 0.0, 0.0, 0.0];
260 assert!(angular_distance(q, q, true).abs() < TOL);
261 assert!(angular_distance(q, q, false).abs() < TOL);
262 }
263
264 #[test]
265 fn antipodal_quaternions_zero_with_equivalence() {
266 let q = [1.0_f64, 0.0, 0.0, 0.0];
267 let neg_q = [-1.0_f64, 0.0, 0.0, 0.0];
268 assert!(angular_distance(q, neg_q, true).abs() < TOL);
271 let theta = angular_distance(q, neg_q, false);
273 assert!((theta - 2.0 * std::f64::consts::PI).abs() < TOL);
274 }
275
276 #[test]
277 fn ninety_degree_rotation_about_z() {
278 let q = [
280 std::f64::consts::FRAC_PI_4.cos(),
281 0.0,
282 0.0,
283 std::f64::consts::FRAC_PI_4.sin(),
284 ];
285 let identity = [1.0_f64, 0.0, 0.0, 0.0];
286 let theta = angular_distance(q, identity, true);
287 assert!((theta - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
289 }
290
291 #[test]
292 fn global_shape_and_values() {
293 let q = vec![[1.0_f64, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]];
294 let g = vec![[1.0_f64, 0.0, 0.0, 0.0]];
295 let r = &AngularSeparationGlobal::new()
296 .compute(
297 &[&frame()],
298 AngularSeparationGlobalArgs {
299 query: &q,
300 global: &g,
301 },
302 )
303 .unwrap()[0];
304 assert_eq!(r.angles.dim(), (2, 1));
305 assert!(r.angles[[0, 0]].abs() < TOL);
306 assert!((r.angles[[1, 0]] - std::f64::consts::PI).abs() < 1e-12);
309 }
310
311 #[test]
312 fn empty_inputs_error() {
313 let err = AngularSeparationGlobal::new()
314 .compute(
315 &[&frame()],
316 AngularSeparationGlobalArgs {
317 query: &[],
318 global: &[[1.0, 0.0, 0.0, 0.0]],
319 },
320 )
321 .unwrap_err();
322 assert!(matches!(err, ComputeError::EmptyInput));
323 }
324}