← robotics test bench

research notebook / static_support_boundary

static support boundary

where does static support end?

a recorded ballast sweep comparing signed support margin with measured contact loads.

6 saved output blockssource f15f8a1static export · never re-executed
reading this record

code is folded so the argument and saved outputs come first. expand any cell to inspect the implementation. saved outputs are historical evidence from the source notebook, not a new run or independent verification.

Static support boundary

Goal

Test the initial prediction for issue #24: does the signed support margin reach zero before the rear feet unload?

Human prediction: a centered ballast keeps the COM centered; a forward ballast moves it toward the front; beyond the front support edge creates a tipping hazard. The human predicted rear-foot load would increase with forward ballast and estimated a 20–30° forward post-tip pitch.

This notebook is the analysis front end. It invokes the focused headless MuJoCo fixture only to obtain contact loads; it does not open a viewer or make a C-1N capability claim.

Setup

Key assumptions

  • Three feet remain fixed relative to the torso.
  • Only an abstract payload position changes.
  • The model is a support-mechanics fixture, not C-1N morphology.
  • A two-second rollout is used to let each condition settle or expose loss of rear contact.
inspect code · cell 01
In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from static_support_boundary import PAYLOAD_SHIFT_LIMIT, observe

Steps

1. Sweep payload position

The sweep is coarse by design. Use the first transition to choose a narrower next sweep.

inspect code · cell 02
In [2]:
coarse_shifts = np.round(np.linspace(0.0, 0.9, 10), 2)
near_boundary_shifts = np.array([0.92, 0.94, 0.96, 0.98, 1.00, 1.10, 1.20])
shifts = np.concatenate((coarse_shifts, near_boundary_shifts))
records = []
for shift in shifts:
    observation = observe(float(shift), seconds=2.0)
    loads = observation['foot_normal_loads_n']
    records.append({
        'payload_shift_m': shift,
        'support_margin_m': observation['support_margin_m'],
        'front_load_n': loads[0],
        'rear_left_load_n': loads[1],
        'rear_right_load_n': loads[2],
        'minimum_rear_load_n': min(loads[1:]),
        'com_x_m': observation['com_world_xy_m'][0],
        'roll_deg': np.rad2deg(observation['torso_roll_rad']),
        'pitch_deg': np.rad2deg(observation['torso_pitch_rad']),
        'standing_metric_pass': bool(observation['standing_metric_pass']),
    })
