3.1 Appendix: Smoothing Splines#
This notebook shows several examples of smoothing splines.
Deep dive into smoothing splines#
Make sure to read https://docs.scipy.org/doc/scipy/tutorial/interpolate/smoothing_splines.html from Scipy docs that explains smoothing splines in detail.
Setting up the Jupyter notebook for experimentation#
This documentation has been generated from a Jupyter notebook and is available in the repo source code (see link below).
The next couple of cells are designated to the setup of the notebook environment. If you are not interested in the experimentation and only want to read the documentation, feel free to skip them.
The first step is to enable live python modules reloading, so changes in the python code of imported files are immediately reflected in the notebook without restarting the kernel.
# Enable python modules live reloading
%load_ext autoreload
%autoreload 2
The next step is configuring matplotlib backend. Widget backend allows to interact with the plots in the notebook and is supported in Google Colab and VSCode.
%config InlineBackend.figure_formats = ['svg']
%matplotlib widget
from IPython.display import display
import matplotlib.pyplot as plt
plt.ioff() # this is equivalent to using inline backend, but figures have to be displayed manually
<contextlib.ExitStack at 0x7a0d9494d040>
BSpline#
# Example from https://docs.scipy.org/doc/scipy/tutorial/interpolate/smoothing_splines.html
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import make_splrep
x = np.arange(0, 2 * np.pi + np.pi / 4, 2 * np.pi / 16)
rng = np.random.default_rng()
y = np.sin(x) + 0.4 * rng.standard_normal(size=len(x))
xnew = np.arange(0, 9 / 4, 1 / 50) * np.pi
fig = plt.figure()
fig.set_figheight(6)
plt.plot(xnew, np.sin(xnew), '-.', label='sin(x)')
plt.plot(xnew, make_splrep(x, y, s=0, k=4)(xnew), '-', label='s=0')
plt.plot(xnew, make_splrep(x, y, s=len(x), k=3)(xnew), '-', label=f's={len(x)}, k=3')
plt.plot(xnew, make_splrep(x, y, s=len(x), k=4)(xnew), '-', label=f's={len(x)}, k=4')
plt.plot(xnew, make_splrep(x, y, s=len(x), k=5)(xnew), '-', label=f's={len(x)}, k=5')
plt.plot(x, y, 'o')
plt.legend()
display(fig)
plt.close(fig)
BSpline in application to gaits trajectory approximation#
from scipy.interpolate import make_interp_spline
def bezier_spline(control_points, num_points=100):
"""
Compute a smooth spline interpolation of control points using make_interp_spline.
:param control_points: List of control points [(x0, y0), (x1, y1), ...]
:param num_points: Number of points in the interpolated curve
:return: Interpolated curve points
"""
control_x, control_y = zip(*control_points)
t = np.linspace(0, 1, len(control_points))
spline_x = make_interp_spline(t, control_x, k=3)
spline_y = make_interp_spline(t, control_y, k=3)
return spline_x, spline_y
control_points = np.array([[0, 0], [1, 2], [3, 3], [4, 0]])
spline_x, spline_y = bezier_spline(control_points)
# Plot curve and control points
control_x, control_y = zip(*control_points)
# Evaluate the spline at a sequence of points
t = np.linspace(0, 1, 100)
curve_x = spline_x(t)
curve_y = spline_y(t)
fig = plt.figure()
fig.set_figheight(6)
plt.plot(curve_x, curve_y, label='Spline Curve')
plt.scatter(control_x, control_y, color='red', label='Control Points')
plt.plot(control_x, control_y, 'r--', alpha=0.5)
plt.legend()
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Spline Interpolation Curve')
display(fig)
plt.close(fig)
Bezier curve using B-spline#
The SciPy’s BSpline class can represent Bezier curves, as Bezier curves are a special case of B-splines.
To define a Bezier curve in SciPy, one needs to specify the control points and the degree of the curve. For example, a cubic Bezier curve (degree 3) requires four control points. The BSpline class then creates a spline object, which can be evaluated at any point to obtain the corresponding point on the curve.
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import BSpline
# Define control points for a cubic Bezier curve
control_points = np.array([[0, 0], [1, 2], [3, 3], [4, 0]])
# Define the degree of the curve (cubic)
degree = 3
# Create a clamped knot vector for a cubic Bézier curve with 4 control points
knots = [0, 0, 0, 0, 1, 1, 1, 1]
# Create the BSpline object (each coordinate must be passed separately)
spl_x = BSpline(knots, control_points[:, 0], degree)
spl_y = BSpline(knots, control_points[:, 1], degree)
# Evaluate the spline at a sequence of points
t = np.linspace(0, 1, 100)
curve_points_x = spl_x(t)
curve_points_y = spl_y(t)
# Plot the curve and control points
fig = plt.figure()
fig.set_figheight(6)
plt.plot(curve_points_x, curve_points_y, label='Bézier Curve')
plt.plot(control_points[:, 0], control_points[:, 1], 'o--', label='Control Points')
plt.legend()
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Cubic Bézier Curve using B-Spline')
plt.grid()
display(fig)
plt.close(fig)
from math import comb
import matplotlib.pyplot as plt
def bezier_curve(control_points: np.ndarray, t):
"""
Compute a point on a Bézier curve using the Bernstein polynomial form.
:param control_points: List of control points [(x0, y0), (x1, y1), ...]
:param t: Parameter in range [0,1]
:return: (x, y) coordinate of the curve at parameter t
"""
control_points = np.array(control_points)
n = len(control_points) - 1
result = np.zeros(control_points.shape[1])
for i, p in enumerate(control_points):
bernstein = comb(n, i) * (t**i) * ((1 - t) ** (n - i))
result += bernstein * p
return result
spline_x_all = np.array([0, 0.2, 0.3, 0.5, 0.6, 1.0])
spline_z_all = np.array(
[
0,
1,
1,
0.5,
0,
0,
]
)
for num_items in range(2, len(spline_x_all) + 1):
fig, axes = plt.subplots(1, 1)
spline_x = np.array([0, 0.2, 0.3, 0.5, 0.6, 1.0])[0:num_items]
spline_z = np.array(
[
0,
1,
1,
0.5,
0,
0,
]
)[0:num_items]
direct_trajectory = make_interp_spline(spline_x, spline_z, k=1) if num_items > 1 else None
bspline_trajectory = make_interp_spline(spline_x, spline_z, k=2) if num_items > 2 else None
phase = np.linspace(0, spline_x[-1], 100)
##############################
ax = axes
ax.set_xlabel('phase')
ax.set_ylabel('x')
ax.set_title(f'B-spline trajectory mixing {num_items=}')
ax.plot(
phase, direct_trajectory(phase), linestyle='-.', label='Direct'
) if direct_trajectory else None
ax.plot(phase, bspline_trajectory(phase), label='B-spline') if bspline_trajectory else None
ax.plot(
phase,
[bezier_curve(np.array([spline_x, spline_z]).T, t)[1] for t in phase],
linestyle='--',
label='Bezier',
)
ax.scatter(spline_x, spline_z, c='r', label='Trajectory points')
ax.legend()
display(fig)
plt.close(fig)
Pure python implementation of Bézier curve#
Here’s a pure Python function for a Bézier curve of any degree, parameterized by \(t\) in the range \([0,1]\). It uses the Bernstein polynomial form to compute the interpolated point for a given \(t\).
Bezier Interpolation#
This function computes a Bézier curve point for a given \(t\) by iterating through control points and applying the Bernstein polynomial formula.
import matplotlib.pyplot as plt
def bezier_curve(control_points: np.ndarray, t):
"""
Compute a point on a Bézier curve using the Bernstein polynomial form.
:param control_points: List of control points [(x0, y0), (x1, y1), ...]
:param t: Parameter in range [0,1]
:return: (x, y) coordinate of the curve at parameter t
"""
control_points = np.array(control_points)
n = len(control_points) - 1
result = np.zeros(control_points.shape[1])
for i, p in enumerate(control_points):
bernstein = comb(n, i) * (t**i) * ((1 - t) ** (n - i))
result += bernstein * p
return result
# Example usage
control_pts = [(0, 0), (1, 2), (3, 3), (4, 0)]
t_values = [i / 100 for i in range(101)]
points = [bezier_curve(control_pts, t) for t in t_values]
# Plot curve and control points
curve_x, curve_y = zip(*points)
control_x, control_y = zip(*control_pts)
fig = plt.figure()
fig.set_figheight(6)
plt.plot(curve_x, curve_y, label='Bézier Curve')
plt.scatter(control_x, control_y, color='red', label='Control Points')
plt.plot(control_x, control_y, 'r--', alpha=0.5)
plt.legend()
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Bézier Curve')
display(fig)
plt.close(fig)
phase = np.linspace(0, 1, 100)
def leg_control_point(phase):
lift_duration = 1 / 6
step_height = 40
half_step = 50 / 2
if phase > 0.0001 and phase < lift_duration:
# Swing phase - leg in air moving forward
x_control_point = half_step
z_control_point = step_height
else:
# Stance phase - leg on ground moving backward
x_control_point = -half_step
z_control_point = 0 # On ground
return x_control_point, z_control_point
fig, ax = plt.subplots(2, 1)
ax[0].set_xlabel('phase')
ax[0].set_ylabel('x offset')
ax[0].set_title('Control Points\nX offset')
ax[0].plot(phase, [leg_control_point(p)[0] for p in phase])
ax[1].set_xlabel('phase')
ax[1].set_ylabel('z offset')
ax[1].set_title('Z offset')
ax[1].plot(phase, [leg_control_point(p)[1] for p in phase])
display(fig)
plt.close(fig)
phase = np.linspace(0, 1, 100)
def leg_gait_point(phase):
lift_duration = 1 / 6
step_height = 40
half_step = 50 / 2
if phase > 0.0001 and phase < lift_duration:
# Swing phase - leg in air moving forward
t = np.interp(phase, [0, lift_duration], [0, 1])
x_control_point = np.interp(phase, [0, lift_duration], [-half_step, half_step])
z_control_point = np.sin(t * np.pi) * step_height
else:
# Stance phase - leg on ground moving backward
x_control_point = np.interp(phase, [lift_duration, 1], [half_step, -half_step])
z_control_point = 0 # On ground
return x_control_point, z_control_point
fig, ax = plt.subplots(2, 1)
ax[0].set_xlabel('phase')
ax[0].set_ylabel('x offset')
ax[0].set_title('Leg Gait\nX offset')
ax[0].plot(phase, [leg_gait_point(p)[0] for p in phase])
ax[0].plot(phase, [leg_control_point(p)[0] for p in phase])
ax[1].set_xlabel('phase')
ax[1].set_ylabel('z offset')
ax[1].set_title('Z offset')
ax[1].plot(phase, [leg_gait_point(p)[1] for p in phase])
ax[1].plot(phase, [leg_control_point(p)[1] for p in phase])
display(fig)
plt.close(fig)
gait_points = np.array([leg_gait_point(p) for p in phase])
control_pts = np.array([leg_control_point(p) for p in phase])
bezier_control_pts = np.array([bezier_curve(control_pts, t) for t in phase])
bezier_gaits_points = np.array([bezier_curve(gait_points, t) for t in phase])
bezier_control_pts_pairs = []
for i in range(len(control_pts) - 1):
p = bezier_curve(control_pts[i : i + 2], i / len(control_pts))
bezier_control_pts_pairs.append(p)
bezier_control_pts_pairs = np.array(bezier_control_pts_pairs)
fig, ax = plt.subplots(2, 1)
ax[0].set_xlabel('phase')
ax[0].set_ylabel('x offset')
ax[0].set_title('Leg Bézier Curve from Control\nX offset')
ax[0].plot(phase, gait_points[:, 0], label='Gait')
ax[0].plot(phase, control_pts[:, 0], label='Control')
ax[0].plot(phase, bezier_control_pts[:, 0], label='Bezier (Control)')
ax[0].plot(phase, bezier_gaits_points[:, 0], label='Bezier (Gait)')
ax[0].plot(phase[0:-1], bezier_control_pts_pairs[:, 0], label='Bezier (Control Pairs)')
ax[0].legend()
ax[1].set_xlabel('phase')
ax[1].set_ylabel('z offset')
ax[1].set_title('Z offset')
ax[1].plot(phase, gait_points[:, 1], label='Gait')
ax[1].plot(phase, control_pts[:, 1], label='Control')
ax[1].plot(phase, bezier_control_pts[:, 1], label='Bezier (Control)')
ax[1].plot(phase, bezier_gaits_points[:, 1], label='Bezier (Gait)')
ax[1].plot(phase[0:-1], bezier_control_pts_pairs[:, 1], label='Bezier (Control Pairs)')
ax[1].legend()
display(fig)
plt.close(fig)
phase = np.linspace(0, 1, 50)
def leg_bezier_gait_point(last_pos, phase):
lift_duration = 1 / 6
step_height = 40
half_step = 50 / 2
if phase > 0.0001 and phase < lift_duration:
# Swing phase - leg in air moving forward
t = np.interp(phase, [0, lift_duration], [0, 1])
x_control_point = half_step
z_control_point = step_height
else:
# Stance phase - leg on ground moving backward
t = np.interp(phase, [lift_duration, 1], [0, 1])
x_control_point = -half_step
z_control_point = 0 # On ground
control_pos = np.array([x_control_point, z_control_point])
return bezier_curve(np.array([last_pos, control_pos]), t)
gait_points = np.array([leg_gait_point(p) for p in phase])
control_pts = np.array([leg_control_point(p) for p in phase])
leg_bezier_gait_points = []
last_pos = np.array([0, 0])
for p in phase:
last_pos = leg_bezier_gait_point(last_pos, p)
leg_bezier_gait_points.append(last_pos)
leg_bezier_gait_points = np.array(leg_bezier_gait_points)
fig, ax = plt.subplots(3, 1)
ax[0].set_xlabel('phase')
ax[0].set_ylabel('x offset')
ax[0].set_title('Leg Bézier Curve from Control\nX offset')
ax[0].plot(phase, gait_points[:, 0], label='Gait')
ax[0].plot(phase, control_pts[:, 0], label='Control')
ax[0].plot(phase, leg_bezier_gait_points[:, 0], label='leg_bezier_gait_points')
ax[0].legend()
ax[1].set_xlabel('phase')
ax[1].set_ylabel('z offset')
ax[1].set_title('Z offset')
ax[1].plot(phase, gait_points[:, 1], label='Gait')
ax[1].plot(phase, control_pts[:, 1], label='Control')
ax[1].plot(phase, leg_bezier_gait_points[:, 1], label='leg_bezier_gait_points')
ax[1].legend()
# plot both axis
ax[2].set_xlabel('x offset')
ax[2].set_ylabel('z offset')
ax[2].set_title('Both axis')
ax[2].plot(gait_points[:, 0], gait_points[:, 1], label='Gait')
ax[2].plot(control_pts[:, 0], control_pts[:, 1], label='Control')
ax[2].plot(
leg_bezier_gait_points[:, 0],
leg_bezier_gait_points[:, 1],
label='leg_bezier_gait_points',
)
ax[2].legend()
display(fig)
plt.close(fig)
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import make_interp_spline
# Given non-uniformly spaced time points
t = np.array([0, 1, 2.5, 4, 6]) # Time values (not uniformly spaced)
x = np.array([0, 2, 3, 5, 8]) # X trajectory
y = np.array([1, 3, 1, 4, 6]) # Y trajectory
z = np.array([0, 1, 0, 2, 3]) # Z trajectory
# Define a finer time grid for smooth interpolation
t_fine = np.linspace(t[0], t[-1], 100)
# Create B-spline interpolators for each dimension
spline_x = make_interp_spline(t, x, k=3) # Cubic B-spline
spline_y = make_interp_spline(t, y, k=3)
spline_z = make_interp_spline(t, z, k=3)
# Evaluate splines at the finer time grid
x_smooth = spline_x(t_fine)
y_smooth = spline_y(t_fine)
z_smooth = spline_z(t_fine)
# Plot the results
fig = plt.figure()
fig.set_figheight(6)
ax = fig.add_subplot(111, projection='3d')
ax.plot(x, y, z, 'ro', label='Waypoints') # Original points
ax.plot(x_smooth, y_smooth, z_smooth, 'b-', label='B-spline trajectory')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.legend()
display(fig)
plt.close(fig)
from collections import deque
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import make_interp_spline
# Rolling buffer for waypoints (FIFO queue)
max_waypoints = 5 # Keep last N points
time_buffer = deque(maxlen=max_waypoints)
x_buffer = deque(maxlen=max_waypoints)
y_buffer = deque(maxlen=max_waypoints)
z_buffer = deque(maxlen=max_waypoints)
# Initialize with some waypoints
initial_waypoints = [(0, 0, 1, 0), (1, 2, 3, 1), (2.5, 3, 1, 0), (4, 5, 4, 2)]
for t, x, y, z in initial_waypoints:
time_buffer.append(t)
x_buffer.append(x)
y_buffer.append(y)
z_buffer.append(z)
def update_spline(new_waypoint):
"""Update B-spline when a new waypoint arrives."""
# Add new point to buffer
t_new, x_new, y_new, z_new = new_waypoint
time_buffer.append(t_new)
x_buffer.append(x_new)
y_buffer.append(y_new)
z_buffer.append(z_new)
# Convert buffers to NumPy arrays
t = np.array(time_buffer)
x = np.array(x_buffer)
y = np.array(y_buffer)
z = np.array(z_buffer)
# Ensure we have enough points for cubic interpolation
if len(t) >= 4:
t_fine = np.linspace(t[0], t[-1], 100)
# Create cubic B-splines for each axis
spline_x = make_interp_spline(t, x, k=3)
spline_y = make_interp_spline(t, y, k=3)
spline_z = make_interp_spline(t, z, k=3)
# Evaluate spline
x_smooth = spline_x(t_fine)
y_smooth = spline_y(t_fine)
z_smooth = spline_z(t_fine)
# Plot updated trajectory
plt.clf()
fig = plt.figure()
fig.set_figheight(6)
ax = fig.add_subplot(111, projection='3d')
ax.plot(x, y, z, 'ro', label='Waypoints')
ax.plot(x_smooth, y_smooth, z_smooth, 'b-', label='B-spline Trajectory')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.legend()
display(fig)
plt.close(fig)
# Simulate incoming new waypoints dynamically
for new_wp in [(6, 8, 6, 3), (8, 10, 9, 5), (10, 12, 11, 7)]:
update_spline(new_wp)
from collections import deque
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import make_interp_spline
# Rolling buffer for waypoints
max_waypoints = 5 # Maintain last N points
waypoint_buffer = deque(maxlen=max_waypoints)
# Initialize with some waypoints (x, y, z)
initial_waypoints = [(0, 0, 1), (2, 3, 3), (3, 1, 0), (5, 4, 2)]
for wp in initial_waypoints:
waypoint_buffer.append(wp)
def compute_arc_length(points):
"""Compute cumulative arc length for given waypoints."""
points = np.array(points)
diffs = np.diff(points, axis=0)
segment_lengths = np.linalg.norm(diffs, axis=1) # Euclidean distance
arc_length = np.insert(np.cumsum(segment_lengths), 0, 0) # Cumulative sum
return arc_length
def update_spline(new_waypoint):
"""Update B-spline when a new waypoint arrives using arc-length parameterization."""
waypoint_buffer.append(new_waypoint)
if len(waypoint_buffer) < 4: # Need at least 4 points for cubic spline
return
# Convert buffer to NumPy array
waypoints = np.array(waypoint_buffer)
# Compute cumulative arc length
s = compute_arc_length(waypoints)
# Create finer arc-length parameter grid
s_fine = np.linspace(s[0], s[-1], 100)
# Create B-spline for each coordinate
spline_x = make_interp_spline(s, waypoints[:, 0], k=3)
spline_y = make_interp_spline(s, waypoints[:, 1], k=3)
spline_z = make_interp_spline(s, waypoints[:, 2], k=3)
# Evaluate splines
x_smooth = spline_x(s_fine)
y_smooth = spline_y(s_fine)
z_smooth = spline_z(s_fine)
# Plot updated trajectory
plt.clf()
fig = plt.figure()
fig.set_figheight(6)
ax = fig.add_subplot(111, projection='3d')
ax.plot(waypoints[:, 0], waypoints[:, 1], waypoints[:, 2], 'ro', label='Waypoints')
ax.plot(x_smooth, y_smooth, z_smooth, 'b-', label='B-spline Trajectory')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.legend()
display(fig)
plt.close(fig)
# Simulate incoming waypoints dynamically
for new_wp in [(8, 6, 3), (10, 9, 5), (12, 11, 7)]:
update_spline(new_wp)
from smoothing_splines import plot_spline, SplineType
t = np.array([0, 1, 2.5, 4, 6]) # Time values (not uniformly spaced)
x = np.array([0, 2, 3, 5, 8]) # X trajectory
y = np.array([1, 3, 1, 4, 6]) # Y trajectory
control_points = np.array([x, y, t]).T
def plot_with_legend(plot: callable):
fig, ax = plt.subplots(1, 1)
fig.set_figheight(4, forward=True)
ax.scatter(x, y, c='k', label='Control points')
plot(ax)
ax.legend(bbox_to_anchor=(1.0, 0.97), loc='lower right')
display(fig)
plt.close(fig)
plot_with_legend(lambda ax: plot_spline(ax, 0, control_points, 3, derivatives=2))
plot_with_legend(lambda ax: plot_spline(ax, 0, control_points, 3, derivatives=2, bc_type='natural'))
plot_with_legend(lambda ax: plot_spline(ax, 0, control_points, 3, derivatives=2, bc_type='clamped'))
plot_with_legend(
lambda ax: plot_spline(
ax, 0, control_points, 0.5, derivatives=2, spline_type=SplineType.smoothing
)
)
plot_with_legend(
lambda ax: plot_spline(ax, 0, control_points, 0.5, derivatives=2, spline_type=SplineType.splrep)
)
plot_with_legend(
lambda ax: (
plot_spline(ax, 0, control_points, 0.01, spline_type=SplineType.smoothing),
plot_spline(ax, 0, control_points, 0.5, spline_type=SplineType.splprep),
plot_spline(ax, 0, control_points, 1, spline_type=SplineType.splprep),
)
)
plot_with_legend(
lambda ax: (
plot_spline(ax, 0, control_points, 1, mix=1),
plot_spline(ax, 0, control_points, 3, mix=0.5),
plot_spline(ax, 0, control_points, 3, mix=1),
)
)
An abandoned attempt to use smoothing splines for gait generation#
Summary and steps forward (pun intended)#
With the current approach we have achieved decent results and it helped us to get a basic understanding of gaits generation, however it has a serious limitations:
It is not possible to transition between gaits as they are implemented as separate classes
There is no transition in and out of the gait from standing position.
Different gaits have different trajectories, however the only thing that has to change is the order in which legs are lifted.
Phase logic is mixed with trajectory logic.
Lets rework the code to address all these issues and have production ready solution we will use in the next notebook that will be taking all we have learned so far to real ROS implementation controlling a simulated robot in Gazebo.
Our new approach should satisfy the following requirements:
Allow defining a gait trajectory.
Allow defining a gait sequence.
Allow defining a gait generator function that will combine the two above given a set of parameters.
Allow steering, turing and transitioning between gaits and positions.
1 and 2 are fairly straightforward and we have seen some solution using trigonometrical functions and polynomials already, however they do not allow proper mixing of the trajectories needed to achieve smooth transitions. 3 and 4 require a function that allows mixing. That is a task for a smoothing spline function, e.g. a B-spline. It allows a smooth transition between control points while remaining stable if some of the control points are changed.
Gait trajectory function#
A good trajectory function has a smooth lift stage and flat stance stage. A simple rectified sinusoidal function did the trick in the previous example of tripod gait:
(see plot below)
The next step is to define control points for the B-spline that will give a similar trajectory.
import matplotlib.pyplot as plt
from scipy.interpolate import make_interp_spline
def rectified_sin_trajectory(phase):
t = phase * 2 * np.pi
return np.maximum(0, np.sin(t))
phase = np.linspace(0, 1, 100)
rectified_sin_z = rectified_sin_trajectory(phase)
##############################
fig, axes = plt.subplots(2, 1)
ax = axes[0]
ax.set_xlabel('phase')
ax.set_ylabel('z offset')
ax.set_title('Rectified sine')
ax.plot(phase, rectified_sin_z)
##############################
num_items = 9
spline_x = np.array([0, 0.2, 0.3, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])[0:num_items]
spline_z = np.array([0, 1, 1, 0, 0, 0, 0, 0, 0])[0:num_items]
bspline_trajectory = make_interp_spline(spline_x, spline_z, k=3)
##############################
ax = axes[1]
ax.set_xlabel('phase')
ax.set_ylabel('z offset')
ax.set_title('B-spline')
phase_new = np.linspace(0, spline_x[-1], 50)
ax.plot(phase_new, bspline_trajectory(phase_new))
ax.scatter(spline_x, spline_z, c='r')
display(fig)
plt.close(fig)
For the sake of completeness, this is how bezier curve will look like. As you can see, it doesn’t follow control points, and thus is not the best choice for our use case.
fig, ax = plt.subplots(1, 1)
ax.set_xlabel('phase')
ax.set_ylabel('z offset')
ax.set_title('Bezier curve')
xnew = np.linspace(0, 1, 10)
points = np.array([spline_x, spline_z]).T
ax.plot(phase, [bezier_curve(points, t)[1] for t in phase])
ax.scatter(points[:, 0], points[:, 1], c='r')
display(fig)
plt.close(fig)
Defining gaits as b-spline control points#
Having BSplines as interpolation mechanism it is easy to define gaits as a sequence of control points.
As we already know every gait has two phases: swing and stance. The first parameter for those phases is the duration.
Each phase has its own additional parameters:
swing phase:
lift height
step length
swing speed
stance phase:
step length
stance speed
To keep things simple for the first iteration we are going to keep speed constant and equal between phases.
from abc import abstractmethod
import enum
from gait_generators import GaitGenerator
from models import HexapodLeg
from point import Point3D
from scipy.interpolate import make_interp_spline
class Gait(enum.Enum):
ripple = enum.auto()
wave = enum.auto()
tripod = enum.auto()
class GaitParameters:
def __init__(self, step_length=50, step_height=40):
self.step_length = step_length
self.step_height = step_height
@property
@abstractmethod
def lift_duration(self):
pass
@property
def phase_offset_for_leg(self, leg):
pass
class ParametricGaitGenerator(GaitGenerator):
def __init__(
self,
gait=Gait.wave,
step_length=50, # Length of each step in meters
step_height=40, # Height of leg lift in meters
):
"""
Initialize the ParametricGaitGenerator.
Parameters:
-----------
step_length: Length of each step in meters
step_height: Height of leg lift in meters
"""
# Store parameters as member fields
self.step_length = step_length
self.step_height = step_height
self.full_phase = 1.0
self.__gait = gait
# Leg phase is split into lift and push phases.
# 0..lift_duration - lift phase
# lift_duration..1.0 - push phase
if self.__gait == Gait.wave:
__leg_sequence = [
HexapodLeg.right_back,
HexapodLeg.right_middle,
HexapodLeg.right_front,
HexapodLeg.left_back,
HexapodLeg.left_middle,
HexapodLeg.left_front,
]
leg_count = len(__leg_sequence)
self.lift_duration = self.full_phase / leg_count
self.leg_phase_offset = {
leg: self.lift_duration * i for i, leg in enumerate(__leg_sequence)
}
self.last_offset = {leg: np.array([0, 0, 0]) for leg in __leg_sequence}
self.last_phase = {leg: 0 for leg in __leg_sequence}
else:
raise NotImplementedError()
super().__init__()
def get_offsets_at_phase(
self, phase, gait=Gait.wave, return_control_points=False
) -> dict[str, Point3D]:
return {
leg: self.get_offsets_at_phase_for_leg(leg, phase, gait, return_control_points)
for leg in self.leg_phase_offset
}
def get_offsets_at_phase_for_leg(
self, leg, phase, gait=Gait.wave, return_control_points=True
) -> Point3D:
leg_phase = self.leg_phase_offset[leg] + phase
# leg_phase = phase # ignore leg offset for now, all legs lift at the same time
leg_phase %= self.full_phase
next_control_point = self.get_control_points_at_phase(leg_phase)
if return_control_points:
return Point3D(next_control_point)
# last_phase = self.last_phase[leg]
# since_last_phase = leg_phase - self.last_phase[leg]
self.last_phase[leg] = leg_phase
# spline_phase_step = 0.1
spline = np.array(
[
self.last_offset[leg],
next_control_point,
# self.get_control_points_at_phase(leg_phase + spline_phase_step),
# self.get_control_points_at_phase(leg_phase + spline_phase_step * 2),
# self.get_control_points_at_phase(leg_phase + spline_phase_step * 3),
# self.get_control_points_at_phase(leg_phase + spline_phase_step * 4),
]
)
if leg_phase < self.lift_duration:
# Swing phase - leg in air moving forward
spline_phase = np.interp(leg_phase, [0, self.lift_duration], [0, 1])
else:
# Stance phase - leg on ground moving backward
spline_phase = np.interp(leg_phase, [self.lift_duration, 1], [0, 1])
result = bezier_curve(spline, spline_phase)
result[0] = next_control_point[0]
# spline_x, spline_y, spline_z = self.__bspline(spline)
# spline_phase = np.interp(leg_phase, [last_phase, last_phase + since_last_phase + spline_phase_step * 4], [0, 1])
# result = [spline_x(spline_phase), spline_y(spline_phase), spline_z(spline_phase)]
self.last_offset[leg] = result
return Point3D(result)
def get_control_points_at_phase(self, leg_phase) -> np.ndarray:
half_step = self.step_length / 2
if leg_phase < self.lift_duration:
# Swing phase - leg in air moving forward
t = np.interp(leg_phase, [0, self.lift_duration], [0, 1])
x_control_point = np.interp(leg_phase, [0, self.lift_duration], [-half_step, half_step])
z_control_point = np.sin(t * np.pi) * self.step_height
else:
# Stance phase - leg on ground moving backward
x_control_point = np.interp(leg_phase, [self.lift_duration, 1], [half_step, -half_step])
z_control_point = 0 # On ground
return np.array(
[
x_control_point,
0,
z_control_point,
]
)
@staticmethod
def __bspline(control_points: np.ndarray):
"""
Compute a smooth B-spline interpolation of control points using make_interp_spline.
:param control_points: List of control points [[x0, y0], [x1, y1], ...]
:return: Interpolated curve points
"""
t = np.linspace(0, 1, len(control_points))
control_x, control_y, control_z = zip(*control_points)
spline_x = make_interp_spline(t, control_x)
spline_y = make_interp_spline(t, control_y)
spline_z = make_interp_spline(t, control_z)
return spline_x, spline_y, spline_z
gait_gen = ParametricGaitGenerator()
gait_gen.visualize_continuous(_steps=100, return_control_points=True)
_ = gait_gen.visualize_continuous_in_3d(_steps=100, return_control_points=True)
# gait_gen.visualize_continuous(_steps=100, return_control_points=False)
# _ = gait_gen.visualize_continuous_in_3d(_steps=100, return_control_points=False)
# hexapod = HexapodModel()
# hexapod.forward_kinematics(0, -25, 110)
# directional_param_gait_gen = DirectionalGaitGenerator(gait_gen)
# anim = animate_hexapod_gait_with_direction(
# hexapod,
# directional_param_gait_gen,
# interactive=False,
# skip=True, ######
# animate_trajectory=True,
# animate_direction_degrees=False,
# repeat=1,
# )
# inter = animate_hexapod_gait_with_direction(
# hexapod, directional_param_gait_gen, interactive=True, skip=False
# )