PyDiffGame 0.1.2__py3-none-any.whl → 2.0.0__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.
@@ -1,17 +1,33 @@
1
- # Imports
1
+ """Hierarchical quadrotor control via continuous differential games.
2
+
3
+ A quadrotor is stabilised by a two-layer controller:
4
+
5
+ * a *low-level* angular-rate controller, designed as a 3-player continuous
6
+ differential game on the body-rate dynamics (one player per body axis), and
7
+ * a *high-level* outflow/position controller, designed as a 2-player continuous
8
+ differential game on a 9-state outer-loop model.
9
+
10
+ Both layers solve their feedback Nash gains with
11
+ :class:`~PyDiffGame.continuous.ContinuousPyDiffGame` and the resulting commands
12
+ drive the full nonlinear rigid-body dynamics, integrated with
13
+ :func:`scipy.integrate.odeint`.
14
+
15
+ Run directly to simulate a short trajectory headless::
16
+
17
+ python -m PyDiffGame.examples.QuadRotorControl
18
+ """
19
+
20
+ from __future__ import annotations
2
21
 
3
22
  import numpy as np
23
+ from numpy import arctan, cos, sin
4
24
  from numpy.linalg import inv
5
- import matplotlib.pyplot as plt
6
25
  from scipy.integrate import odeint
7
- from tqdm import tqdm
8
- from math import cos, sin
9
26
  from scipy.optimize import nnls
10
- from numpy import sin, cos, arctan
11
- from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
12
- from mpl_toolkits.axes_grid1.inset_locator import mark_inset
13
27
 
14
- from PyDiffGame.ContinuousPyDiffGame import ContinuousPyDiffGame
28
+ from PyDiffGame.continuous import ContinuousPyDiffGame
29
+ from PyDiffGame.objective import GameObjective
30
+ from PyDiffGame.plotting import show
15
31
 
16
32
  # Global Constants
17
33
 
@@ -21,7 +37,7 @@ Ixx = 7.5e-3
21
37
  Iyy = 7.5e-3
22
38
  Izz = 1.3e-2
23
39
  m = 0.65
24
- l = 0.23
40
+ length = 0.23
25
41
  Jr = 6e-5
26
42
  b = 3.13e-5
27
43
  d = 7.5e-7
@@ -32,18 +48,19 @@ a3 = (Izz - Ixx) / Iyy
32
48
  a4 = Jr / Iyy
33
49
  a5 = (Ixx - Iyy) / Izz
34
50
 
35
- b1 = l / Ixx
36
- b2 = l / Iyy
51
+ b1 = length / Ixx
52
+ b2 = length / Iyy
37
53
  b3 = 1 / Izz
38
54
 
39
- v_d_s_0_2 = 0
40
- h_d_s_0_2 = 0
55
+ v_d_s_0_2 = 0.0
56
+ h_d_s_0_2 = 0.0
41
57
  sum_theta = sum_theta_init
42
- sum_theta_2 = 0
58
+ sum_theta_2 = 0.0
43
59
 
44
60
 
45
61
  # Low-Level Control
46
62
 
63
+
47
64
  def quad_rotor_state_diff_eqn_for_given_pqrT(X, _, p, q, r, T, Plast):
48
65
  phi, dPhidt, theta, dThetadt, psi, dPsidt, z, dzdt, x, dxdt, y, dydt = X
49
66
  u_x = cos(phi) * sin(theta) * cos(psi) + sin(phi) * sin(psi)
@@ -51,16 +68,15 @@ def quad_rotor_state_diff_eqn_for_given_pqrT(X, _, p, q, r, T, Plast):
51
68
 
52
69
  K = cos(phi) * cos(theta) / m
53
70
  U = low_level_angular_rate_controller([dPhidt, dThetadt, dPsidt], p, q, r, T, Plast)
54
- omegas_squared_coeffs = np.array([[b] * 4,
55
- [0, -b, 0, b],
56
- [b, 0, -b, 0],
57
- [-d, d, -d, d]
58
- ])
71
+ omegas_squared_coeffs = np.array([[b] * 4, [0, -b, 0, b], [b, 0, -b, 0], [-d, d, -d, d]])
59
72
  u1, u2, u3, u4 = U
60
73
  omegas_squared = nnls(omegas_squared_coeffs, np.array([u1, u2, u3, u4]))[0]
61
74
  omegas = np.sqrt(omegas_squared)
62
75
 
63
- u5 = d * (omegas[0] - omegas[1] + omegas[2] - omegas[3])
76
+ # Residual rotor angular rate Omega_r = w1 - w2 + w3 - w4 (rad/s) driving the
77
+ # gyroscopic coupling. It must NOT be scaled by the drag coefficient d, which
78
+ # would shrink it by ~1e6 and zero out the gyroscopic terms.
79
+ u5 = omegas[0] - omegas[1] + omegas[2] - omegas[3]
64
80
 
65
81
  dPhiddt = dThetadt * (dPsidt * a1 + u5 * a2) + b1 * u2
66
82
  dThetaddt = dPhidt * (dPsidt * a3 - u5 * a4) + b2 * u3
@@ -69,22 +85,18 @@ def quad_rotor_state_diff_eqn_for_given_pqrT(X, _, p, q, r, T, Plast):
69
85
  dxddt = u_x * u1 / m
70
86
  dyddt = u_y * u1 / m
71
87
 
72
- return np.array([dPhidt, dPhiddt, dThetadt, dThetaddt, dPsidt, dPsiddt, dzdt,
73
- dzddt, dxdt, dxddt, dydt, dyddt], dtype='float64')
88
+ return np.array(
89
+ [dPhidt, dPhiddt, dThetadt, dThetaddt, dPsidt, dPsiddt, dzdt, dzddt, dxdt, dxddt, dydt, dyddt],
90
+ dtype=np.float64,
91
+ )
74
92
 
75
93
 
