1use serde::{Deserialize, Serialize};
7
8use super::sphere::sphere_volume_prefactor;
9use crate::{BoundingSphereRadius, IntersectsAt, IntersectsAtGlobal, SupportMapping, Volume};
10use hoomd_utility::valid::PositiveReal;
11use hoomd_vector::{Cartesian, InnerProduct, Rotate, Rotation};
12
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
72pub struct Capsule<const N: usize> {
73 pub radius: PositiveReal,
75 pub height: PositiveReal,
77}
78
79pub type Spherocylinder = Capsule<3>;
81
82impl<const N: usize> SupportMapping<Cartesian<N>> for Capsule<N> {
83 #[inline]
84 fn support_mapping(&self, n: &Cartesian<N>) -> Cartesian<N> {
85 let mut v_tip = [0.0; N];
87 v_tip[N - 1] = self.height.get() / 2.0;
88 let v_tip = v_tip.into();
89
90 let mut v_base = [0.0; N];
91 v_base[N - 1] = -self.height.get() / 2.0;
92 let v_base = v_base.into();
93
94 let (v_tip_dot_n, v_base_dot_n) = (n.dot(&v_tip), n.dot(&v_base));
95
96 let rshift = *n / n.norm() * self.radius.get();
97 if v_tip_dot_n > v_base_dot_n {
98 v_tip + rshift
99 } else {
100 v_base + rshift
101 }
102 }
103}
104
105impl<const N: usize> BoundingSphereRadius for Capsule<N> {
106 #[inline]
107 fn bounding_sphere_radius(&self) -> PositiveReal {
108 (self.height.get() / 2.0 + self.radius.get())
109 .try_into()
110 .expect("this expression should evaluate to a positive real")
111 }
112}
113
114impl<const N: usize> Volume for Capsule<N> {
115 #[inline]
116 fn volume(&self) -> f64 {
117 if N == 0 {
118 return 0.0;
119 }
120 let r_n_minus_one = self.radius.get().powi(
121 (N - 1)
122 .try_into()
123 .expect("dimension {N}-1 should fit in an i32"),
124 );
125 let cylinder_volume = sphere_volume_prefactor(N - 1) * r_n_minus_one * self.height.get();
126 cylinder_volume + sphere_volume_prefactor(N) * (r_n_minus_one * self.radius.get())
127 }
128}
129
130#[inline]
132fn axis_aligned_cartesian<const N: usize>(h: f64) -> Cartesian<N> {
133 Cartesian::from(std::array::from_fn(|i| if i == (N - 1) { h } else { 0.0 }))
134}
135
136impl<const N: usize, R> IntersectsAtGlobal<Capsule<N>, Cartesian<N>, R> for Capsule<N>
137where
138 R: Rotation + Rotate<Cartesian<N>>,
139{
140 #[inline]
141 fn intersects_at_global(
142 &self,
143 other: &Capsule<N>,
144 r_self: &Cartesian<N>,
145 o_self: &R,
146 r_other: &Cartesian<N>,
147 o_other: &R,
148 ) -> bool {
149 let (v_ij, o_ij) = hoomd_vector::pair_system_to_local(r_self, o_self, r_other, o_other);
150
151 self.intersects_at(other, &v_ij, &o_ij)
152 }
153}
154
155impl<const N: usize, R> IntersectsAt<Capsule<N>, Cartesian<N>, R> for Capsule<N>
156where
157 R: Rotate<Cartesian<N>>,
158{
159 #[inline]
160 fn intersects_at(&self, other: &Capsule<N>, v_ij: &Cartesian<N>, o_ij: &R) -> bool {
161 let d1 = axis_aligned_cartesian::<N>(self.height.get());
167 let p1 = d1 * -0.5;
168
169 let d2 = o_ij.rotate(&axis_aligned_cartesian(other.height.get()));
170 let p2 = *v_ij - d2 * 0.5;
171
172 let distance_between_centers = p1 - p2;
173
174 let d1_norm_sq = d1.dot(&d1);
175 let d2_norm_sq = d2.dot(&d2);
176 let f = d2.dot(&distance_between_centers);
177
178 let c = d1.dot(&distance_between_centers);
179
180 let d1_dot_d2 = d1.dot(&d2);
182 let denom = d1_norm_sq * d2_norm_sq - d1_dot_d2 * d1_dot_d2;
183
184 let s = if denom == 0.0 {
187 0.0
188 } else {
189 ((d1_dot_d2 * f - c * d2_norm_sq) / denom).clamp(0.0, 1.0)
190 };
191
192 let tnom = d1_dot_d2 * s + f;
195 let (t, s) = if tnom < 0.0 {
196 (0.0, (-c / d1_norm_sq).clamp(0.0, 1.0))
197 } else if tnom > d2_norm_sq {
198 (1.0, ((d1_dot_d2 - c) / d1_norm_sq).clamp(0.0, 1.0))
199 } else {
200 (tnom / d2_norm_sq, s)
201 };
202
203 let (c1, c2) = (p1 + d1 * s, p2 + d2 * t);
204 let dist_sq = (c1 - c2).norm_squared();
205
206 let total_radius = self.radius.get() + other.radius.get();
207 dist_sq <= total_radius.powi(2)
208 }
209}
210#[cfg(test)]
211mod tests {
212
213 use crate::{
214 Convex, IntersectsAt,
215 shape::{Circle, Cylinder, Hypersphere},
216 };
217 use hoomd_vector::{Angle, Versor};
218 use rand::{RngExt, SeedableRng};
219
220 use super::*;
221 use approxim::assert_relative_eq;
222 use rstest::*;
223 use std::f64::consts::PI;
224
225 #[rstest(
226 radius => [1e-6, 1.0, 34.56],
227 height => [1e-6, 1.0, 34.56],
228 )]
229 fn test_elongated_capsule_volume(radius: f64, height: f64) {
230 let capsule = Capsule::<3> {
231 radius: radius.try_into().expect("test value is a positive real"),
232 height: height.try_into().expect("test value is a positive real"),
233 };
234 assert_relative_eq!(
235 capsule.volume(),
236 Hypersphere::<3> {
237 radius: radius.try_into().expect("test value is a positive real")
238 }
239 .volume()
240 + Cylinder {
241 radius: radius.try_into().expect("test value is a positive real"),
242 height: capsule.height
243 }
244 .volume()
245 );
246
247 assert_relative_eq!(
248 capsule.bounding_sphere_radius().get(),
249 radius + height / 2.0
250 );
251 }
252
253 #[test]
254 fn intersect_xenocollide_2d() {
255 let capsule_tall = Convex(Capsule::<2> {
256 radius: 0.5.try_into().expect("test value is a positive real"),
257 height: 6.0.try_into().expect("test value is a positive real"),
258 });
259
260 let circle = Convex(Circle::with_radius(
261 0.5.try_into().expect("test value is a positive real"),
262 ));
263
264 let identity = Angle::default();
265 let rotate = Angle::from(PI / 2.0);
266
267 assert!(!capsule_tall.intersects_at(&circle, &[0.0, 4.1].into(), &identity));
268 assert!(capsule_tall.intersects_at(&circle, &[0.0, 3.9].into(), &identity));
269 assert!(!circle.intersects_at(&capsule_tall, &[0.0, 4.1].into(), &identity));
270 assert!(circle.intersects_at(&capsule_tall, &[0.0, 3.9].into(), &identity));
271 assert!(!circle.intersects_at(&capsule_tall, &[4.1, 0.0].into(), &rotate));
272 assert!(circle.intersects_at(&capsule_tall, &[3.9, 0.0].into(), &rotate));
273
274 assert!(capsule_tall.intersects_at(&capsule_tall, &[0.2, -0.4].into(), &rotate));
275 assert!(capsule_tall.intersects_at(&capsule_tall, &[3.9, 2.0].into(), &rotate));
276 assert!(!capsule_tall.intersects_at(&capsule_tall, &[4.1, -2.0].into(), &rotate));
277 }
278
279 #[test]
280 fn intersect_xenocollide_3d() {
281 let capsule_tall = Convex(Capsule::<3> {
282 radius: 0.5.try_into().expect("test value is a positive real"),
283 height: 6.0.try_into().expect("test value is a positive real"),
284 });
285
286 let sphere = Convex(Circle::with_radius(
287 0.5.try_into().expect("test value is a positive real"),
288 ));
289
290 let identity = Versor::default();
291 let rotate = Versor::from_axis_angle(
292 [0.0, 1.0, 0.0]
293 .try_into()
294 .expect("hard-coded vector is non-zero"),
295 PI / 2.0,
296 );
297
298 assert!(!capsule_tall.intersects_at(&sphere, &[0.0, 0.0, 4.1].into(), &identity));
299 assert!(capsule_tall.intersects_at(&sphere, &[0.0, 0.0, 3.9].into(), &identity));
300 assert!(!sphere.intersects_at(&capsule_tall, &[0.0, 0.0, 4.1].into(), &identity));
301 assert!(sphere.intersects_at(&capsule_tall, &[0.0, 0.0, 3.9].into(), &identity));
302 assert!(!sphere.intersects_at(&capsule_tall, &[4.1, 0.0, 0.0].into(), &rotate));
303 assert!(sphere.intersects_at(&capsule_tall, &[3.9, 0.0, 0.0].into(), &rotate));
304
305 assert!(capsule_tall.intersects_at(&capsule_tall, &[0.2, -0.4, 0.0].into(), &rotate));
306 assert!(capsule_tall.intersects_at(&capsule_tall, &[3.9, 0.0, 2.0].into(), &rotate));
307 assert!(!capsule_tall.intersects_at(&capsule_tall, &[4.1, 0.0, -2.0].into(), &rotate));
308 }
309
310 #[test]
311 fn support_mapping_2d() {
312 let capsule = Convex(Capsule::<3> {
313 radius: 0.5.try_into().expect("test value is a positive real"),
314 height: 6.0.try_into().expect("test value is a positive real"),
315 });
316
317 assert_relative_eq!(
319 capsule.support_mapping(&[0.0, 0.0, 1.0].into()),
320 [0.0, 0.0, 3.5].into()
321 );
322 assert_relative_eq!(
323 capsule.support_mapping(&[0.0, 0.0, -1.0].into()),
324 [0.0, 0.0, -3.5].into()
325 );
326
327 assert_relative_eq!(
329 capsule.support_mapping(&[1.0, 0.0, 1e-12].into()),
330 [0.5, 0.0, 3.0].into(),
331 epsilon = 1e-6
332 );
333 assert_relative_eq!(
334 capsule.support_mapping(&[-1.0, 0.0, 1e-12].into()),
335 [-0.5, 0.0, 3.0].into(),
336 epsilon = 1e-6
337 );
338 assert_relative_eq!(
339 capsule.support_mapping(&[0.0, 1.0, 1e-12].into()),
340 [0.0, 0.5, 3.0].into(),
341 epsilon = 1e-6
342 );
343 assert_relative_eq!(
344 capsule.support_mapping(&[0.0, -1.0, 1e-12].into()),
345 [0.0, -0.5, 3.0].into(),
346 epsilon = 1e-6
347 );
348
349 assert_relative_eq!(
351 capsule.support_mapping(&[1.0, 0.0, -1e-12].into()),
352 [0.5, 0.0, -3.0].into(),
353 epsilon = 1e-6
354 );
355 assert_relative_eq!(
356 capsule.support_mapping(&[-1.0, 0.0, -1e-12].into()),
357 [-0.5, 0.0, -3.0].into(),
358 epsilon = 1e-6
359 );
360 assert_relative_eq!(
361 capsule.support_mapping(&[0.0, 1.0, -1e-12].into()),
362 [0.0, 0.5, -3.0].into(),
363 epsilon = 1e-6
364 );
365 assert_relative_eq!(
366 capsule.support_mapping(&[0.0, -1.0, -1e-12].into()),
367 [0.0, -0.5, -3.0].into(),
368 epsilon = 1e-6
369 );
370
371 }
373
374 #[rstest]
375 #[case(true, 1.999_999, 0.0, 0.0)]
376 #[case(true, 2.0, 0.0, 0.0)]
377 #[case(false, 2.000_001, 0.0, 0.0)]
378 fn test_intersect_capsule_capsule_2d(
379 #[case] expected: bool,
380 #[case] x: f64,
381 #[case] y: f64,
382 #[case] angle: f64,
383 ) {
384 let capsule1 = Capsule::<2> {
385 radius: 1.0.try_into().unwrap(),
386 height: 2.0.try_into().unwrap(),
387 };
388 let capsule2 = Capsule::<2> {
389 radius: 1.0.try_into().unwrap(),
390 height: 2.0.try_into().unwrap(),
391 };
392
393 let v_ij = [x, y].into();
394 let o_ij = Angle::from(angle);
395 assert_eq!(capsule1.intersects_at(&capsule2, &v_ij, &o_ij), expected);
396 assert_eq!(
397 capsule2.intersects_at(&capsule1, &(-v_ij), &o_ij.inverted()),
398 expected
399 );
400 }
401
402 #[rstest]
403 #[case(true, 0.0, 2.0, 90.0)]
404 #[case(true, 0.0, 3.0, 90.0)]
405 #[case(false, 0.0, 3.000_001, 90.0)]
406 fn test_intersect_capsule_capsule_2d_rotated(
407 #[case] expected: bool,
408 #[case] x: f64,
409 #[case] y: f64,
410 #[case] angle: f64,
411 ) {
412 let capsule1 = Capsule::<2> {
413 radius: 1.0.try_into().unwrap(),
414 height: 2.0.try_into().unwrap(),
415 };
416 let capsule2 = Capsule::<2> {
417 radius: 1.0.try_into().unwrap(),
418 height: 2.0.try_into().unwrap(),
419 };
420
421 let v_ij = [x, y].into();
422 let o_ij = Angle::from(angle.to_radians());
423 assert_eq!(capsule1.intersects_at(&capsule2, &v_ij, &o_ij), expected);
424 assert_eq!(
425 capsule2.intersects_at(&capsule1, &(-v_ij), &o_ij.inverted()),
426 expected
427 );
428 }
429
430 #[test]
432 fn test_intersect_degenerate_capsules() {
433 let sphere1 = Capsule::<3> {
434 radius: 1.0.try_into().unwrap(),
435 height: 1e-12.try_into().unwrap(),
436 };
437 let sphere2 = Capsule::<3> {
438 radius: 1.0.try_into().unwrap(),
439 height: 1e-12.try_into().unwrap(),
440 };
441
442 let o_ij = Versor::identity();
443 let v_ij = [1.999_999, 0.0, 0.0].into();
445 assert!(sphere1.intersects_at(&sphere2, &v_ij, &o_ij));
446 let v_ij = [2.0, 0.0, 0.0].into();
448 assert!(sphere1.intersects_at(&sphere2, &v_ij, &o_ij));
449 let v_ij = [2.000_001, 0.0, 0.0].into();
451 assert!(!sphere1.intersects_at(&sphere2, &v_ij, &o_ij));
452
453 let capsule = Capsule::<3> {
454 radius: 1.0.try_into().unwrap(),
455 height: 2.0.try_into().unwrap(),
456 };
457 let v_ij = [1.999_999, 0.0, 0.0].into();
459 assert!(sphere1.intersects_at(&capsule, &v_ij, &o_ij));
460 assert!(capsule.intersects_at(&sphere1, &v_ij, &o_ij));
461 let v_ij = [2.0, 0.0, 0.0].into();
463 assert!(sphere1.intersects_at(&capsule, &v_ij, &o_ij));
464 assert!(capsule.intersects_at(&sphere1, &v_ij, &o_ij));
465 let v_ij = [2.000_001, 0.0, 0.0].into();
467 assert!(!sphere1.intersects_at(&capsule, &v_ij, &o_ij));
468 assert!(!capsule.intersects_at(&sphere1, &v_ij, &o_ij));
469 }
470
471 #[test]
472 fn test_intersect_capsule_capsule_complex_3d_random() -> Result<(), Box<dyn std::error::Error>>
473 {
474 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
475
476 for _ in 0..10_000 {
477 let r1 = rng.random_range(0.1..10.0);
478 let h1 = rng.random_range(0.1..10.0);
479 let r2 = rng.random_range(0.1..10.0);
480 let h2 = rng.random_range(0.1..10.0);
481
482 let capsule1 = Capsule::<3> {
483 radius: r1.try_into()?,
484 height: h1.try_into()?,
485 };
486 let capsule2 = Capsule::<3> {
487 radius: r2.try_into()?,
488 height: h2.try_into()?,
489 };
490
491 let v_ij = (rng.random::<Cartesian<3>>() * 10.0) - Cartesian::from([5.0; 3]);
492
493 let o_ij = rng.random::<Versor>();
494
495 let result_direct = capsule1.intersects_at(&capsule2, &v_ij, &o_ij);
496
497 let result_xeno = Convex(capsule1).intersects_at(&Convex(capsule2), &v_ij, &o_ij);
498 assert_eq!(
499 result_direct, result_xeno,
500 "Failed with r1={r1}, h1={h1}, r2={r2}, h2={h2}, v_ij={v_ij:?}, o_ij={o_ij:?}"
501 );
502 }
503 Ok(())
504 }
505}