Skip to content

Commit 1c89ddb

Browse files
pierfabreArmandpl
andauthored
NMPC Swing Up Controller based on Crocoddyl (#86)
* add NMPC swing up controller using crocoddyl * tune to make it work with the new urdf * nmpc swing up : working in simulation in closed loop at 100Hz on measured data. Rm support for constraints for now while waiting for mim_solvers package. * cast rwd * urdf : absolute path to urdf * logs : absolute path and create log dir in repo if it doesn't exist yet * pid : rm config file and read json utils * isort --------- Co-authored-by: Armandpl <adpl33@gmail.com>
1 parent 9e5f19c commit 1c89ddb

12 files changed

Lines changed: 507 additions & 251 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
**/replay_buffer/*
88

99
data/*
10+
logs/
1011

1112
# ignore everything except notebooks in the notebook folder
1213
notebooks/*

furuta/controls/controllers.py

Lines changed: 133 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,148 @@
1+
import typing as tp
12
from abc import ABC, abstractmethod
23

4+
import crocoddyl
35
import numpy as np
6+
import pinocchio as pin
47
from simple_pid import PID
58

69

710
class Controller(ABC):
811
@abstractmethod
9-
def compute_command(self, position: float):
12+
def compute_command(self, state):
1013
pass
1114

12-
@staticmethod
13-
def build_controller(parameters: dict):
14-
controller_type = parameters["controller_type"]
15-
# match controller_type:
16-
# case "PIDController":
17-
# return PIDController(parameters)
18-
# case _:
19-
# raise ValueError(f"Invalid controller type: {controller_type}")
20-
if controller_type == "PIDController":
21-
return PIDController(parameters)
22-
else:
23-
raise ValueError(f"Invalid controller type: {controller_type}")
24-
2515

2616
class PIDController(Controller):
27-
def __init__(self, parameters):
28-
try:
29-
sample_time = 1 / parameters["control_frequency"]
30-
except KeyError:
31-
sample_time = None
32-
33-
try:
34-
self.pid = PID(
35-
Kp=parameters["Kp"],
36-
Ki=parameters["Ki"],
37-
Kd=parameters["Kd"],
38-
setpoint=np.deg2rad(parameters["setpoint"]),
39-
sample_time=sample_time,
40-
)
41-
except KeyError:
42-
raise ValueError("Invalid PID parameters")
17+
def __init__(
18+
self,
19+
dt: float = None,
20+
Kp: float = 0.0,
21+
Ki: float = 0.0,
22+
Kd: float = 0.0,
23+
setpoint: float = 0.0,
24+
):
25+
26+
self.pid = PID(
27+
Kp=Kp,
28+
Ki=Ki,
29+
Kd=Kd,
30+
setpoint=setpoint,
31+
sample_time=dt,
32+
)
4333

4434
def compute_command(self, position: float):
4535
return self.pid(position)
36+
37+
38+
class SwingUpController(Controller):
39+
class FurutaActuationModel(crocoddyl.ActuationModelAbstract):
40+
def __init__(self, state, nu):
41+
crocoddyl.ActuationModelAbstract.__init__(self, state, nu=nu)
42+
43+
def calc(self, data, x, u):
44+
assert len(data.tau) == 2
45+
# Map the control dimensions to the joint torque
46+
data.tau[0] = u
47+
data.tau[1] = 0 # Underactuated joint
48+
49+
def calcDiff(self, data, x, u):
50+
# Specify the actuation jacobian
51+
data.dtau_du[0] = 1
52+
data.dtau_du[1] = 0
53+
54+
def __init__(
55+
self,
56+
robot: pin.RobotWrapper,
57+
x_target: np.ndarray,
58+
control_freq: float = 100.0,
59+
t_final: float = 0.5,
60+
u_lim: float = 0.1,
61+
Q: np.ndarray = np.array([10, 50, 1, 1]),
62+
R: np.ndarray = np.array([0.1]),
63+
S: np.ndarray = np.array([1.0]),
64+
M: int = 10,
65+
):
66+
self.u_lim = u_lim
67+
68+
# Time variables
69+
dt = 1 / control_freq # Time step
70+
self.N = int(t_final * control_freq)
71+
72+
# Instantiate robot as a pinocchio RobotWrapper
73+
self.robot = robot
74+
75+
# State
76+
state = crocoddyl.StateMultibody(robot.model)
77+
78+
# Actuation model
79+
nu = 1
80+
actuation = self.FurutaActuationModel(state, nu)
81+
82+
# State Cost
83+
state_residual = crocoddyl.ResidualModelState(state, xref=x_target, nu=nu)
84+
state_residual_activation = crocoddyl.ActivationModelWeightedQuad(Q)
85+
state_cost = crocoddyl.CostModelResidual(state, state_residual_activation, state_residual)
86+
87+
# Control Cost
88+
control_residual = crocoddyl.ResidualModelControl(state, nu=nu)
89+
control_residual_activation = crocoddyl.ActivationModelWeightedQuad(R)
90+
control_cost = crocoddyl.CostModelResidual(
91+
state, control_residual_activation, control_residual
92+
)
93+
94+
# Control rate cost
95+
self.control_rate_residual = crocoddyl.ResidualModelControl(state, uref=np.array([0.0]))
96+
control_rate_residual_activation = crocoddyl.ActivationModelWeightedQuad(S)
97+
control_rate_cost = crocoddyl.CostModelResidual(
98+
state, control_rate_residual_activation, self.control_rate_residual
99+
)
100+
101+
self.running_models = []
102+
for k in range(self.N):
103+
running_cost = crocoddyl.CostModelSum(state, nu=nu)
104+
running_cost.addCost("state_cost", cost=state_cost, weight=1.0)
105+
running_cost.addCost("control_cost", cost=control_cost, weight=1.0)
106+
running_cost.addCost("control_rate_cost", cost=control_rate_cost, weight=np.exp(M - k))
107+
108+
running_model = crocoddyl.IntegratedActionModelEuler(
109+
crocoddyl.DifferentialActionModelFreeFwdDynamics(state, actuation, running_cost),
110+
dt,
111+
)
112+
self.running_models.append(running_model)
113+
114+
# Terminal cost
115+
terminal_cost = crocoddyl.CostModelSum(state, nu=nu)
116+
terminal_cost.addCost("state_cost", cost=state_cost, weight=self.N)
117+
118+
self.terminal_model = crocoddyl.IntegratedActionModelEuler(
119+
crocoddyl.DifferentialActionModelFreeFwdDynamics(state, actuation, terminal_cost),
120+
0.0,
121+
)
122+
123+
def create_problem(self, state: np.ndarray):
124+
self.problem = crocoddyl.ShootingProblem(state, self.running_models, self.terminal_model)
125+
126+
def init_solver(self):
127+
self.solver = crocoddyl.SolverFDDP(self.problem)
128+
callbacks = []
129+
callbacks.append(crocoddyl.CallbackVerbose())
130+
self.solver.setCallbacks(callbacks)
131+
132+
def compute_command(self, state: np.ndarray, max_iter: int = 500, x_ws=[], u_ws=[]) -> float:
133+
self.create_problem(state)
134+
self.init_solver()
135+
self.solver.solve(x_ws, u_ws, max_iter, False, 1e-5)
136+
u = np.clip(self.solver.us[0][0], -self.u_lim, self.u_lim)
137+
return u
138+
139+
def get_trajectoy(self) -> np.ndarray:
140+
return self.solver.xs.tolist()
141+
142+
def get_command(self) -> np.ndarray:
143+
return self.solver.us.tolist()
144+
145+
def get_warm_start(self) -> tp.Tuple[np.ndarray, np.ndarray]:
146+
x_ws = self.solver.xs.tolist()[1:] + [self.solver.xs[-1]]
147+
u_ws = self.solver.us.tolist()[1:] + [self.solver.us[-1]]
148+
return x_ws, u_ws

furuta/controls/utils.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

furuta/rl/envs/furuta_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def step(self, action):
124124
terminated = not self.state_space.contains(self._state)
125125
truncated = False
126126

127-
return obs, rwd, terminated, truncated, {}
127+
return obs, float(rwd), terminated, truncated, {}
128128

129129
def get_obs(self):
130130
obs = np.float32(

furuta/robot.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
import pinocchio as pin
77
import serial
88

9+
import furuta
10+
911

1012
class RobotModel:
1113
def __init__(self):
12-
self.robot = None
13-
base_path = Path("robot/hardware/v2/")
14-
if base_path.exists():
15-
urdf_path = str(base_path / "robot.urdf")
16-
stls_path = str(base_path / "stl")
17-
self.robot = pin.RobotWrapper.BuildFromURDF(urdf_path, [stls_path])
14+
base_path = Path(furuta.__path__[0]).parent / "robot" / "hardware" / "v2"
15+
urdf_path = base_path / "robot.urdf"
16+
stls_path = base_path / "stl"
17+
self.robot = pin.RobotWrapper.BuildFromURDF(urdf_path, [stls_path])
1818

1919

2020
class Robot:

furuta/viewer.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,16 @@ def display(self, state: np.ndarray) -> np.ndarray:
2121
def close(self):
2222
pass
2323

24-
def animate(self, times: np.ndarray, states: np.ndarray):
24+
def animate(self, times: np.ndarray, states: np.ndarray | list[np.ndarray]):
2525
# Initial state
26-
q = states[:, :2]
27-
self.display(q[0])
26+
self.display(states[0][:2])
2827
time.sleep(1.0)
2928
tic = time.time()
3029
for i in range(1, len(times)):
3130
toc = time.time()
3231
time.sleep(max(0, times[i] - times[i - 1] - (toc - tic)))
3332
tic = time.time()
34-
self.display(q[i])
33+
self.display(states[i][:2])
3534

3635

3736
class Viewer3D(AbstractViewer):

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
[project.optional-dependencies]
1919
dev = [
2020
"ipykernel>=6.26.0",
21-
"matplotlib==3.7.3",
21+
"matplotlib>=3.7.3",
2222
"pre-commit>=3.5.0",
2323
"stable-baselines3>=2.2.1",
2424
"wandb>=0.16.2",
@@ -30,8 +30,7 @@ dev = [
3030
"jax-metal>=0.0.5; sys_platform == 'darwin'",
3131
"sb3-contrib",
3232
"pytest>=7.4.4",
33-
"pin>=2.7.0",
34-
"crocoddyl>=2.0.2",
33+
"crocoddyl>=3.0.0",
3534
"onshape-to-robot>=0.3.26",
3635
"meshcat",
3736
]

scripts/configs/control/parameters.json

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)