76
94
  def low_level_angular_rate_controller(x, p, q, r, T, Plast):
77
- B1 = np.array([[b1],
78
- [0],
79
- [0]])
95
+ B1 = np.array([[b1], [0], [0]])
80
96
 
81
- B2 = np.array([[0],
82
- [b2],
83
- [0]])
97
+ B2 = np.array([[0], [b2], [0]])
84
98
 
85
- B3 = np.array([[0],
86
- [0],
87
- [b3]])
99
+ B3 = np.array([[0], [0], [b3]])
88
100
 
89
101
  R1 = np.array([[0.1]])
90
102
  R2 = np.array([[0.1]])
@@ -95,9 +107,13 @@ def low_level_angular_rate_controller(x, p, q, r, T, Plast):
95
107
  P_sol = Plast
96
108
  reduced_X = np.array(x) - np.array([p, q, r])
97
109
  reduced_X_tr = reduced_X.T
98
- inv_Rs = [inv(r) for r in R]
99
- B_t = [b.T for b in B]
100
- U_angular = np.array([- r @ b @ p @ reduced_X_tr for r, b, p in zip(inv_Rs, B_t, P_sol)]).reshape(3, )
110
+ inv_Rs = [inv(r_i) for r_i in R]
111
+ B_t = [b_i.T for b_i in B]
112
+ U_angular = np.array(
113
+ [-r_i @ b_i @ p_i @ reduced_X_tr for r_i, b_i, p_i in zip(inv_Rs, B_t, P_sol)]
114
+ ).reshape(
115
+ 3,
116
+ )
101
117
  u2, u3, u4 = U_angular
102
118
  U = [T, u2, u3, u4]
103
119
 
@@ -105,56 +121,55 @@ def low_level_angular_rate_controller(x, p, q, r, T, Plast):
105
121
 
106
122
 
107
123
  def get_P_quad_given_angular_rates(x, P_sol):
108
- A = np.array([[0, (1 / 2) * a1 * x[2], (1 / 2) * a1 * x[1]],
109
- [(1 / 2) * a3 * x[2], 0, (1 / 2) * a3 * x[0]],
110
- [(1 / 2) * a5 * x[1], (1 / 2) * a5 * x[0], 0]])
124
+ A = np.array(
125
+ [
126
+ [0, (1 / 2) * a1 * x[2], (1 / 2) * a1 * x[1]],
127
+ [(1 / 2) * a3 * x[2], 0, (1 / 2) * a3 * x[0]],
128
+ [(1 / 2) * a5 * x[1], (1 / 2) * a5 * x[0], 0],
129
+ ]
130
+ )
111
131
 
112
- B1 = np.array([[b1],
113
- [0],
114
- [0]])
132
+ B1 = np.array([[b1], [0], [0]])
115
133
 
116
- B2 = np.array([[0],
117
- [b2],
118
- [0]])
134
+ B2 = np.array([[0], [b2], [0]])
119
135
 
120
- B3 = np.array([[0],
121
- [0],
122
- [b3]])
136
+ B3 = np.array([[0], [0], [b3]])
123
137
 
124
- Q1 = np.array([[1000, 0, 0],
125
- [0, 10, 0],
126
- [0, 0, 10]])
138
+ Q1 = np.array([[1000, 0, 0], [0, 10, 0], [0, 0, 10]])
127
139
 
128
- Q2 = np.array([[10, 0, 0],
129
- [0, 1000, 0],
130
- [0, 0, 10]])
140
+ Q2 = np.array([[10, 0, 0], [0, 1000, 0], [0, 0, 10]])
131
141
 
132
- Q3 = np.array([[10, 0, 0],
133
- [0, 10, 0],
134
- [0, 0, 1000]])
142
+ Q3 = np.array([[10, 0, 0], [0, 10, 0], [0, 0, 1000]])
135
143
 
136
144
  R1 = np.array([[0.1]])
137
145
  R2 = np.array([[0.1]])
138
146
  R3 = np.array([[0.1]])
139
147
 
140
- B = [B1, B2, B3]
148
+ Bs = [B1, B2, B3]
141
149
  R = [R1, R2, R3]
142
150
  Q = [Q1, Q2, Q3]
143
- game = ContinuousPyDiffGame(A=A, Bs=B, Qs=Q, Rs=R, P_f=P_sol, show_legend=False)
144
- game()
145
- Plast = game.P[-1]
146
151
 
147
- return Plast
152
+ # Each player drives one body axis; the decomposition matrices are the rows
153
+ # of the identity so that ``concat(M) == I`` and ``B == hstack(Bs)``.
154
+ identity = np.eye(3)
155
+ Ms = [identity[i : i + 1, :] for i in range(3)]
156
+ objectives = [GameObjective(Q=Q_i, R=R_i, M=M_i) for Q_i, R_i, M_i in zip(Q, R, Ms)]
157
+
158
+ game = ContinuousPyDiffGame(A=A, objectives=objectives, Bs=Bs, P_f=P_sol, show_legend=False)
159
+ game.solve()
160
+
161
+ return game.P
148
162
 
149
163
 
150
164
  # High-Level Control
151
165
 
166
+
152
167
  def get_mf_numerator(F3, R11, F1, R31, a_y, R12, R32):
153
168
  return F3 * R11 - F1 * R31 + a_y * (R12 * R31 - R11 * R32)
154
169
 
155
170
 
156
171
  def get_mf_denominator(F2, R11, F1, R21, a_y, R22, R12):
157
- return - F2 * R11 + F1 * R21 + a_y * (R11 * R22 - R12 * R21)
172
+ return -F2 * R11 + F1 * R21 + a_y * (R11 * R22 - R12 * R21)
158
173
 
159
174
 
160
175
  def get_mc_numerator(mf_numerator, a_z, R31, R13, R11, R33):
@@ -179,19 +194,18 @@ def calculate_Bs(u_sizes, dividing_matrix, B):
179
194
 
180
195
  last = 0
