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,218 +1,119 @@
1
- import numpy as np
2
- from typing import Sequence, Optional, Union
3
-
4
- from PyDiffGame.PyDiffGame import PyDiffGame
5
- from PyDiffGame.PyDiffGameLQRComparison import PyDiffGameLQRComparison
6
- from PyDiffGame.Objective import GameObjective, LQRObjective
7
-
8
-
9
- class MassesWithSpringsComparison(PyDiffGameLQRComparison):
10
- def __init__(self,
11
- N: int,
12
- m: float,
13
- k: float,
14
- q: Union[float, Sequence[float], Sequence[Sequence[float]]],
15
- r: float,
16
- Ms: Optional[Sequence[np.array]] = None,
17
- x_0: Optional[np.array] = None,
18
- x_T: Optional[np.array] = None,
19
- T_f: Optional[float] = None,
20
- epsilon_x: Optional[float] = PyDiffGame.epsilon_x_default,
21
- epsilon_P: Optional[float] = PyDiffGame.epsilon_P_default,
22
- L: Optional[int] = PyDiffGame.L_default,
23
- eta: Optional[int] = PyDiffGame.eta_default):
24
- I_N = np.eye(N)
25
- Z_N = np.zeros((N, N))
26
-
27
- M_masses = m * I_N
28
- K = k * (2 * I_N - np.array([[int(abs(i - j) == 1) for j in range(N)] for i in range(N)]))
29
- M_masses_inv = np.linalg.inv(M_masses)
1
+ """Coupled masses-and-springs benchmark: differential game vs. LQR.
30
2
 
31
- M_inv_K = M_masses_inv @ K
3
+ ``N`` unit-coupled masses connected by springs form a ``2N``-dimensional linear
4
+ system (positions and velocities). The physical input space is decomposed along
5
+ the *modal* directions of ``M^{-1}K`` so that each vibration mode becomes one
6
+ player of a differential game, and the result is compared against a single
7
+ LQR controller acting on the whole system.
32
8
 
33
- if Ms is None:
34
- eigenvectors = np.linalg.eig(M_inv_K)[1]
35
- Ms = [eigenvector.reshape(1, N) for eigenvector in eigenvectors]
9
+ Run directly to solve a small instance and report the costs::
36
10
 
37
- A = np.block([[Z_N, I_N],
38
- [-M_inv_K, Z_N]])
39
- B = np.block([[Z_N],
40
- [M_masses_inv]])
11
+ python -m PyDiffGame.examples.MassesWithSpringsComparison
12
+ """
41
13
 
42
- Qs = [np.diag([0.0] * i + [q] + [0.0] * (N - 1) + [q] + [0.0] * (N - i - 1))
43
- if isinstance(q, (int, float)) else
44
- np.diag([0.0] * i + [q[i]] + [0.0] * (N - 1) + [q[i]] + [0.0] * (N - i - 1)) for i in range(N)]
14
+ from __future__ import annotations
45
15
 
46
- M = np.concatenate(Ms,
47
- axis=0)
16
+ from collections.abc import Sequence
48
17
 
49
- assert np.all(np.abs(np.linalg.inv(M) - M.T) < 10e-12)
50
-
51
- Q_mat = np.kron(a=np.eye(2),
52
- b=M)
53
-
54
- Qs = [Q_mat.T @ Q @ Q_mat for Q in Qs]
55
-
56
- Rs = [np.array([r])] * N
57
- R_lqr = 1 / 4 * r * I_N
58
- Q_lqr = q * np.eye(2 * N) if isinstance(q, (int, float)) else np.diag(2 * q)
18
+ import numpy as np
59
19
 
60
- state_variables_names = ['x_{' + str(i) + '}' for i in range(1, N + 1)] + \
61
- ['\\dot{x}_{' + str(i) + '}' for i in range(1, N + 1)]
62
- args = {'A': A,
63
- 'B': B,
64
- 'x_0': x_0,
65
- 'x_T': x_T,
66
- 'T_f': T_f,
67
- 'state_variables_names': state_variables_names,
68
- 'epsilon_x': epsilon_x,
69
- 'epsilon_P': epsilon_P,
70
- 'L': L,
71
- 'eta': eta}
20
+ from PyDiffGame.base import PyDiffGame
21
+ from PyDiffGame.comparison import PyDiffGameLQRComparison
22
+ from PyDiffGame.objective import GameObjective, LQRObjective
23
+ from PyDiffGame.plotting import show
72
24
 
