PyDiffGame 0.1.0__py3-none-any.whl → 0.1.2__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/ContinuousPyDiffGame.py +2 -2
- PyDiffGame/DiscretePyDiffGame.py +8 -9
- PyDiffGame/PyDiffGame.py +19 -12
- PyDiffGame/PyDiffGameLQRComparison.py +9 -10
- PyDiffGame/examples/InvertedPendulumComparison.py +257 -0
- PyDiffGame/examples/MassesWithSpringsComparison.py +218 -0
- PyDiffGame/examples/PVTOL.py +222 -0
- PyDiffGame/examples/PVTOLComparison.py +111 -0
- PyDiffGame/examples/QuadRotorControl.py +548 -0
- PyDiffGame/examples/figures/2/2-players_large_1.png +0 -0
- PyDiffGame/examples/figures/2/2-players_large_2.png +0 -0
- PyDiffGame/examples/figures/2/LQR_large_1.png +0 -0
- PyDiffGame/examples/figures/2/LQR_large_2.png +0 -0
- PyDiffGame/examples/figures/2/two_masses_tikz.png +0 -0
- PyDiffGame/examples/figures/4/4-players_large_1.png +0 -0
- PyDiffGame/examples/figures/4/4-players_large_2.png +0 -0
- PyDiffGame/examples/figures/4/LQR_large_1.png +0 -0
- PyDiffGame/examples/figures/4/LQR_large_2.png +0 -0
- PyDiffGame/examples/figures/8/8-players_large_1.png +0 -0
- PyDiffGame/examples/figures/8/8-players_large_2.png +0 -0
- PyDiffGame/examples/figures/8/LQR_large_1.png +0 -0
- PyDiffGame/examples/figures/8/LQR_large_2.png +0 -0
- PyDiffGame/examples/figures/PVTOL/PVTOL1.png +0 -0
- PyDiffGame/examples/figures/PVTOL/PVTOL10.png +0 -0
- PyDiffGame/examples/figures/PVTOL/PVTOL100.png +0 -0
- PyDiffGame/examples/figures/PVTOL/PVTOL1000.png +0 -0
- PyDiffGame/examples/figures/PVTOL0001.png +0 -0
- PyDiffGame/examples/figures/PVTOL001.png +0 -0
- PyDiffGame/examples/figures/PVTOL01.png +0 -0
- PyDiffGame/examples/figures/PVTOL1.png +0 -0
- {PyDiffGame-0.1.0.dist-info → pydiffgame-0.1.2.dist-info}/METADATA +65 -54
- pydiffgame-0.1.2.dist-info/RECORD +37 -0
- {PyDiffGame-0.1.0.dist-info → pydiffgame-0.1.2.dist-info}/WHEEL +1 -2
- {PyDiffGame-0.1.0.dist-info → pydiffgame-0.1.2.dist-info/licenses}/LICENSE +1 -1
- PyDiffGame-0.1.0.dist-info/RECORD +0 -12
- PyDiffGame-0.1.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
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)
|
|
30
|
+
|
|
31
|
+
M_inv_K = M_masses_inv @ K
|
|
32
|
+
|
|
33
|
+
if Ms is None:
|
|
34
|
+
eigenvectors = np.linalg.eig(M_inv_K)[1]
|
|
35
|
+
Ms = [eigenvector.reshape(1, N) for eigenvector in eigenvectors]
|
|
36
|
+
|
|
37
|
+
A = np.block([[Z_N, I_N],
|
|
38
|
+
[-M_inv_K, Z_N]])
|
|
39
|
+
B = np.block([[Z_N],
|
|
40
|
+
[M_masses_inv]])
|
|
41
|
+
|
|
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)]
|
|
45
|
+
|
|
46
|
+
M = np.concatenate(Ms,
|
|
47
|
+
axis=0)
|
|
48
|
+
|
|
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)
|
|
59
|
+
|
|
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}
|
|
72
|
+
|
|
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
|
+
|
|
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)
|
|
218
|
+
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# pvtol_lqr.m - LQR design for vectored thrust aircraft
|
|
2
|
+
# RMM, 14 Jan 03
|
|
3
|
+
#
|
|
4
|
+
# This file works through an LQR based design problem, using the
|
|
5
|
+
# planar vertical takeoff and landing (PVTOLComparison.py) aircraft example from
|
|
6
|
+
# Astrom and Murray, Chapter 5. It is intended to demonstrate the
|
|
7
|
+
# basic functionality of the python-control package.
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import numpy as np
|
|
12
|
+
import matplotlib.pyplot as plt # MATLAB-like plotting functions
|
|
13
|
+
import control as ct
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# System dynamics
|
|
17
|
+
#
|
|
18
|
+
# These are the dynamics for the PVTOLComparison.py system, written in state space
|
|
19
|
+
# form.
|
|
20
|
+
#
|
|
21
|
+
|
|
22
|
+
# System parameters
|
|
23
|
+
m = 4 # mass of aircraft
|
|
24
|
+
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
|
+
|
|
29
|
+
# State space dynamics
|
|
30
|
+
xe = [0, 0, 0, 0, 0, 0] # equilibrium point of interest
|
|
31
|
+
ue = [0, m * g] # (note these are lists, not matrices)
|
|
32
|
+
|
|
33
|
+
# This will involve re-working the subsequent equations as the shapes
|
|
34
|
+
# See below.
|
|
35
|
+
|
|
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]]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Input matrix
|
|
46
|
+
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]]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Output matrix
|
|
54
|
+
C = np.array([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]])
|
|
55
|
+
D = np.array([[0, 0], [0, 0]])
|
|
56
|
+
|
|
57
|
+
#
|
|
58
|
+
# Construct inputs and outputs corresponding to steps in xy position
|
|
59
|
+
#
|
|
60
|
+
# 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
|
|
62
|
+
# corresponding outputs.
|
|
63
|
+
#
|
|
64
|
+
# The way these vectors are used is to compute the closed loop system
|
|
65
|
+
# dynamics as
|
|
66
|
+
#
|
|
67
|
+
# xdot = Ax + B u => xdot = (A-BK)x + K xd
|
|
68
|
+
# u = -K(x - xd) y = Cx
|
|
69
|
+
#
|
|
70
|
+
# The closed loop dynamics can be simulated using the "step" command,
|
|
71
|
+
# with K*xd as the input vector (assumes that the "input" is unit size,
|
|
72
|
+
# so that xd corresponds to the desired steady state.
|
|
73
|
+
#
|
|
74
|
+
|
|
75
|
+
xd = np.array([[1], [0], [0], [0], [0], [0]])
|
|
76
|
+
yd = np.array([[0], [1], [0], [0], [0], [0]])
|
|
77
|
+
|
|
78
|
+
#
|
|
79
|
+
# Extract the relevant dynamics for use with SISO library
|
|
80
|
+
#
|
|
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.
|
|
86
|
+
#
|
|
87
|
+
|
|
88
|
+
# Indices for the parts of the state that we want
|
|
89
|
+
lat = (0, 2, 3, 5)
|
|
90
|
+
alt = (1, 4)
|
|
91
|
+
|
|
92
|
+
# Decoupled dynamics
|
|
93
|
+
Ax = A[np.ix_(lat, lat)]
|
|
94
|
+
Bx = B[np.ix_(lat, [0])]
|
|
95
|
+
Cx = C[np.ix_([0], lat)]
|
|
96
|
+
Dx = D[np.ix_([0], [0])]
|
|
97
|
+
|
|
98
|
+
Ay = A[np.ix_(alt, alt)]
|
|
99
|
+
By = B[np.ix_(alt, [1])]
|
|
100
|
+
Cy = C[np.ix_([1], alt)]
|
|
101
|
+
Dy = D[np.ix_([1], [1])]
|
|
102
|
+
|
|
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
|
+
|
|
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()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from PyDiffGame.PyDiffGame import PyDiffGame
|
|
5
|
+
from PyDiffGame.PyDiffGameLQRComparison import PyDiffGameLQRComparison
|
|
6
|
+
from PyDiffGame.Objective import GameObjective, LQRObjective
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PVTOLComparison(PyDiffGameLQRComparison):
|
|
10
|
+
def __init__(self,
|
|
11
|
+
x_0: Optional[np.array] = None,
|
|
12
|
+
x_T: Optional[np.array] = None,
|
|
13
|
+
T_f: Optional[float] = None,
|
|
14
|
+
epsilon_x: Optional[float] = PyDiffGame.epsilon_x_default,
|
|
15
|
+
epsilon_P: Optional[float] = PyDiffGame.epsilon_P_default,
|
|
16
|
+
L: Optional[int] = PyDiffGame.L_default,
|
|
17
|
+
eta: Optional[int] = PyDiffGame.eta_default):
|
|
18
|
+
m = 4 # mass of aircraft
|
|
19
|
+
J = 0.0475 # inertia around pitch axis
|
|
20
|
+
r = 0.25 # distance to center of force
|
|
21
|
+
c = 0.05 # damping factor (estimated)
|
|
22
|
+
|
|
23
|
+
A_11 = np.zeros((3, 3))
|
|
24
|
+
A_12 = np.eye(3)
|
|
25
|
+
A_21 = np.array([[0, 0, -PyDiffGame.g],
|
|
26
|
+
[0, 0, 0],
|
|
27
|
+
[0, 0, 0]])
|
|
28
|
+
A_22 = np.diag([-c / m, -c / m, 0])
|
|
29
|
+
|
|
30
|
+
A = np.block([[A_11, A_12],
|
|
31
|
+
[A_21, A_22]])
|
|
32
|
+
|
|
33
|
+
# Input matrix
|
|
34
|
+
B = np.array(
|
|
35
|
+
[[0, 0],
|
|
36
|
+
[0, 0],
|
|
37
|
+
[0, 0],
|
|
38
|
+
[1 / m, 0],
|
|
39
|
+
[0, 1 / m],
|
|
40
|
+
[r / J, 0]]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
psi = 1
|
|
44
|
+
|
|
45
|
+
M_x = [1, 0]
|
|
46
|
+
M_y = [0, 1]
|
|
47
|
+
M_theta = [1, 0]
|
|
48
|
+
|
|
49
|
+
Ms = [np.array(M_i).reshape(1, 2) for M_i in [M_x, M_y, M_theta]]
|
|
50
|
+
M = np.concatenate(Ms, axis=0)
|
|
51
|
+
|
|
52
|
+
self.figure_filename_generator = lambda g: ('LQR_' if g.is_LQR() else '') + f"PVTOL{str(psi).replace('.', '')}"
|
|
53
|
+
|
|
54
|
+
Q_x_diag = [psi, 0, 0]
|
|
55
|
+
Q_y_diag = [0, 1, 0]
|
|
56
|
+
Q_theta_diag = [0, 0, 1]
|
|
57
|
+
|
|
58
|
+
r_x = 1
|
|
59
|
+
r_y = 1
|
|
60
|
+
r_theta = 1
|
|
61
|
+
|
|
62
|
+
Qs = [np.diag(Q_i_diag * 2) for Q_i_diag in [Q_x_diag, Q_y_diag, Q_theta_diag]]
|
|
63
|
+
Rs = [np.array([r_i]) for r_i in [r_x, r_y, r_theta]]
|
|
64
|
+
|
|
65
|
+
variables = ['x', 'y', '\\theta']
|
|
66
|
+
|
|
67
|
+
self.state_variables_names = [f'{v}(t)' for v in variables] + ['\\dot{' + str(v) + '}(t)' for v in variables]
|
|
68
|
+
|
|
69
|
+
Q_lqr = sum(Qs)
|
|
70
|
+
R_lqr = np.diag([r_x + r_theta, r_y])
|
|
71
|
+
|
|
72
|
+
lqr_objective = [LQRObjective(Q=Q_lqr,
|
|
73
|
+
R_ii=R_lqr)]
|
|
74
|
+
game_objectives = [GameObjective(Q=Q_i,
|
|
75
|
+
R_ii=R_i,
|
|
76
|
+
M_i=M_i) for Q_i, R_i, M_i in zip(Qs, Rs, Ms)]
|
|
77
|
+
games_objectives = [lqr_objective,
|
|
78
|
+
game_objectives]
|
|
79
|
+
|
|
80
|
+
args = {'A': A,
|
|
81
|
+
'B': B,
|
|
82
|
+
'x_0': x_0,
|
|
83
|
+
'x_T': x_T,
|
|
84
|
+
'T_f': T_f,
|
|
85
|
+
'state_variables_names': self.state_variables_names,
|
|
86
|
+
'epsilon_x': epsilon_x,
|
|
87
|
+
'epsilon_P': epsilon_P,
|
|
88
|
+
'L': L,
|
|
89
|
+
'eta': eta}
|
|
90
|
+
|
|
91
|
+
super().__init__(args=args,
|
|
92
|
+
M=M,
|
|
93
|
+
games_objectives=games_objectives)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == '__main__':
|
|
97
|
+
epsilon_x = epsilon_P = 10 ** (-8)
|
|
98
|
+
|
|
99
|
+
x_0 = np.zeros((6,))
|
|
100
|
+
x_d = y_d = 50
|
|
101
|
+
x_T = np.array([x_d, y_d] + [0] * 4)
|
|
102
|
+
T_f = 25
|
|
103
|
+
|
|
104
|
+
pvtol = PVTOLComparison(x_0=x_0,
|
|
105
|
+
x_T=x_T,
|
|
106
|
+
T_f=T_f,
|
|
107
|
+
epsilon_x=epsilon_x,
|
|
108
|
+
epsilon_P=epsilon_P)
|
|
109
|
+
pvtol(plot_state_spaces=True,
|
|
110
|
+
save_figure=True,
|
|
111
|
+
figure_filename=pvtol.figure_filename_generator)
|