181
196
  for u_size in u_sizes:
182
- Bs += [block_matrix[:, last:u_size + last]]
197
+ Bs += [block_matrix[:, last : u_size + last]]
183
198
  last = u_size
184
199
 
185
200
  return Bs
186
201
 
187
202
 
188
203
  def wall_punishment(wall_distance, a_y):
189
- return 3 * (10 ** 2) * (wall_distance / a_y) ** 2
204
+ return 3 * (10**2) * (wall_distance / a_y) ** 2
190
205
 
191
206
 
192
207
  def get_higher_level_control2(state, st, a_y):
193
208
  global v_d_s_0_2, h_d_s_0_2, sum_theta, sum_theta_2
194
- # a_y = 1
195
209
  a_z = -2.5
196
210
 
197
211
  x = state[8]
@@ -212,7 +226,7 @@ def get_higher_level_control2(state, st, a_y):
212
226
  tanpsi = spsi / cpsi
213
227
 
214
228
  vp_x = sectheta * (sphi * stheta - cphi * tanpsi)
215
- vp_y = sectheta * (- cphi * stheta - sphi * tanpsi)
229
+ vp_y = sectheta * (-cphi * stheta - sphi * tanpsi)
216
230
 
217
231
  R11 = ctheta * cpsi
218
232
  R21 = cpsi * stheta * sphi - cphi * spsi
@@ -220,7 +234,7 @@ def get_higher_level_control2(state, st, a_y):
220
234
  R12 = ctheta * spsi
221
235
  R22 = spsi * stheta * sphi + cphi * cpsi
222
236
  R32 = spsi * stheta * cphi - sphi * cpsi
223
- R13 = - stheta
237
+ R13 = -stheta
224
238
  R23 = ctheta * sphi
225
239
  R33 = ctheta * cphi
226
240
  r = np.array([[R11, R21, R31], [R12, R22, R32], [R13, R23, R33]])
@@ -236,8 +250,12 @@ def get_higher_level_control2(state, st, a_y):
236
250
  mfr = mfr_numerator / mfr_denominator
237
251
  mfl = mfl_numerator / mfl_denominator
238
252
 
239
- mcr = get_mc_numerator(mfr_numerator, a_z, R31, R13, R11, R33) / get_mc_denominator(mfr_denominator, a_z, R11, R23)
240
- mcl = get_mc_numerator(mfl_numerator, a_z, R31, R13, R11, R33) / get_mc_denominator(mfl_denominator, a_z, R11, R23)
253
+ mcr = get_mc_numerator(mfr_numerator, a_z, R31, R13, R11, R33) / get_mc_denominator(
254
+ mfr_denominator, a_z, R11, R23
255
+ )
256
+ mcl = get_mc_numerator(mfl_numerator, a_z, R31, R13, R11, R33) / get_mc_denominator(
257
+ mfl_denominator, a_z, R11, R23
258
+ )
241
259
 
242
260
  at_mcl = arctan(mcl)
243
261
  at_mcr = arctan(mcr)
@@ -266,48 +284,54 @@ def get_higher_level_control2(state, st, a_y):
266
284
  v_d_s_0_2 = v_d_s
267
285
  h_d_s_0_2 = h_d_s
268
286
 
269
- Q1 = np.array([[1000, 0, 0, 0, 0, 0, 0, 0, 0],
270
- [0, 1000, 0, 0, 0, 0, 0, 0, 0],
271
- [0, 0, 1000, 0, 0, 0, 0, 0, 0],
272
- [0, 0, 0, 0.1, 0, 0, 0, 0, 0],
273
- [0, 0, 0, 0, 0.1, 0, 0, 0, 0],
274
- [0, 0, 0, 0, 0, 10, 0, 0, 0],
275
- [0, 0, 0, 0, 0, 0, 0.05, 0, 0],
276
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
277
- [0, 0, 0, 0, 0, 0, 0, 0, 0]])
278
-
279
- R1 = np.array([[10, 0, 0, 0],
280
- [0, 10, 0, 0],
281
- [0, 0, 10, 0],
282
- [0, 0, 0, 0.01]])
283
-
284
- A = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
285
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
286
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
287
- [0, 0, 0, 0, 1, 0, 0, 0, 0],
288
- [g, 0, 0, 0, 0, 0, 0, 0, 0],
289
- [0, 0, 0, 0, 0, 0, 1, 0, 0],
290
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
291
- [0, 1, 0, 0, 0, 0, 0, 0, 0],
292
- [0, 1, 0, 0, 0, 0, 0, 0, 0]])
293
-
294
- B = np.array([[1, 0, 0, 0],
295
- [0, 1, 0, 0],
296
- [0, 0, 1, 0],
297
- [0, 0, 0, 0],
298
- [0, 0, 0, 0],
299
- [0, 0, 0, 0],
300
- [0, 0, 0, -1 / m],
301
- [0, 0, 0, 0],
302
- [0, 0, 0, 0]])
303
-
304
- dividing_matrix = np.array([[1, 0, 0, 0, 0],
305
- [0, 1, 0, 0, 1],
306
- [0, 0, 1, 0, 0],
307
- [0, 0, 0, 1, 0]])
287
+ Q1 = np.array(
288
+ [
289
+ [1000, 0, 0, 0, 0, 0, 0, 0, 0],
290
+ [0, 1000, 0, 0, 0, 0, 0, 0, 0],
291
+ [0, 0, 1000, 0, 0, 0, 0, 0, 0],
292
+ [0, 0, 0, 0.1, 0, 0, 0, 0, 0],
293
+ [0, 0, 0, 0, 0.1, 0, 0, 0, 0],
294
+ [0, 0, 0, 0, 0, 10, 0, 0, 0],
295
+ [0, 0, 0, 0, 0, 0, 0.05, 0, 0],
296
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
297
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
298
+ ]
299
+ )
300
+
301
+ R1 = np.array([[10, 0, 0, 0], [0, 10, 0, 0], [0, 0, 10, 0], [0, 0, 0, 0.01]])
302
+
303
+ A = np.array(
304
+ [
305
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
306
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
307
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
308
+ [0, 0, 0, 0, 1, 0, 0, 0, 0],
309
+ [g, 0, 0, 0, 0, 0, 0, 0, 0],
310
+ [0, 0, 0, 0, 0, 0, 1, 0, 0],
311
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
312
+ [0, 1, 0, 0, 0, 0, 0, 0, 0],
313
+ [0, 1, 0, 0, 0, 0, 0, 0, 0],
314
+ ]
315
+ )
316
+
317
+ B = np.array(
318
+ [
319
+ [1, 0, 0, 0],
320
+ [0, 1, 0, 0],
321
+ [0, 0, 1, 0],
322
+ [0, 0, 0, 0],
323
+ [0, 0, 0, 0],
324
+ [0, 0, 0, 0],
325
+ [0, 0, 0, -1 / m],
326
+ [0, 0, 0, 0],
327
+ [0, 0, 0, 0],
328
+ ]
329
+ )
330
+
331
+ dividing_matrix = np.array([[1, 0, 0, 0, 0], [0, 1, 0, 0, 1], [0, 0, 1, 0, 0], [0, 0, 0, 1, 0]])
308
332
 