results = pd.DataFrame(records)
results
Out[2]:
payload_shift_m support_margin_m front_load_n rear_left_load_n rear_right_load_n minimum_rear_load_n com_x_m roll_deg pitch_deg standing_metric_pass
0 0.00 0.130408 7.599474 5.765013 5.765013 5.765013 -0.001359 -3.072152e-18 0.005018 True
1 0.10 0.116914 8.792316 5.168592 5.168592 5.168592 0.029828 1.233045e-17 0.009814 True
2 0.20 0.103424 9.984834 4.572333 4.572333 4.572333 0.061006 -1.787620e-16 0.014506 True
3 0.30 0.089937 11.177012 3.976244 3.976244 3.976244 0.092175 -1.586728e-16 0.019086 True
4 0.40 0.076454 12.368841 3.380329 3.380329 3.380329 0.123335 2.336176e-17 0.023554 True
5 0.50 0.062976 13.560317 2.784591 2.784591 2.784591 0.154485 -1.639864e-16 0.027907 True
6 0.60 0.049501 14.751442 2.189028 2.189028 2.189028 0.185626 1.416759e-16 0.032147 True
7 0.70 0.036030 15.942219 1.593639 1.593639 1.593639 0.216757 5.748162e-17 0.036276 True
8 0.80 0.022564 17.132655 0.998421 0.998421 0.998421 0.247879 9.481841e-17 0.040294 True
9 0.90 0.009101 18.322758 0.403370 0.403370 0.403370 0.278993 3.048139e-18 0.044205 True
10 0.92 0.006409 18.560738 0.284379 0.284379 0.284379 0.285215 -1.422508e-16 0.044975 True
11 0.94 0.003717 18.798711 0.165393 0.165393 0.165393 0.291437 -7.185765e-17 0.045740 True
12 0.96 0.001007 19.038238 0.045629 0.045629 0.045629 0.297705 1.073486e-16 0.046506 True
13 0.98 -0.096543 15.158607 0.000000 0.000000 0.000000 0.545929 2.489353e-14 23.588235 False
14 1.00 -0.095626 15.146059 0.000000 0.000000 0.000000 0.542643 -5.733115e-11 23.934518 False
15 1.10 -0.093017 15.111429 0.000000 0.000000 0.000000 0.541255 -6.283008e-08 25.073569 False
16 1.20 -0.094944 15.138791 0.000000 0.000000 0.000000 0.549230 -1.344223e-07 24.208645 False
inspect code · cell 03
In [3]:
fig, axes = plt.subplots(2, 1, figsize=(8, 6), sharex=True)
axes[0].plot(results.payload_shift_m, results.support_margin_m, marker='o', color='#2f69ad')
axes[0].axhline(0, color='black', linewidth=1)
axes[0].set_ylabel('support margin (m)')
axes[0].set_title('Fixed-foot support boundary sweep')
axes[1].plot(results.payload_shift_m, results.front_load_n, marker='o', label='front')
axes[1].plot(results.payload_shift_m, results.rear_left_load_n, marker='o', label='rear left')
axes[1].plot(results.payload_shift_m, results.rear_right_load_n, marker='o', label='rear right')
axes[1].axhline(0, color='black', linewidth=1)
axes[1].set_xlabel('payload shift (m)')
axes[1].set_ylabel('normal load (N)')
axes[1].legend()
fig.tight_layout()
No description has been provided for this image

Checks

Fixture standing metric: all three feet have more than 0.001 N normal load after a two-second rollout, and both roll and pitch remain within ±5°. This is a test-bench metric, not the future C-1N STAND criterion.

A fixed-foot quasi-static boundary should pair an exhausted support margin with unloading of the limiting rear contact. The coarse sweep can bracket the transition, but it cannot establish an exact threshold.

inspect code · cell 04
In [4]:
last_positive_margin = results.loc[results.support_margin_m > 0].tail(1)
first_unloaded_rear = results.loc[results.minimum_rear_load_n <= 1e-3].head(1)
print('Last sampled positive margin:')
display(last_positive_margin)
print('First sampled unloaded rear contact:')
display(first_unloaded_rear)
Last sampled positive margin:
payload_shift_m support_margin_m front_load_n rear_left_load_n rear_right_load_n minimum_rear_load_n com_x_m roll_deg pitch_deg standing_metric_pass
12 0.96 0.001007 19.038238 0.045629 0.045629 0.045629 0.297705 1.073486e-16 0.046506 True
First sampled unloaded rear contact:
payload_shift_m support_margin_m front_load_n rear_left_load_n rear_right_load_n minimum_rear_load_n com_x_m roll_deg pitch_deg standing_metric_pass
13 0.98 -0.096543 15.158607 0.0 0.0 0.0 0.545929 2.489353e-14 23.588235 False

Takeaways

The centered case passes the fixture metric. At shift = 0.96 m, the margin is +0.0010 m and each rear foot carries 0.046 N; it also passes. At 0.98 m, both rear contacts are unloaded, the metric fails, and pitch is +23.6° after tipping.

The observed model corrects one prediction: forward ballast increases the front-foot load and decreases rear-foot load. The COM-location and forward-pitch predictions held. The support-margin and rear-unloading events are bracketed in the same 0.96–0.98 m interval.

A later C-1N experiment must separately measure changing support geometry and contact set during gait; this fixed-foot fixture cannot answer that dynamic question.