73
- lqr_objective = [LQRObjective(Q=Q_lqr,
74
- R_ii=R_lqr)]
75
- game_objectives = [GameObjective(Q=Q,
76
- R_ii=R,
77
- M_i=M_i) for Q, R, M_i in zip(Qs, Rs, Ms)]
78
- games_objectives = [lqr_objective,
79
- game_objectives]
80
25
 
81
- self.figure_filename_generator = lambda g: ('LQR' if g.is_LQR() else f'{N}-players') + \
82
- f'_large_{1 if q[0] > q[1] else 2}'
83
-
84
- super().__init__(args=args,
85
- M=M,
86
- games_objectives=games_objectives,
87
- continuous=True)
88
-
89
- @staticmethod
90
- def are_all_orthogonal(vectors: Sequence[np.array]) -> bool:
91
- """
92
- Check if a set of vectors are all orthogonal to each other.
93
-
94
- Parameters
95
- ----------
96
-
97
- vectors: sequence of vectors of len(n)
98
- A sequence of numpy arrays representing the vectors
99
-
100
- Returns
101
- ----------
102
-
103
- all_orthogonal: bool
104
- True if all the vectors are orthogonal to each other, False otherwise
105
- """
106
-
107
- all_orthogonal = not any([any([abs(vectors[i] @ vectors[j].T) > 1e-10 for j in range(i + 1, len(vectors))])
108
- for i in range(len(vectors))])
109
-
110
- return all_orthogonal
111
-
112
- @staticmethod
113
- def are_all_unit_vectors(vectors: Sequence[np.array]) -> bool:
114
- """
115
- Check if a set of vectors are all of length 1.
116
-
117
- Parameters
118
- ----------
119
-
120
- vectors: sequence of vectors of len(n)
121
- A sequence of numpy arrays representing the vectors
122
-
123
- Returns
124
- ----------
125
-
126
- all_orthogonal: bool
127
- True if all the vectors are orthogonal to each other, False otherwise
128
- """
129
-
130
- all_unit_vectors = all([abs(np.linalg.norm(v) - 1) < 1e-10 for v in vectors])
131
-
132
- return all_unit_vectors
133
-
134
-
135
- N2_output_variables_names = ['$\\frac{x_1 + x_2}{\\sqrt{2}}$',
136
- '$\\frac{x_2 - x_1}{\\sqrt{2}}$',
137
- '$\\frac{\\dot{x}_1 + \\dot{x}_2}{\\sqrt{2}}$',
138
- '$\\frac{\\dot{x}_2 - \\dot{x}_1}{\\sqrt{2}}$']
139
-
140
-
141
- def multiprocess_worker_function(N: int,
142
- k: float,
143
- m: float,
144
- q: float,
145
- r: float,
146
- epsilon_x: float,
147
- epsilon_P: float):
148
- x_0 = np.array([10 * i for i in range(1, N + 1)] + [0] * N)
149
- x_T = x_0 * 10 if N == 2 else np.array([(10 * i) ** 3 for i in range(1, N + 1)] + [0] * N)
150
-
151
- output_variables_names = N2_output_variables_names \
152
- if N == 2 else [f'$q_{i}$' for i in range(1, N + 1)] + ['$\\dot{q}_{' + str(i) + '}$' for i in range(1, N + 1)]
153
-
154
- T_f = 25
155
- masses_with_springs = MassesWithSpringsComparison(N=N,
156
- m=m,
157
- k=k,
158
- q=q,
159
- r=r,
160
- x_0=x_0,
161
- x_T=x_T,
162
- T_f=T_f,
163
- epsilon_x=epsilon_x,
164
- epsilon_P=epsilon_P)
165
-
166
- masses_with_springs(plot_state_spaces=True,
167
- plot_Mx=True,
168
- output_variables_names=output_variables_names,
169
- save_figure=True,
170
- figure_filename=masses_with_springs.figure_filename_generator
171
- )
172
-
173
-
174
- if __name__ == '__main__':
175
- d = 0.2
176
- N = 8
177
-
178
- Ns = [N]
179
- ks = [10]
180
- ms = [50]
181
- rs = [1]
182
- epsilon_xs = [10e-8]
183
- epsilon_Ps = [10e-8]
184
- qs = [[500, 2000], [500, 250]] if N == 2 else \
185
- [[500 * (1 + d) ** i for i in range(N)], [500 * (1 - d) ** i for i in range(N)]]
186
-
187
- # N, k, m, r, epsilon_x, epsilon_P = Ns[0], ks[0], ms[0], rs[0], epsilon_xs[0], epsilon_Ps[0]
188
- # q = [500, 2000] if N == 2 else [500 * (1 + d) ** i for i in range(N)]
189
- # x_0 = np.array([10 * i for i in range(1, N + 1)] + [0] * N)
190
- # x_T = x_0 * 10 if N == 2 else np.array([(10 * i) ** 3 for i in range(1, N + 1)] + [0] * N)
191
- #
192
- # masses_with_springs = MassesWithSpringsComparison(N=N,
193
- # m=m,
194
- # k=k,
195
- # q=q,
196
- # r=r,
197
- # x_0=x_0,
198
- # x_T=x_T,
199
- # T_f=25,
200
- # epsilon_x=epsilon_x,
201
- # epsilon_P=epsilon_P)
202
- #
203
- # output_variables_names = N2_output_variables_names \
204
- # if N == 2 else [f'$q_{i}$' for i in range(1, N + 1)] + ['$\\dot{q}_{' + str(i) + '}$' for i in range(1, N + 1)]
205
- #
206
- # masses_with_springs(plot_state_spaces=True,
207
- # plot_Mx=True,
208
- # output_variables_names=output_variables_names,
209
- # save_figure=True,
210
- # figure_filename=masses_with_springs.figure_filename_generator
211
- # )
212
-
213
- #
214
-
215
- params = [Ns, ks, ms, qs, rs, epsilon_xs, epsilon_Ps]
216
- PyDiffGameLQRComparison.run_multiprocess(multiprocess_worker_function=multiprocess_worker_function,
217
- values=params)
26
+ class MassesWithSpringsComparison(PyDiffGameLQRComparison):
27
+ """Compare a modal differential game with an LQR for ``N`` masses on springs."""
28
+
29
+ def __init__(
30
+ self,
31
+ N: int,
32
+ m: float,
33
+ k: float,
34
+ q: float | Sequence[float],
35
+ r: float,
36
+ x_0: np.ndarray | None = None,
37
+ x_T: np.ndarray | None = None,
38
+ T_f: float | None = None,
39
+ epsilon_x: float = PyDiffGame.epsilon_x_default,
40
+ epsilon_P: float = PyDiffGame.epsilon_P_default,
41
+ L: int = PyDiffGame.L_default,
42
+ eta: int = PyDiffGame.eta_default,
43
+ ) -> None:
44
+ I_N = np.eye(N)
45
+ Z_N = np.zeros((N, N))
218
46
 
