raddo 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raddo/__init__.py +0 -0
- raddo/controllers/__init__.py +7 -0
- raddo/controllers/acados_controller.py +356 -0
- raddo/controllers/base_controller.py +18 -0
- raddo/controllers/cem_controller.py +198 -0
- raddo/controllers/feedback_controller.py +95 -0
- raddo/controllers/mppi_controller.py +187 -0
- raddo/controllers/pid_controller.py +72 -0
- raddo/controllers/qp_controller.py +369 -0
- raddo/controllers/sqp_controller.py +383 -0
- raddo/controllers/tvlqr_controller.py +108 -0
- raddo/dynamics/__init__.py +7 -0
- raddo/dynamics/base_dynamics.py +213 -0
- raddo/dynamics/double_integrator_dynamics.py +93 -0
- raddo/dynamics/magnetic_field.py +208 -0
- raddo/dynamics/magnetorquer_dynamics.py +189 -0
- raddo/dynamics/orbit_dynamics.py +162 -0
- raddo/dynamics/planetary_params.py +164 -0
- raddo/dynamics/quadrotor_dynamics.py +163 -0
- raddo/dynamics/quaternion_functions.py +81 -0
- raddo/dynamics/spacecraft_dynamics.py +262 -0
- raddo/utils/__init__.py +12 -0
- raddo/utils/adaptation.py +78 -0
- raddo/utils/coord_transforms.py +148 -0
- raddo/utils/learned_model_implementation.py +161 -0
- raddo/utils/learning.py +459 -0
- raddo/utils/models.py +118 -0
- raddo/utils/plotting.py +674 -0
- raddo/utils/propagate.py +629 -0
- raddo/utils/save_df.py +92 -0
- raddo-0.1.4.dist-info/METADATA +142 -0
- raddo-0.1.4.dist-info/RECORD +34 -0
- raddo-0.1.4.dist-info/WHEEL +4 -0
- raddo-0.1.4.dist-info/licenses/LICENSE +21 -0
raddo/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .base_controller import Controller
|
|
2
|
+
from .cem_controller import CEMController
|
|
3
|
+
from .feedback_controller import FeedbackController
|
|
4
|
+
from .mppi_controller import MPPIController
|
|
5
|
+
from .qp_controller import QPController
|
|
6
|
+
from .sqp_controller import SQPController
|
|
7
|
+
from .tvlqr_controller import TVLQRController
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import casadi as cs
|
|
6
|
+
import equinox as eqx
|
|
7
|
+
import jax
|
|
8
|
+
import jax.numpy as jnp
|
|
9
|
+
import numpy as np
|
|
10
|
+
from acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver
|
|
11
|
+
from jax.typing import ArrayLike
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from raddo.dynamics.base_dynamics import Dynamics
|
|
15
|
+
from raddo.utils.propagate import TrajectoryGenerator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpacecraftAcadosModel(eqx.Module):
|
|
19
|
+
"""
|
|
20
|
+
Constructs the AcadosModel used for defining the system dynamics.
|
|
21
|
+
"""
|
|
22
|
+
propagator: TrajectoryGenerator
|
|
23
|
+
dynamics: Dynamics
|
|
24
|
+
|
|
25
|
+
name: str
|
|
26
|
+
mass: float
|
|
27
|
+
inertia: jax.Array
|
|
28
|
+
inertia_inv: jax.Array
|
|
29
|
+
state_limits: jax.Array
|
|
30
|
+
control_limits: jax.Array
|
|
31
|
+
|
|
32
|
+
dt: float
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
propagator: TrajectoryGenerator,
|
|
37
|
+
state_limits: jax.Array,
|
|
38
|
+
control_limits: jax.Array,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Constructor for the spacecraft model
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
name (str): model name.
|
|
45
|
+
mass (float): robot mass [kg].
|
|
46
|
+
inertia (np.ndarray): 3x3 inertia tensor [kg*m^2].
|
|
47
|
+
omega_max (float): maximum (and defines minimum) angular rotation [rad/s].
|
|
48
|
+
moment_max (float): maximum moment applied along any axis [N*m].
|
|
49
|
+
dt (float): integration time step.
|
|
50
|
+
"""
|
|
51
|
+
self.name = 'spacecraft_acados_model'
|
|
52
|
+
self.propagator = propagator
|
|
53
|
+
self.dynamics = propagator.dynamics
|
|
54
|
+
self.dt = propagator.dt
|
|
55
|
+
self.mass = self.dynamics.dynamics_params['mass']
|
|
56
|
+
self.inertia = np.array(self.dynamics.dynamics_params['inertia'])
|
|
57
|
+
self.inertia_inv = np.array(self.dynamics.dynamics_params['inertia_inv'])
|
|
58
|
+
|
|
59
|
+
self.state_limits = state_limits
|
|
60
|
+
self.control_limits = control_limits
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def S(u: cs.MX | cs.SX | cs.DM) -> cs.MX | cs.SX | cs.DM:
|
|
64
|
+
"""
|
|
65
|
+
Skew symmetric map (lie algebra of SO(3), solves a x b = S(a)b)
|
|
66
|
+
"""
|
|
67
|
+
return cs.vertcat(
|
|
68
|
+
cs.horzcat(0, -u[2], u[1]),
|
|
69
|
+
cs.horzcat(u[2], 0, -u[0]),
|
|
70
|
+
cs.horzcat(-u[1], u[0], 0))
|
|
71
|
+
|
|
72
|
+
def q_left(self, q: cs.MX | cs.SX | cs.DM) -> cs.MX | cs.SX | cs.DM:
|
|
73
|
+
"""
|
|
74
|
+
Left quaternion product matrix.
|
|
75
|
+
Assumes q is [qs, qx, qy, qz] (scalar first).
|
|
76
|
+
"""
|
|
77
|
+
qs = q[0]
|
|
78
|
+
qv = q[1:]
|
|
79
|
+
|
|
80
|
+
# Top row: [qs, -qv.T]
|
|
81
|
+
row_top = cs.horzcat(qs, -qv.T)
|
|
82
|
+
|
|
83
|
+
# Bottom block: [qv, qs*I + S(qv)] (3x3)
|
|
84
|
+
row_bottom = cs.horzcat(qv, qs * cs.MX.eye(3) + self.S(qv))
|
|
85
|
+
|
|
86
|
+
return cs.vertcat(row_top, row_bottom)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def get_rotation(q: cs.MX | cs.SX | cs.DM) -> cs.MX | cs.SX | cs.DM:
|
|
90
|
+
"""Quaternion to rotation matrix"""
|
|
91
|
+
qw, qx, qy, qz = q[0], q[1], q[2], q[3]
|
|
92
|
+
qw2 = qw * qw
|
|
93
|
+
qx2 = qx * qx
|
|
94
|
+
qy2 = qy * qy
|
|
95
|
+
qz2 = qz * qz
|
|
96
|
+
return cs.vertcat(
|
|
97
|
+
cs.horzcat(qw2 + qx2 - qy2 - qz2, 2 * (qx * qy - qw * qz), 2 * (qx * qz + qw * qy)),
|
|
98
|
+
cs.horzcat(2 * (qx * qy + qw * qz), qw2 - qx2 + qy2 - qz2, 2 * (qy * qz - qw * qx)),
|
|
99
|
+
cs.horzcat(2 * (qx * qz - qw * qy), 2 * (qy * qz + qw * qx), qw2 - qx2 - qy2 + qz2))
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def q_conj(q: cs.MX | cs.SX | cs.DM) -> cs.MX | cs.SX | cs.DM:
|
|
103
|
+
"""Conjugate quaternion"""
|
|
104
|
+
return cs.vertcat(q[0], -q[1], -q[2], -q[3])
|
|
105
|
+
|
|
106
|
+
def get_acados_model(self) -> AcadosModel:
|
|
107
|
+
"""
|
|
108
|
+
Constructs the AcadosModel used for the 3-DoF attitude
|
|
109
|
+
planning problem for spacecraft.
|
|
110
|
+
Constructed w help from https://github.com/acados/acados/blob/main/examples/acados_python/getting_started/pendulum_model.py
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
model (AcadosModel): model for spacecraft slewing problem.
|
|
114
|
+
"""
|
|
115
|
+
# Set up states
|
|
116
|
+
q = cs.MX.sym('q',4,1)
|
|
117
|
+
v = cs.MX.sym('v', 3, 1)
|
|
118
|
+
x = cs.vertcat(q,v)
|
|
119
|
+
|
|
120
|
+
# Set up control input (moment)
|
|
121
|
+
M = cs.MX.sym('M', 3, 1)
|
|
122
|
+
|
|
123
|
+
# Set up params (b-field and target_state
|
|
124
|
+
b = cs.MX.sym('b',3,1)
|
|
125
|
+
x_g = cs.MX.sym('xg',7,1)
|
|
126
|
+
p = cs.vertcat(b,x_g)
|
|
127
|
+
|
|
128
|
+
# Magnetorquer torque: rotate b-field from PCI to body frame
|
|
129
|
+
b_body = self.get_rotation(self.q_conj(q)) @ b
|
|
130
|
+
tau = cs.cross(M, b_body) * 1e-9 # convert nT to T
|
|
131
|
+
|
|
132
|
+
# Set up discrete dynamics
|
|
133
|
+
qdot = 0.5*self.q_left(q) @ cs.vertcat(0.0, v)
|
|
134
|
+
J = cs.MX(self.inertia)
|
|
135
|
+
J_inv = cs.MX(self.inertia_inv)
|
|
136
|
+
|
|
137
|
+
# Use torque directly (M) or use dipole (tau)
|
|
138
|
+
vdot = J_inv @ (tau - cs.cross(v, J @ v)) # use dipole
|
|
139
|
+
#vdot = J_inv @ (M - cs.cross(v, J @ v)) # use torque directly
|
|
140
|
+
|
|
141
|
+
f_disc = cs.vertcat((q + qdot*self.dt)/cs.norm_2(q + qdot*self.dt), v + vdot*self.dt)
|
|
142
|
+
|
|
143
|
+
model = AcadosModel()
|
|
144
|
+
|
|
145
|
+
model.disc_dyn_expr = f_disc
|
|
146
|
+
model.x = x
|
|
147
|
+
model.u = M
|
|
148
|
+
model.p = p
|
|
149
|
+
model.name = self.name
|
|
150
|
+
|
|
151
|
+
return model
|
|
152
|
+
|
|
153
|
+
class AcadosController(eqx.Module):
|
|
154
|
+
model: SpacecraftAcadosModel
|
|
155
|
+
horizon: int
|
|
156
|
+
|
|
157
|
+
Q: jax.Array
|
|
158
|
+
Qf: jax.Array
|
|
159
|
+
R: jax.Array
|
|
160
|
+
|
|
161
|
+
max_iter: int
|
|
162
|
+
ocp_solver: AcadosOcpSolver = eqx.field(static=True)
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
model: SpacecraftAcadosModel,
|
|
167
|
+
Q: jax.Array,
|
|
168
|
+
Qf: jax.Array,
|
|
169
|
+
R: jax.Array,
|
|
170
|
+
horizon: int,
|
|
171
|
+
max_iter: int,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Attributes:
|
|
175
|
+
model (AcadosModel): acados model.
|
|
176
|
+
ocp_solver (AcadosOcpSolver): acados solver.
|
|
177
|
+
N (int): number of time steps in optimal control horizon.
|
|
178
|
+
x0 (np.ndarray): initial state of system.
|
|
179
|
+
Q_mat (list[float]): diagonal elements of the Q cost term.
|
|
180
|
+
R_mat (list[float]): diagonal elements of the R cost term.
|
|
181
|
+
"""
|
|
182
|
+
self.model = model
|
|
183
|
+
self.horizon = horizon
|
|
184
|
+
|
|
185
|
+
# set cost
|
|
186
|
+
self.Q = Q
|
|
187
|
+
self.Qf = Qf
|
|
188
|
+
self.R = R
|
|
189
|
+
|
|
190
|
+
self.max_iter = max_iter
|
|
191
|
+
self.ocp_solver = self.setup(self.horizon)
|
|
192
|
+
|
|
193
|
+
def setup(self, N_horizon:int) -> AcadosOcpSolver:
|
|
194
|
+
"""
|
|
195
|
+
Method to set up the AcadosOcpSolver; this is where the constraints and cost terms
|
|
196
|
+
are explicitly defined for the system using the AcadosModel.
|
|
197
|
+
|
|
198
|
+
Parameters:
|
|
199
|
+
x0 (np.ndarray): initial state of system.
|
|
200
|
+
N_horizon (int): number of time steps in optimal control horizon.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
ocp_solver (AcadosOcpSolver): acados solver.
|
|
204
|
+
"""
|
|
205
|
+
# Create ocp object to formulate the OCP
|
|
206
|
+
ocp = AcadosOcp()
|
|
207
|
+
|
|
208
|
+
# set model
|
|
209
|
+
ocp.model = self.model.get_acados_model()
|
|
210
|
+
|
|
211
|
+
# set prediction horizon
|
|
212
|
+
ocp.solver_options.N_horizon = N_horizon
|
|
213
|
+
ocp.solver_options.tf = N_horizon*self.model.dt
|
|
214
|
+
|
|
215
|
+
# init param
|
|
216
|
+
ocp.parameter_values = np.zeros(10) #b-field (3) and target_state (7)
|
|
217
|
+
target_state = ocp.model.p[3:]
|
|
218
|
+
q_g = target_state[:4]
|
|
219
|
+
omega_g = target_state[4:]
|
|
220
|
+
|
|
221
|
+
# path cost
|
|
222
|
+
ocp.cost.cost_type = 'NONLINEAR_LS'
|
|
223
|
+
|
|
224
|
+
q_error = 1 - cs.dot(ocp.model.x[:4], q_g)**2
|
|
225
|
+
omega_error = omega_g - ocp.model.x[4:7]
|
|
226
|
+
|
|
227
|
+
ocp.model.cost_y_expr = cs.vertcat(q_error, omega_error, ocp.model.u)
|
|
228
|
+
ocp.cost.yref = np.zeros(7)
|
|
229
|
+
ocp.cost.W = np.array(jax.scipy.linalg.block_diag(self.Q, self.R))
|
|
230
|
+
|
|
231
|
+
# terminal cost
|
|
232
|
+
ocp.cost.cost_type_e = 'NONLINEAR_LS'
|
|
233
|
+
ocp.model.cost_y_expr_e = cs.vertcat(q_error, omega_error)
|
|
234
|
+
ocp.cost.yref_e = np.zeros(4)
|
|
235
|
+
ocp.cost.W_e = np.array(self.Qf)
|
|
236
|
+
|
|
237
|
+
# set constraints
|
|
238
|
+
ocp.constraints.x0 = np.array([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) # placeholder, gets set in solve
|
|
239
|
+
ocp.constraints.ubu = np.array(self.model.control_limits[:,1])
|
|
240
|
+
ocp.constraints.lbu = np.array(self.model.control_limits[:,0])
|
|
241
|
+
ocp.constraints.idxbu = np.array([0, 1, 2])
|
|
242
|
+
|
|
243
|
+
ocp.constraints.ubx = np.array(self.model.state_limits[:,1])
|
|
244
|
+
ocp.constraints.lbx = np.array(self.model.state_limits[:,0])
|
|
245
|
+
ocp.constraints.idxbx = np.arange(3)
|
|
246
|
+
|
|
247
|
+
# Set solver details
|
|
248
|
+
ocp.solver_options.integrator_type = 'DISCRETE'
|
|
249
|
+
ocp.solver_options.nlp_solver_type = 'SQP'
|
|
250
|
+
ocp.solver_options.nlp_solver_max_iter = self.max_iter
|
|
251
|
+
|
|
252
|
+
#ocp.solver_options.qp_solver = 'PARTIAL_CONDENSING_HPIPM'
|
|
253
|
+
#ocp.solver_options.hessian_approx = 'GAUSS_NEWTON'
|
|
254
|
+
#ocp.solver_options.qp_solver_iter_max = 1000
|
|
255
|
+
#ocp.solver_options.tol = 1e-4
|
|
256
|
+
|
|
257
|
+
ocp_solver = AcadosOcpSolver(ocp, verbose=False)
|
|
258
|
+
|
|
259
|
+
return ocp_solver
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def solve(
|
|
263
|
+
self,
|
|
264
|
+
x0: jax.Array,
|
|
265
|
+
target_state: jax.Array,
|
|
266
|
+
key: jax.Array,
|
|
267
|
+
external_params: jax.Array,
|
|
268
|
+
nominal_traj: jax.Array,
|
|
269
|
+
nominal_cntrl: jax.Array,
|
|
270
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
271
|
+
"""
|
|
272
|
+
Solves the optimal control problem starting at an initial state x0.
|
|
273
|
+
|
|
274
|
+
Parameters:
|
|
275
|
+
x0 (np.ndarray: initial state.
|
|
276
|
+
verbose (bool): verbosity level of solver.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
x_traj (np.ndarray): state trajectory for system.
|
|
280
|
+
u_traj (np.ndarray): control trajectory for system.
|
|
281
|
+
"""
|
|
282
|
+
x_traj = np.zeros((self.horizon+1, len(x0)))
|
|
283
|
+
u_traj = np.zeros((self.horizon, 3))
|
|
284
|
+
|
|
285
|
+
self.ocp_solver.set(0, 'lbx', np.array(x0))
|
|
286
|
+
self.ocp_solver.set(0, 'ubx', np.array(x0))
|
|
287
|
+
|
|
288
|
+
nominal_traj = np.array(nominal_traj)
|
|
289
|
+
nominal_cntrl = np.array(nominal_cntrl)
|
|
290
|
+
|
|
291
|
+
for i in range(self.horizon):
|
|
292
|
+
# Different parameter at each node
|
|
293
|
+
p = jnp.concatenate([external_params[i,:], target_state])
|
|
294
|
+
self.ocp_solver.set(i, 'p', np.array(p))
|
|
295
|
+
|
|
296
|
+
# initialize to init traj
|
|
297
|
+
self.ocp_solver.set(i, 'x', nominal_traj[i, :])
|
|
298
|
+
self.ocp_solver.set(i, 'u', nominal_cntrl[i, :])
|
|
299
|
+
|
|
300
|
+
# terminal condition
|
|
301
|
+
p = jnp.concatenate([external_params[-1,:], target_state])
|
|
302
|
+
self.ocp_solver.set(self.horizon, 'p', np.array(p))
|
|
303
|
+
self.ocp_solver.set(self.horizon, 'x', nominal_traj[-1, :])
|
|
304
|
+
|
|
305
|
+
status = self.ocp_solver.solve()
|
|
306
|
+
|
|
307
|
+
verbose=False
|
|
308
|
+
|
|
309
|
+
if verbose:
|
|
310
|
+
if status == 0:
|
|
311
|
+
print("Success")
|
|
312
|
+
elif status == 1:
|
|
313
|
+
print("Acados failure or nan")
|
|
314
|
+
elif status == 2:
|
|
315
|
+
print("Max iterations")
|
|
316
|
+
elif status == 3:
|
|
317
|
+
print("Acados minstep")
|
|
318
|
+
elif status == 4:
|
|
319
|
+
print("QP Failure")
|
|
320
|
+
|
|
321
|
+
for i in range(self.horizon):
|
|
322
|
+
x_traj[i,:] = self.ocp_solver.get(i, 'x')
|
|
323
|
+
u_traj[i,:] = self.ocp_solver.get(i, 'u')
|
|
324
|
+
x_traj[self.horizon,:] = self.ocp_solver.get(self.horizon, 'x')
|
|
325
|
+
|
|
326
|
+
return jnp.array(x_traj), jnp.array(u_traj)
|
|
327
|
+
|
|
328
|
+
def __call__(
|
|
329
|
+
self,
|
|
330
|
+
x0: jax.Array,
|
|
331
|
+
target_state: jax.Array,
|
|
332
|
+
key: jax.Array,
|
|
333
|
+
external_params: jax.Array,
|
|
334
|
+
nominal_traj: jax.Array,
|
|
335
|
+
nominal_cntrl: jax.Array,
|
|
336
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
337
|
+
# Ensure shapes use concrete integers (self.horizon)
|
|
338
|
+
shape1 = jax.ShapeDtypeStruct((self.horizon + 1, x0.shape[-1]), jnp.float32)
|
|
339
|
+
shape2 = jax.ShapeDtypeStruct((self.horizon, 3), jnp.float32)
|
|
340
|
+
|
|
341
|
+
# Wrap the call to avoid hashing 'self'
|
|
342
|
+
def callback_wrapper(
|
|
343
|
+
x0: np.ndarray,
|
|
344
|
+
target: np.ndarray,
|
|
345
|
+
k: np.ndarray,
|
|
346
|
+
ext: np.ndarray,
|
|
347
|
+
nom_t: np.ndarray,
|
|
348
|
+
nom_c: np.ndarray,
|
|
349
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
350
|
+
return self.solve(x0, target, k, ext, nom_t, nom_c)
|
|
351
|
+
|
|
352
|
+
return jax.pure_callback(
|
|
353
|
+
callback_wrapper,
|
|
354
|
+
(shape1, shape2),
|
|
355
|
+
x0, target_state, key, external_params, nominal_traj, nominal_cntrl,
|
|
356
|
+
vmap_method='sequential')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import equinox as eqx
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import jax
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Controller(eqx.Module):
|
|
12
|
+
def __call__(
|
|
13
|
+
self,
|
|
14
|
+
state: jax.Array,
|
|
15
|
+
target: jax.Array,
|
|
16
|
+
key: jax.Array,
|
|
17
|
+
) -> jax.Array | tuple[jax.Array, jax.Array]:
|
|
18
|
+
raise NotImplementedError("Must define a controller if no control sequence is given")
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import equinox as eqx
|
|
6
|
+
import jax
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
import jax.random as jrandom
|
|
9
|
+
|
|
10
|
+
from .base_controller import Controller
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from raddo.dynamics.base_dynamics import Dynamics
|
|
14
|
+
from raddo.utils.propagate import TrajectoryGenerator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CEMController(Controller):
|
|
18
|
+
propagator: TrajectoryGenerator
|
|
19
|
+
dynamics: Dynamics
|
|
20
|
+
|
|
21
|
+
# Parameters
|
|
22
|
+
Q: jax.Array
|
|
23
|
+
Qf: jax.Array
|
|
24
|
+
R: jax.Array
|
|
25
|
+
control_limits: jax.Array
|
|
26
|
+
|
|
27
|
+
# Hyperparameters (Static) (ints and floats default to static)
|
|
28
|
+
num_samples: int
|
|
29
|
+
num_iter: int
|
|
30
|
+
horizon: int
|
|
31
|
+
elite_percent: float
|
|
32
|
+
init_std: float
|
|
33
|
+
|
|
34
|
+
# Useful for working with dynamics
|
|
35
|
+
quat_start: int
|
|
36
|
+
nx: int
|
|
37
|
+
nu: int
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
propagator: TrajectoryGenerator,
|
|
42
|
+
Q: jax.Array,
|
|
43
|
+
Qf: jax.Array,
|
|
44
|
+
R: jax.Array,
|
|
45
|
+
control_limits: jax.Array,
|
|
46
|
+
num_samples: int,
|
|
47
|
+
num_iter: int,
|
|
48
|
+
horizon: int,
|
|
49
|
+
elite_percent: float=0.1,
|
|
50
|
+
init_std: float=0.1,
|
|
51
|
+
) -> None:
|
|
52
|
+
object.__setattr__(self, "propagator", propagator)
|
|
53
|
+
object.__setattr__(self, "dynamics", self.propagator.dynamics)
|
|
54
|
+
object.__setattr__(self, "Q", (Q + Q.T)/2) # ensure symmetric
|
|
55
|
+
object.__setattr__(self, "Qf", (Qf + Qf.T)/2)
|
|
56
|
+
object.__setattr__(self, "R", (R+R.T)/2)
|
|
57
|
+
object.__setattr__(self, "num_samples", num_samples)
|
|
58
|
+
object.__setattr__(self, "num_iter", num_iter)
|
|
59
|
+
object.__setattr__(self, "horizon", horizon)
|
|
60
|
+
object.__setattr__(self, "elite_percent", elite_percent)
|
|
61
|
+
object.__setattr__(self, "init_std", init_std)
|
|
62
|
+
object.__setattr__(self, "nu", self.dynamics.num_controls)
|
|
63
|
+
object.__setattr__(self, "control_limits", control_limits)
|
|
64
|
+
|
|
65
|
+
self.nx = self.dynamics.num_states
|
|
66
|
+
nx = self.dynamics.num_states
|
|
67
|
+
quat_start = self.dynamics.params.get("quat_start")
|
|
68
|
+
|
|
69
|
+
# Check if attitude Jacobian is square (whether using mrp's or not)
|
|
70
|
+
if not self.dynamics.E(jnp.ones(nx)).shape[0] == self.dynamics.E(jnp.ones(nx)).shape[1]:
|
|
71
|
+
nx = self.dynamics.num_states - 1
|
|
72
|
+
|
|
73
|
+
object.__setattr__(self, "nx", nx)
|
|
74
|
+
object.__setattr__(self, "quat_start", quat_start)
|
|
75
|
+
|
|
76
|
+
def compute_cost(
|
|
77
|
+
self,
|
|
78
|
+
states: jax.Array,
|
|
79
|
+
controls: jax.Array,
|
|
80
|
+
target_state: jax.Array,
|
|
81
|
+
) -> jax.Array:
|
|
82
|
+
"""Computes the total quadratic cost for a trajectory and control sequence, including terminal cost.
|
|
83
|
+
Inputs:
|
|
84
|
+
states: Sequence of states, shape (T+1, nx)
|
|
85
|
+
controls: Sequence of controls, shape (T, nu)
|
|
86
|
+
Outputs:
|
|
87
|
+
cost: Scalar total cost (float)
|
|
88
|
+
"""
|
|
89
|
+
# Terminal cost
|
|
90
|
+
final_error = self.dynamics.get_error_coordinates(states[-1,:], target_state)
|
|
91
|
+
J_terminal = 0.5 * final_error.T @ self.Qf @ final_error
|
|
92
|
+
|
|
93
|
+
# Stage cost (vectorized over time)
|
|
94
|
+
diff_stage = states[:-1, :] - target_state
|
|
95
|
+
|
|
96
|
+
def stage_step_cost(x_k: jax.Array, u_k: jax.Array) -> jax.Array:
|
|
97
|
+
error = self.dynamics.get_error_coordinates(x_k, target_state)
|
|
98
|
+
return (0.5 * error.T @ self.Q @ error + 0.5 * u_k.T @ self.R @ u_k)*self.propagator.dt
|
|
99
|
+
|
|
100
|
+
J_stage = jax.vmap(stage_step_cost)(states[:-1], controls)
|
|
101
|
+
|
|
102
|
+
return J_terminal + jnp.sum(J_stage)
|
|
103
|
+
|
|
104
|
+
@eqx.filter_jit
|
|
105
|
+
def __call__(
|
|
106
|
+
self,
|
|
107
|
+
x0: jax.Array,
|
|
108
|
+
x_goal: jax.Array,
|
|
109
|
+
key: jax.Array,
|
|
110
|
+
external_dynamics_params: jax.Array,
|
|
111
|
+
nominal_traj: jax.Array,
|
|
112
|
+
nominal_cntrl: jax.Array,
|
|
113
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
114
|
+
"""
|
|
115
|
+
Cross-Entropy (CEM) algorithm.
|
|
116
|
+
Returns:
|
|
117
|
+
nominal state trajectory: (T+1, nx)
|
|
118
|
+
nominal control trajectory: (T, nu)
|
|
119
|
+
"""
|
|
120
|
+
n_elites = int(self.num_samples * self.elite_percent)
|
|
121
|
+
nu = self.nu
|
|
122
|
+
|
|
123
|
+
# Initialize mean and covariance (std dev)
|
|
124
|
+
mu = jnp.zeros((self.horizon, nu)) # init to 0
|
|
125
|
+
# mu = nominal_cntrl # warm-start
|
|
126
|
+
sigma = self.init_std * jnp.ones((self.horizon, nu)) # Initial wide exploration
|
|
127
|
+
|
|
128
|
+
# Optimization Loop
|
|
129
|
+
def scan_step(
|
|
130
|
+
carry: tuple[jax.Array, jax.Array, jax.Array],
|
|
131
|
+
_: None,
|
|
132
|
+
) -> tuple[tuple[jax.Array, jax.Array, jax.Array], jax.Array]:
|
|
133
|
+
curr_mu, curr_sigma, key = carry
|
|
134
|
+
|
|
135
|
+
key, sample_key, traj_key = jrandom.split(key, 3)
|
|
136
|
+
|
|
137
|
+
# Sample controls: (K, T, nu)
|
|
138
|
+
# z ~ N(0, 1) -> u = mu + sigma * z
|
|
139
|
+
z = jrandom.normal(sample_key, shape=(self.num_samples, self.horizon, nu))
|
|
140
|
+
u_candidates = curr_mu + curr_sigma * z
|
|
141
|
+
|
|
142
|
+
# Clip
|
|
143
|
+
u_candidates = jnp.clip(
|
|
144
|
+
u_candidates,
|
|
145
|
+
self.control_limits[:, 0],
|
|
146
|
+
self.control_limits[:, 1]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Rollout batch of trajectories from different u_candidates
|
|
150
|
+
states_batch, _ = self.propagator._generate_trajectory_batch_seq(
|
|
151
|
+
initial_states = x0,
|
|
152
|
+
target_states = x_goal,
|
|
153
|
+
key = traj_key,
|
|
154
|
+
batch_size = self.num_samples,
|
|
155
|
+
noise_std = .001, #jnp.array([0.001]*4 + [0.005]*3)
|
|
156
|
+
|
|
157
|
+
num_steps = self.horizon,
|
|
158
|
+
external_dynamics_params = external_dynamics_params,
|
|
159
|
+
control_sequence = u_candidates)
|
|
160
|
+
|
|
161
|
+
# Compute costs (vmap over the batch dimension)
|
|
162
|
+
costs = jax.vmap(self.compute_cost, in_axes=(0, 0, None))(states_batch, u_candidates, x_goal)
|
|
163
|
+
|
|
164
|
+
# Select Elites
|
|
165
|
+
_, idx_elites = jax.lax.top_k(-costs, n_elites)
|
|
166
|
+
|
|
167
|
+
# Gather elite samples: (n_elites, T, nu)
|
|
168
|
+
elites = u_candidates[idx_elites]
|
|
169
|
+
elite_traj = states_batch[idx_elites]
|
|
170
|
+
|
|
171
|
+
# Update Distribution
|
|
172
|
+
new_mu = jnp.mean(elites, axis=0)
|
|
173
|
+
new_sigma = jnp.std(elites, axis=0)
|
|
174
|
+
|
|
175
|
+
# Get mean cost of elites
|
|
176
|
+
cost = jnp.mean(costs[idx_elites])
|
|
177
|
+
|
|
178
|
+
#return (new_mu, new_sigma, key), carry # [[ 1 ]] For running in closed loop
|
|
179
|
+
return (new_mu, new_sigma, key), cost#, states_batch, elite_traj) # [[ 2 ]] Store all samples for debugging
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# Run optimization
|
|
183
|
+
init_carry = (mu, sigma, key)
|
|
184
|
+
(final_mu, final_sigma, final_key), _ = jax.lax.scan(scan_step, init_carry, length=self.num_iter)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
final_u_nominal = jnp.clip(final_mu, self.control_limits[:, 0], self.control_limits[:, 1])
|
|
188
|
+
|
|
189
|
+
best_states, best_controls = self.propagator._generate_trajectory_seq(
|
|
190
|
+
initial_state = x0,
|
|
191
|
+
target_state = x_goal,
|
|
192
|
+
key = final_key,
|
|
193
|
+
noise_std = 0.0,
|
|
194
|
+
num_steps = self.horizon,
|
|
195
|
+
external_dynamics_params = external_dynamics_params,
|
|
196
|
+
control_sequence = final_u_nominal)
|
|
197
|
+
|
|
198
|
+
return best_states, best_controls
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import equinox as eqx
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
|
|
8
|
+
from .base_controller import Controller
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import jax
|
|
12
|
+
|
|
13
|
+
from raddo.dynamics.base_dynamics import Dynamics
|
|
14
|
+
from raddo.utils.propagate import TrajectoryGenerator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FeedbackController(Controller):
|
|
18
|
+
propagator: TrajectoryGenerator
|
|
19
|
+
dynamics: Dynamics
|
|
20
|
+
cntrl_param_init: jax.Array
|
|
21
|
+
|
|
22
|
+
# Parameters
|
|
23
|
+
Q: jax.Array
|
|
24
|
+
R: jax.Array
|
|
25
|
+
control_limits: jax.Array
|
|
26
|
+
|
|
27
|
+
nx: int = eqx.field(static=True)
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
propagator: TrajectoryGenerator,
|
|
32
|
+
Q: jax.Array,
|
|
33
|
+
Qf: jax.Array,
|
|
34
|
+
R: jax.Array,
|
|
35
|
+
control_limits: jax.Array,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.propagator = propagator
|
|
38
|
+
self.dynamics = self.propagator.dynamics
|
|
39
|
+
self.Q = Q
|
|
40
|
+
self.cntrl_param_init = Qf
|
|
41
|
+
self.R = R
|
|
42
|
+
self.control_limits = control_limits
|
|
43
|
+
|
|
44
|
+
nx = self.dynamics.num_states
|
|
45
|
+
|
|
46
|
+
# Check if attitude Jacobian is square (whether using mrp's or not)
|
|
47
|
+
if not self.dynamics.E(jnp.ones(nx)).shape[0] == self.dynamics.E(jnp.ones(nx)).shape[1]:
|
|
48
|
+
nx = self.dynamics.num_states - 1
|
|
49
|
+
|
|
50
|
+
object.__setattr__(self, "nx", nx)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@eqx.filter_jit
|
|
54
|
+
def __call__(
|
|
55
|
+
self,
|
|
56
|
+
x0: jax.Array,
|
|
57
|
+
x_goal: jax.Array,
|
|
58
|
+
key: jax.Array,
|
|
59
|
+
external_dynamics_params: jax.Array,
|
|
60
|
+
nominal_traj: jax.Array,
|
|
61
|
+
nominal_cntrl: jax.Array,
|
|
62
|
+
cntrl_param: jax.Array,
|
|
63
|
+
) -> tuple[jax.Array, jax.Array]:
|
|
64
|
+
"""
|
|
65
|
+
Feedback controller for tracking a nominal trajectory
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
nominal state trajectory: (T+1, nx)
|
|
69
|
+
nominal control trajectory: (T, nu)
|
|
70
|
+
"""
|
|
71
|
+
dt = self.propagator.dt
|
|
72
|
+
A, B = self.dynamics.linearize_and_discretize(jnp.array([x0, x0]), nominal_cntrl[:1], external_dynamics_params[:1], dt)
|
|
73
|
+
#A, B = self.dynamics.linearize_and_discretize(nominal_traj, nominal_cntrl, external_dynamics_params, dt)
|
|
74
|
+
|
|
75
|
+
A = A[0]
|
|
76
|
+
B = B[0]
|
|
77
|
+
Q = self.Q
|
|
78
|
+
R = self.R
|
|
79
|
+
S = cntrl_param
|
|
80
|
+
|
|
81
|
+
dx = self.dynamics.get_error_coords(x0, nominal_traj[0])
|
|
82
|
+
|
|
83
|
+
# Ricatti equation
|
|
84
|
+
eps = 1e-8*jnp.ones(R.shape) # to avoid singular matrix
|
|
85
|
+
K = jnp.linalg.solve(R + B.T @ S @ B + eps, B.T @ S @ A)
|
|
86
|
+
# S = Q + K.T @ R @ K + (A - B@K).T @ S @ (A - B@K)
|
|
87
|
+
S = Q + A.T @ S @ A - A.T @ S @ B @ K
|
|
88
|
+
|
|
89
|
+
# Control law
|
|
90
|
+
u = nominal_cntrl[0] - K @ dx
|
|
91
|
+
|
|
92
|
+
# Clip controls
|
|
93
|
+
u = jnp.clip(u, self.control_limits[:, 0], self.control_limits[:, 1])
|
|
94
|
+
|
|
95
|
+
return u, S
|