309
333
  u_sizes = [4, 1]
310
- Ms = [dividing_matrix[:, :u_sizes[0]].T, dividing_matrix[:, u_sizes[0]:u_sizes[0] + u_sizes[1]].T]
334
+ Ms = [dividing_matrix[:, : u_sizes[0]].T, dividing_matrix[:, u_sizes[0] : u_sizes[0] + u_sizes[1]].T]
311
335
  Bs = calculate_Bs(u_sizes, dividing_matrix, B)
312
336
 
313
337
  R2 = np.array([[10]])
@@ -315,24 +339,32 @@ def get_higher_level_control2(state, st, a_y):
315
339
  max_punishment = wall_punishment(a_y, a_y)
316
340
  curr_punishment = min(max_punishment, wall_punishment(p_y_tilda[0], a_y))
317
341
 
318
- Q_wall_0 = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
319
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
320
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
321
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
322
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
323
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
324
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
325
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
326
- [0, 0, 0, 0, 0, 0, 0, 0, curr_punishment]])
327
- Q_speed_up_0 = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
328
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
329
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
330
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
331
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
332
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
333
- [0, 0, 0, 0, 0, 0, 0, 0, 0],
334
- [0, 0, 0, 0, 0, 0, 0, 1000, 0],
335
- [0, 0, 0, 0, 0, 0, 0, 0, 0]])
342
+ Q_wall_0 = np.array(
343
+ [
344
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
345
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
346
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
347
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
348
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
349
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
350
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
351
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
352
+ [0, 0, 0, 0, 0, 0, 0, 0, curr_punishment],
353
+ ]
354
+ )
355
+ Q_speed_up_0 = np.array(
356
+ [
357
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
358
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
359
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
360
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
361
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
362
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
363
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
364
+ [0, 0, 0, 0, 0, 0, 0, 1000, 0],
365
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
366
+ ]
367
+ )
336
368
 
337
369
  R = [R1, R2]
338
370
  if abs(p_y_tilda[0] / a_y) < 0.5:
@@ -341,21 +373,19 @@ def get_higher_level_control2(state, st, a_y):
341
373
  Q = [0.01 * Q1, 0.01 * Q_wall_0]
342
374
  P_sol = [0.01 * Q1, 0.01 * Q1]
343
375
 
344
- game = ContinuousPyDiffGame(A=A, Bs=Bs, Qs=Q, Rs=R, P_f=P_sol, show_legend=False)
345
- game()
346
- Plast = game.P[-1]
347
- N = 2
348
- M = 9
349
- P_size = M ** 2
350
- Plast = [(Plast[i * P_size:(i + 1) * P_size]).reshape(M, M) for i in range(N)]
376
+ objectives = [GameObjective(Q=Q_i, R=R_i, M=M_i) for Q_i, R_i, M_i in zip(Q, R, Ms)]
377
+ game = ContinuousPyDiffGame(A=A, objectives=objectives, Bs=Bs, P_f=P_sol, show_legend=False)
378
+ game.solve()
379
+ Plast = game.P
351
380
 
352
- inv_Rs = [inv(r) for r in R]
353
- B_t = [b.T for b in Bs]
381
+ inv_Rs = [inv(r_i) for r_i in R]
382
+ B_t = [b_i.T for b_i in Bs]
354
383
 
355
- U_Agenda1 = - inv_Rs[0] @ B_t[0] @ Plast[0] @ np.array(
356
- [-phi_tilda[0], -vp_y, -vp_x, -p_y_tilda[0], -h_d[0], -p_z_tilda[0], -v_d[0], sum_theta, sum_theta_2])
357
- U_Agenda2 = - inv_Rs[1] @ B_t[1] @ Plast[1] @ np.array(
358
- [-phi_tilda[0], -vp_y, -vp_x, -p_y_tilda[0], -h_d[0], -p_z_tilda[0], -v_d[0], sum_theta, sum_theta_2])
384
+ reduced_state = np.array(
385
+ [-phi_tilda[0], -vp_y, -vp_x, -p_y_tilda[0], -h_d[0], -p_z_tilda[0], -v_d[0], sum_theta, sum_theta_2]
386
+ )
387
+ U_Agenda1 = -inv_Rs[0] @ B_t[0] @ Plast[0] @ reduced_state
388
+ U_Agenda2 = -inv_Rs[1] @ B_t[1] @ Plast[1] @ reduced_state
359
389
 
360
390
  Us = [U_Agenda1, U_Agenda2]
361
391
 
@@ -363,7 +393,8 @@ def get_higher_level_control2(state, st, a_y):
363
393
  p_r, q_r, r_r, t_r = U_all_Out
