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)