47
+ mass_matrix = m * I_N
48
+ stiffness = k * (2 * I_N - np.array([[int(abs(i - j) == 1) for j in range(N)] for i in range(N)]))
49
+ mass_inv = np.linalg.inv(mass_matrix)
50
+ mass_inv_stiffness = mass_inv @ stiffness
51
+
52
+ # Modal directions: eigh gives orthonormal eigenvectors, so M is orthogonal.
53
+ _, eigenvectors = np.linalg.eigh(mass_inv_stiffness)
54
+ Ms = [eigenvectors[:, i].reshape(1, N) for i in range(N)]
55
+ M = np.concatenate(Ms, axis=0)
56
+ assert np.allclose(np.linalg.inv(M), M.T, atol=1e-9), "modal transform must be orthogonal"
57
+
58
+ A = np.block([[Z_N, I_N], [-mass_inv_stiffness, Z_N]])
59
+ B = np.block([[Z_N], [mass_inv]])
60
+
61
+ q_per_mode = [q] * N if isinstance(q, (int, float)) else list(q)
62
+ modal_to_state = np.kron(np.eye(2), M)
63
+ game_objectives = []
64
+ for i, (q_i, M_i) in enumerate(zip(q_per_mode, Ms)):
65
+ modal_weight = np.diag([0.0] * i + [q_i] + [0.0] * (N - 1) + [q_i] + [0.0] * (N - i - 1))
66
+ Q_i = modal_to_state.T @ modal_weight @ modal_to_state
67
+ game_objectives.append(GameObjective(Q=Q_i, R=r, M=M_i))
68
+
69
+ # The LQR baseline optimizes *exactly* the aggregate of the game's
70
+ # objectives, so it is the genuine monolithic optimum for a fair
71
+ # comparison: sum_i Q_i, and the matching physical input weight r * I
72
+ # (the per-player modal weight r maps to r * I in physical input space
73
+ # because the modal transform M is orthogonal). The game, being a
74
+ # decomposed design, can only match or exceed this optimal cost.
75
+ modal_weight_total = np.diag(q_per_mode + q_per_mode)
76
+ Q_lqr = modal_to_state.T @ modal_weight_total @ modal_to_state
77
+ lqr_objective = [LQRObjective(Q=Q_lqr, R=r * I_N)]
78
+
79
+ state_variables_names = [f"x_{{{i}}}" for i in range(1, N + 1)] + [
80
+ rf"\dot{{x}}_{{{i}}}" for i in range(1, N + 1)
81
+ ]
82
+
83
+ super().__init__(
84
+ A=A,
85
+ B=B,
86
+ games_objectives=[lqr_objective, game_objectives],
87
+ continuous=True,
88
+ x_0=x_0,
89
+ x_T=x_T,
90
+ T_f=T_f,
91
+ L=L,
92
+ eta=eta,
93
+ epsilon_x=epsilon_x,
94
+ epsilon_P=epsilon_P,
95
+ state_variables_names=state_variables_names,
96
+ )
97
+
98
+
99
+ def main(N: int = 2, *, plot: bool = True, save_figure: bool = False) -> None:
100
+ """Solve a single masses-and-springs comparison and print the costs."""
101
+
102
+ m, k, r = 50.0, 10.0, 1.0
103
+ q = [500.0, 2000.0] if N == 2 else [500.0 * 1.2**i for i in range(N)]
104
+ x_0 = np.array([10.0 * i for i in range(1, N + 1)] + [0.0] * N)
105
+ x_T = x_0 * 10.0
106
+
107
+ comparison = MassesWithSpringsComparison(N=N, m=m, k=k, q=q, r=r, x_0=x_0, x_T=x_T, T_f=25.0, L=300)
108
+ comparison.run(plot_state_spaces=plot, save_figure=save_figure)
109
+
110
+ lqr_cost, game_cost = comparison.costs()
111
+ print(f"LQR cost = {lqr_cost:.4g}")
112
+ print(f"Game cost = {game_cost:.4g}")
113
+ print(f"All games controllable: {comparison.are_all_controllable()}")
114
+ if plot:
115
+ show()
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
@@ -2,55 +2,61 @@
2
2
  # RMM, 14 Jan 03