364
394
 
365
395
  tilda_state = np.array(
366
- [phi_tilda[0], vp_y, vp_x, p_y_tilda[0], h_d[0], p_z_tilda[0], v_d[0], sum_theta, sum_theta_2])
396
+ [phi_tilda[0], vp_y, vp_x, p_y_tilda[0], h_d[0], p_z_tilda[0], v_d[0], sum_theta, sum_theta_2]
397
+ )
367
398
  sum_theta = sum_theta - vp_y * 0.1
368
399
  sum_theta_2 = sum_theta_2 - vp_y * 0.1
369
400
 
@@ -372,177 +403,236 @@ def get_higher_level_control2(state, st, a_y):
372
403
 
373
404
  # Simulation
374
405
 
375
- Q1 = np.array([[1000, 0, 0],
376
- [0, 10, 0],
377
- [0, 0, 10]])
378
- Q2 = np.array([[10, 0, 0],
379
- [0, 1000, 0],
380
- [0, 0, 10]])
381
- Q3 = np.array([[10, 0, 0],
382
- [0, 10, 0],
383
- [0, 0, 1000]])
384
- Q = [Q1, Q2, Q3]
385
- M = 3
386
- P_size = M ** 2
387
- N = 3
388
- tTotal = [0]
389
- tTotal_low = [0]
390
- a_ys = [0.55]
391
- quad_rotor_state_PD_dynamic = {}
392
- quad_rotor_omega_D_dynamic = {}
393
- quad_rotor_state_PD_dynamic_low = {}
394
- tilda_state_dynamic = {}
395
-
396
- for a_y in a_ys:
397
-
398
- T_start = 0
399
- deltaTstate = 0.1
400
- X_rotor_0 = np.array([0.1, 0, 0, 0, 0.1, 0, -1, 0, 0, 0, 0.3, 0])
401
- omega_rotor_0 = np.array([0, 0, 0])
402
- quad_rotor_state_PD_dynamic[a_y] = [X_rotor_0]
403
- quad_rotor_omega_D_dynamic[a_y] = [omega_rotor_0]
404
- quad_rotor_state_PD_dynamic_low[a_y] = [X_rotor_0]
405
- tilda_state = np.array([0, 0, 0, 0, 0, 0, 0, sum_theta_init, 0])
406
- tilda_state_dynamic[a_y] = [tilda_state]
407
-
408
- X_rotor_0_PD = X_rotor_0
409
- Plast = [Q1, Q2, Q3]
410
- v_d_s_0_2 = 0
411
- h_d_s_0_2 = 0
412
- sum_theta = sum_theta_init
413
- sum_theta_2 = 0
414
-
415
- for i in tqdm(range(300)):
416
- # p_r, q_r, r_r, t_r, tilda_state_l = get_higher_level_control(X_rotor_0_PD,T_start, a_y)
417
- p_r2, q_r2, r_r2, t_r2, tilda_state_l2 = get_higher_level_control2(X_rotor_0_PD, T_start, a_y)
418
- tilda_state = [tilda_state_l2]
419
- tilda_state_dynamic[a_y] = np.append(tilda_state_dynamic[a_y], tilda_state, axis=0)
420
- T_end = T_start + deltaTstate
421
- data_points = 100
422
- t = np.linspace(T_start, T_end, data_points)
423
- Plast = get_P_quad_given_angular_rates([X_rotor_0_PD[1], X_rotor_0_PD[3], X_rotor_0_PD[5]], Plast)
424
- Plast = [(Plast[k * P_size:(k + 1) * P_size]).reshape(M, M) for k in range(N)]
425
- quad_rotor_state_PD = odeint(quad_rotor_state_diff_eqn_for_given_pqrT, X_rotor_0_PD, t,
426
- args=(p_r2, q_r2, r_r2, t_r2, Plast))
427
- omega_rotor_i = np.array([p_r2, q_r2, r_r2])
428
- X_rotor_0_PD = quad_rotor_state_PD[-1]
429
- T_start = T_end
430
- quad_rotor_state_PD_dynamic[a_y] = np.append(quad_rotor_state_PD_dynamic[a_y],
431
- quad_rotor_state_PD, axis=0)
432
- quad_rotor_omega_D_dynamic[a_y] = np.append(quad_rotor_omega_D_dynamic[a_y],
433
- [omega_rotor_i], axis=0)
434
- quad_rotor_state_PD_dynamic_low[a_y] = np.append(quad_rotor_state_PD_dynamic_low[a_y],
435
- [X_rotor_0_PD], axis=0)
436
- if a_y == a_ys[0]:
437
- tTotal = np.append(tTotal, t)
438
- tTotal_low = np.append(tTotal_low, T_end)
439
-
440
- angles = {'phi': [0, 0], 'theta': [2, 1], 'psi': [4, 2]}
441
- positions = {'x': [8, 7], 'z': [6, 5], 'y': [10, 3]}
442
- velocities = {'y_dot': [11, 4], 'z_dot': [7, 6], 'x_dot': 9}
443
406
 
