@@ -15,6 +15,13 @@ def linear_interpolation(t: np.ndarray, pos_begin: np.ndarray, pos_end: np.ndarr
1515def 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