3
3
  #
4
4
  # This file works through an LQR based design problem, using the
5
- # planar vertical takeoff and landing (PVTOLComparison.py) aircraft example from
5
+ # planar vertical takeoff and landing (PVTOL) aircraft example from
6
6
  # Astrom and Murray, Chapter 5. It is intended to demonstrate the
7
7
  # basic functionality of the python-control package.
8
8
  #
9
9
 
10
- import os
11
- import numpy as np
12
- import matplotlib.pyplot as plt # MATLAB-like plotting functions
10
+ from __future__ import annotations
11
+
13
12
  import control as ct
13
+ import matplotlib.pyplot as plt # MATLAB-like plotting functions
14
+ import numpy as np
15
+
16
+ from PyDiffGame.plotting import show
14
17
 
15
18
  #
16
19
  # System dynamics
17
20
  #
18
- # These are the dynamics for the PVTOLComparison.py system, written in state space
21
+ # These are the dynamics for the PVTOL system, written in state space
19
22
  # form.
20
23
  #
21
24
 
22
25
  # System parameters
23
- m = 4 # mass of aircraft
26
+ m = 4 # mass of aircraft
24
27
  J = 0.0475 # inertia around pitch axis
25
- r = 0.25 # distance to center of force
26
- g = 9.8 # gravitational constant
27
- c = 0.05 # damping factor (estimated)
28
+ r = 0.25 # distance to center of force
29
+ g = 9.8 # gravitational constant
30
+ c = 0.05 # damping factor (estimated)
28
31
 
