Module pyastrobee.control.force_torque_control
Force + torque PID control of the Astrobee body
Note: Forces and torques are currently applied in world frame
View Source
"""Force + torque PID control of the Astrobee body
Note: Forces and torques are currently applied in world frame
"""
# TODO: Add better handling of body-frame forces and torques (more aligned with how Astrobee's thrusters operate)
# TODO: Operational space control
from typing import Optional
import pybullet
from pybullet_utils.bullet_client import BulletClient
import numpy as np
import numpy.typing as npt
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from pyastrobee.trajectories.trajectory import (
Trajectory,
TrajectoryLogger,
stopping_criteria,
)
from pyastrobee.utils.quaternions import quaternion_angular_error
from pyastrobee.utils.rotations import quat_to_rmat
class ForceTorqueController:
"""PID-style force/torque control
Args:
robot_id (int): Pybullet ID of the robot to control
mass (float): Mass of the robot
inertia (np.ndarray): Inertia tensor for the robot, shape (3, 3)
kp (float): Gain for position error
kv (float): Gain for velocity error
kq (float): Gain for orientation (quaternion) error
kw (float): Gain for angular velocity (omega) error
dt (float): Timestep
pos_tol (float, optional): Stopping tolerance on position error magnitude. Defaults to 1e-2.
orn_tol (float, optional): Stopping tolerance on quaternion distance between cur/des. Defaults to 1e-2.
vel_tol (float, optional): Stopping tolerance on linear velocity error magnitude. Defaults to 1e-2.
ang_vel_tol (float, optional): Stopping tolerance on angular velocity error magnitude. Defaults to 5e-3.
max_force (Optional[float]): Limit on the applied force magnitude. Defaults to None (no limit)
max_torque (Optional[float]): Limit on the applied torque magnitude. Defaults to None (no limit)
client (BulletClient, optional): If connecting to multiple physics servers, include the client
(the class instance, not just the ID) here. Defaults to None (use default connected client)
"""
def __init__(
self,
robot_id: int,
mass: float,
inertia: np.ndarray,
kp: float,
kv: float,
kq: float,
kw: float,
dt: float,
pos_tol: float = 1e-2,
orn_tol: float = 1e-2,
vel_tol: float = 1e-2,
ang_vel_tol: float = 5e-3,
max_force: Optional[float] = None,
max_torque: Optional[float] = None,
client: Optional[BulletClient] = None,
):
self.id = robot_id
self.mass = mass
self.inertia = inertia
self.kp = kp
self.kv = kv
self.kq = kq
self.kw = kw
self.dt = dt
self.pos_tol = pos_tol
self.orn_tol = orn_tol
self.vel_tol = vel_tol
self.ang_vel_tol = ang_vel_tol
self.max_force = max_force
self.max_torque = max_torque
self.traj_log = TrajectoryLogger()
self.control_log = ControlLogger()
self.client: pybullet = pybullet if client is None else client
# Parameters for checking if there was a quaternion sign flip
self.last_quat = np.array(self.client.getBasePositionAndOrientation(self.id)[1])
self.quat_sign = 1
# TODO figure out how world/robot frame should be handled
def get_force(
self,
cur_pos: npt.ArrayLike,
cur_vel: npt.ArrayLike,
des_pos: npt.ArrayLike,
des_vel: npt.ArrayLike,
des_accel: npt.ArrayLike,
) -> np.ndarray:
"""Calculates the required force to achieve a desired pos/vel/accel
Args:
cur_pos (npt.ArrayLike): Current position, shape (3,)
cur_vel (npt.ArrayLike): Current velocity, shape (3,)
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_vel (npt.ArrayLike): Desired velocity, shape (3,)
des_accel (npt.ArrayLike): Desired acceleration, shape (3,)
Returns:
np.ndarray: Force, (Fx, Fy, Fz), shape (3,)
"""
M = self.mass * np.eye(3)
pos_err = np.subtract(cur_pos, des_pos)
vel_err = np.subtract(cur_vel, des_vel)
return M @ np.asarray(des_accel) - self.kv * vel_err - self.kp * pos_err
def get_torque(
self,
cur_q: npt.ArrayLike,
cur_w: npt.ArrayLike,
des_q: npt.ArrayLike,
des_w: npt.ArrayLike,
des_a: npt.ArrayLike,
) -> np.ndarray:
"""Calculates the required torque to achieve a desired orientation/ang vel/ang accel
Args:
cur_q (npt.ArrayLike): Current orientation (XYZW quaternion), shape (4,)
cur_w (npt.ArrayLike): Current angular velocity (omega), shape (3,)
des_q (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
des_w (npt.ArrayLike): Desired angular velocity (omega), shape (3,)
des_a (npt.ArrayLike): Desired angular acceleration (alpha), shape (3,)
Returns:
np.ndarray: Torque, (Tx, Ty, Tz), shape (3,)
"""
if np.allclose(cur_q, -1 * self.last_quat, atol=1e-3):
print("Quaternion flip detected")
self.quat_sign *= -1
ang_err = quaternion_angular_error(cur_q * self.quat_sign, des_q)
self.last_quat = cur_q
ang_vel_err = cur_w - des_w
R = quat_to_rmat(cur_q)
world_inertia = R @ self.inertia @ R.T
# Standard 3D free-body torque equation based on desired ang. accel and current ang. vel
# Note: this ignores the m * r x a term
torque = world_inertia @ des_a + np.cross(cur_w, world_inertia @ cur_w)
# Add in the proportional and derivative terms
return torque - self.kw * ang_vel_err - self.kq * ang_err
def follow_traj(
self,
traj: Trajectory,
stop_at_end: bool = True,
max_stop_iters: Optional[int] = None,
) -> None:
"""Use PID force/torque control to follow a trajectory
Args:
traj (Trajectory): Trajectory with position, orientation, and derivative info across time
stop_at_end (bool, optional): Whether or not to command the robot to come to a stop at the last
pose in the trajectory. Defaults to True
max_stop_iters (int, optional): If stop_at_end is True, this gives a maximum number of iterations to
allow for stopping. Defaults to None (keep controlling until stopped)
"""
for i in range(traj.num_timesteps):
pos, orn, lin_vel, ang_vel = self.get_current_state()
self.step(
pos,
lin_vel,
orn,
ang_vel,
traj.positions[i, :],
traj.linear_velocities[i, :],
traj.linear_accels[i, :],
traj.quaternions[i, :],
traj.angular_velocities[i, :],
traj.angular_accels[i, :],
)
if stop_at_end:
self.stop(traj.positions[-1, :], traj.quaternions[-1, :], max_stop_iters)
def stop(
self,
des_pos: npt.ArrayLike,
des_quat: npt.ArrayLike,
max_iters: Optional[int] = None,
) -> None:
"""Controls the robot to stop at a desired position/orientation
Args:
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_quat (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
max_iters (int, optional): Maximum number of control iterations to allow for stopping.
Defaults to None (keep controlling until stopped)
"""
des_vel = np.zeros(3)
des_accel = np.zeros(3)
des_omega = np.zeros(3)
des_alpha = np.zeros(3)
iters = 0
while True:
pos, orn, lin_vel, ang_vel = self.get_current_state()
if stopping_criteria(
pos,
orn,
lin_vel,
ang_vel,
des_pos,
des_quat,
self.pos_tol,
self.orn_tol,
self.vel_tol,
self.ang_vel_tol,
):
return
if max_iters is not None and iters >= max_iters:
print("Maximum iterations reached, stopping unsuccessful")
return
self.step(
pos,
lin_vel,
orn,
ang_vel,
des_pos,
des_vel,
des_accel,
des_quat,
des_omega,
des_alpha,
)
iters += 1
def step(
self,
pos: npt.ArrayLike,
vel: npt.ArrayLike,
orn: npt.ArrayLike,
omega: npt.ArrayLike,
des_pos: npt.ArrayLike,
des_vel: npt.ArrayLike,
des_accel: npt.ArrayLike,
des_orn: npt.ArrayLike,
des_omega: npt.ArrayLike,
des_alpha: npt.ArrayLike,
step_sim: bool = True,
) -> None:
"""Steps the controller and the simulation
Args:
pos (npt.ArrayLike): Current position, shape (3,)
vel (npt.ArrayLike): Current linear velocity, shape (3,)
orn (npt.ArrayLike): Current orientation (XYZW quaternion), shape (4,)
omega (npt.ArrayLike): Current angular velocity, shape (3,)
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_vel (npt.ArrayLike): Desired linear velocity, shape (3,)
des_accel (npt.ArrayLike): Desired linear acceleration, shape (3,)
des_orn (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
des_omega (npt.ArrayLike): Desired angular velocity, shape (3,)
des_alpha (npt.ArrayLike): Desired angular acceleration, shape (3,)
step_sim (bool, optional): Whether to step the sim or not (This should almost always be true except for if
there are multiple active controllers in the simulation. In that case, the sim must be stepped manually
with this flag as False on each controller). Defaults to True.
"""
force = self.get_force(pos, vel, des_pos, des_vel, des_accel)
torque = self.get_torque(orn, omega, des_orn, des_omega, des_alpha)
# Clamp the maximum force/torque if needed
if self.max_force is not None:
force_mag = np.linalg.norm(force)
if force_mag > self.max_force:
force = self.max_force * (force / force_mag)
if self.max_torque is not None:
torque_mag = np.linalg.norm(torque)
if torque_mag > self.max_torque:
torque = self.max_torque * (torque / torque_mag)
self.control_log.log_control(force, torque, self.dt)
self.client.applyExternalForce(
self.id, -1, force, list(pos), pybullet.WORLD_FRAME
)
self.client.applyExternalTorque(self.id, -1, list(torque), pybullet.WORLD_FRAME)
if step_sim:
self.client.stepSimulation()
def get_current_state(
self,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""Determines the current dynamics state of the robot
Returns:
tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
np.ndarray: Current position, shape (3,)
np.ndarray: Current orientation (XYZW quaternion), shape (4,)
np.ndarray: Current linear velocity, shape (3,)
np.ndarray: Current angular velocity, shape (3,)
"""
pos, quat = self.client.getBasePositionAndOrientation(self.id)
lin_vel, ang_vel = self.client.getBaseVelocity(self.id)
self.traj_log.log_state(pos, quat, lin_vel, ang_vel, self.dt)
# These are originally tuples, so convert to numpy
return np.array(pos), np.array(quat), np.array(lin_vel), np.array(ang_vel)
class ControlLogger:
"""Class for maintaining a history of control inputs for plottting or further analysis
Any conversions between world frame / robot frame should be done before storing the force/torque
data, depending on what is of interest
"""
def __init__(self):
self._forces = []
self._torques = []
self._times = []
@property
def forces(self) -> np.ndarray:
return np.atleast_2d(self._forces)
@property
def torques(self) -> np.ndarray:
return np.atleast_2d(self._torques)
@property
def times(self) -> np.ndarray:
return np.array(self._times)
def log_control(
self, force: npt.ArrayLike, torque: npt.ArrayLike, dt: Optional[float] = None
) -> None:
"""Logs the forces and torques applied in a simulation step
Args:
force (npt.ArrayLike): Applied force (Fx, Fy, Fz), shape (3,)
torque (npt.ArrayLike): Applied torque (Tx, Ty, Tz), shape (3,)
dt (Optional[float]): Time elapsed since the previous step. Defaults to None.
"""
self._forces.append(force)
self._torques.append(torque)
if dt is not None and len(self._times) == 0:
self._times.append(0.0)
elif dt is not None:
self._times.append(self._times[-1] + dt)
def plot(
self,
max_force: Optional[npt.ArrayLike] = None,
max_torque: Optional[npt.ArrayLike] = None,
show: bool = True,
) -> Figure:
"""Plot the stored history of control inputs
Args:
max_force (Optional[npt.ArrayLike]): Applied force limits (Fx_max, Fy_max, Fz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
max_torque (Optional[npt.ArrayLike]): Applied torque limits (Tx_max, Ty_max, Tz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
show (bool, optional): Whether or not to show the plot. Defaults to True
Returns:
Figure: Matplotlib figure containing the plots
"""
return plot_control(
self.forces, self.torques, self.times, max_force, max_torque, show
)
def plot_control(
forces: np.ndarray,
torques: np.ndarray,
times: Optional[np.ndarray] = None,
max_force: Optional[npt.ArrayLike] = None,
max_torque: Optional[npt.ArrayLike] = None,
show: bool = True,
fmt: str = "k-",
) -> Figure:
"""Plots a recorded history of force/torque control inputs
Args:
forces (np.ndarray): Sequence of force inputs (Fx, Fy, Fz), shape (n, 3)
torques (np.ndarray): Sequence of torque inputs (Tx, Ty, Tz), shape (n, 3)
times (Optional[np.ndarray], optional): Times corresponding to control inputs, shape (n,).
Defaults to None, in which case control inputs will be plotted against timesteps
max_force (Optional[npt.ArrayLike]): Applied force limits (Fx_max, Fy_max, Fz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
max_torque (Optional[npt.ArrayLike]): Applied torque limits (Tx_max, Ty_max, Tz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
show (bool, optional): Whether or not to display the plot. Defaults to True.
fmt (str, optional): Matplotlib line specification. Defaults to "k-"
Returns:
Figure: Matplotlib figure containing the plots
"""
fig = plt.figure()
if times is not None:
x_axis = times
x_label = "Time, s"
else:
x_axis = np.arange(forces.shape[0])
x_label = "Timesteps"
subfigs = fig.subfigures(2, 1)
top_axes = subfigs[0].subplots(1, 3)
bot_axes = subfigs[1].subplots(1, 3)
force_labels = ["Fx", "Fy", "Fz"]
torque_labels = ["Tx", "Ty", "Tz"]
# Plot force info on the top axes
for i, ax in enumerate(top_axes):
ax.plot(x_axis, forces[:, i], fmt)
if max_force is not None:
ax.plot(x_axis, max_force[i] * np.ones_like(x_axis), "--")
ax.set_title(force_labels[i])
ax.set_xlabel(x_label)
# Plot torque info on the bottom axes
for i, ax in enumerate(bot_axes):
ax.plot(x_axis, torques[:, i], fmt)
if max_torque is not None:
ax.plot(x_axis, max_torque[i] * np.ones_like(x_axis), "--")
ax.set_title(torque_labels[i])
ax.set_xlabel(x_label)
if show:
plt.show()
return fig
Functions
plot_control
def plot_control(
forces: numpy.ndarray,
torques: numpy.ndarray,
times: Optional[numpy.ndarray] = None,
max_force: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]], NoneType] = None,
max_torque: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]], NoneType] = None,
show: bool = True,
fmt: str = 'k-'
) -> matplotlib.figure.Figure
Plots a recorded history of force/torque control inputs
Parameters:
Name | Type | Description | Default |
---|---|---|---|
forces | np.ndarray | Sequence of force inputs (Fx, Fy, Fz), shape (n, 3) | None |
torques | np.ndarray | Sequence of torque inputs (Tx, Ty, Tz), shape (n, 3) | None |
times | Optional[np.ndarray] | Times corresponding to control inputs, shape (n,). Defaults to None, in which case control inputs will be plotted against timesteps |
None |
max_force | Optional[npt.ArrayLike] | Applied force limits (Fx_max, Fy_max, Fz_max), shape (3,) Defaults to None (Don't indicate the limit on the plots) |
None |
max_torque | Optional[npt.ArrayLike] | Applied torque limits (Tx_max, Ty_max, Tz_max), shape (3,) Defaults to None (Don't indicate the limit on the plots) |
None |
show | bool | Whether or not to display the plot. Defaults to True. | True |
fmt | str | Matplotlib line specification. Defaults to "k-" | None |
Returns:
Type | Description |
---|---|
Figure | Matplotlib figure containing the plots |
View Source
def plot_control(
forces: np.ndarray,
torques: np.ndarray,
times: Optional[np.ndarray] = None,
max_force: Optional[npt.ArrayLike] = None,
max_torque: Optional[npt.ArrayLike] = None,
show: bool = True,
fmt: str = "k-",
) -> Figure:
"""Plots a recorded history of force/torque control inputs
Args:
forces (np.ndarray): Sequence of force inputs (Fx, Fy, Fz), shape (n, 3)
torques (np.ndarray): Sequence of torque inputs (Tx, Ty, Tz), shape (n, 3)
times (Optional[np.ndarray], optional): Times corresponding to control inputs, shape (n,).
Defaults to None, in which case control inputs will be plotted against timesteps
max_force (Optional[npt.ArrayLike]): Applied force limits (Fx_max, Fy_max, Fz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
max_torque (Optional[npt.ArrayLike]): Applied torque limits (Tx_max, Ty_max, Tz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
show (bool, optional): Whether or not to display the plot. Defaults to True.
fmt (str, optional): Matplotlib line specification. Defaults to "k-"
Returns:
Figure: Matplotlib figure containing the plots
"""
fig = plt.figure()
if times is not None:
x_axis = times
x_label = "Time, s"
else:
x_axis = np.arange(forces.shape[0])
x_label = "Timesteps"
subfigs = fig.subfigures(2, 1)
top_axes = subfigs[0].subplots(1, 3)
bot_axes = subfigs[1].subplots(1, 3)
force_labels = ["Fx", "Fy", "Fz"]
torque_labels = ["Tx", "Ty", "Tz"]
# Plot force info on the top axes
for i, ax in enumerate(top_axes):
ax.plot(x_axis, forces[:, i], fmt)
if max_force is not None:
ax.plot(x_axis, max_force[i] * np.ones_like(x_axis), "--")
ax.set_title(force_labels[i])
ax.set_xlabel(x_label)
# Plot torque info on the bottom axes
for i, ax in enumerate(bot_axes):
ax.plot(x_axis, torques[:, i], fmt)
if max_torque is not None:
ax.plot(x_axis, max_torque[i] * np.ones_like(x_axis), "--")
ax.set_title(torque_labels[i])
ax.set_xlabel(x_label)
if show:
plt.show()
return fig
Classes
ControlLogger
class ControlLogger(
)
Class for maintaining a history of control inputs for plottting or further analysis
Any conversions between world frame / robot frame should be done before storing the force/torque data, depending on what is of interest
View Source
class ControlLogger:
"""Class for maintaining a history of control inputs for plottting or further analysis
Any conversions between world frame / robot frame should be done before storing the force/torque
data, depending on what is of interest
"""
def __init__(self):
self._forces = []
self._torques = []
self._times = []
@property
def forces(self) -> np.ndarray:
return np.atleast_2d(self._forces)
@property
def torques(self) -> np.ndarray:
return np.atleast_2d(self._torques)
@property
def times(self) -> np.ndarray:
return np.array(self._times)
def log_control(
self, force: npt.ArrayLike, torque: npt.ArrayLike, dt: Optional[float] = None
) -> None:
"""Logs the forces and torques applied in a simulation step
Args:
force (npt.ArrayLike): Applied force (Fx, Fy, Fz), shape (3,)
torque (npt.ArrayLike): Applied torque (Tx, Ty, Tz), shape (3,)
dt (Optional[float]): Time elapsed since the previous step. Defaults to None.
"""
self._forces.append(force)
self._torques.append(torque)
if dt is not None and len(self._times) == 0:
self._times.append(0.0)
elif dt is not None:
self._times.append(self._times[-1] + dt)
def plot(
self,
max_force: Optional[npt.ArrayLike] = None,
max_torque: Optional[npt.ArrayLike] = None,
show: bool = True,
) -> Figure:
"""Plot the stored history of control inputs
Args:
max_force (Optional[npt.ArrayLike]): Applied force limits (Fx_max, Fy_max, Fz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
max_torque (Optional[npt.ArrayLike]): Applied torque limits (Tx_max, Ty_max, Tz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
show (bool, optional): Whether or not to show the plot. Defaults to True
Returns:
Figure: Matplotlib figure containing the plots
"""
return plot_control(
self.forces, self.torques, self.times, max_force, max_torque, show
)
Instance variables
forces
times
torques
Methods
log_control
def log_control(
self,
force: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
torque: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
dt: Optional[float] = None
) -> None
Logs the forces and torques applied in a simulation step
Parameters:
Name | Type | Description | Default |
---|---|---|---|
force | npt.ArrayLike | Applied force (Fx, Fy, Fz), shape (3,) | None |
torque | npt.ArrayLike | Applied torque (Tx, Ty, Tz), shape (3,) | None |
dt | Optional[float] | Time elapsed since the previous step. Defaults to None. | None |
View Source
def log_control(
self, force: npt.ArrayLike, torque: npt.ArrayLike, dt: Optional[float] = None
) -> None:
"""Logs the forces and torques applied in a simulation step
Args:
force (npt.ArrayLike): Applied force (Fx, Fy, Fz), shape (3,)
torque (npt.ArrayLike): Applied torque (Tx, Ty, Tz), shape (3,)
dt (Optional[float]): Time elapsed since the previous step. Defaults to None.
"""
self._forces.append(force)
self._torques.append(torque)
if dt is not None and len(self._times) == 0:
self._times.append(0.0)
elif dt is not None:
self._times.append(self._times[-1] + dt)
plot
def plot(
self,
max_force: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]], NoneType] = None,
max_torque: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]], NoneType] = None,
show: bool = True
) -> matplotlib.figure.Figure
Plot the stored history of control inputs
Parameters:
Name | Type | Description | Default |
---|---|---|---|
max_force | Optional[npt.ArrayLike] | Applied force limits (Fx_max, Fy_max, Fz_max), shape (3,) Defaults to None (Don't indicate the limit on the plots) |
None |
max_torque | Optional[npt.ArrayLike] | Applied torque limits (Tx_max, Ty_max, Tz_max), shape (3,) Defaults to None (Don't indicate the limit on the plots) |
None |
show | bool | Whether or not to show the plot. Defaults to True | None |
Returns:
Type | Description |
---|---|
Figure | Matplotlib figure containing the plots |
View Source
def plot(
self,
max_force: Optional[npt.ArrayLike] = None,
max_torque: Optional[npt.ArrayLike] = None,
show: bool = True,
) -> Figure:
"""Plot the stored history of control inputs
Args:
max_force (Optional[npt.ArrayLike]): Applied force limits (Fx_max, Fy_max, Fz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
max_torque (Optional[npt.ArrayLike]): Applied torque limits (Tx_max, Ty_max, Tz_max), shape (3,)
Defaults to None (Don't indicate the limit on the plots)
show (bool, optional): Whether or not to show the plot. Defaults to True
Returns:
Figure: Matplotlib figure containing the plots
"""
return plot_control(
self.forces, self.torques, self.times, max_force, max_torque, show
)
ForceTorqueController
class ForceTorqueController(
robot_id: int,
mass: float,
inertia: numpy.ndarray,
kp: float,
kv: float,
kq: float,
kw: float,
dt: float,
pos_tol: float = 0.01,
orn_tol: float = 0.01,
vel_tol: float = 0.01,
ang_vel_tol: float = 0.005,
max_force: Optional[float] = None,
max_torque: Optional[float] = None,
client: Optional[pybullet_utils.bullet_client.BulletClient] = None
)
PID-style force/torque control
Attributes
Name | Type | Description | Default |
---|---|---|---|
robot_id | int | Pybullet ID of the robot to control | None |
mass | float | Mass of the robot | None |
inertia | np.ndarray | Inertia tensor for the robot, shape (3, 3) | None |
kp | float | Gain for position error | None |
kv | float | Gain for velocity error | None |
kq | float | Gain for orientation (quaternion) error | None |
kw | float | Gain for angular velocity (omega) error | None |
dt | float | Timestep | None |
pos_tol | float | Stopping tolerance on position error magnitude. Defaults to 1e-2. | 1e-2 |
orn_tol | float | Stopping tolerance on quaternion distance between cur/des. Defaults to 1e-2. | 1e-2 |
vel_tol | float | Stopping tolerance on linear velocity error magnitude. Defaults to 1e-2. | 1e-2 |
ang_vel_tol | float | Stopping tolerance on angular velocity error magnitude. Defaults to 5e-3. | 5e-3 |
max_force | Optional[float] | Limit on the applied force magnitude. Defaults to None (no limit) | None |
max_torque | Optional[float] | Limit on the applied torque magnitude. Defaults to None (no limit) | None |
client | BulletClient | If connecting to multiple physics servers, include the client (the class instance, not just the ID) here. Defaults to None (use default connected client) |
None |
View Source
class ForceTorqueController:
"""PID-style force/torque control
Args:
robot_id (int): Pybullet ID of the robot to control
mass (float): Mass of the robot
inertia (np.ndarray): Inertia tensor for the robot, shape (3, 3)
kp (float): Gain for position error
kv (float): Gain for velocity error
kq (float): Gain for orientation (quaternion) error
kw (float): Gain for angular velocity (omega) error
dt (float): Timestep
pos_tol (float, optional): Stopping tolerance on position error magnitude. Defaults to 1e-2.
orn_tol (float, optional): Stopping tolerance on quaternion distance between cur/des. Defaults to 1e-2.
vel_tol (float, optional): Stopping tolerance on linear velocity error magnitude. Defaults to 1e-2.
ang_vel_tol (float, optional): Stopping tolerance on angular velocity error magnitude. Defaults to 5e-3.
max_force (Optional[float]): Limit on the applied force magnitude. Defaults to None (no limit)
max_torque (Optional[float]): Limit on the applied torque magnitude. Defaults to None (no limit)
client (BulletClient, optional): If connecting to multiple physics servers, include the client
(the class instance, not just the ID) here. Defaults to None (use default connected client)
"""
def __init__(
self,
robot_id: int,
mass: float,
inertia: np.ndarray,
kp: float,
kv: float,
kq: float,
kw: float,
dt: float,
pos_tol: float = 1e-2,
orn_tol: float = 1e-2,
vel_tol: float = 1e-2,
ang_vel_tol: float = 5e-3,
max_force: Optional[float] = None,
max_torque: Optional[float] = None,
client: Optional[BulletClient] = None,
):
self.id = robot_id
self.mass = mass
self.inertia = inertia
self.kp = kp
self.kv = kv
self.kq = kq
self.kw = kw
self.dt = dt
self.pos_tol = pos_tol
self.orn_tol = orn_tol
self.vel_tol = vel_tol
self.ang_vel_tol = ang_vel_tol
self.max_force = max_force
self.max_torque = max_torque
self.traj_log = TrajectoryLogger()
self.control_log = ControlLogger()
self.client: pybullet = pybullet if client is None else client
# Parameters for checking if there was a quaternion sign flip
self.last_quat = np.array(self.client.getBasePositionAndOrientation(self.id)[1])
self.quat_sign = 1
# TODO figure out how world/robot frame should be handled
def get_force(
self,
cur_pos: npt.ArrayLike,
cur_vel: npt.ArrayLike,
des_pos: npt.ArrayLike,
des_vel: npt.ArrayLike,
des_accel: npt.ArrayLike,
) -> np.ndarray:
"""Calculates the required force to achieve a desired pos/vel/accel
Args:
cur_pos (npt.ArrayLike): Current position, shape (3,)
cur_vel (npt.ArrayLike): Current velocity, shape (3,)
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_vel (npt.ArrayLike): Desired velocity, shape (3,)
des_accel (npt.ArrayLike): Desired acceleration, shape (3,)
Returns:
np.ndarray: Force, (Fx, Fy, Fz), shape (3,)
"""
M = self.mass * np.eye(3)
pos_err = np.subtract(cur_pos, des_pos)
vel_err = np.subtract(cur_vel, des_vel)
return M @ np.asarray(des_accel) - self.kv * vel_err - self.kp * pos_err
def get_torque(
self,
cur_q: npt.ArrayLike,
cur_w: npt.ArrayLike,
des_q: npt.ArrayLike,
des_w: npt.ArrayLike,
des_a: npt.ArrayLike,
) -> np.ndarray:
"""Calculates the required torque to achieve a desired orientation/ang vel/ang accel
Args:
cur_q (npt.ArrayLike): Current orientation (XYZW quaternion), shape (4,)
cur_w (npt.ArrayLike): Current angular velocity (omega), shape (3,)
des_q (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
des_w (npt.ArrayLike): Desired angular velocity (omega), shape (3,)
des_a (npt.ArrayLike): Desired angular acceleration (alpha), shape (3,)
Returns:
np.ndarray: Torque, (Tx, Ty, Tz), shape (3,)
"""
if np.allclose(cur_q, -1 * self.last_quat, atol=1e-3):
print("Quaternion flip detected")
self.quat_sign *= -1
ang_err = quaternion_angular_error(cur_q * self.quat_sign, des_q)
self.last_quat = cur_q
ang_vel_err = cur_w - des_w
R = quat_to_rmat(cur_q)
world_inertia = R @ self.inertia @ R.T
# Standard 3D free-body torque equation based on desired ang. accel and current ang. vel
# Note: this ignores the m * r x a term
torque = world_inertia @ des_a + np.cross(cur_w, world_inertia @ cur_w)
# Add in the proportional and derivative terms
return torque - self.kw * ang_vel_err - self.kq * ang_err
def follow_traj(
self,
traj: Trajectory,
stop_at_end: bool = True,
max_stop_iters: Optional[int] = None,
) -> None:
"""Use PID force/torque control to follow a trajectory
Args:
traj (Trajectory): Trajectory with position, orientation, and derivative info across time
stop_at_end (bool, optional): Whether or not to command the robot to come to a stop at the last
pose in the trajectory. Defaults to True
max_stop_iters (int, optional): If stop_at_end is True, this gives a maximum number of iterations to
allow for stopping. Defaults to None (keep controlling until stopped)
"""
for i in range(traj.num_timesteps):
pos, orn, lin_vel, ang_vel = self.get_current_state()
self.step(
pos,
lin_vel,
orn,
ang_vel,
traj.positions[i, :],
traj.linear_velocities[i, :],
traj.linear_accels[i, :],
traj.quaternions[i, :],
traj.angular_velocities[i, :],
traj.angular_accels[i, :],
)
if stop_at_end:
self.stop(traj.positions[-1, :], traj.quaternions[-1, :], max_stop_iters)
def stop(
self,
des_pos: npt.ArrayLike,
des_quat: npt.ArrayLike,
max_iters: Optional[int] = None,
) -> None:
"""Controls the robot to stop at a desired position/orientation
Args:
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_quat (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
max_iters (int, optional): Maximum number of control iterations to allow for stopping.
Defaults to None (keep controlling until stopped)
"""
des_vel = np.zeros(3)
des_accel = np.zeros(3)
des_omega = np.zeros(3)
des_alpha = np.zeros(3)
iters = 0
while True:
pos, orn, lin_vel, ang_vel = self.get_current_state()
if stopping_criteria(
pos,
orn,
lin_vel,
ang_vel,
des_pos,
des_quat,
self.pos_tol,
self.orn_tol,
self.vel_tol,
self.ang_vel_tol,
):
return
if max_iters is not None and iters >= max_iters:
print("Maximum iterations reached, stopping unsuccessful")
return
self.step(
pos,
lin_vel,
orn,
ang_vel,
des_pos,
des_vel,
des_accel,
des_quat,
des_omega,
des_alpha,
)
iters += 1
def step(
self,
pos: npt.ArrayLike,
vel: npt.ArrayLike,
orn: npt.ArrayLike,
omega: npt.ArrayLike,
des_pos: npt.ArrayLike,
des_vel: npt.ArrayLike,
des_accel: npt.ArrayLike,
des_orn: npt.ArrayLike,
des_omega: npt.ArrayLike,
des_alpha: npt.ArrayLike,
step_sim: bool = True,
) -> None:
"""Steps the controller and the simulation
Args:
pos (npt.ArrayLike): Current position, shape (3,)
vel (npt.ArrayLike): Current linear velocity, shape (3,)
orn (npt.ArrayLike): Current orientation (XYZW quaternion), shape (4,)
omega (npt.ArrayLike): Current angular velocity, shape (3,)
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_vel (npt.ArrayLike): Desired linear velocity, shape (3,)
des_accel (npt.ArrayLike): Desired linear acceleration, shape (3,)
des_orn (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
des_omega (npt.ArrayLike): Desired angular velocity, shape (3,)
des_alpha (npt.ArrayLike): Desired angular acceleration, shape (3,)
step_sim (bool, optional): Whether to step the sim or not (This should almost always be true except for if
there are multiple active controllers in the simulation. In that case, the sim must be stepped manually
with this flag as False on each controller). Defaults to True.
"""
force = self.get_force(pos, vel, des_pos, des_vel, des_accel)
torque = self.get_torque(orn, omega, des_orn, des_omega, des_alpha)
# Clamp the maximum force/torque if needed
if self.max_force is not None:
force_mag = np.linalg.norm(force)
if force_mag > self.max_force:
force = self.max_force * (force / force_mag)
if self.max_torque is not None:
torque_mag = np.linalg.norm(torque)
if torque_mag > self.max_torque:
torque = self.max_torque * (torque / torque_mag)
self.control_log.log_control(force, torque, self.dt)
self.client.applyExternalForce(
self.id, -1, force, list(pos), pybullet.WORLD_FRAME
)
self.client.applyExternalTorque(self.id, -1, list(torque), pybullet.WORLD_FRAME)
if step_sim:
self.client.stepSimulation()
def get_current_state(
self,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""Determines the current dynamics state of the robot
Returns:
tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
np.ndarray: Current position, shape (3,)
np.ndarray: Current orientation (XYZW quaternion), shape (4,)
np.ndarray: Current linear velocity, shape (3,)
np.ndarray: Current angular velocity, shape (3,)
"""
pos, quat = self.client.getBasePositionAndOrientation(self.id)
lin_vel, ang_vel = self.client.getBaseVelocity(self.id)
self.traj_log.log_state(pos, quat, lin_vel, ang_vel, self.dt)
# These are originally tuples, so convert to numpy
return np.array(pos), np.array(quat), np.array(lin_vel), np.array(ang_vel)
Methods
follow_traj
def follow_traj(
self,
traj: pyastrobee.trajectories.trajectory.Trajectory,
stop_at_end: bool = True,
max_stop_iters: Optional[int] = None
) -> None
Use PID force/torque control to follow a trajectory
Parameters:
Name | Type | Description | Default |
---|---|---|---|
traj | Trajectory | Trajectory with position, orientation, and derivative info across time | None |
stop_at_end | bool | Whether or not to command the robot to come to a stop at the last pose in the trajectory. Defaults to True |
None |
max_stop_iters | int | If stop_at_end is True, this gives a maximum number of iterations to allow for stopping. Defaults to None (keep controlling until stopped) |
None |
View Source
def follow_traj(
self,
traj: Trajectory,
stop_at_end: bool = True,
max_stop_iters: Optional[int] = None,
) -> None:
"""Use PID force/torque control to follow a trajectory
Args:
traj (Trajectory): Trajectory with position, orientation, and derivative info across time
stop_at_end (bool, optional): Whether or not to command the robot to come to a stop at the last
pose in the trajectory. Defaults to True
max_stop_iters (int, optional): If stop_at_end is True, this gives a maximum number of iterations to
allow for stopping. Defaults to None (keep controlling until stopped)
"""
for i in range(traj.num_timesteps):
pos, orn, lin_vel, ang_vel = self.get_current_state()
self.step(
pos,
lin_vel,
orn,
ang_vel,
traj.positions[i, :],
traj.linear_velocities[i, :],
traj.linear_accels[i, :],
traj.quaternions[i, :],
traj.angular_velocities[i, :],
traj.angular_accels[i, :],
)
if stop_at_end:
self.stop(traj.positions[-1, :], traj.quaternions[-1, :], max_stop_iters)
get_current_state
def get_current_state(
self
) -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]
Determines the current dynamics state of the robot
Returns:
Type | Description |
---|---|
tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] | np.ndarray: Current position, shape (3,) np.ndarray: Current orientation (XYZW quaternion), shape (4,) np.ndarray: Current linear velocity, shape (3,) np.ndarray: Current angular velocity, shape (3,) |
View Source
def get_current_state(
self,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""Determines the current dynamics state of the robot
Returns:
tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
np.ndarray: Current position, shape (3,)
np.ndarray: Current orientation (XYZW quaternion), shape (4,)
np.ndarray: Current linear velocity, shape (3,)
np.ndarray: Current angular velocity, shape (3,)
"""
pos, quat = self.client.getBasePositionAndOrientation(self.id)
lin_vel, ang_vel = self.client.getBaseVelocity(self.id)
self.traj_log.log_state(pos, quat, lin_vel, ang_vel, self.dt)
# These are originally tuples, so convert to numpy
return np.array(pos), np.array(quat), np.array(lin_vel), np.array(ang_vel)
get_force
def get_force(
self,
cur_pos: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
cur_vel: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_pos: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_vel: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_accel: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]]
) -> numpy.ndarray
Calculates the required force to achieve a desired pos/vel/accel
Parameters:
Name | Type | Description | Default |
---|---|---|---|
cur_pos | npt.ArrayLike | Current position, shape (3,) | None |
cur_vel | npt.ArrayLike | Current velocity, shape (3,) | None |
des_pos | npt.ArrayLike | Desired position, shape (3,) | None |
des_vel | npt.ArrayLike | Desired velocity, shape (3,) | None |
des_accel | npt.ArrayLike | Desired acceleration, shape (3,) | None |
Returns:
Type | Description |
---|---|
np.ndarray | Force, (Fx, Fy, Fz), shape (3,) |
View Source
def get_force(
self,
cur_pos: npt.ArrayLike,
cur_vel: npt.ArrayLike,
des_pos: npt.ArrayLike,
des_vel: npt.ArrayLike,
des_accel: npt.ArrayLike,
) -> np.ndarray:
"""Calculates the required force to achieve a desired pos/vel/accel
Args:
cur_pos (npt.ArrayLike): Current position, shape (3,)
cur_vel (npt.ArrayLike): Current velocity, shape (3,)
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_vel (npt.ArrayLike): Desired velocity, shape (3,)
des_accel (npt.ArrayLike): Desired acceleration, shape (3,)
Returns:
np.ndarray: Force, (Fx, Fy, Fz), shape (3,)
"""
M = self.mass * np.eye(3)
pos_err = np.subtract(cur_pos, des_pos)
vel_err = np.subtract(cur_vel, des_vel)
return M @ np.asarray(des_accel) - self.kv * vel_err - self.kp * pos_err
get_torque
def get_torque(
self,
cur_q: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
cur_w: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_q: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_w: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_a: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]]
) -> numpy.ndarray
Calculates the required torque to achieve a desired orientation/ang vel/ang accel
Parameters:
Name | Type | Description | Default |
---|---|---|---|
cur_q | npt.ArrayLike | Current orientation (XYZW quaternion), shape (4,) | None |
cur_w | npt.ArrayLike | Current angular velocity (omega), shape (3,) | None |
des_q | npt.ArrayLike | Desired orientation (XYZW quaternion), shape (4,) | None |
des_w | npt.ArrayLike | Desired angular velocity (omega), shape (3,) | None |
des_a | npt.ArrayLike | Desired angular acceleration (alpha), shape (3,) | None |
Returns:
Type | Description |
---|---|
np.ndarray | Torque, (Tx, Ty, Tz), shape (3,) |
View Source
def get_torque(
self,
cur_q: npt.ArrayLike,
cur_w: npt.ArrayLike,
des_q: npt.ArrayLike,
des_w: npt.ArrayLike,
des_a: npt.ArrayLike,
) -> np.ndarray:
"""Calculates the required torque to achieve a desired orientation/ang vel/ang accel
Args:
cur_q (npt.ArrayLike): Current orientation (XYZW quaternion), shape (4,)
cur_w (npt.ArrayLike): Current angular velocity (omega), shape (3,)
des_q (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
des_w (npt.ArrayLike): Desired angular velocity (omega), shape (3,)
des_a (npt.ArrayLike): Desired angular acceleration (alpha), shape (3,)
Returns:
np.ndarray: Torque, (Tx, Ty, Tz), shape (3,)
"""
if np.allclose(cur_q, -1 * self.last_quat, atol=1e-3):
print("Quaternion flip detected")
self.quat_sign *= -1
ang_err = quaternion_angular_error(cur_q * self.quat_sign, des_q)
self.last_quat = cur_q
ang_vel_err = cur_w - des_w
R = quat_to_rmat(cur_q)
world_inertia = R @ self.inertia @ R.T
# Standard 3D free-body torque equation based on desired ang. accel and current ang. vel
# Note: this ignores the m * r x a term
torque = world_inertia @ des_a + np.cross(cur_w, world_inertia @ cur_w)
# Add in the proportional and derivative terms
return torque - self.kw * ang_vel_err - self.kq * ang_err
step
def step(
self,
pos: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
vel: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
orn: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
omega: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_pos: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_vel: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_accel: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_orn: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_omega: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_alpha: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
step_sim: bool = True
) -> None
Steps the controller and the simulation
Parameters:
Name | Type | Description | Default |
---|---|---|---|
pos | npt.ArrayLike | Current position, shape (3,) | None |
vel | npt.ArrayLike | Current linear velocity, shape (3,) | None |
orn | npt.ArrayLike | Current orientation (XYZW quaternion), shape (4,) | None |
omega | npt.ArrayLike | Current angular velocity, shape (3,) | None |
des_pos | npt.ArrayLike | Desired position, shape (3,) | None |
des_vel | npt.ArrayLike | Desired linear velocity, shape (3,) | None |
des_accel | npt.ArrayLike | Desired linear acceleration, shape (3,) | None |
des_orn | npt.ArrayLike | Desired orientation (XYZW quaternion), shape (4,) | None |
des_omega | npt.ArrayLike | Desired angular velocity, shape (3,) | None |
des_alpha | npt.ArrayLike | Desired angular acceleration, shape (3,) | None |
step_sim | bool | Whether to step the sim or not (This should almost always be true except for if there are multiple active controllers in the simulation. In that case, the sim must be stepped manually with this flag as False on each controller). Defaults to True. |
None |
View Source
def step(
self,
pos: npt.ArrayLike,
vel: npt.ArrayLike,
orn: npt.ArrayLike,
omega: npt.ArrayLike,
des_pos: npt.ArrayLike,
des_vel: npt.ArrayLike,
des_accel: npt.ArrayLike,
des_orn: npt.ArrayLike,
des_omega: npt.ArrayLike,
des_alpha: npt.ArrayLike,
step_sim: bool = True,
) -> None:
"""Steps the controller and the simulation
Args:
pos (npt.ArrayLike): Current position, shape (3,)
vel (npt.ArrayLike): Current linear velocity, shape (3,)
orn (npt.ArrayLike): Current orientation (XYZW quaternion), shape (4,)
omega (npt.ArrayLike): Current angular velocity, shape (3,)
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_vel (npt.ArrayLike): Desired linear velocity, shape (3,)
des_accel (npt.ArrayLike): Desired linear acceleration, shape (3,)
des_orn (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
des_omega (npt.ArrayLike): Desired angular velocity, shape (3,)
des_alpha (npt.ArrayLike): Desired angular acceleration, shape (3,)
step_sim (bool, optional): Whether to step the sim or not (This should almost always be true except for if
there are multiple active controllers in the simulation. In that case, the sim must be stepped manually
with this flag as False on each controller). Defaults to True.
"""
force = self.get_force(pos, vel, des_pos, des_vel, des_accel)
torque = self.get_torque(orn, omega, des_orn, des_omega, des_alpha)
# Clamp the maximum force/torque if needed
if self.max_force is not None:
force_mag = np.linalg.norm(force)
if force_mag > self.max_force:
force = self.max_force * (force / force_mag)
if self.max_torque is not None:
torque_mag = np.linalg.norm(torque)
if torque_mag > self.max_torque:
torque = self.max_torque * (torque / torque_mag)
self.control_log.log_control(force, torque, self.dt)
self.client.applyExternalForce(
self.id, -1, force, list(pos), pybullet.WORLD_FRAME
)
self.client.applyExternalTorque(self.id, -1, list(torque), pybullet.WORLD_FRAME)
if step_sim:
self.client.stepSimulation()
stop
def stop(
self,
des_pos: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
des_quat: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]],
max_iters: Optional[int] = None
) -> None
Controls the robot to stop at a desired position/orientation
Parameters:
Name | Type | Description | Default |
---|---|---|---|
des_pos | npt.ArrayLike | Desired position, shape (3,) | None |
des_quat | npt.ArrayLike | Desired orientation (XYZW quaternion), shape (4,) | None |
max_iters | int | Maximum number of control iterations to allow for stopping. Defaults to None (keep controlling until stopped) |
None |
View Source
def stop(
self,
des_pos: npt.ArrayLike,
des_quat: npt.ArrayLike,
max_iters: Optional[int] = None,
) -> None:
"""Controls the robot to stop at a desired position/orientation
Args:
des_pos (npt.ArrayLike): Desired position, shape (3,)
des_quat (npt.ArrayLike): Desired orientation (XYZW quaternion), shape (4,)
max_iters (int, optional): Maximum number of control iterations to allow for stopping.
Defaults to None (keep controlling until stopped)
"""
des_vel = np.zeros(3)
des_accel = np.zeros(3)
des_omega = np.zeros(3)
des_alpha = np.zeros(3)
iters = 0
while True:
pos, orn, lin_vel, ang_vel = self.get_current_state()
if stopping_criteria(
pos,
orn,
lin_vel,
ang_vel,
des_pos,
des_quat,
self.pos_tol,
self.orn_tol,
self.vel_tol,
self.ang_vel_tol,
):
return
if max_iters is not None and iters >= max_iters:
print("Maximum iterations reached, stopping unsuccessful")
return
self.step(
pos,
lin_vel,
orn,
ang_vel,
des_pos,
des_vel,
des_accel,
des_quat,
des_omega,
des_alpha,
)
iters += 1