Skip to content

Commit e3acd1e

Browse files
committed
add rod-gravity-surface contact module
1 parent 02ddd5c commit e3acd1e

6 files changed

Lines changed: 300 additions & 189 deletions

File tree

src/virtual_field/runtime/foraging_elastica/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
idle_policy_like,
88
rotate_policy_by_angle,
99
)
10-
from .forcing import SuckerActuation, YSurfaceBallwGravity
10+
from .forcing import SuckerActuation, YSurfaceBallwGravity, YSurfaceRodwGravity
1111

1212
__all__ = [
1313
"BaseSphereTether",
@@ -17,6 +17,7 @@
1717
"SegmentExtensionActuation",
1818
"SuckerActuation",
1919
"YSurfaceBallwGravity",
20+
"YSurfaceRodwGravity",
2021
"current_activation",
2122
"idle_policy_like",
2223
"rotate_policy_by_angle",

src/virtual_field/runtime/foraging_elastica/forcing.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,112 @@ def _apply_sphere_plane_hertz(
324324
s = -f_t / speed_t
325325
sphere_external_force[0, 0] += s * tx
326326
sphere_external_force[2, 0] += s * tz
327+
328+
329+
class YSurfaceRodwGravity(NoForces):
330+
"""Cosserat rod on a horizontal floor with gravity and dry friction.
331+
332+
Applies gravity in ``-Y`` at rod nodes. Contact uses the existing
333+
rod-plane normal response against ``y = plane_origin`` with fixed ``+Y``
334+
surface normal. A local Coulomb-style tangential friction term is applied
335+
on contacting elements and distributed to adjacent nodes.
336+
"""
337+
338+
def __init__(
339+
self,
340+
k_c: float,
341+
nu_c: float,
342+
*,
343+
mu: float = 5.0,
344+
plane_origin: float = 0.0,
345+
) -> None:
346+
super().__init__()
347+
self.k_c = float(k_c)
348+
self.nu_c = float(nu_c)
349+
self.mu = float(mu)
350+
self.plane_origin = float(plane_origin)
351+
self._plane_origin = np.array(
352+
[[0.0], [self.plane_origin], [0.0]], dtype=np.float64
353+
)
354+
self._plane_normal = np.array([0.0, 1.0, 0.0], dtype=np.float64)
355+
self._surface_tol = 1e-4
356+
357+
def apply_forces(
358+
self,
359+
system: RodType,
360+
time: np.float64 = np.float64(0.0),
361+
) -> None:
362+
_apply_uniform_gravity_to_nodes(system.external_forces, system.mass, 9.81)
363+
_calculate_contact_forces_rod_plane(
364+
self._plane_origin,
365+
self._plane_normal,
366+
self._surface_tol,
367+
self.k_c,
368+
self.nu_c,
369+
system.radius,
370+
system.mass,
371+
system.position_collection,
372+
system.velocity_collection,
373+
system.internal_forces,
374+
system.external_forces,
375+
)
376+
_apply_rod_plane_coulomb_friction(
377+
system.position_collection,
378+
system.velocity_collection,
379+
system.radius,
380+
system.external_forces,
381+
self.plane_origin,
382+
self.k_c,
383+
self.nu_c,
384+
self.mu,
385+
)
386+
387+
388+
@njit(cache=True)
389+
def _apply_uniform_gravity_to_nodes(
390+
external_forces: np.ndarray,
391+
nodal_mass: np.ndarray,
392+
gravity_mag: float,
393+
) -> None:
394+
for i in range(nodal_mass.shape[0]):
395+
external_forces[1, i] -= gravity_mag * nodal_mass[i]
396+
397+
398+
@njit(cache=True, fastmath=True)
399+
def _apply_rod_plane_coulomb_friction(
400+
rod_positions: np.ndarray,
401+
rod_velocities: np.ndarray,
402+
rod_radii: np.ndarray,
403+
rod_external_forces: np.ndarray,
404+
plane_origin: float,
405+
k_c: float,
406+
nu_c: float,
407+
mu: float,
408+
) -> None:
409+
element_count = rod_radii.shape[0]
410+
for idx in range(element_count):
411+
center_y = 0.5 * (rod_positions[1, idx] + rod_positions[1, idx + 1])
412+
penetration = rod_radii[idx] - (center_y - plane_origin)
413+
if penetration <= 0.0:
414+
continue
415+
416+
vy = 0.5 * (rod_velocities[1, idx] + rod_velocities[1, idx + 1])
417+
normal_force = k_c * penetration ** (3.0 / 2.0) - nu_c * vy
418+
if normal_force <= 0.0:
419+
continue
420+
421+
vx = 0.5 * (rod_velocities[0, idx] + rod_velocities[0, idx + 1])
422+
vz = 0.5 * (rod_velocities[2, idx] + rod_velocities[2, idx + 1])
423+
speed_t_sq = vx * vx + vz * vz
424+
if speed_t_sq <= 1.0e-12:
425+
continue
426+
427+
speed_t = np.sqrt(speed_t_sq)
428+
scale = -(mu * normal_force) / speed_t
429+
fx = scale * vx
430+
fz = scale * vz
431+
432+
rod_external_forces[0, idx] += 0.5 * fx
433+
rod_external_forces[2, idx] += 0.5 * fz
434+
rod_external_forces[0, idx + 1] += 0.5 * fx
435+
rod_external_forces[2, idx + 1] += 0.5 * fz

src/virtual_field/runtime/noel_c4_simulation.py

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ class NoelObstacleSet:
2323

2424
def load_noel_c4_obstacles() -> NoelObstacleSet:
2525
obstacle_path = (
26-
Path(__file__).resolve().parents[3]
27-
/ "externals"
28-
/ "noel-c4-obstacles-aug.npz"
26+
Path(__file__).resolve().parents[3] / "externals" / "noel-c4-obstacles-aug.npz"
2927
# / "noel-c4-obstacles.npz"
3028
)
3129
data = np.load(obstacle_path, allow_pickle=True)
@@ -107,7 +105,7 @@ def load_noel_c4_obstacles() -> NoelObstacleSet:
107105
ends = starts + directions * lengths[:, None]
108106
center = np.vstack([starts, ends]).mean(axis=0)
109107
starts[:, 0] -= 0.10
110-
starts[:, 1] += 1.0 - center[1] + 0.25
108+
starts[:, 1] += 1.0 - center[1] + 0.15
111109
starts[:, 2] -= 0.75
112110

113111
return NoelObstacleSet(
@@ -123,9 +121,7 @@ def load_noel_c4_obstacles() -> NoelObstacleSet:
123121
class NoelC4Simulation(DualArmSimulationBase):
124122
tip_haptic_max_penetration: float = 0.01
125123
_obstacles: NoelObstacleSet = field(init=False)
126-
_tip_penetration_by_arm: dict[str, float] = field(
127-
init=False, default_factory=dict
128-
)
124+
_tip_penetration_by_arm: dict[str, float] = field(init=False, default_factory=dict)
129125
_haptic_events: list[HapticEvent] = field(init=False, default_factory=list)
130126

131127
def build_simulation(self) -> None:
@@ -138,8 +134,8 @@ def build_simulation(self) -> None:
138134
# from virtual_field.runtime.spirob_elastica.constraints import (
139135
# _SpirobBendConstraint,
140136
# )
141-
from virtual_field.runtime.spirob_elastica.sdf_objects import (
142-
SDFObstacleCylinders,
137+
from virtual_field.runtime.spirob_elastica.sdf_objects_hash import (
138+
SDFObstacleCylindersHash,
143139
)
144140

145141
self._obstacles = load_noel_c4_obstacles()
@@ -193,7 +189,7 @@ class _Simulator(
193189
self.simulator.append(self.left_rod)
194190
self.simulator.append(self.right_rod)
195191

196-
p_linear = 200.0
192+
p_linear = 500.0
197193
p_angular = 5.0
198194

199195
self.simulator.add_forcing_to(self.left_rod).using(
@@ -226,24 +222,25 @@ class _Simulator(
226222
constrained_director_idx=(0,),
227223
)
228224

229-
self.simulator.detect_contact_between(
230-
self.left_rod, self.right_rod
231-
).using(ea.RodRodContact, k=1e4, nu=3)
232-
self.simulator.detect_contact_between(
233-
self.right_rod, self.right_rod
234-
).using(ea.RodSelfContact, k=1e4, nu=3)
225+
self.simulator.detect_contact_between(self.left_rod, self.right_rod).using(
226+
ea.RodRodContact, k=1e4, nu=3
227+
)
228+
self.simulator.detect_contact_between(self.right_rod, self.right_rod).using(
229+
ea.RodSelfContact, k=1e4, nu=3
230+
)
231+
self.simulator.detect_contact_between(self.left_rod, self.left_rod).using(
232+
ea.RodSelfContact, k=1e4, nu=3
233+
)
235234
# self.simulator.add_forcing_to(self.left_rod).using(
236235
# _SpirobBendConstraint,
237236
# kt=0,
238237
# allowed_angle_in_deg=30,
239238
# )
240239

241240
for rod in (self.left_rod, self.right_rod):
242-
arm_id = (
243-
self.arm_ids[0] if rod is self.left_rod else self.arm_ids[1]
244-
)
241+
arm_id = self.arm_ids[0] if rod is self.left_rod else self.arm_ids[1]
245242
self.simulator.add_forcing_to(rod).using(
246-
SDFObstacleCylinders,
243+
SDFObstacleCylindersHash,
247244
starts=self._obstacles.starts,
248245
directions=self._obstacles.directions,
249246
lengths=self._obstacles.lengths,
@@ -293,7 +290,7 @@ def haptic_events(self) -> list[HapticEvent]:
293290
arm_id = event.arm_id
294291
penetration = self._tip_penetration_by_arm.get(arm_id, 0.0)
295292
intensity = penetration / self.tip_haptic_max_penetration
296-
intensity = max(0.0, min(1.0, intensity))
293+
intensity = max(0.0, min(0.5, intensity)) # cap at 50%
297294
event.active = intensity > 0.0
298295
event.intensity = intensity
299296
return self._haptic_events

0 commit comments

Comments
 (0)