Skip to content

Commit 9eb3b03

Browse files
authored
Merge pull request #16 from rdesarz/14-implement-foot-planner-to-generate-dynamically-the-trajectory-of-the-swinging-foot
- Add a new example to test dynamic walking controller instead of precomputed trajectories - Add a method to generate dynamically the swing foot trajectory - Split steps sequence computation and feet trajectories generation - Improve plot of contact forces - Add walking state machine to a new module - Update documentation
2 parents 82f6cb5 + 41879f3 commit 9eb3b03

14 files changed

Lines changed: 829 additions & 295 deletions

biped_walking_controller/foot.py

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@
99
"""
1010

1111
import math
12+
1213
import numpy as np
1314
import shapely
1415
from shapely import Polygon, Point, affinity, union
1516
from shapely.ops import nearest_points
1617

17-
18-
import math
19-
import numpy as np
20-
from typing import Tuple
18+
from biped_walking_controller.state_machine import WalkingStateMachineParams, Foot
2119

2220

2321
def bezier_quintic(P: np.ndarray, s: np.ndarray) -> np.ndarray:
@@ -185,15 +183,44 @@ def __call__(self, p_start: np.ndarray, p_end: np.ndarray, s: np.ndarray) -> np.
185183
return path
186184

187185

188-
def compute_feet_path_and_poses(
186+
def compute_steps_sequence(
187+
rf_initial_pose: np.ndarray,
188+
lf_initial_pose: np.ndarray,
189+
n_steps: int,
190+
l_stride: float,
191+
):
192+
steps_pose = np.zeros((n_steps + 2, 3))
193+
steps_pose[0] = rf_initial_pose
194+
steps_ids = [Foot.RIGHT]
195+
for i in range(1, n_steps + 1):
196+
sign = -1.0 if i % 2 == 0 else 1.0
197+
steps_ids.append(Foot.RIGHT if i % 2 == 0 else Foot.LEFT)
198+
steps_pose[i] = np.array([i * l_stride, sign * math.fabs(lf_initial_pose[1]), 0.0])
199+
200+
# Add a last step to have both feet at the same level
201+
steps_pose[-1] = steps_pose[-2]
202+
steps_pose[-1][1] = steps_pose[-1][1] * -1.0
203+
204+
return steps_pose, steps_ids
205+
206+
207+
def compute_time_vector(t_ss, t_ds, t_init, t_final, n_steps, dt):
208+
total_time = t_init + n_steps * (t_ss + t_ds) + (t_ss + t_final)
209+
N = int(total_time / dt)
210+
t = np.arange(N) * dt
211+
212+
return t
213+
214+
215+
def compute_feet_trajectories(
189216
rf_initial_pose,
190217
lf_initial_pose,
191218
n_steps,
219+
steps_pose,
192220
t_ss,
193221
t_ds,
194222
t_init,
195223
t_final,
196-
l_stride,
197224
dt,
198225
traj_generator=BezierCurveFootPathGenerator(foot_height=0.1),
199226
):
@@ -221,7 +248,7 @@ def compute_feet_path_and_poses(
221248
Double-support duration between steps.
222249
t_init : float
223250
Initial DS duration before stepping.
224-
t_final : float
251+
t_end : float
225252
Final DS duration to center the CoM.
226253
l_stride : float
227254
Step length along +x for each new foothold.
@@ -258,30 +285,14 @@ def compute_feet_path_and_poses(
258285
# the same level and a double support step to place the CoM in the
259286
# middle of the feet
260287

261-
total_time = t_init + n_steps * (t_ss + t_ds) + (t_ss + t_final)
262-
N = int(total_time / dt)
263-
t = np.arange(N) * dt
288+
t = compute_time_vector(t_ss, t_ds, t_init, t_final, n_steps, dt)
289+
N = len(t)
264290

265291
# Initialize path
266292
rf_path = np.ones([N, 3]) * rf_initial_pose
267293
lf_path = np.ones([N, 3]) * lf_initial_pose
268294
phases = np.ones(N)
269295

270-
# Switch of the CoM to the right foot implies both feet stays at the same position
271-
mask = t < t_init
272-
rf_path[mask, :] = rf_initial_pose
273-
lf_path[mask, :] = lf_initial_pose
274-
275-
steps_pose = np.zeros((n_steps + 2, 3))
276-
steps_pose[0] = rf_initial_pose
277-
for i in range(1, n_steps + 1):
278-
sign = -1.0 if i % 2 == 0 else 1.0
279-
steps_pose[i] = np.array([i * l_stride, sign * math.fabs(lf_initial_pose[1]), 0.0])
280-
281-
# Add a last step to have both feet at the same level
282-
steps_pose[-1] = steps_pose[-2]
283-
steps_pose[-1][1] = steps_pose[-1][1] * -1.0
284-
285296
# Compute motion of left foot
286297
for k in range(0, n_steps, 2):
287298
t_begin = t_init + k * (t_ss + t_ds)
@@ -301,7 +312,7 @@ def compute_feet_path_and_poses(
301312

302313
# # Add constant part till the next step
303314
t_begin = t_init + k * (t_ss + t_ds) + t_ss
304-
t_end = total_time
315+
t_end = t[-1]
305316
mask = (t >= t_begin) & (t < t_end)
306317
lf_path[mask, 0] = steps_pose[k + 1][0]
307318

@@ -324,11 +335,11 @@ def compute_feet_path_and_poses(
324335

325336
# # Add constant part till the next step
326337
t_begin = t_init + k * (t_ss + t_ds) + t_ss
327-
t_end = total_time
338+
t_end = t[-1]
328339
mask = (t >= t_begin) & (t < t_end)
329340
rf_path[mask, 0] = steps_pose[k + 1][0]
330341

331-
return t, lf_path, rf_path, steps_pose, phases
342+
return t, lf_path, rf_path, phases
332343

333344

334345
def clamp_to_polygon(pnt: np.ndarray, poly: Polygon):
@@ -442,3 +453,37 @@ def get_active_polygon(t: float, steps_pose, t_ss: float, t_ds: float, foot_shap
442453
return compute_single_support_polygon(steps_pose[-1], foot_shape)
443454
else:
444455
return compute_double_support_polygon(steps_pose[i], steps_pose[i + 1], foot_shape)
456+
457+
458+
def compute_swing_foot_pose(
459+
t_state: float,
460+
params: WalkingStateMachineParams,
461+
step_start: np.ndarray,
462+
step_target: np.ndarray,
463+
touchdown_extension_vel: float,
464+
path_generator,
465+
) -> np.ndarray:
466+
"""
467+
Compute swing foot pose for current state time with late touchdown extension.
468+
469+
- For t_state in [0, t_ss]: nominal min-jerk swing between step_start and step_target.
470+
- For t_state > t_ss and no contact: keep moving the *commanded* foot down
471+
below the nominal ground height at constant velocity.
472+
The physics/contact solver will clamp penetration.
473+
- As soon as contact_force > force_threshold: freeze at step_target.
474+
"""
475+
t_ss = params.t_ss
476+
477+
if t_state <= t_ss:
478+
# Normal swing phase
479+
s = np.clip(t_state / t_ss, 0.0, 1.0)
480+
481+
return path_generator(step_start, step_target, s)
482+
483+
# Late-touchdown extension phase
484+
# We assume step_target[2] is the nominal ground height.
485+
# After nominal end of swing, keep commanding the foot below that plane.
486+
dt = t_state - t_ss
487+
pos = step_target.copy()
488+
pos[2] = step_target[2] - touchdown_extension_vel * dt
489+
return pos

biped_walking_controller/plot.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
compute_double_support_polygon,
33
compute_single_support_polygon,
44
)
5-
6-
from biped_walking_controller.preview_control import WalkingState
5+
from biped_walking_controller.state_machine import WalkingState
76

87

98
def plot_steps(axes, steps_pose, step_shape):
@@ -188,7 +187,7 @@ def traj2d(arr): # drop last 2 samples as in your code
188187
ax2.set_title(f"{title_prefix} — plan view (x–y)")
189188

190189

191-
def plot_contact_forces(
190+
def plot_contact_forces_and_state(
192191
t: float,
193192
force_rf: float,
194193
force_lf: float,
@@ -200,18 +199,24 @@ def plot_contact_forces(
200199
force_lf = np.asarray(force_lf).ravel()
201200
assert t.size == force_rf.size == force_lf.size
202201

203-
fig, ax = plt.subplots(2, figsize=(12, 8), layout="constrained")
204-
ax[0].plot(t, force_rf, label="right foot")
205-
ax[0].plot(t, force_lf, label="left foot")
206-
ax[0].set_xlabel("t [s]")
202+
fig, ax = plt.subplots(2, figsize=(12, 8), layout="constrained", sharex=True)
203+
ax[0].plot(t, force_rf, label="right foot", marker="*")
204+
ax[0].plot(t, force_lf, label="left foot", marker="*")
207205
ax[0].set_ylabel("Normal force [N]")
208-
ax[0].set_title(title)
206+
ax[0].set_title("Contact Force Fx")
209207
ax[0].grid(True)
210208
ax[0].legend()
211209

212-
ax[1].plot(t, states, label="state")
210+
ticks = []
211+
labels = []
212+
for data in WalkingState:
213+
ticks.append(data.value)
214+
labels.append(data.name)
215+
216+
ax[1].plot(t, states, label="state", marker="*")
213217
ax[1].set_xlabel("t [s]")
214218
ax[1].set_ylabel("State [-]")
215-
ax[1].set_title(title)
219+
ax[1].set_title("State")
220+
ax[1].set_yticks(ticks, labels)
216221
ax[1].grid(True)
217222
ax[1].legend()

0 commit comments

Comments
 (0)