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 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