1#[derive(Debug, Clone, Copy, PartialEq)]
49pub struct SocialForceParams {
50 pub tau: f64,
53
54 pub a_social: f64,
56
57 pub b_social: f64,
59
60 pub k_body: f64,
62
63 pub kappa_friction: f64,
65
66 pub a_wall: f64,
68
69 pub b_wall: f64,
71
72 pub k_wall: f64,
74
75 pub kappa_wall: f64,
77
78 pub max_speed: f64,
80
81 pub mass: f64,
88}
89
90impl Default for SocialForceParams {
91 fn default() -> Self {
97 Self {
98 tau: 0.5,
99 a_social: 2000.0,
100 b_social: 0.08,
101 k_body: 1.2e5,
102 kappa_friction: 2.4e5,
103 a_wall: 2000.0,
104 b_wall: 0.08,
105 k_wall: 1.2e5,
106 kappa_wall: 2.4e5,
107 max_speed: 2.5,
108 mass: 80.0,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq)]
118pub struct WallSegment {
119 pub x1: f64,
121 pub y1: f64,
123 pub x2: f64,
125 pub y2: f64,
127}
128
129impl WallSegment {
130 pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
132 Self { x1, y1, x2, y2 }
133 }
134
135 pub fn closest_point(&self, px: f64, py: f64) -> (f64, f64) {
139 let dx = self.x2 - self.x1;
140 let dy = self.y2 - self.y1;
141 let len_sq = dx * dx + dy * dy;
142 if len_sq < 1e-12 {
143 return (self.x1, self.y1);
144 }
145 let t = ((px - self.x1) * dx + (py - self.y1) * dy) / len_sq;
146 let t = t.clamp(0.0, 1.0);
147 (self.x1 + t * dx, self.y1 + t * dy)
148 }
149}
150
151#[allow(clippy::too_many_arguments)]
166pub fn desired_force_2d(
167 x: f64,
168 y: f64,
169 vx: f64,
170 vy: f64,
171 dest_x: f64,
172 dest_y: f64,
173 desired_speed: f64,
174 tau: f64,
175) -> (f64, f64) {
176 let dx = dest_x - x;
177 let dy = dest_y - y;
178 let dist = (dx * dx + dy * dy).sqrt();
179 if dist < 1e-12 {
180 return (0.0, 0.0);
181 }
182 let ex = dx / dist;
183 let ey = dy / dist;
184 let fx = (desired_speed * ex - vx) / tau;
185 let fy = (desired_speed * ey - vy) / tau;
186 (fx, fy)
187}
188
189#[allow(clippy::too_many_arguments)]
217pub fn social_repulsion_2d(
218 x: f64,
219 y: f64,
220 vx: f64,
221 vy: f64,
222 radius: f64,
223 nx: f64,
224 ny: f64,
225 nvx: f64,
226 nvy: f64,
227 n_radius: f64,
228 params: &SocialForceParams,
229) -> (f64, f64) {
230 let dx = x - nx;
231 let dy = y - ny;
232 let dist = (dx * dx + dy * dy).sqrt();
233 if dist < 1e-12 {
234 return (0.0, 0.0);
235 }
236
237 let r_ij = radius + n_radius;
238 let overlap = r_ij - dist;
240
241 let ux = dx / dist;
243 let uy = dy / dist;
244
245 let tx = -uy;
247 let ty = ux;
248
249 let inv_m = 1.0 / params.mass;
252
253 let f_social = params.a_social * (overlap / params.b_social).exp() * inv_m;
255 let mut fx = f_social * ux;
256 let mut fy = f_social * uy;
257
258 if overlap > 0.0 {
260 fx += params.k_body * inv_m * overlap * ux;
262 fy += params.k_body * inv_m * overlap * uy;
263
264 let dvx = nvx - vx;
266 let dvy = nvy - vy;
267 let dv_tangential = dvx * tx + dvy * ty;
268 fx += params.kappa_friction * inv_m * overlap * dv_tangential * tx;
269 fy += params.kappa_friction * inv_m * overlap * dv_tangential * ty;
270 }
271
272 (fx, fy)
273}
274
275pub fn wall_repulsion_2d(
291 x: f64,
292 y: f64,
293 vx: f64,
294 vy: f64,
295 radius: f64,
296 wall: &WallSegment,
297 params: &SocialForceParams,
298) -> (f64, f64) {
299 let (cx, cy) = wall.closest_point(x, y);
300 let dx = x - cx;
301 let dy = y - cy;
302 let dist = (dx * dx + dy * dy).sqrt();
303 if dist < 1e-12 {
304 return (0.0, 0.0);
305 }
306
307 let overlap = radius - dist;
309
310 let ux = dx / dist;
312 let uy = dy / dist;
313
314 let tx = -uy;
316 let ty = ux;
317
318 let inv_m = 1.0 / params.mass;
319
320 let f_rep = params.a_wall * (overlap / params.b_wall).exp() * inv_m;
322 let mut fx = f_rep * ux;
323 let mut fy = f_rep * uy;
324
325 if overlap > 0.0 {
327 fx += params.k_wall * inv_m * overlap * ux;
329 fy += params.k_wall * inv_m * overlap * uy;
330
331 let v_tangential = vx * tx + vy * ty;
333 fx -= params.kappa_wall * inv_m * overlap * v_tangential * tx;
334 fy -= params.kappa_wall * inv_m * overlap * v_tangential * ty;
335 }
336
337 (fx, fy)
338}
339
340#[allow(clippy::too_many_arguments)]
350pub fn integrate_euler_2d(
351 x: f64,
352 y: f64,
353 vx: f64,
354 vy: f64,
355 fx: f64,
356 fy: f64,
357 dt: f64,
358 params: &SocialForceParams,
359) -> (f64, f64, f64, f64) {
360 let mut new_vx = vx + fx * dt;
361 let mut new_vy = vy + fy * dt;
362
363 let speed = (new_vx * new_vx + new_vy * new_vy).sqrt();
365 if speed > params.max_speed {
366 let scale = params.max_speed / speed;
367 new_vx *= scale;
368 new_vy *= scale;
369 }
370
371 let new_x = x + new_vx * dt;
372 let new_y = y + new_vy * dt;
373 (new_x, new_y, new_vx, new_vy)
374}
375
376#[allow(clippy::too_many_arguments)]
386pub fn desired_force_3d(
387 x: f64,
388 y: f64,
389 z: f64,
390 vx: f64,
391 vy: f64,
392 vz: f64,
393 dest_x: f64,
394 dest_y: f64,
395 dest_z: f64,
396 desired_speed: f64,
397 tau: f64,
398) -> (f64, f64, f64) {
399 let dx = dest_x - x;
400 let dy = dest_y - y;
401 let dz = dest_z - z;
402 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
403 if dist < 1e-12 {
404 return (0.0, 0.0, 0.0);
405 }
406 let ex = dx / dist;
407 let ey = dy / dist;
408 let ez = dz / dist;
409 let fx = (desired_speed * ex - vx) / tau;
410 let fy = (desired_speed * ey - vy) / tau;
411 let fz = (desired_speed * ez - vz) / tau;
412 (fx, fy, fz)
413}
414
415#[allow(clippy::too_many_arguments)]
424pub fn social_repulsion_3d(
425 x: f64,
426 y: f64,
427 z: f64,
428 vx: f64,
429 vy: f64,
430 vz: f64,
431 radius: f64,
432 nx: f64,
433 ny: f64,
434 nz: f64,
435 nvx: f64,
436 nvy: f64,
437 nvz: f64,
438 n_radius: f64,
439 params: &SocialForceParams,
440) -> (f64, f64, f64) {
441 let dx = x - nx;
442 let dy = y - ny;
443 let dz = z - nz;
444 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
445 if dist < 1e-12 {
446 return (0.0, 0.0, 0.0);
447 }
448
449 let r_ij = radius + n_radius;
450 let overlap = r_ij - dist;
451
452 let ux = dx / dist;
454 let uy = dy / dist;
455 let uz = dz / dist;
456
457 let inv_m = 1.0 / params.mass;
458
459 let f_social = params.a_social * (overlap / params.b_social).exp() * inv_m;
461 let mut fx = f_social * ux;
462 let mut fy = f_social * uy;
463 let mut fz = f_social * uz;
464
465 if overlap > 0.0 {
467 fx += params.k_body * inv_m * overlap * ux;
469 fy += params.k_body * inv_m * overlap * uy;
470 fz += params.k_body * inv_m * overlap * uz;
471
472 let dvx = nvx - vx;
474 let dvy = nvy - vy;
475 let dvz = nvz - vz;
476 let dv_normal = dvx * ux + dvy * uy + dvz * uz;
477 let tvx = dvx - dv_normal * ux;
478 let tvy = dvy - dv_normal * uy;
479 let tvz = dvz - dv_normal * uz;
480 fx += params.kappa_friction * inv_m * overlap * tvx;
481 fy += params.kappa_friction * inv_m * overlap * tvy;
482 fz += params.kappa_friction * inv_m * overlap * tvz;
483 }
484
485 (fx, fy, fz)
486}
487
488#[derive(Debug, Clone, Copy, PartialEq)]
493pub struct WallSegment3D {
494 pub x1: f64,
496 pub y1: f64,
498 pub z1: f64,
500 pub x2: f64,
502 pub y2: f64,
504 pub z2: f64,
506}
507
508impl WallSegment3D {
509 pub fn new(x1: f64, y1: f64, z1: f64, x2: f64, y2: f64, z2: f64) -> Self {
511 Self {
512 x1,
513 y1,
514 z1,
515 x2,
516 y2,
517 z2,
518 }
519 }
520
521 pub fn closest_point(&self, px: f64, py: f64, pz: f64) -> (f64, f64, f64) {
523 let dx = self.x2 - self.x1;
524 let dy = self.y2 - self.y1;
525 let dz = self.z2 - self.z1;
526 let len_sq = dx * dx + dy * dy + dz * dz;
527 if len_sq < 1e-12 {
528 return (self.x1, self.y1, self.z1);
529 }
530 let t = ((px - self.x1) * dx + (py - self.y1) * dy + (pz - self.z1) * dz) / len_sq;
531 let t = t.clamp(0.0, 1.0);
532 (self.x1 + t * dx, self.y1 + t * dy, self.z1 + t * dz)
533 }
534}
535
536#[allow(clippy::too_many_arguments)]
542pub fn wall_repulsion_3d(
543 x: f64,
544 y: f64,
545 z: f64,
546 vx: f64,
547 vy: f64,
548 vz: f64,
549 radius: f64,
550 wall: &WallSegment3D,
551 params: &SocialForceParams,
552) -> (f64, f64, f64) {
553 let (cx, cy, cz) = wall.closest_point(x, y, z);
554 let dx = x - cx;
555 let dy = y - cy;
556 let dz = z - cz;
557 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
558 if dist < 1e-12 {
559 return (0.0, 0.0, 0.0);
560 }
561
562 let overlap = radius - dist;
563
564 let ux = dx / dist;
566 let uy = dy / dist;
567 let uz = dz / dist;
568
569 let inv_m = 1.0 / params.mass;
570
571 let f_rep = params.a_wall * (overlap / params.b_wall).exp() * inv_m;
573 let mut fx = f_rep * ux;
574 let mut fy = f_rep * uy;
575 let mut fz = f_rep * uz;
576
577 if overlap > 0.0 {
579 fx += params.k_wall * inv_m * overlap * ux;
581 fy += params.k_wall * inv_m * overlap * uy;
582 fz += params.k_wall * inv_m * overlap * uz;
583
584 let v_normal = vx * ux + vy * uy + vz * uz;
586 let tvx = vx - v_normal * ux;
587 let tvy = vy - v_normal * uy;
588 let tvz = vz - v_normal * uz;
589 fx -= params.kappa_wall * inv_m * overlap * tvx;
590 fy -= params.kappa_wall * inv_m * overlap * tvy;
591 fz -= params.kappa_wall * inv_m * overlap * tvz;
592 }
593
594 (fx, fy, fz)
595}
596
597#[allow(clippy::too_many_arguments)]
604pub fn integrate_euler_3d(
605 x: f64,
606 y: f64,
607 z: f64,
608 vx: f64,
609 vy: f64,
610 vz: f64,
611 fx: f64,
612 fy: f64,
613 fz: f64,
614 dt: f64,
615 params: &SocialForceParams,
616) -> (f64, f64, f64, f64, f64, f64) {
617 let mut new_vx = vx + fx * dt;
618 let mut new_vy = vy + fy * dt;
619 let mut new_vz = vz + fz * dt;
620
621 let speed = (new_vx * new_vx + new_vy * new_vy + new_vz * new_vz).sqrt();
622 if speed > params.max_speed {
623 let scale = params.max_speed / speed;
624 new_vx *= scale;
625 new_vy *= scale;
626 new_vz *= scale;
627 }
628
629 let new_x = x + new_vx * dt;
630 let new_y = y + new_vy * dt;
631 let new_z = z + new_vz * dt;
632 (new_x, new_y, new_z, new_vx, new_vy, new_vz)
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638
639 #[test]
640 fn desired_force_points_toward_destination() {
641 let (fx, fy) = desired_force_2d(0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 1.3, 0.5);
642 assert!(fx > 0.0, "force should point toward destination (+x)");
643 assert!(
644 fy.abs() < 1e-12,
645 "no y component when destination is on x axis"
646 );
647 }
648
649 #[test]
650 fn desired_force_zero_at_destination() {
651 let (fx, fy) = desired_force_2d(5.0, 5.0, 1.0, 1.0, 5.0, 5.0, 1.3, 0.5);
652 assert!(fx.abs() < 1e-10);
653 assert!(fy.abs() < 1e-10);
654 }
655
656 #[test]
657 fn social_repulsion_pushes_agents_apart() {
658 let params = SocialForceParams::default();
659 let (fx, _fy) =
660 social_repulsion_2d(1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, 0.25, ¶ms);
661 assert!(fx > 0.0, "repulsion should push agent in +x direction");
662 }
663
664 #[test]
665 fn physical_contact_activates_on_overlap() {
666 let params = SocialForceParams::default();
667 let (fx_overlap, _) =
669 social_repulsion_2d(0.3, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, 0.25, ¶ms);
670 let (fx_far, _) =
672 social_repulsion_2d(5.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, 0.25, ¶ms);
673 assert!(
674 fx_overlap > fx_far,
675 "overlapping agents should have stronger force"
676 );
677 }
678
679 #[test]
680 fn wall_repulsion_pushes_away() {
681 let params = SocialForceParams::default();
682 let wall = WallSegment::new(0.0, 0.0, 10.0, 0.0);
683 let (fx, fy) = wall_repulsion_2d(5.0, 0.1, 0.0, 0.0, 0.25, &wall, ¶ms);
685 assert!(
686 fx.abs() < fy.abs(),
687 "force should be mostly in +y (away from wall)"
688 );
689 assert!(fy > 0.0, "should push agent away from wall in +y direction");
690 }
691
692 #[test]
693 fn integration_clamps_speed() {
694 let params = SocialForceParams {
695 max_speed: 2.0,
696 ..Default::default()
697 };
698 let (_, _, new_vx, new_vy) = integrate_euler_2d(0.0, 0.0, 0.0, 0.0, 1e6, 0.0, 0.1, ¶ms);
700 let speed = (new_vx * new_vx + new_vy * new_vy).sqrt();
701 assert!(
702 (speed - 2.0).abs() < 1e-10,
703 "speed should be clamped to max_speed"
704 );
705 }
706
707 #[test]
708 fn wall_segment_closest_point() {
709 let wall = WallSegment::new(0.0, 0.0, 10.0, 0.0);
710 let (cx, cy) = wall.closest_point(5.0, 3.0);
711 assert!((cx - 5.0).abs() < 1e-12);
712 assert!(cy.abs() < 1e-12);
713
714 let (cx, cy) = wall.closest_point(15.0, 3.0);
716 assert!((cx - 10.0).abs() < 1e-12);
717 assert!(cy.abs() < 1e-12);
718
719 let (cx, cy) = wall.closest_point(-5.0, 3.0);
721 assert!(cx.abs() < 1e-12);
722 assert!(cy.abs() < 1e-12);
723 }
724
725 #[test]
726 fn wall_segment_3d_closest_point() {
727 let wall = WallSegment3D::new(0.0, 0.0, 0.0, 10.0, 0.0, 0.0);
728 let (cx, cy, cz) = wall.closest_point(5.0, 3.0, 4.0);
729 assert!((cx - 5.0).abs() < 1e-12);
730 assert!(cy.abs() < 1e-12);
731 assert!(cz.abs() < 1e-12);
732 }
733
734 #[test]
735 fn desired_force_3d_points_toward_destination() {
736 let (fx, fy, fz) = desired_force_3d(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 1.3, 0.5);
737 assert!(fx > 0.0);
738 assert!(fy.abs() < 1e-12);
739 assert!(fz.abs() < 1e-12);
740 }
741
742 #[test]
743 fn social_repulsion_3d_pushes_apart() {
744 let params = SocialForceParams::default();
745 let (fx, _, _) = social_repulsion_3d(
746 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, ¶ms,
747 );
748 assert!(fx > 0.0);
749 }
750
751 #[test]
752 fn integration_3d_clamps_speed() {
753 let params = SocialForceParams {
754 max_speed: 2.0,
755 ..Default::default()
756 };
757 let (_, _, _, nvx, nvy, nvz) =
758 integrate_euler_3d(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1e6, 1e6, 1e6, 0.1, ¶ms);
759 let speed = (nvx * nvx + nvy * nvy + nvz * nvz).sqrt();
760 assert!((speed - 2.0).abs() < 1e-10);
761 }
762}