29
32
  # State space dynamics
30
33
  xe = [0, 0, 0, 0, 0, 0] # equilibrium point of interest
31
34
  ue = [0, m * g] # (note these are lists, not matrices)
32
35
 
33
- # This will involve re-working the subsequent equations as the shapes
34
- # See below.
35
-
36
36
  A = np.array(
37
- [[0, 0, 0, 1, 0, 0],
38
- [0, 0, 0, 0, 1, 0],
39
- [0, 0, 0, 0, 0, 1],
40
- [0, 0, (-ue[0]*np.sin(xe[2]) - ue[1]*np.cos(xe[2]))/m, -c/m, 0, 0],
41
- [0, 0, (ue[0]*np.cos(xe[2]) - ue[1]*np.sin(xe[2]))/m, 0, -c/m, 0],
42
- [0, 0, 0, 0, 0, 0]]
37
+ [
38
+ [0, 0, 0, 1, 0, 0],
39
+ [0, 0, 0, 0, 1, 0],
40
+ [0, 0, 0, 0, 0, 1],
41
+ [0, 0, (-ue[0] * np.sin(xe[2]) - ue[1] * np.cos(xe[2])) / m, -c / m, 0, 0],
42
+ [0, 0, (ue[0] * np.cos(xe[2]) - ue[1] * np.sin(xe[2])) / m, 0, -c / m, 0],
43
+ [0, 0, 0, 0, 0, 0],
44
+ ]
43
45
  )
44
46
 
45
47
  # Input matrix
46
48
  B = np.array(
47
- [[0, 0], [0, 0], [0, 0],
48
- [np.cos(xe[2])/m, -np.sin(xe[2])/m],
49
- [np.sin(xe[2])/m, np.cos(xe[2])/m],
50
- [r/J, 0]]
49
+ [
50
+ [0, 0],
51
+ [0, 0],
52
+ [0, 0],
53
+ [np.cos(xe[2]) / m, -np.sin(xe[2]) / m],
54
+ [np.sin(xe[2]) / m, np.cos(xe[2]) / m],
55
+ [r / J, 0],
56
+ ]
51
57
  )
52
58
 
53
- # Output matrix
59
+ # Output matrix
54
60
  C = np.array([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]])
55
61
  D = np.array([[0, 0], [0, 0]])
56
62
 
@@ -58,7 +64,7 @@ D = np.array([[0, 0], [0, 0]])
58
64
  # Construct inputs and outputs corresponding to steps in xy position
59
65
  #
60
66
  # The vectors xd and yd correspond to the states that are the desired
61
- # equilibrium states for the system. The matrices Cx and Cy are the
67
+ # equilibrium states for the system. The matrices Cx and Cy are the
62
68
  # corresponding outputs.
63
69
  #
64
70
  # The way these vectors are used is to compute the closed loop system
@@ -67,7 +73,7 @@ D = np.array([[0, 0], [0, 0]])
67
73
  # xdot = Ax + B u => xdot = (A-BK)x + K xd
68
74
  # u = -K(x - xd) y = Cx
69
75
  #
70
- # The closed loop dynamics can be simulated using the "step" command,
76
+ # The closed loop dynamics can be simulated using the "step" command,
71
77
  # with K*xd as the input vector (assumes that the "input" is unit size,
72
78
  # so that xd corresponds to the desired steady state.
73
79
  #
@@ -78,11 +84,8 @@ yd = np.array([[0], [1], [0], [0], [0], [0]])
78
84
  #
79
85
  # Extract the relevant dynamics for use with SISO library
80
86
  #
81
- # The current python-control library only supports SISO transfer
82
- # functions, so we have to modify some parts of the original MATLAB
83
- # code to extract out SISO systems. To do this, we define the 'lat' and
84
- # 'alt' index vectors to consist of the states that are are relevant
85
- # to the lateral (x) and vertical (y) dynamics.
87
+ # We define the 'lat' and 'alt' index vectors to consist of the states
88
+ # that are relevant to the lateral (x) and vertical (y) dynamics.
86
89
  #
