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.
inspect code · cell 01
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from static_support_boundary import PAYLOAD_SHIFT_LIMIT, observe
inspect code · cell 02
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
inspect code · cell 03
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()
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
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)
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.