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>> + Rotation,
158 Cartesian<N>: From<[f64; N]>,
159{
160 #[inline]
161 fn intersects_at(&self, other: &Capsule<N>, v_ij: &Cartesian<N>, o_ij: &R) -> bool {
162 let d1 = axis_aligned_cartesian::<N>(self.height.get());
168 let p1 = d1 * -0.5;
169
170 let d2 = o_ij.rotate(&axis_aligned_cartesian(other.height.get()));
171 let p2 = *v_ij - d2 * 0.5;
172
173 let distance_between_centers = p1 - p2;
174
175 let d1_norm_sq = d1.dot(&d1);
176 let d2_norm_sq = d2.dot(&d2);
177 let f = d2.dot(&distance_between_centers);
178
179 let c = d1.dot(&distance_between_centers);
180
181 let d1_dot_d2 = d1.dot(&d2);
183 let denom = d1_norm_sq * d2_norm_sq - d1_dot_d2 * d1_dot_d2;
184
185 let s = if denom == 0.0 {
188 0.0
189 } else {
190 ((d1_dot_d2 * f - c * d2_norm_sq) / denom).clamp(0.0, 1.0)
191 };
192
193 let tnom = d1_dot_d2 * s + f;
196 let (t, s) = if tnom < 0.0 {
197 (0.0, (-c / d1_norm_sq).clamp(0.0, 1.0))
198 } else if tnom > d2_norm_sq {
199 (1.0, ((d1_dot_d2 - c) / d1_norm_sq).clamp(0.0, 1.0))
200 } else {
201 (tnom / d2_norm_sq, s)
202 };
203
204 let (c1, c2) = (p1 + d1 * s, p2 + d2 * t);
205 let dist_sq = (c1 - c2).norm_squared();
206
207 let total_radius = self.radius.get() + other.radius.get();
208 dist_sq <= total_radius.powi(2)
209 }
210}
211#[cfg(test)]
212mod tests {
213
214 use crate::{
215 Convex, IntersectsAt,
216 shape::{Circle, Cylinder, Hypersphere},
217 };
218 use hoomd_vector::{Angle, Versor};
219 use rand::{RngExt, SeedableRng};
220
221 use super::*;
222 use approxim::assert_relative_eq;
223 use rstest::*;
224 use std::f64::consts::PI;
225
226 #[rstest(
227 radius => [1e-6, 1.0, 34.56],
228 height => [1e-6, 1.0, 34.56],
229 )]
230 fn test_elongated_capsule_volume(radius: f64, height: f64) {
231 let capsule = Capsule::<3> {
232 radius: radius.try_into().expect("test value is a positive real"),
233 height: height.try_into().expect("test value is a positive real"),
234 };
235 assert_relative_eq!(
236 capsule.volume(),
237 Hypersphere::<3> {
238 radius: radius.try_into().expect("test value is a positive real")
239 }
240 .volume()
241 + Cylinder {
242 radius: radius.try_into().expect("test value is a positive real"),
243 height: capsule.height
244 }
245 .volume()
246 );
247
248 assert_relative_eq!(
249 capsule.bounding_sphere_radius().get(),
250 radius + height / 2.0
251 );
252 }
253
254 #[test]
255 fn intersect_xenocollide_2d() {
256 let capsule_tall = Convex(Capsule::<2> {
257 radius: 0.5.try_into().expect("test value is a positive real"),
258 height: 6.0.try_into().expect("test value is a positive real"),
259 });
260
261 let circle = Convex(Circle::with_radius(
262 0.5.try_into().expect("test value is a positive real"),
263 ));
264
265 let identity = Angle::default();
266 let rotate = Angle::from(PI / 2.0);
267
268 assert!(!capsule_tall.intersects_at(&circle, &[0.0, 4.1].into(), &identity));
269 assert!(capsule_tall.intersects_at(&circle, &[0.0, 3.9].into(), &identity));
270 assert!(!circle.intersects_at(&capsule_tall, &[0.0, 4.1].into(), &identity));
271 assert!(circle.intersects_at(&capsule_tall, &[0.0, 3.9].into(), &identity));
272 assert!(!circle.intersects_at(&capsule_tall, &[4.1, 0.0].into(), &rotate));
273 assert!(circle.intersects_at(&capsule_tall, &[3.9, 0.0].into(), &rotate));
274
275 assert!(capsule_tall.intersects_at(&capsule_tall, &[0.2, -0.4].into(), &rotate));
276 assert!(capsule_tall.intersects_at(&capsule_tall, &[3.9, 2.0].into(), &rotate));
277 assert!(!capsule_tall.intersects_at(&capsule_tall, &[4.1, -2.0].into(), &rotate));
278 }
279
280 #[test]
281 fn intersect_xenocollide_3d() {
282 let capsule_tall = Convex(Capsule::<3> {
283 radius: 0.5.try_into().expect("test value is a positive real"),
284 height: 6.0.try_into().expect("test value is a positive real"),
285 });
286
287 let sphere = Convex(Circle::with_radius(
288 0.5.try_into().expect("test value is a positive real"),
289 ));
290
291 let identity = Versor::default();
292 let rotate = Versor::from_axis_angle(
293 [0.0, 1.0, 0.0]
294 .try_into()
295 .expect("hard-coded vector is non-zero"),
296 PI / 2.0,
297 );
298
299 assert!(!capsule_tall.intersects_at(&sphere, &[0.0, 0.0, 4.1].into(), &identity));
300 assert!(capsule_tall.intersects_at(&sphere, &[0.0, 0.0, 3.9].into(), &identity));
301 assert!(!sphere.intersects_at(&capsule_tall, &[0.0, 0.0, 4.1].into(), &identity));
302 assert!(sphere.intersects_at(&capsule_tall, &[0.0, 0.0, 3.9].into(), &identity));
303 assert!(!sphere.intersects_at(&capsule_tall, &[4.1, 0.0, 0.0].into(), &rotate));
304 assert!(sphere.intersects_at(&capsule_tall, &[3.9, 0.0, 0.0].into(), &rotate));
305
306 assert!(capsule_tall.intersects_at(&capsule_tall, &[0.2, -0.4, 0.0].into(), &rotate));
307 assert!(capsule_tall.intersects_at(&capsule_tall, &[3.9, 0.0, 2.0].into(), &rotate));
308 assert!(!capsule_tall.intersects_at(&capsule_tall, &[4.1, 0.0, -2.0].into(), &rotate));
309 }
310
311 #[test]
312 fn support_mapping_2d() {
313 let capsule = Convex(Capsule::<3> {
314 radius: 0.5.try_into().expect("test value is a positive real"),
315 height: 6.0.try_into().expect("test value is a positive real"),
316 });
317
318 assert_relative_eq!(
320 capsule.support_mapping(&[0.0, 0.0, 1.0].into()),
321 [0.0, 0.0, 3.5].into()
322 );
323 assert_relative_eq!(
324 capsule.support_mapping(&[0.0, 0.0, -1.0].into()),
325 [0.0, 0.0, -3.5].into()
326 );
327
328 assert_relative_eq!(
330 capsule.support_mapping(&[1.0, 0.0, 1e-12].into()),
331 [0.5, 0.0, 3.0].into(),
332 epsilon = 1e-6
333 );
334 assert_relative_eq!(
335 capsule.support_mapping(&[-1.0, 0.0, 1e-12].into()),
336 [-0.5, 0.0, 3.0].into(),
337 epsilon = 1e-6
338 );
339 assert_relative_eq!(
340 capsule.support_mapping(&[0.0, 1.0, 1e-12].into()),
341 [0.0, 0.5, 3.0].into(),
342 epsilon = 1e-6
343 );
344 assert_relative_eq!(
345 capsule.support_mapping(&[0.0, -1.0, 1e-12].into()),
346 [0.0, -0.5, 3.0].into(),
347 epsilon = 1e-6
348 );
349
350 assert_relative_eq!(
352 capsule.support_mapping(&[1.0, 0.0, -1e-12].into()),
353 [0.5, 0.0, -3.0].into(),
354 epsilon = 1e-6
355 );
356 assert_relative_eq!(
357 capsule.support_mapping(&[-1.0, 0.0, -1e-12].into()),
358 [-0.5, 0.0, -3.0].into(),
359 epsilon = 1e-6
360 );
361 assert_relative_eq!(
362 capsule.support_mapping(&[0.0, 1.0, -1e-12].into()),
363 [0.0, 0.5, -3.0].into(),
364 epsilon = 1e-6
365 );
366 assert_relative_eq!(
367 capsule.support_mapping(&[0.0, -1.0, -1e-12].into()),
368 [0.0, -0.5, -3.0].into(),
369 epsilon = 1e-6
370 );
371
372 }
374
375 #[rstest]
376 #[case(true, 1.999_999, 0.0, 0.0)]
377 #[case(true, 2.0, 0.0, 0.0)]
378 #[case(false, 2.000_001, 0.0, 0.0)]
379 fn test_intersect_capsule_capsule_2d(
380 #[case] expected: bool,
381 #[case] x: f64,
382 #[case] y: f64,
383 #[case] angle: f64,
384 ) {
385 let capsule1 = Capsule::<2> {
386 radius: 1.0.try_into().unwrap(),
387 height: 2.0.try_into().unwrap(),
388 };
389 let capsule2 = Capsule::<2> {
390 radius: 1.0.try_into().unwrap(),
391 height: 2.0.try_into().unwrap(),
392 };
393
394 let v_ij = [x, y].into();
395 let o_ij = Angle::from(angle);
396 assert_eq!(capsule1.intersects_at(&capsule2, &v_ij, &o_ij), expected);
397 assert_eq!(
398 capsule2.intersects_at(&capsule1, &(-v_ij), &o_ij.inverted()),
399 expected
400 );
401 }
402
403 #[rstest]
404 #[case(true, 0.0, 2.0, 90.0)]
405 #[case(true, 0.0, 3.0, 90.0)]
406 #[case(false, 0.0, 3.000_001, 90.0)]
407 fn test_intersect_capsule_capsule_2d_rotated(
408 #[case] expected: bool,
409 #[case] x: f64,
410 #[case] y: f64,
411 #[case] angle: f64,
412 ) {
413 let capsule1 = Capsule::<2> {
414 radius: 1.0.try_into().unwrap(),
415 height: 2.0.try_into().unwrap(),
416 };
417 let capsule2 = Capsule::<2> {
418 radius: 1.0.try_into().unwrap(),
419 height: 2.0.try_into().unwrap(),
420 };
421
422 let v_ij = [x, y].into();
423 let o_ij = Angle::from(angle.to_radians());
424 assert_eq!(capsule1.intersects_at(&capsule2, &v_ij, &o_ij), expected);
425 assert_eq!(
426 capsule2.intersects_at(&capsule1, &(-v_ij), &o_ij.inverted()),
427 expected
428 );
429 }
430
431 #[test]
433 fn test_intersect_degenerate_capsules() {
434 let sphere1 = Capsule::<3> {
435 radius: 1.0.try_into().unwrap(),
436 height: 1e-12.try_into().unwrap(),
437 };
438 let sphere2 = Capsule::<3> {
439 radius: 1.0.try_into().unwrap(),
440 height: 1e-12.try_into().unwrap(),
441 };
442
443 let o_ij = Versor::identity();
444 let v_ij = [1.999_999, 0.0, 0.0].into();
446 assert!(sphere1.intersects_at(&sphere2, &v_ij, &o_ij));
447 let v_ij = [2.0, 0.0, 0.0].into();
449 assert!(sphere1.intersects_at(&sphere2, &v_ij, &o_ij));
450 let v_ij = [2.000_001, 0.0, 0.0].into();
452 assert!(!sphere1.intersects_at(&sphere2, &v_ij, &o_ij));
453
454 let capsule = Capsule::<3> {
455 radius: 1.0.try_into().unwrap(),
456 height: 2.0.try_into().unwrap(),
457 };
458 let v_ij = [1.999_999, 0.0, 0.0].into();
460 assert!(sphere1.intersects_at(&capsule, &v_ij, &o_ij));
461 assert!(capsule.intersects_at(&sphere1, &v_ij, &o_ij));
462 let v_ij = [2.0, 0.0, 0.0].into();
464 assert!(sphere1.intersects_at(&capsule, &v_ij, &o_ij));
465 assert!(capsule.intersects_at(&sphere1, &v_ij, &o_ij));
466 let v_ij = [2.000_001, 0.0, 0.0].into();
468 assert!(!sphere1.intersects_at(&capsule, &v_ij, &o_ij));
469 assert!(!capsule.intersects_at(&sphere1, &v_ij, &o_ij));
470 }
471
472 #[test]
473 fn test_intersect_capsule_capsule_complex_3d_random() -> Result<(), Box<dyn std::error::Error>>
474 {
475 let mut rng = rand::rngs::StdRng::seed_from_u64(0);
476
477 for _ in 0..10_000 {
478 let r1 = rng.random_range(0.1..10.0);
479 let h1 = rng.random_range(0.1..10.0);
480 let r2 = rng.random_range(0.1..10.0);
481 let h2 = rng.random_range(0.1..10.0);
482
483 let capsule1 = Capsule::<3> {
484 radius: r1.try_into()?,
485 height: h1.try_into()?,
486 };
487 let capsule2 = Capsule::<3> {
488 radius: r2.try_into()?,
489 height: h2.try_into()?,
490 };
491
492 let v_ij = (rng.random::<Cartesian<3>>() * 10.0) - Cartesian::from([5.0; 3]);
493
494 let o_ij = rng.random::<Versor>();
495
496 let result_direct = capsule1.intersects_at(&capsule2, &v_ij, &o_ij);
497
498 let result_xeno = Convex(capsule1).intersects_at(&Convex(capsule2), &v_ij, &o_ij);
499 assert_eq!(
500 result_direct, result_xeno,
501 "Failed with r1={r1}, h1={h1}, r2={r2}, h2={h2}, v_ij={v_ij:?}, o_ij={o_ij:?}"
502 );
503 }
504 Ok(())
505 }
506}