Skip to content

Commit 392b589

Browse files
committed
Add a centroidal planner
1 parent 82f6cb5 commit 392b589

1 file changed

Lines changed: 247 additions & 0 deletions

File tree

biped_walking_controller/preview_control.py

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ def linear_interpolation(t: np.ndarray, pos_begin: np.ndarray, pos_end: np.ndarr
1515
def cubic_spline_interpolation(
1616
t: np.ndarray, pos_begin: np.ndarray, pos_end: np.ndarray
1717
) -> np.ndarray:
18+
if isinstance(t, float):
19+
return (
20+
pos_begin
21+
+ 3.0 * (pos_end - pos_begin) * np.square(t)
22+
- 2.0 * (pos_end - pos_begin) * np.pow(t, 3)
23+
)
24+
1825
return (
1926
pos_begin
2027
+ 3.0 * (pos_end - pos_begin) * np.square(t)[:, None]
@@ -449,3 +456,243 @@ def _try_transition_to_ds_or_end(self, t: float, contact_force: float):
449456
self.t_start = t
450457
self.state = WalkingState.END if self._is_last_step() else WalkingState.DS
451458
self.step_idx += 1
459+
460+
461+
def build_zmp_horizon(
462+
com_initial_target,
463+
t_horizon: float,
464+
t_state: float,
465+
state: WalkingState,
466+
delta_t: float,
467+
current_step_idx: int,
468+
steps_sequence: np.ndarray,
469+
steps_foot: typing.List[Foot],
470+
ss_t: float,
471+
ds_t: float,
472+
t_init: float,
473+
t_end: float,
474+
interp_fn=cubic_spline_interpolation,
475+
) -> tuple[np.ndarray, np.ndarray]:
476+
"""
477+
Build a ZMP reference over a preview horizon based on the current walking state.
478+
479+
Parameters
480+
----------
481+
t_horizon : float
482+
Length of the preview horizon [s].
483+
t_state : float
484+
Time elapsed in the current state [s].
485+
state : WalkingState
486+
Current walking state (INIT, DS, SS_LEFT, SS_RIGHT, END).
487+
delta_t : float
488+
Sampling period of the preview horizon [s].
489+
current_step_idx : int
490+
Index of the current step in `steps_sequence`. Interpreted as:
491+
- In SS: index of the current support foot.
492+
- In DS: index of the *target* foot (previous is current_step_idx - 1).
493+
steps_sequence : np.ndarray
494+
Sequence of footsteps. Shape (N, 2) or (N, >=2).
495+
Only the (x, y) components are used as ZMP targets.
496+
ss_t : float
497+
Duration of a single-support phase [s].
498+
ds_t : float
499+
Duration of a double-support phase [s].
500+
t_init : float
501+
Duration of the INIT phase [s].
502+
t_end : float
503+
Duration of the END phase [s].
504+
interp_fn : callable
505+
Interpolation function used in double support.
506+
Expected signature: interp_fn(alpha, p0, p1)
507+
where alpha in [0, 1], p0, p1 are 2D numpy arrays.
508+
509+
Returns
510+
-------
511+
t_samples : np.ndarray, shape (N,)
512+
Relative time samples over the horizon, starting at 0.
513+
zmp_horizon : np.ndarray, shape (N, 2)
514+
ZMP reference (x, y) at each time sample.
515+
516+
Notes
517+
-----
518+
- This function does NOT enforce continuity with the *previous* ZMP reference.
519+
At each call, the horizon is built from scratch from the current state.
520+
- State-transition logic is a simple cyclic model:
521+
INIT -> DS -> SS -> DS -> SS -> ... -> END
522+
and the step index increments when leaving an SS state.
523+
"""
524+
# Sanity checks and normalization
525+
steps = np.asarray(steps_sequence, dtype=float)
526+
if steps.ndim != 2 or steps.shape[0] == 0:
527+
raise ValueError("steps_sequence must be a non-empty (N, D) array")
528+
529+
# Use only x, y
530+
steps_xy = steps[:, :2]
531+
n_steps = steps_xy.shape[0]
532+
533+
step_idx = int(np.clip(current_step_idx, 0, n_steps - 1))
534+
535+
def state_duration(s: WalkingState) -> float:
536+
if s == WalkingState.INIT:
537+
return t_init
538+
if s == WalkingState.DS:
539+
return ds_t
540+
if s in (WalkingState.SS_LEFT, WalkingState.SS_RIGHT):
541+
return ss_t
542+
if s == WalkingState.END:
543+
return t_end
544+
raise ValueError(f"Unknown state: {s}")
545+
546+
def next_state_and_step(
547+
s: WalkingState, idx: int, steps_foot: typing.List[Foot]
548+
) -> tuple[WalkingState, int]:
549+
"""Simple progression model over footsteps.
550+
551+
INIT -> DS(step 0)
552+
DS(k) -> SS(k)
553+
SS(k) -> DS(k+1) (clamped to last step)
554+
END stays END
555+
"""
556+
if s == WalkingState.DS or s == WalkingState.INIT:
557+
# Land on target step
558+
if idx < n_steps - 1:
559+
return (
560+
WalkingState.SS_LEFT if steps_foot[idx] is Foot.LEFT else WalkingState.SS_RIGHT
561+
), idx
562+
else:
563+
return WalkingState.END, idx
564+
565+
if s in (WalkingState.SS_LEFT, WalkingState.SS_RIGHT):
566+
# Move to next DS between current and next step
567+
if idx < n_steps - 1:
568+
return WalkingState.DS, idx + 1
569+
570+
if s == WalkingState.END:
571+
return WalkingState.END, idx
572+
573+
raise ValueError(f"Unknown state: {s}")
574+
575+
def zmp_for_state(s: WalkingState, idx: int, t_in_state: float) -> np.ndarray:
576+
"""Return ZMP position (x, y) for a given state and time in state."""
577+
# Clamp index
578+
idx = int(np.clip(idx, 0, n_steps - 1))
579+
580+
if s == WalkingState.INIT:
581+
# Keep ZMP on the first support
582+
p0 = com_initial_target
583+
p1 = steps_xy[0]
584+
585+
alpha = np.clip(t_in_state / t_init, 0.0, 1.0)
586+
return interp_fn(alpha, p0, p1)
587+
588+
if s in (WalkingState.SS_LEFT, WalkingState.SS_RIGHT):
589+
# ZMP fixed at current support foot
590+
return steps_xy[idx]
591+
592+
if s == WalkingState.DS:
593+
# ZMP moves between previous and current step
594+
if idx <= 0:
595+
p0 = com_initial_target
596+
p1 = steps_xy[0]
597+
else:
598+
p0 = steps_xy[idx - 1]
599+
p1 = steps_xy[idx]
600+
601+
alpha = np.clip(t_in_state / max(ds_t, 1e-6), 0.0, 1.0)
602+
return interp_fn(alpha, p0, p1)
603+
604+
if s == WalkingState.END:
605+
# Keep ZMP on last support
606+
return (steps_xy[-2] + steps_xy[-1]) / 2.0
607+
608+
raise ValueError(f"Unknown state: {s}")
609+
610+
# Allocate horizon
611+
n_samples = int(np.floor(t_horizon / delta_t))
612+
t_samples = np.arange(n_samples, dtype=float) * delta_t
613+
zmp_horizon = np.zeros((n_samples, 2), dtype=float)
614+
615+
# Simulated state over the horizon
616+
sim_state = state
617+
time_in_state = t_state
618+
sim_step_idx = step_idx
619+
620+
for k in range(n_samples):
621+
# Advance state machine if needed (except END which just saturates)
622+
while sim_state != WalkingState.END and time_in_state >= state_duration(sim_state):
623+
time_in_state -= state_duration(sim_state)
624+
sim_state, sim_step_idx = next_state_and_step(sim_state, sim_step_idx, steps_foot)
625+
626+
# Compute ZMP for current simulated state
627+
zmp_horizon[k] = zmp_for_state(sim_state, sim_step_idx, time_in_state)
628+
629+
# Advance local time
630+
time_in_state += delta_t
631+
632+
return t_samples, zmp_horizon
633+
634+
635+
class CentroidalPlanner:
636+
def __init__(
637+
self,
638+
dt: float,
639+
com_initial_target: np.ndarray,
640+
params: PreviewControllerParams,
641+
):
642+
self.params = params
643+
self.dt = dt
644+
self.ctrler_mat = compute_preview_control_matrices(params, dt)
645+
self.x = np.array([0.0, com_initial_target[0], 0.0, 0.0], dtype=float)
646+
self.y = np.array([0.0, com_initial_target[1], 0.0, 0.0], dtype=float)
647+
self.steps_sequence = None
648+
self.steps_foot = None
649+
self.step_idx = 0
650+
self.zmp_ref_horizon = None
651+
652+
def set_steps_sequence(self, steps_sequence: np.ndarray, steps_foot: typing.List[Foot]):
653+
# We assign the new sequence and reset the step counter
654+
self.steps_sequence = steps_sequence
655+
self.steps_foot = steps_foot
656+
self.step_idx = 0
657+
658+
def update(
659+
self,
660+
com_initial_target,
661+
state: WalkingState,
662+
step_idx: int,
663+
t_state: float,
664+
t_init: float,
665+
t_end: float,
666+
t_ss: float,
667+
t_ds: float,
668+
):
669+
# Update control
670+
_, self.zmp_ref_horizon = build_zmp_horizon(
671+
com_initial_target,
672+
t_horizon=(self.params.n_preview_steps - 1) * self.dt,
673+
t_state=t_state,
674+
state=state,
675+
delta_t=self.dt,
676+
current_step_idx=step_idx,
677+
steps_sequence=self.steps_sequence,
678+
steps_foot=self.steps_foot,
679+
ss_t=t_ss,
680+
ds_t=t_ds,
681+
t_init=t_init,
682+
t_end=t_end,
683+
interp_fn=cubic_spline_interpolation,
684+
)
685+
686+
_, self.x, self.y = update_control(
687+
self.ctrler_mat,
688+
self.zmp_ref_horizon[0],
689+
self.zmp_ref_horizon,
690+
self.x.copy(),
691+
self.y.copy(),
692+
)
693+
694+
def get_com_pos(self) -> typing.Tuple[float, float]:
695+
return self.x[1], self.y[1]
696+
697+
def get_ref_horizon(self):
698+
return self.zmp_ref_horizon

0 commit comments

Comments
 (0)