File size: 6,273 Bytes
2eeee5a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | """Snow sinkage force (Step 2, part 2).
Follows the resistance law in hackathon-everest's `physics.py::_resistance_n`:
F = k_eff * d^1.12 + c * max(v_down, 0) + 3 * k_eff * max(d - d_support, 0)
with a void band that collapses resistance to 8%. Term by term:
* `k_eff * d^1.12` progressive compaction -- snow stiffens as it packs.
* `c * v_down` rate dependence -- snow resists a fast foot harder than a slow one.
Only the downward half; snow does not pull a lifting foot back.
* `3 * k_eff * (...)` bottoming out on the hard substrate once the foot passes through
the snow layer. The factor 3 makes that transition sharp.
* void a hollow under a crust: the foot meets almost nothing, then the
substrate term catches it. This is the breakthrough the policy must survive.
Applied to the feet as `xfrc_applied`, not as contact geometry -- see terrain.py for why.
The brief calls for depth-and-velocity dependence and explicitly not granular physics.
"""
from __future__ import annotations
import jax
import jax.numpy as jnp
COMPACTION_EXPONENT = 1.12
SUBSTRATE_STIFFNESS_GAIN = 3.0
VOID_RESISTANCE_FACTOR = 0.08
MAX_FORCE_N = 2_000.0 # numerical guard; ~6x G1 body weight
# Ploughing. Vertical support alone is NOT enough to make deep snow hard: under uniform
# stiffness a foot reaches the same equilibrium sinkage in 0.07 m and 0.19 m of snow, so
# the two regimes would feel identical and the policy would have nothing to learn.
# What actually punishes deep snow is dragging the swing foot through it. Resistance scales
# with submerged frontal area (~ depth x foot width) and snow strength, saturating in speed.
# Sized so a foot swung through deep snow at walking speed meets ~30 N -- roughly 9% of body
# weight, enough to alter gait without immobilising the robot. The learning signal comes from
# clearance: a foot lifted clear of the surface ploughs nothing, so deeper snow demands a
# higher swing to escape the same drag.
PLOUGH_COEFF = 0.25
FOOT_WIDTH_M = 0.09 # G1 foot, approximate
PLOUGH_REFERENCE_SPEED_MPS = 0.4
def resistance_n(
penetration_m: jax.Array,
vertical_velocity_mps: jax.Array,
snow_depth_m: jax.Array,
stiffness_n_per_m: jax.Array,
damping_ns_per_m: jax.Array,
void: jax.Array,
include_substrate: bool = True,
) -> jax.Array:
"""Upward resisting force on a foot. Zero when the foot is above the snow surface.
penetration_m: depth below the snow surface, negative when clear of it.
vertical_velocity_mps: foot velocity, negative downward.
include_substrate: whether to add the bottoming-out term. TRUE when deriving bearing
capacity as a label (everest has no geometry, so the term is the only hard ground there).
FALSE when computing a force to APPLY in MuJoCo, where the substrate is a real plane with
a real contact solver -- adding it there double-counts the floor and fights the solver,
which produced spikes to 1530 N (~4.5x body weight) against the 2000 N clamp.
"""
d = jnp.maximum(penetration_m, 0.0)
v_down = jnp.maximum(-vertical_velocity_mps, 0.0)
force = stiffness_n_per_m * d**COMPACTION_EXPONENT + damping_ns_per_m * v_down
# Void collapses the snow-column term only; the substrate below still stops the foot.
force = jnp.where(void, force * VOID_RESISTANCE_FACTOR, force)
if include_substrate:
force = force + SUBSTRATE_STIFFNESS_GAIN * stiffness_n_per_m * jnp.maximum(
d - snow_depth_m, 0.0
)
return jnp.clip(jnp.where(d > 0.0, force, 0.0), 0.0, MAX_FORCE_N)
def plough_force_n(
penetration_m: jax.Array,
horizontal_speed_mps: jax.Array,
stiffness_n_per_m: jax.Array,
void: jax.Array,
) -> jax.Array:
"""Magnitude of the horizontal drag on a foot moving through snow.
Opposes horizontal motion. Zero above the surface; a void offers almost nothing to plough.
"""
submerged = jnp.maximum(penetration_m, 0.0)
strength = jnp.where(void, stiffness_n_per_m * VOID_RESISTANCE_FACTOR, stiffness_n_per_m)
force = (
PLOUGH_COEFF * strength * submerged * FOOT_WIDTH_M
* jnp.tanh(horizontal_speed_mps / PLOUGH_REFERENCE_SPEED_MPS)
)
return jnp.clip(force, 0.0, MAX_FORCE_N)
def foot_force_world_n(
foot_pos_z_m: jax.Array,
foot_vel_xyz_mps: jax.Array,
ground_z_m: jax.Array,
snow_depth_m: jax.Array,
stiffness_n_per_m: jax.Array,
damping_ns_per_m: jax.Array,
void: jax.Array,
) -> jax.Array:
"""World-frame (fx, fy, fz) per foot, ready for `data.xfrc_applied[body, :3]`.
The snow surface sits at `ground_z_m + snow_depth_m`; the hard substrate at `ground_z_m`.
Vertical term supports the foot; horizontal term ploughs against its motion.
"""
penetration = ground_z_m + snow_depth_m - foot_pos_z_m
# include_substrate=False: MuJoCo's floor plane already stops the foot.
fz = resistance_n(penetration, foot_vel_xyz_mps[..., 2], snow_depth_m,
stiffness_n_per_m, damping_ns_per_m, void, include_substrate=False)
v_h = foot_vel_xyz_mps[..., :2]
speed = jnp.linalg.norm(v_h, axis=-1)
drag = plough_force_n(penetration, speed, stiffness_n_per_m, void)
direction = v_h / jnp.maximum(speed, 1e-6)[..., None]
return jnp.concatenate([-drag[..., None] * direction, fz[..., None]], axis=-1)
def equilibrium_sinkage_m(
load_n: jax.Array,
snow_depth_m: jax.Array,
stiffness_n_per_m: jax.Array,
void: jax.Array,
*,
iterations: int = 40,
) -> jax.Array:
"""Static sinkage where resistance balances `load_n`, by bisection.
Diagnostic and test helper -- the training loop integrates the force instead.
"""
def body(bounds, _):
lo, hi = bounds
mid = 0.5 * (lo + hi)
f = resistance_n(mid, jnp.zeros_like(mid), snow_depth_m, stiffness_n_per_m,
jnp.zeros_like(mid), void)
too_soft = f < load_n
return (jnp.where(too_soft, mid, lo), jnp.where(too_soft, hi, mid)), None
lo = jnp.zeros_like(snow_depth_m)
hi = jnp.full_like(snow_depth_m, 1.0)
(lo, hi), _ = jax.lax.scan(body, (lo, hi), None, length=iterations)
return 0.5 * (lo + hi)
|