87
90
 
88
91
  # Indices for the parts of the state that we want
@@ -100,123 +103,114 @@ By = B[np.ix_(alt, [1])]
100
103
  Cy = C[np.ix_([1], alt)]
101
104
  Dy = D[np.ix_([1], [1])]
102
105
 
103
- # Label the plot
104
- plt.clf()
105
- plt.suptitle("LQR controllers for vectored thrust aircraft (pvtol-lqr)")
106
-
107
- #
108
- # LQR design
109
- #
110
-
111
- # Start with a diagonal weighting
112
- Qx1 = np.diag([1, 1, 1, 1, 1, 1])
113
- Qu1a = np.diag([1, 1])
114
- K1a, X, E = ct.lqr(A, B, Qx1, Qu1a)
115
-
116
- # Close the loop: xdot = Ax - B K (x-xd)
117
- #
118
- # Note: python-control requires we do this 1 input at a time
119
- # H1a = ss(A-B*K1a, B*K1a*concatenate((xd, yd), axis=1), C, D);
120
- # (T, Y) = step_response(H1a, T=np.linspace(0,10,100));
121
- #
122
-
123
- # Step response for the first input
124
- H1ax = ct.ss(Ax - Bx @ K1a[np.ix_([0], lat)],
125
- Bx @ K1a[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
126
- Tx, Yx = ct.step_response(H1ax, T=np.linspace(0, 10, 100))
127
-
128
- # Step response for the second input
129
- H1ay = ct.ss(Ay - By @ K1a[np.ix_([1], alt)],
130
- By @ K1a[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
131
- Ty, Yy = ct.step_response(H1ay, T=np.linspace(0, 10, 100))
132
-
133
- plt.subplot(221)
134
- plt.title("Identity weights")
135
- # plt.plot(T, Y[:,1, 1], '-', T, Y[:,2, 2], '--')
136
- plt.plot(Tx.T, Yx.T, '-', Ty.T, Yy.T, '--')
137
- plt.plot([0, 10], [1, 1], 'k-')
138
-
139
- plt.axis([0, 10, -0.1, 1.4])
140
- plt.ylabel('position')
141
- plt.legend(('x', 'y'), loc='lower right')
142
-
143
- # Look at different input weightings
144
- Qu1a = np.diag([1, 1])
145
- K1a, X, E = ct.lqr(A, B, Qx1, Qu1a)
146
- H1ax = ct.ss(Ax - Bx @ K1a[np.ix_([0], lat)],
147
- Bx @ K1a[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
148
-
149
- Qu1b = (40 ** 2)*np.diag([1, 1])
150
- K1b, X, E = ct.lqr(A, B, Qx1, Qu1b)
151
- H1bx = ct.ss(Ax - Bx @ K1b[np.ix_([0], lat)],
152
- Bx @ K1b[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
153
-
154
- Qu1c = (200 ** 2)*np.diag([1, 1])
155
- K1c, X, E = ct.lqr(A, B, Qx1, Qu1c)
156
- H1cx = ct.ss(Ax - Bx @ K1c[np.ix_([0], lat)],
157
- Bx @ K1c[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
158
-
159
- T1, Y1 = ct.step_response(H1ax, T=np.linspace(0, 10, 100))
160
- T2, Y2 = ct.step_response(H1bx, T=np.linspace(0, 10, 100))
161
- T3, Y3 = ct.step_response(H1cx, T=np.linspace(0, 10, 100))
162
-
163
- plt.subplot(222)
164
- plt.title("Effect of input weights")
165
- plt.plot(T1.T, Y1.T, 'b-')
166
- plt.plot(T2.T, Y2.T, 'b-')
167
- plt.plot(T3.T, Y3.T, 'b-')
168
- plt.plot([0, 10], [1, 1], 'k-')
169
-
170
- plt.axis([0, 10, -0.1, 1.4])
171
-
172
- # arcarrow([1.3, 0.8], [5, 0.45], -6)
173
- plt.text(5.3, 0.4, r'$\rho$')
174
-
175
- # Output weighting - change Qx to use outputs
176
- Qx2 = C.T @ C
177
- Qu2 = 0.1 * np.diag([1, 1])
178
- K2, X, E = ct.lqr(A, B, Qx2, Qu2)
179
-
180
- H2x = ct.ss(Ax - Bx @ K2[np.ix_([0], lat)],
181
- Bx @ K2[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
182
- H2y = ct.ss(Ay - By @ K2[np.ix_([1], alt)],
183
- By @ K2[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
184
-
185
- plt.subplot(223)
186
- plt.title("Output weighting")
187
- T2x, Y2x = ct.step_response(H2x, T=np.linspace(0, 10, 100))
188
- T2y, Y2y = ct.step_response(H2y, T=np.linspace(0, 10, 100))
189
- plt.plot(T2x.T, Y2x.T, T2y.T, Y2y.T)
190
- plt.ylabel('position')
191
- plt.xlabel('time')
192
- plt.ylabel('position')
193
- plt.legend(('x', 'y'), loc='lower right')
194
-
195
- #
196
- # Physically motivated weighting
197
- #
198
- # Shoot for 1 cm error in x, 10 cm error in y. Try to keep the angle
199
- # less than 5 degrees in making the adjustments. Penalize side forces
200
- # due to loss in efficiency.
201
- #
202
106
 
203
- Qx3 = np.diag([100, 10, 2*np.pi/5, 0, 0, 0])
204
- Qu3 = 0.1*np.diag([1, 10])
205
- K3, X, E = ct.lqr(A, B, Qx3, Qu3)
206
-
207
- H3x = ct.ss(Ax - Bx @ K3[np.ix_([0], lat)],
208
- Bx @ K3[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
209
- H3y = ct.ss(Ay - By @ K3[np.ix_([1], alt)],
210
- By @ K3[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
211
- plt.subplot(224)
212
- # step_response(H3x, H3y, 10)
213
- T3x, Y3x = ct.step_response(H3x, T=np.linspace(0, 10, 100))
214
- T3y, Y3y = ct.step_response(H3y, T=np.linspace(0, 10, 100))
215
- plt.plot(T3x.T, Y3x.T, T3y.T, Y3y.T)
216
- plt.title("Physically motivated weights")
217
- plt.xlabel('time')
218
- plt.legend(('x', 'y'), loc='lower right')
219
- plt.tight_layout()
220
-
221
- if 'PYCONTROL_TEST_EXAMPLES' not in os.environ:
222
- plt.show()
107
+ def main() -> None:
108
+ """Run the LQR design study and draw the comparison plots."""
109
+
110
+ # Label the plot
111
+ plt.clf()
112
+ plt.suptitle("LQR controllers for vectored thrust aircraft (pvtol-lqr)")
113
+
114
+ #
115
+ # LQR design
116
+ #
117
+
118
+ # Start with a diagonal weighting
119
+ Qx1 = np.diag([1, 1, 1, 1, 1, 1])
120
+ Qu1a = np.diag([1, 1])
121
+ K1a, _X, _E = ct.lqr(A, B, Qx1, Qu1a)
122
+
123
+ # Close the loop: xdot = Ax - B K (x-xd)
124
+ #
125
+ # Note: python-control requires we do this 1 input at a time.
126
+
127
+ # Step response for the first input
128
+ H1ax = ct.ss(Ax - Bx @ K1a[np.ix_([0], lat)], Bx @ K1a[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
129
+ Tx, Yx = ct.step_response(H1ax, T=np.linspace(0, 10, 100))
130
+
131
+ # Step response for the second input
132
+ H1ay = ct.ss(Ay - By @ K1a[np.ix_([1], alt)], By @ K1a[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
133
+ Ty, Yy = ct.step_response(H1ay, T=np.linspace(0, 10, 100))
134
+
135
+ plt.subplot(221)
136
+ plt.title("Identity weights")
137
+ plt.plot(Tx.T, Yx.T, "-", Ty.T, Yy.T, "--")
138
+ plt.plot([0, 10], [1, 1], "k-")
139
+
140
+ plt.axis([0, 10, -0.1, 1.4])
141
+ plt.ylabel("position")
142
+ plt.legend(("x", "y"), loc="lower right")
143
+
144
+ # Look at different input weightings
145
+ Qu1a = np.diag([1, 1])
146
+ K1a, _X, _E = ct.lqr(A, B, Qx1, Qu1a)
147
+ H1ax = ct.ss(Ax - Bx @ K1a[np.ix_([0], lat)], Bx @ K1a[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
148
+
149
+ Qu1b = (40**2) * np.diag([1, 1])
150
+ K1b, _X, _E = ct.lqr(A, B, Qx1, Qu1b)
151
+ H1bx = ct.ss(Ax - Bx @ K1b[np.ix_([0], lat)], Bx @ K1b[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
152
+
153
+ Qu1c = (200**2) * np.diag([1, 1])
154
+ K1c, _X, _E = ct.lqr(A, B, Qx1, Qu1c)
155
+ H1cx = ct.ss(Ax - Bx @ K1c[np.ix_([0], lat)], Bx @ K1c[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
156
+
157
+ T1, Y1 = ct.step_response(H1ax, T=np.linspace(0, 10, 100))
158
+ T2, Y2 = ct.step_response(H1bx, T=np.linspace(0, 10, 100))
159
+ T3, Y3 = ct.step_response(H1cx, T=np.linspace(0, 10, 100))
160
+
161
+ plt.subplot(222)
162
+ plt.title("Effect of input weights")
163
+ plt.plot(T1.T, Y1.T, "b-")
164
+ plt.plot(T2.T, Y2.T, "b-")
165
+ plt.plot(T3.T, Y3.T, "b-")
166
+ plt.plot([0, 10], [1, 1], "k-")
167
+
168
+ plt.axis([0, 10, -0.1, 1.4])
169
+
170
+ plt.text(5.3, 0.4, r"$\rho$")
171
+
172
+ # Output weighting - change Qx to use outputs
173
+ Qx2 = C.T @ C
174
+ Qu2 = 0.1 * np.diag([1, 1])
175
+ K2, _X, _E = ct.lqr(A, B, Qx2, Qu2)
176
+
177
+ H2x = ct.ss(Ax - Bx @ K2[np.ix_([0], lat)], Bx @ K2[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
178
+ H2y = ct.ss(Ay - By @ K2[np.ix_([1], alt)], By @ K2[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
179
+
180
+ plt.subplot(223)
181
+ plt.title("Output weighting")
182
+ T2x, Y2x = ct.step_response(H2x, T=np.linspace(0, 10, 100))
183
+ T2y, Y2y = ct.step_response(H2y, T=np.linspace(0, 10, 100))
184
+ plt.plot(T2x.T, Y2x.T, T2y.T, Y2y.T)
185
+ plt.xlabel("time")
186
+ plt.ylabel("position")
187
+ plt.legend(("x", "y"), loc="lower right")
188
+
189
+ #
190
+ # Physically motivated weighting
191
+ #
192
+ # Shoot for 1 cm error in x, 10 cm error in y. Try to keep the angle
193
+ # less than 5 degrees in making the adjustments. Penalize side forces
194
+ # due to loss in efficiency.
195
+ #
196
+
197
+ Qx3 = np.diag([100, 10, 2 * np.pi / 5, 0, 0, 0])
198
+ Qu3 = 0.1 * np.diag([1, 10])
199
+ K3, _X, _E = ct.lqr(A, B, Qx3, Qu3)
200
+
201
+ H3x = ct.ss(Ax - Bx @ K3[np.ix_([0], lat)], Bx @ K3[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
202
+ H3y = ct.ss(Ay - By @ K3[np.ix_([1], alt)], By @ K3[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
203
+ plt.subplot(224)
204
+ T3x, Y3x = ct.step_response(H3x, T=np.linspace(0, 10, 100))
205
+ T3y, Y3y = ct.step_response(H3y, T=np.linspace(0, 10, 100))
206
+ plt.plot(T3x.T, Y3x.T, T3y.T, Y3y.T)
207
+ plt.title("Physically motivated weights")
208
+ plt.xlabel("time")
209
+ plt.legend(("x", "y"), loc="lower right")
210
+ plt.tight_layout()
211
+
212
+ show()
213
+
214
+
215
+ if __name__ == "__main__":
216
+ main()