Overview
The AdaptiveGaitController extends the diagonal gait pattern with online adaptation capabilities. It supports both high-level gait parameter modulation (step height, step length, cycle time, body height) and low-level per-leg residual corrections, enabling the robot to adapt its gait to rough terrain during operation.
Class Definition
class AdaptiveGaitController:
"""Gait controller with online parameter adaptation."""
def __init__(
self,
base_params: Optional[GaitParameters] = None,
residual_scale: float = 0.01,
param_ranges: Optional[Dict[str, tuple]] = None,
) -> None:
base_params
GaitParameters | None
default:"None"
Nominal gait parameters used as baseline. If not provided, uses default GaitParameters().
Maximum absolute residual offset per axis [meters]. Residuals are clipped to [-residual_scale, +residual_scale].
param_ranges
Dict[str, tuple] | None
default:"None"
Min/max bounds for each trainable parameter. If not provided, uses DEFAULT_RANGES. Each tuple format: (min_val, max_val, default_val).
Attributes:
base_params (GaitParameters): Reference baseline parameters
current_params (GaitParameters): Currently active adapted parameters
residual_scale (float): Residual clipping threshold
param_ranges (Dict[str, tuple]): Parameter bounds
base_controller (DiagonalGaitController): Underlying gait generator
Default Parameter Ranges
DEFAULT_RANGES = {
"step_height": (0.03, 0.090, 0.04), # meters
"step_length": (0.030, 0.100, 0.06), # meters
"cycle_time": (0.800, 1.500, 0.80), # seconds (min clamped to 0.8)
"body_height": (0.04, 0.055, 0.05), # meters
}
Each tuple defines (minimum, maximum, default) values for the trainable parameters.
Methods
update_with_residuals
def update_with_residuals(
self,
dt: float,
residuals: Dict[str, np.ndarray],
param_deltas: Optional[Dict[str, float]] = None,
) -> Dict[str, np.ndarray]:
"""Update gait with parameter adaptation and residuals."""
Main update method that combines parameter adaptation and residual corrections.
Timestep in seconds to advance the gait
residuals
Dict[str, np.ndarray]
required
Per-leg residual offsets. Keys are leg names ("FL", "FR", "RL", "RR"), values are [dx, dy, dz] arrays in meters.
param_deltas
Dict[str, float] | None
default:"None"
Optional parameter deltas to apply this step. Keys: "step_height", "step_length", "cycle_time", "body_height". Values are deltas (not absolute values).
Returns: Dict[str, np.ndarray]
Final foot targets per leg after applying base trajectory, parameter adaptation, and residuals.
Behavior:
- Applies parameter deltas if provided (with range clipping)
- Rebuilds base controller if parameters changed
- Gets nominal targets from base controller
- Adds clipped residuals to targets
update_parameters
def update_parameters(self, param_deltas: Dict[str, float]) -> None:
"""Apply deltas to gait parameters with clipping."""
Updates gait parameters by applying deltas and clipping to valid ranges.
Dictionary with parameter names as keys and delta values. Example: {"step_height": 0.01, "cycle_time": -0.1}
Note: This marks parameters as “dirty” and triggers controller rebuild on next update.
reset
def reset(self) -> None:
"""Reset to base parameters and phase."""
Resets the controller to initial base parameters and rebuilds the underlying gait controller.
get_current_parameters
def get_current_parameters(self) -> Dict[str, float]:
"""Return current adapted parameter values."""
Returns: Dict[str, float]
Dictionary with keys: "step_height", "step_length", "cycle_time", "body_height" and their current values.
get_phase_info
def get_phase_info(self) -> Dict[str, float]:
"""Return gait phase information."""
Returns: Dict[str, float]
{
"phase_elapsed": float, # Time elapsed in current state [seconds]
"state_duration": float, # Duration of current state [seconds]
"phase_normalized": float, # Normalized phase ∈ [0, 1]
"active_pair": float, # 0 for pair_a_swing, 1 for pair_b_swing
}
get_swing_stance_flags
def get_swing_stance_flags(self) -> Dict[str, int]:
"""Return 1 for swing legs, 0 for stance legs."""
Returns: Dict[str, int]
Mapping from leg name to binary flag (1 = swing, 0 = stance).
Usage Examples
Basic Usage
from controllers.adaptive_gait_controller import AdaptiveGaitController
from gait_controller import GaitParameters, LEG_NAMES
import numpy as np
# Initialize with base parameters
base_params = GaitParameters(
body_height=0.05,
step_length=0.06,
step_height=0.04,
cycle_time=0.8,
)
controller = AdaptiveGaitController(
base_params=base_params,
residual_scale=0.01,
)
# Update with zero residuals (nominal gait)
zero_residuals = {leg: np.zeros(3) for leg in LEG_NAMES}
targets = controller.update_with_residuals(dt=0.01, residuals=zero_residuals)
Parameter Adaptation
from controllers.adaptive_gait_controller import AdaptiveGaitController
from gait_controller import GaitParameters
import numpy as np
base_params = GaitParameters(
body_height=0.05,
step_length=0.06,
step_height=0.04,
cycle_time=0.8,
)
controller = AdaptiveGaitController(base_params=base_params)
# Adapt parameters during runtime
param_deltas = {
"step_height": 0.01, # Increase step height by 1cm
"step_length": -0.01, # Decrease step length by 1cm
}
zero_res = {leg: np.zeros(3) for leg in ["FL", "FR", "RL", "RR"]}
targets = controller.update_with_residuals(
dt=0.01,
residuals=zero_res,
param_deltas=param_deltas,
)
# Check updated parameters
current = controller.get_current_parameters()
print(f"New step height: {current['step_height']:.3f}m")
print(f"New step length: {current['step_length']:.3f}m")
Combined Adaptation and Residuals
from controllers.adaptive_gait_controller import AdaptiveGaitController
from gait_controller import GaitParameters, LEG_NAMES
import numpy as np
base_params = GaitParameters(
body_height=0.05,
step_length=0.06,
step_height=0.04,
cycle_time=0.8,
)
controller = AdaptiveGaitController(
base_params=base_params,
residual_scale=0.02, # Allow larger residuals
)
# Apply both parameter deltas and residual corrections
param_deltas = {"step_height": 0.005} # Slightly increase lift
residuals = {
"FL": np.array([0.01, 0.0, -0.01]), # Adjust FL leg
"FR": np.array([0.01, 0.0, 0.0]),
"RL": np.array([0.0, 0.0, -0.01]),
"RR": np.array([0.0, 0.0, 0.0]),
}
targets = controller.update_with_residuals(
dt=0.01,
residuals=residuals,
param_deltas=param_deltas,
)
# Query gait state
phase_info = controller.get_phase_info()
swing_flags = controller.get_swing_stance_flags()
print(f"Phase: {phase_info['phase_normalized']:.2f}")
for leg in LEG_NAMES:
state = "swing" if swing_flags[leg] else "stance"
print(f"{leg}: {state} -> {targets[leg]}")
Custom Parameter Ranges
from controllers.adaptive_gait_controller import AdaptiveGaitController
from gait_controller import GaitParameters
# Define custom bounds for training
custom_ranges = {
"step_height": (0.02, 0.08, 0.04), # Wider range
"step_length": (0.04, 0.10, 0.06),
"cycle_time": (0.6, 1.5, 0.8),
"body_height": (0.035, 0.065, 0.05),
}
controller = AdaptiveGaitController(
base_params=GaitParameters(),
residual_scale=0.015,
param_ranges=custom_ranges,
)
Integration with RL Policies
This controller is designed for reinforcement learning policies that output:
- High-level adaptation: Parameter deltas for terrain adaptation
- Low-level corrections: Per-leg residuals for fine control
# Typical RL policy interface
class GaitAdaptationPolicy:
def predict(self, observation):
# Policy outputs parameter deltas and residuals
param_deltas = {
"step_height": 0.01,
"cycle_time": -0.05,
}
residuals = {
"FL": np.array([0.005, 0.0, -0.002]),
"FR": np.array([0.003, 0.0, 0.001]),
"RL": np.array([0.0, 0.0, -0.003]),
"RR": np.array([0.002, 0.0, 0.0]),
}
return param_deltas, residuals
# Use in control loop
policy = GaitAdaptationPolicy()
for step in range(1000):
obs = get_observation()
param_deltas, residuals = policy.predict(obs)
targets = controller.update_with_residuals(
dt=0.01,
residuals=residuals,
param_deltas=param_deltas,
)
apply_targets_to_robot(targets)
Implementation Notes
Controller Rebuilding
When parameters change, the underlying DiagonalGaitController is rebuilt to update:
- Swing Bézier curve control points
- State duration (half of cycle time)
- Body height and step dimensions
The rebuild attempts to preserve gait phase, but some discontinuity may occur if cycle_time changes significantly.
Residual Clipping
Residuals are clipped element-wise to prevent extreme corrections:
res_clipped = np.clip(res, -residual_scale, residual_scale)
This ensures stability even if the policy outputs large values.
Parameter Bounds
All parameter updates are clipped to the defined ranges:
new_val = np.clip(current + delta, min_val, max_val)
This prevents physically invalid configurations (e.g., negative step height).
Source
Location: ~/workspace/source/controllers/adaptive_gait_controller.py
Dependencies:
gait_controller.DiagonalGaitController
gait_controller.GaitParameters
gait_controller.LEG_NAMES