407
+ def run_simulation(a_ys=(0.55,), n_steps=8):
408
+ """Run the hierarchical closed-loop simulation for each wall distance ``a_y``.
409
+
410
+ Returns dictionaries of recorded trajectories keyed by ``a_y`` plus the
411
+ common time vectors.
412
+ """
413
+
414
+ global v_d_s_0_2, h_d_s_0_2, sum_theta, sum_theta_2
415
+
416
+ tTotal = [0]
417
+ tTotal_low = [0]
418
+ quad_rotor_state_PD_dynamic = {}
419
+ quad_rotor_omega_D_dynamic = {}
420
+ quad_rotor_state_PD_dynamic_low = {}
421
+ tilda_state_dynamic = {}
422
+
423
+ for a_y in a_ys:
424
+ T_start = 0
425
+ deltaTstate = 0.1
426
+ X_rotor_0 = np.array([0.1, 0, 0, 0, 0.1, 0, -1, 0, 0, 0, 0.3, 0])
427
+ omega_rotor_0 = np.array([0, 0, 0])
428
+ quad_rotor_state_PD_dynamic[a_y] = [X_rotor_0]
429
+ quad_rotor_omega_D_dynamic[a_y] = [omega_rotor_0]
430
+ quad_rotor_state_PD_dynamic_low[a_y] = [X_rotor_0]
431
+ tilda_state = np.array([0, 0, 0, 0, 0, 0, 0, sum_theta_init, 0])
432
+ tilda_state_dynamic[a_y] = [tilda_state]
433
+
434
+ X_rotor_0_PD = X_rotor_0
435
+ Q1 = np.array([[1000, 0, 0], [0, 10, 0], [0, 0, 10]])
436
+ Q2 = np.array([[10, 0, 0], [0, 1000, 0], [0, 0, 10]])
437
+ Q3 = np.array([[10, 0, 0], [0, 10, 0], [0, 0, 1000]])
438
+ Plast = [Q1, Q2, Q3]
439
+ v_d_s_0_2 = 0.0
440
+ h_d_s_0_2 = 0.0
441
+ sum_theta = sum_theta_init
442
+ sum_theta_2 = 0.0
443
+
444
+ for _ in range(n_steps):
445
+ p_r2, q_r2, r_r2, t_r2, tilda_state_l2 = get_higher_level_control2(X_rotor_0_PD, T_start, a_y)
446
+ tilda_state = [tilda_state_l2]
447
+ tilda_state_dynamic[a_y] = np.append(tilda_state_dynamic[a_y], tilda_state, axis=0)
448
+ T_end = T_start + deltaTstate
449
+ data_points = 100
450
+ t = np.linspace(T_start, T_end, data_points)
451
+ Plast = get_P_quad_given_angular_rates([X_rotor_0_PD[1], X_rotor_0_PD[3], X_rotor_0_PD[5]], Plast)
452
+ quad_rotor_state_PD = odeint(
453
+ quad_rotor_state_diff_eqn_for_given_pqrT,
454
+ X_rotor_0_PD,
455
+ t,
456
+ args=(p_r2, q_r2, r_r2, t_r2, Plast),
457
+ )
458
+ omega_rotor_i = np.array([p_r2, q_r2, r_r2])
459
+ X_rotor_0_PD = quad_rotor_state_PD[-1]
460
+ T_start = T_end
461
+ quad_rotor_state_PD_dynamic[a_y] = np.append(
462
+ quad_rotor_state_PD_dynamic[a_y], quad_rotor_state_PD, axis=0
463
+ )
464
+ quad_rotor_omega_D_dynamic[a_y] = np.append(
465
+ quad_rotor_omega_D_dynamic[a_y], [omega_rotor_i], axis=0
466
+ )
467
+ quad_rotor_state_PD_dynamic_low[a_y] = np.append(
468
+ quad_rotor_state_PD_dynamic_low[a_y], [X_rotor_0_PD], axis=0
469
+ )
470
+ if a_y == a_ys[0]:
471
+ tTotal = np.append(tTotal, t)
472
+ tTotal_low = np.append(tTotal_low, T_end)
473
+
474
+ return (
475
+ quad_rotor_state_PD_dynamic,
476
+ quad_rotor_omega_D_dynamic,
477
+ quad_rotor_state_PD_dynamic_low,
478
+ tilda_state_dynamic,
479
+ np.asarray(tTotal),
480
+ np.asarray(tTotal_low),
481
+ )
482
+
483
+
484
+ # Plotting (guarded; only runs when explicitly enabled)
485
+
486
+ angles = {"phi": [0, 0], "theta": [2, 1], "psi": [4, 2]}
487
+ positions = {"x": [8, 7], "z": [6, 5], "y": [10, 3]}
488
+ velocities = {"y_dot": [11, 4], "z_dot": [7, 6], "x_dot": 9}
444
489
  plot_vars = {**angles, **positions, **velocities}
445
490
 
446
491
 
