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.
- PyDiffGame/__init__.py +50 -0
- PyDiffGame/_typing.py +25 -0
- PyDiffGame/base.py +468 -0
- PyDiffGame/comparison.py +121 -0
- PyDiffGame/continuous.py +223 -0
- PyDiffGame/discrete.py +211 -0
- PyDiffGame/examples/InvertedPendulumComparison.py +211 -236
- PyDiffGame/examples/MassesWithSpringsComparison.py +109 -208
- PyDiffGame/examples/PVTOL.py +143 -149
- PyDiffGame/examples/PVTOLComparison.py +75 -69
- PyDiffGame/examples/QuadRotorControl.py +394 -304
- PyDiffGame/lqr.py +30 -0
- PyDiffGame/objective.py +108 -0
- PyDiffGame/plotting.py +98 -0
- pydiffgame-2.0.0.dist-info/METADATA +408 -0
- {pydiffgame-0.1.2.dist-info → pydiffgame-2.0.0.dist-info}/RECORD +18 -16
- {pydiffgame-0.1.2.dist-info → pydiffgame-2.0.0.dist-info}/WHEEL +1 -1
- PyDiffGame/ContinuousPyDiffGame.py +0 -275
- PyDiffGame/DiscretePyDiffGame.py +0 -359
- PyDiffGame/LQR.py +0 -73
- PyDiffGame/Objective.py +0 -62
- PyDiffGame/PyDiffGame.py +0 -1273
- PyDiffGame/PyDiffGameLQRComparison.py +0 -169
- pydiffgame-0.1.2.dist-info/METADATA +0 -306
- {pydiffgame-0.1.2.dist-info → pydiffgame-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,218 +1,119 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
B = np.block([[Z_N],
|
|
40
|
-
[M_masses_inv]])
|
|
11
|
+
python -m PyDiffGame.examples.MassesWithSpringsComparison
|
|
12
|
+
"""
|
|
41
13
|
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
axis=0)
|
|
16
|
+
from collections.abc import Sequence
|
|
48
17
|
|
|
49
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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()
|
PyDiffGame/examples/PVTOL.py
CHANGED
|
@@ -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 (
|
|
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
|
|
11
|
-
|
|
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
|
|
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
|
|
26
|
+
m = 4 # mass of aircraft
|
|
24
27
|
J = 0.0475 # inertia around pitch axis
|
|
25
|
-
r = 0.25
|
|
26
|
-
g = 9.8
|
|
27
|
-
c = 0.05
|
|
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
|
-
[
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
[
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
#
|
|
82
|
-
#
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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()
|