447
- def plot_var(var):
448
- var_indices = plot_vars[var]
492
+ def plot_results(
493
+ quad_rotor_state_PD_dynamic,
494
+ quad_rotor_omega_D_dynamic,
495
+ quad_rotor_state_PD_dynamic_low,
496
+ tilda_state_dynamic,
497
+ tTotal,
498
+ tTotal_low,
499
+ a_ys,
500
+ ):
501
+ """Produce the full set of diagnostic figures (not shown under Agg)."""
502
+
503
+ import matplotlib.pyplot as plt
504
+ from mpl_toolkits.axes_grid1.inset_locator import mark_inset, zoomed_inset_axes
505
+
506
+ def plot_var(var):
507
+ var_indices = plot_vars[var]
508
+ for a_y in quad_rotor_state_PD_dynamic_low.keys():
509
+ plt.figure(dpi=130)
510
+ plt.title(r"$a _y = \ $" + str(a_y) + r"$ \ [m]$", fontsize=16)
511
+ plt.plot(
512
+ tTotal_low[1:],
513
+ quad_rotor_state_PD_dynamic_low[a_y][1:, var_indices[0] if var != "x_dot" else var_indices],
514
+ )
515
+ if var in positions.keys():
516
+ plt.plot(tTotal_low[1:], tilda_state_dynamic[a_y][1:, var_indices[1]])
517
+ else:
518
+ plt.plot(tTotal_low[1:], -tilda_state_dynamic[a_y][1:, var_indices[1]])
519
+ if var in positions.keys():
520
+ plt.legend([r"$" + var + r" \\ [m]$", r"$\tilde{" + var + r"} \\ [m]$"])
521
+ else:
522
+ if "dot" in var:
523
+ plt.legend(
524
+ [
525
+ r"$\dot{" + var[0] + r"} \ \left[ \frac{m}{sec} \right]$",
526
+ r"$\tilde{\dot{" + var[0] + r"}} \ \left[ \frac{m}{sec} \right]$",
527
+ ]
528
+ )
529
+ else:
530
+ plt.legend([r"$\\" + var + r" \ [rad]$", r"$\tilde{\\" + var + r"} \ [rad]$"])
531
+ plt.xlabel(r"$t \ [sec]$", fontsize=16)
532
+ plt.grid()
533
+
534
+ for curr_a_y, var in quad_rotor_state_PD_dynamic_low.items():
535
+ plt.figure(dpi=300)
536
+ plt.plot(tTotal_low[1:], var[1:, 9])
537
+ plt.plot(tTotal_low[1:], abs(tilda_state_dynamic[curr_a_y][1:, 3] / curr_a_y))
538
+ plt.plot(tTotal_low[1:], 0.5 * np.ones(len(tTotal_low[1:])))
539
+ plt.grid()
540
+ plt.xlabel(r"$t \ [sec]$", fontsize=12)
541
+ plt.legend(["Forward Velocity", "Wall Distance Measure"])
542
+
543
+ for var in ["x", "y", "y_dot", "z", "z_dot", "phi", "theta", "psi"]:
544
+ plot_var(var)
545
+
546
+ time_ratio1 = len(tTotal_low) / max(tTotal_low)
547
+ time_ratio2 = len(tTotal) / max(tTotal)
548
+
549
+ for a_y in quad_rotor_omega_D_dynamic.keys():
550
+ fig, ax = plt.subplots(1, dpi=150)
551
+ t01 = int(time_ratio1 * 0.8)
552
+ t1 = int(time_ratio1 * 1.1)
553
+ t02 = int(time_ratio2 * 0.8)
554
+ t2 = int(time_ratio2 * 1)
555
+ x1 = tTotal_low[t01:t1]
556
+ y1 = quad_rotor_omega_D_dynamic[a_y][t01:t1, 1]
557
+ x2 = tTotal[t02:t2]
558
+ y2 = quad_rotor_state_PD_dynamic[a_y][t02:t2, 3]
559
+ ax.step(x1, y1, linewidth=1)
560
+ ax.plot(x2, y2, linewidth=1)
561
+ plt.xlabel(r"$t \ [sec]$", fontsize=14)
562
+ plt.grid()
563
+ axins = zoomed_inset_axes(ax, 3.5, borderpad=3)
564
+ mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
565
+ axins.set_xlim([0.898, 0.905])
566
+ axins.set_ylim([0.193, 0.205])
567
+ axins.step(x1, y1, linewidth=1)
568
+ axins.plot(x2, y2, linewidth=1)
569
+ plt.grid()
570
+ fig.legend(
571
+ [
572
+ r"$\dot{\theta}_d \ \left[ \frac{rad}{sec} \right]$",
573
+ r"$\dot{\theta} \ \left[ \frac{rad}{sec} \right]$",
574
+ ],
575
+ bbox_to_anchor=(0.28, 0.35) if a_y != 0.6 else (0.3, 0.35),
576
+ )
577
+
578
+ for sol in quad_rotor_state_PD_dynamic_low.values():
579
+ plt.figure(dpi=300)
580
+ plt.plot(tTotal_low[0:], sol[0:, 0:6])
581
+ plt.xlabel(r"$t \ [sec]$", fontsize=14)
582
+ plt.legend(
583
+ [
584
+ r"$\phi[rad]$",
585
+ r"$\dot{\phi}\left[ \frac{rad}{sec} \right]$",
586
+ r"$\theta[rad]$",
587
+ r"$\dot{\theta}\left[ \frac{rad}{sec} \right]$",
588
+ r"$\psi[rad]$",
589
+ r"$\dot{\psi}\left[ \frac{rad}{sec} \right]$",
590
+ ],
591
+ ncol=2,
592
+ loc="upper right",
593
+ )
594
+ plt.grid()
449
595
 
450
- for a_y in quad_rotor_state_PD_dynamic_low.keys():
451
- plt.figure(dpi=130)
596
+ for sol in quad_rotor_state_PD_dynamic_low.values():
597
+ plt.figure(dpi=300)
598
+ plt.plot(tTotal_low[0:], sol[0:, 6:8], tTotal_low[0:], sol[0:, 10:12])
599
+ plt.xlabel(r"$t \ [sec]$", fontsize=14)
600
+ plt.legend(
601
+ [
602
+ r"$z[m]$",
603
+ r"$\dot{z}\left[ \frac{m}{sec} \right]$",
604
+ r"$y[m]$",
605
+ r"$\dot{y}\left[ \frac{m}{sec} \right]$",
606
+ ]
607
+ )
608
+ plt.grid()
452
609
 
453
- plt.title('$a _y = \ $' + str(a_y) + '$ \ [m]$', fontsize=16)
454
610
 
455
- plt.plot(tTotal_low[1:],
456
- quad_rotor_state_PD_dynamic_low[a_y][1:, var_indices[0] if var != 'x_dot' else var_indices])
611
+ def main(*, n_steps: int = 8, plot: bool = True) -> None:
612
+ """Run a short hierarchical quadrotor simulation and report the result."""
457
613
 
458
- if var in positions.keys():
459
- plt.plot(tTotal_low[1:], tilda_state_dynamic[a_y][1:, var_indices[1]])
460
- else:
461
- plt.plot(tTotal_low[1:], -tilda_state_dynamic[a_y][1:, var_indices[1]])
614
+ a_ys = (0.55,)
615
+ (state_pd, omega_d, state_pd_low, tilda_state_dynamic, tTotal, tTotal_low) = run_simulation(
616
+ a_ys=a_ys, n_steps=n_steps
617
+ )
462
618
 
463
- if var in positions.keys():
464
- plt.legend(['$' + var + ' \\ [m]$', '$\\tilde{' + var + '} \\ [m]$'])
465
- else:
466
- if 'dot' in var:
467
- plt.legend(['$\\dot{' + var[0] + '} \\ \\left[ \\frac{m}{sec} \\right]$',
468
- '$\\tilde{\\dot{' + var[0] + '}} \\ \\left[ \\frac{m}{sec} \\right]$'])
469
- else:
470
- plt.legend(['$\\' + var + ' \\ [rad]$', '$\\tilde{\\' + var + '} \\ [rad]$'])
619
+ if plot:
620
+ plot_results(state_pd, omega_d, state_pd_low, tilda_state_dynamic, tTotal, tTotal_low, a_ys)
621
+ show()
471
622
 
472
- plt.xlabel('$t \ [sec]$', fontsize=16)
473
- plt.grid()
474
- plt.show()
475
-
476
-
477
- for curr_a_y, var in quad_rotor_state_PD_dynamic_low.items():
478
- plt.figure(dpi=300)
479
- plt.plot(tTotal_low[1:], var[1:, 9])
480
- plt.plot(tTotal_low[1:], abs(tilda_state_dynamic[curr_a_y][1:, 3] / curr_a_y))
481
- plt.plot(tTotal_low[1:], 0.5 * np.ones(len(tTotal_low[1:])))
482
- plt.grid()
483
- plt.xlabel('$t \ [sec]$', fontsize=12)
484
- # plt.title('$a_y = ' + str(curr_a_y) + ' \ [m]$', fontsize=12)
485
- # plt.legend(['$\\dot{x} \ [\\frac{m}{s}]$', '$\\frac{\\tilde{y}}{a_y}$'])
486
- plt.legend(['Forward Velocity', 'Wall Distance Measure'])
487
- plt.show()
488
-
489
- for var in ['x', 'y', 'y_dot', 'z', 'z_dot', 'phi', 'theta', 'psi']:
490
- plot_var(var)
491
-
492
- time_ratio1 = len(tTotal_low) / max(tTotal_low)
493
- time_ratio2 = len(tTotal) / max(tTotal)
494
-
495
- for a_y in quad_rotor_omega_D_dynamic.keys():
496
- fig, ax = plt.subplots(1, dpi=150)
497
-
498
- t01 = int(time_ratio1 * 0.8)
499
- t1 = int(time_ratio1 * 1.1)
500
- t02 = int(time_ratio2 * 0.8)
501
- t2 = int(time_ratio2 * 1)
502
-
503
- x1 = tTotal_low[t01:t1]
504
- y1 = quad_rotor_omega_D_dynamic[a_y][t01:t1, 1]
505
- x2 = tTotal[t02:t2]
506
- y2 = quad_rotor_state_PD_dynamic[a_y][t02:t2, 3]
507
-
508
- ax.step(x1, y1, linewidth=1)
509
- ax.plot(x2, y2, linewidth=1)
510
- plt.xlabel('$t \ [sec]$', fontsize=14)
511
- plt.grid()
512
-
513
- axins = zoomed_inset_axes(ax, 3.5, borderpad=3)
514
- mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
515
-
516
- xlim = [0.898, 0.905]
517
- ylim = [0.193, 0.205]
518
-
519
- axins.set_xlim(xlim)
520
- axins.set_ylim(ylim)
521
- axins.step(x1, y1, linewidth=1)
522
- axins.plot(x2, y2, linewidth=1)
523
- plt.grid()
524
-
525
- fig.legend(['$\\dot{\\theta}_d \\ \\left[ \\frac{rad}{sec} \\right]$',
526
- '$\\dot{\\theta} \\ \\left[ \\frac{rad}{sec} \\right]$'],
527
- bbox_to_anchor=(0.28, 0.35) if a_y != 0.6 else (0.3, 0.35))
528
-
529
- plt.show()
530
-
531
- for sol in quad_rotor_state_PD_dynamic_low.values():
532
- plt.figure(dpi=300)
533
- plt.plot(tTotal_low[0:], sol[0:, 0:6])
534
- plt.xlabel('$t \ [sec]$', fontsize=14)
535
- plt.legend(['$\\phi[rad]$', '$\\dot{\phi}\\left[ \\frac{rad}{sec} \\right]$', '$\\theta[rad]$',
536
- '$\\dot{\\theta}\\left[ \\frac{rad}{sec} \\right]$', '$\\psi[rad]$',
537
- '$\\dot{\psi}\\left[ \\frac{rad}{sec} \\right]$'], ncol=2, loc='upper right')
538
- plt.grid()
539
- plt.show()
540
-
541
- for sol in quad_rotor_state_PD_dynamic_low.values():
542
- plt.figure(dpi=300)
543
- plt.plot(tTotal_low[0:], sol[0:, 6:8], tTotal_low[0:], sol[0:, 10:12])
544
- plt.xlabel('$t \ [sec]$', fontsize=14)
545
- plt.legend(
546
- ['$z[m]$', '$\\dot{z}\\left[ \\frac{m}{sec} \\right]$', '$y[m]$', '$\\dot{y}\\left[ \\frac{m}{sec} \\right]$'])
547
- plt.grid()
548
- plt.show()
623
+ final_state = state_pd_low[a_ys[0]][-1]
624
+ print("Quadrotor hierarchical control simulation complete.")
625
+ print(f" wall distances a_y = {list(a_ys)}")
626
+ print(f" high/low-level game steps = {n_steps}")
627
+ print(f" simulated time = {float(tTotal_low[-1]):.2f} s")
628
+ print(
629
+ f" final position (x, y, z) = ({final_state[8]:.3f}, {final_state[10]:.3f}, {final_state[6]:.3f}) m"
630
+ )
631
+ print(
632
+ f" final angles (phi, theta, psi) = "
633
+ f"({final_state[0]:.3f}, {final_state[2]:.3f}, {final_state[4]:.3f}) rad"
634
+ )
635
+
636
+
637
+ if __name__ == "__main__":
638
+ main()