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
PyDiffGame/PyDiffGame.py
DELETED
|
@@ -1,1273 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from tqdm import tqdm
|
|
6
|
-
import numpy as np
|
|
7
|
-
import math
|
|
8
|
-
import matplotlib.pyplot as plt
|
|
9
|
-
import scipy as sp
|
|
10
|
-
import sympy
|
|
11
|
-
import warnings
|
|
12
|
-
from typing import Callable, Final, ClassVar, Optional, Sequence, Union
|
|
13
|
-
from abc import ABC, abstractmethod
|
|
14
|
-
|
|
15
|
-
from PyDiffGame.Objective import Objective, GameObjective, LQRObjective
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class PyDiffGame(ABC, Callable, Sequence):
|
|
19
|
-
"""
|
|
20
|
-
Differential game abstract base class
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Parameters
|
|
24
|
-
----------
|
|
25
|
-
|
|
26
|
-
A: np.array of shape(n, n)
|
|
27
|
-
System dynamics matrix
|
|
28
|
-
B: np.array of shape(n, sum_{i=}1^N m_i), optional
|
|
29
|
-
Input matrix for all virtual control objectives
|
|
30
|
-
Bs: Sequence of np.arrays of len(N), each array B_i of shape(n, m_i), optional
|
|
31
|
-
Input matrices for each virtual control objective
|
|
32
|
-
Qs: Sequence of np.arrays of len(N), each array Q_i of shape(n, n), optional
|
|
33
|
-
State weight matrices for each virtual control objective
|
|
34
|
-
Rs: Sequence of np.arrays of len(N), each array R_i of shape(m_i, m_i), optional
|
|
35
|
-
Input weight matrices for each virtual control objective
|
|
36
|
-
Ms: Sequence of np.arrays of len(N), each array M_i of shape(m_i, m), optional
|
|
37
|
-
Decomposition matrices for each virtual control objective
|
|
38
|
-
objectives: Sequence of Objective objects of len(N), optional
|
|
39
|
-
Desired objectives for the game, each O_i specifying Q_i R_i and M_i
|
|
40
|
-
x_0: np.array of shape(n), optional
|
|
41
|
-
Initial state vector
|
|
42
|
-
x_T: np.array of shape(n), optional
|
|
43
|
-
Final state vector, in case of signal tracking
|
|
44
|
-
T_f: positive float, optional
|
|
45
|
-
System dynamics horizon. Should be given in the case of finite horizon
|
|
46
|
-
P_f: Sequence of np.arrays of len(N), each array P_f_i of shape(n, n), optional
|
|
47
|
-
default = uncoupled solution of scipy's solve_are
|
|
48
|
-
Final condition for the Riccati equation array. Should be given in the case of finite horizon
|
|
49
|
-
show_legend: bool, optional
|
|
50
|
-
Indicates whether to display a legend in the plots
|
|
51
|
-
state_variables_names: Sequence of str objects of len(n), optional
|
|
52
|
-
The state variables' names to display when plotting
|
|
53
|
-
epsilon_x: float in the interval (0,1), optional
|
|
54
|
-
Numerical convergence threshold for the state vector of the system
|
|
55
|
-
epsilon_P: float in the interval (0,1), optional
|
|
56
|
-
Numerical convergence threshold for the matrices P_i
|
|
57
|
-
L: positive int, optional
|
|
58
|
-
Number of data points
|
|
59
|
-
eta: positive int, optional
|
|
60
|
-
The number of last matrix norms to consider for convergence
|
|
61
|
-
debug: bool, optional
|
|
62
|
-
Indicates whether to display debug information
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
# class fields
|
|
66
|
-
__T_f_default: Final[ClassVar[int]] = 20
|
|
67
|
-
_epsilon_x_default: Final[ClassVar[float]] = 10e-7
|
|
68
|
-
_epsilon_P_default: Final[ClassVar[float]] = 10e-7
|
|
69
|
-
_eigvals_tol: Final[ClassVar[float]] = 10e-7
|
|
70
|
-
_L_default: Final[ClassVar[int]] = 1000
|
|
71
|
-
_eta_default: Final[ClassVar[int]] = 3
|
|
72
|
-
_g: Final[ClassVar[float]] = 9.81
|
|
73
|
-
__x_max_convergence_iterations: Final[ClassVar[int]] = 25
|
|
74
|
-
__p_max_convergence_iterations: Final[ClassVar[int]] = 25
|
|
75
|
-
|
|
76
|
-
_default_figures_path = Path(fr'{os.getcwd()}/figures')
|
|
77
|
-
_default_figures_filename = 'image'
|
|
78
|
-
|
|
79
|
-
@classmethod
|
|
80
|
-
@property
|
|
81
|
-
def epsilon_x_default(cls) -> float:
|
|
82
|
-
return cls._epsilon_x_default
|
|
83
|
-
|
|
84
|
-
@classmethod
|
|
85
|
-
@property
|
|
86
|
-
def epsilon_P_default(cls) -> float:
|
|
87
|
-
return cls._epsilon_P_default
|
|
88
|
-
|
|
89
|
-
@classmethod
|
|
90
|
-
@property
|
|
91
|
-
def L_default(cls) -> int:
|
|
92
|
-
return cls._L_default
|
|
93
|
-
|
|
94
|
-
@classmethod
|
|
95
|
-
@property
|
|
96
|
-
def eta_default(cls) -> float:
|
|
97
|
-
return cls._eta_default
|
|
98
|
-
|
|
99
|
-
@classmethod
|
|
100
|
-
@property
|
|
101
|
-
def g(cls) -> float:
|
|
102
|
-
return cls._g
|
|
103
|
-
|
|
104
|
-
@classmethod
|
|
105
|
-
@property
|
|
106
|
-
def default_figures_path(cls) -> Path:
|
|
107
|
-
return cls._default_figures_path
|
|
108
|
-
|
|
109
|
-
@classmethod
|
|
110
|
-
@property
|
|
111
|
-
def default_figures_filename(cls) -> str:
|
|
112
|
-
return cls._default_figures_filename
|
|
113
|
-
|
|
114
|
-
def __init__(self,
|
|
115
|
-
A: np.array,
|
|
116
|
-
B: Optional[np.array] = None,
|
|
117
|
-
Bs: Optional[Sequence[np.array]] = None,
|
|
118
|
-
Qs: Optional[Sequence[np.array]] = None,
|
|
119
|
-
Rs: Optional[Sequence[np.array]] = None,
|
|
120
|
-
Ms: Optional[Sequence[np.array]] = None,
|
|
121
|
-
objectives: Optional[Sequence[Objective]] = None,
|
|
122
|
-
x_0: Optional[np.array] = None,
|
|
123
|
-
x_T: Optional[np.array] = None,
|
|
124
|
-
T_f: Optional[float] = None,
|
|
125
|
-
P_f: Optional[Union[Sequence[np.array], np.array]] = None,
|
|
126
|
-
show_legend: Optional[bool] = True,
|
|
127
|
-
state_variables_names: Optional[Sequence[str]] = None,
|
|
128
|
-
epsilon_x: Optional[float] = _epsilon_x_default,
|
|
129
|
-
epsilon_P: Optional[float] = _epsilon_P_default,
|
|
130
|
-
L: Optional[int] = _L_default,
|
|
131
|
-
eta: Optional[int] = _eta_default,
|
|
132
|
-
debug: Optional[bool] = False):
|
|
133
|
-
|
|
134
|
-
self.__continuous = 'ContinuousPyDiffGame' in [c.__name__ for c in
|
|
135
|
-
set(type(self).__bases__).union({self.__class__})]
|
|
136
|
-
self._A = A
|
|
137
|
-
self._B = B
|
|
138
|
-
self._Bs = Bs
|
|
139
|
-
self._n = self._A.shape[0]
|
|
140
|
-
self._m = self._B.shape[1] if self._B is not None else self._Bs[0].shape[1]
|
|
141
|
-
self.__objectives = objectives
|
|
142
|
-
self._N = len(objectives) if objectives else len(Qs)
|
|
143
|
-
self._P_size = self._n ** 2
|
|
144
|
-
self._Qs = Qs if Qs else [o.Q for o in objectives]
|
|
145
|
-
self._Rs = Rs if Rs else [o.R for o in objectives]
|
|
146
|
-
self._x_0 = x_0
|
|
147
|
-
self._x_T = x_T
|
|
148
|
-
self.__infinite_horizon = T_f is None
|
|
149
|
-
self._T_f = self.__T_f_default if T_f is None else T_f
|
|
150
|
-
|
|
151
|
-
self._Ms = Ms
|
|
152
|
-
|
|
153
|
-
if self._Ms is None and self.__objectives is not None:
|
|
154
|
-
self._Ms = [o.M_i for o in self.__objectives if isinstance(o, GameObjective)]
|
|
155
|
-
|
|
156
|
-
self._M = None
|
|
157
|
-
|
|
158
|
-
if self._Bs is None:
|
|
159
|
-
if self._Ms is not None and len(self._Ms):
|
|
160
|
-
self._M = np.concatenate(self._Ms, axis=0)
|
|
161
|
-
try:
|
|
162
|
-
self._M_inv = np.linalg.inv(self._M)
|
|
163
|
-
except np.linalg.LinAlgError:
|
|
164
|
-
self._M_inv = np.linalg.pinv(self._M)
|
|
165
|
-
l = 0
|
|
166
|
-
self._Bs = []
|
|
167
|
-
|
|
168
|
-
for M_i in self._Ms:
|
|
169
|
-
m_i = M_i.shape[0]
|
|
170
|
-
M_i_tilde = self._M_inv[:, l:l + m_i]
|
|
171
|
-
BM_i_tilde = self._B @ M_i_tilde
|
|
172
|
-
B_i = BM_i_tilde.reshape((self._n, m_i))
|
|
173
|
-
self._Bs += [B_i]
|
|
174
|
-
l += m_i
|
|
175
|
-
else:
|
|
176
|
-
self._Bs = [B]
|
|
177
|
-
|
|
178
|
-
self._P_f = self.__get_are_P_f() if P_f is None else P_f
|
|
179
|
-
# self._P_f = [np.eye(self._n)] * self._N if P_f is None else P_f
|
|
180
|
-
self._L = L
|
|
181
|
-
self._delta = self._T_f / self._L
|
|
182
|
-
self.__show_legend = show_legend
|
|
183
|
-
self.__state_variables_names = state_variables_names
|
|
184
|
-
self._forward_time = np.linspace(start=0,
|
|
185
|
-
stop=self._T_f,
|
|
186
|
-
num=self._L)
|
|
187
|
-
self._backward_time = self._forward_time[::-1]
|
|
188
|
-
self._A_cl = np.empty_like(self._A)
|
|
189
|
-
self._P = []
|
|
190
|
-
self._K = []
|
|
191
|
-
self.__epsilon_x = epsilon_x
|
|
192
|
-
self.__epsilon_P = epsilon_P
|
|
193
|
-
self._x = self._x_0
|
|
194
|
-
self._x_non_linear = None
|
|
195
|
-
self.__eta = eta
|
|
196
|
-
self._converged = False
|
|
197
|
-
self._debug = debug
|
|
198
|
-
self._fig = None
|
|
199
|
-
|
|
200
|
-
self._verify_input()
|
|
201
|
-
|
|
202
|
-
def _verify_input(self):
|
|
203
|
-
"""
|
|
204
|
-
Input checking method
|
|
205
|
-
|
|
206
|
-
Raises
|
|
207
|
-
------
|
|
208
|
-
|
|
209
|
-
ValueError
|
|
210
|
-
If any of the class parameters don't meet the specified requirements
|
|
211
|
-
"""
|
|
212
|
-
|
|
213
|
-
if self._N == 0:
|
|
214
|
-
raise ValueError('At least one objective must be specified')
|
|
215
|
-
if self._N > 1 and self._Ms and not all([M_i.shape[0] == R_ii.shape[0] == R_ii.shape[1]
|
|
216
|
-
if R_ii.ndim > 1 else M_i.shape[0] == R_ii.shape[0]
|
|
217
|
-
for M_i, R_ii in zip(self._Ms, self._Rs)]):
|
|
218
|
-
raise ValueError('R must be a Sequence of square numpy arrays with shape '
|
|
219
|
-
'corresponding to the second dimensions of the arrays in M')
|
|
220
|
-
|
|
221
|
-
if not 1 > self.__epsilon_x > 0:
|
|
222
|
-
raise ValueError('The state convergence tolerance must be in the open interval (0,1)')
|
|
223
|
-
if not 1 > self.__epsilon_P > 0:
|
|
224
|
-
raise ValueError('The convergence tolerance of the matrix P must be in the open interval (0,1)')
|
|
225
|
-
|
|
226
|
-
if self._A.shape != (self._n, self._n):
|
|
227
|
-
raise ValueError(f'A must be a square numpy array with shape nxn = {self._n}x{self._n}')
|
|
228
|
-
if self._B is not None:
|
|
229
|
-
if self._B.shape[0] != self._n:
|
|
230
|
-
raise ValueError(f'B must be a numpy array with n = {self._n} rows')
|
|
231
|
-
if not self.is_fully_controllable():
|
|
232
|
-
warnings.warn("Warning: The given system is not fully controllable")
|
|
233
|
-
else:
|
|
234
|
-
if not all([B_i.shape[0] == self._n and B_i.shape[1] == R_ii.shape[0] == R_ii.shape[1]
|
|
235
|
-
if R_ii.ndim > 1 else B_i.shape[1] == R_ii.shape[0] for B_i, R_ii in zip(self._Bs, self._Rs)]):
|
|
236
|
-
raise ValueError('R must be a Sequence of square numpy arrays with shape '
|
|
237
|
-
'corresponding to the second dimensions of the arrays in Bs')
|
|
238
|
-
if not all([np.all(np.linalg.eigvals(R_ii) > 0) if R_ii.ndim > 1 else R_ii > 0
|
|
239
|
-
for R_ii in self._Rs]):
|
|
240
|
-
raise ValueError('R must be a Sequence of square positive definite numpy arrays')
|
|
241
|
-
if not all([Q_i.shape == (self._n, self._n) for Q_i in self._Qs]):
|
|
242
|
-
raise ValueError(f'Q must be a Sequence of square positive semi-definite numpy arrays with shape nxn '
|
|
243
|
-
f'= {self._n}x{self._n}')
|
|
244
|
-
if not all([all([e_i >= 0 or PyDiffGame._eigvals_tol > abs(e_i) >= 0 for e_i in np.linalg.eigvals(Q_i)])
|
|
245
|
-
for Q_i in self._Qs]):
|
|
246
|
-
raise ValueError('Q must contain positive semi-definite numpy arrays')
|
|
247
|
-
|
|
248
|
-
if self._x_0 is not None and self._x_0.shape != (self._n,):
|
|
249
|
-
raise ValueError(f'x_0 must be a numpy array with length n = {self._n}')
|
|
250
|
-
if self._x_T is not None and self._x_T.shape != (self._n,):
|
|
251
|
-
raise ValueError(f'x_T must be a numpy array with length n= {self._n}')
|
|
252
|
-
if self._T_f is not None and self._T_f <= 0:
|
|
253
|
-
raise ValueError('T_f must be a positive real number')
|
|
254
|
-
if not all([P_f_i.shape[0] == P_f_i.shape[1] == self._n for P_f_i in self._P_f]):
|
|
255
|
-
raise ValueError(f'P_f must be a Sequence of positive semi-definite numpy arrays with shape nxn = '
|
|
256
|
-
f'{self._n}x{self._n}')
|
|
257
|
-
# if not all([eig >= 0 for eig_set in [np.linalg.eigvals(P_f_i) for P_f_i in self._P_f] for eig in eig_set]):
|
|
258
|
-
# warnings.warn("Warning: there is a matrix in P_f that has negative eigenvalues.
|
|
259
|
-
# Convergence may not occur")
|
|
260
|
-
if self.__state_variables_names and len(self.__state_variables_names) != self._n:
|
|
261
|
-
raise ValueError(f'The parameter state_variables_names must be of length n = {self._n}')
|
|
262
|
-
|
|
263
|
-
if self._L <= 0:
|
|
264
|
-
raise ValueError('The number of data points must be a positive integer')
|
|
265
|
-
if self.__eta <= 0:
|
|
266
|
-
raise ValueError('The number of last matrix norms to consider for convergence must be a positive integer')
|
|
267
|
-
|
|
268
|
-
def is_fully_controllable(self) -> bool:
|
|
269
|
-
"""
|
|
270
|
-
Tests full controllability of an LTI system by the equivalent condition of full rank
|
|
271
|
-
of the controllability Gramian
|
|
272
|
-
|
|
273
|
-
Returns
|
|
274
|
-
-------
|
|
275
|
-
|
|
276
|
-
fully_controllable : bool
|
|
277
|
-
Whether the system is fully controllable
|
|
278
|
-
"""
|
|
279
|
-
|
|
280
|
-
controllability_matrix = np.concatenate([self._B] +
|
|
281
|
-
[np.linalg.matrix_power(self._A, i) @ self._B
|
|
282
|
-
for i in range(1, self._n)],
|
|
283
|
-
axis=1)
|
|
284
|
-
controllability_matrix_rank = np.linalg.matrix_rank(A=controllability_matrix,
|
|
285
|
-
tol=10e-5)
|
|
286
|
-
fully_controllable = controllability_matrix_rank == self._n
|
|
287
|
-
|
|
288
|
-
return fully_controllable
|
|
289
|
-
|
|
290
|
-
def is_LQR(self) -> bool:
|
|
291
|
-
"""
|
|
292
|
-
Indicates whether the game has only one objective and is just a regular LQR
|
|
293
|
-
|
|
294
|
-
Returns
|
|
295
|
-
-------
|
|
296
|
-
|
|
297
|
-
is_lqr : bool
|
|
298
|
-
Whether the system is LQR
|
|
299
|
-
"""
|
|
300
|
-
|
|
301
|
-
return len(self) == 1
|
|
302
|
-
|
|
303
|
-
def __print_eigenvalues(self):
|
|
304
|
-
"""
|
|
305
|
-
Prints the eigenvalues of the matrices Q_i and R_ii for all 1 <= i <= N
|
|
306
|
-
with the greatest common divisor of their elements d as a parameter
|
|
307
|
-
"""
|
|
308
|
-
|
|
309
|
-
d = sympy.symbols('d')
|
|
310
|
-
|
|
311
|
-
for matrix in self._Rs + self._Qs:
|
|
312
|
-
curr_gcd = math.gcd(*matrix.ravel())
|
|
313
|
-
sympy_matrix = sympy.Matrix((matrix / curr_gcd).astype(int)) * d
|
|
314
|
-
eigenvalues_multiset = sympy_matrix.eigenvals(multiple=True)
|
|
315
|
-
print(f"{repr(sympy_matrix)}: {eigenvalues_multiset}\n")
|
|
316
|
-
|
|
317
|
-
def __print_characteristic_polynomials(self):
|
|
318
|
-
"""
|
|
319
|
-
Prints the characteristic polynomials of the matrices Q_i and R_ii for all 1 <= i <= N
|
|
320
|
-
as a function of L and with the greatest common divisor of all their elements d as a parameter
|
|
321
|
-
"""
|
|
322
|
-
|
|
323
|
-
d = sympy.symbols('d')
|
|
324
|
-
L = sympy.symbols('L')
|
|
325
|
-
|
|
326
|
-
for matrix in self._Rs + self._Qs:
|
|
327
|
-
curr_gcd = math.gcd(*matrix.ravel())
|
|
328
|
-
sympy_matrix = sympy.Matrix((matrix / curr_gcd).astype(int)) * d
|
|
329
|
-
characteristic_polynomial = sympy_matrix.charpoly(x=L).as_expr()
|
|
330
|
-
factored_characteristic_polynomial_expression = sympy.factor(f=characteristic_polynomial)
|
|
331
|
-
print(f"{repr(sympy_matrix)}: {factored_characteristic_polynomial_expression}\n")
|
|
332
|
-
|
|
333
|
-
def _converge_DREs_to_AREs(self):
|
|
334
|
-
"""
|
|
335
|
-
Solves the game as backwards convergence of the differential
|
|
336
|
-
finite-horizon game for repeated consecutive steps until the matrix norm converges
|
|
337
|
-
"""
|
|
338
|
-
|
|
339
|
-
x_converged = False
|
|
340
|
-
P_converged = False
|
|
341
|
-
last_eta_norms = []
|
|
342
|
-
curr_iteration_T_f = self._T_f
|
|
343
|
-
x_T = self._x_T if self._x_T is not None else np.zeros_like(self._x_0)
|
|
344
|
-
x_convergence_iterations = 0
|
|
345
|
-
|
|
346
|
-
with tqdm(total=self.__x_max_convergence_iterations) as x_progress_bar:
|
|
347
|
-
while (not x_converged) and x_convergence_iterations < self.__x_max_convergence_iterations:
|
|
348
|
-
x_convergence_iterations += 1
|
|
349
|
-
P_convergence_iterations = 0
|
|
350
|
-
|
|
351
|
-
while (not P_converged) and P_convergence_iterations < self.__p_max_convergence_iterations:
|
|
352
|
-
P_convergence_iterations += 1
|
|
353
|
-
self._backward_time = np.linspace(start=self._T_f,
|
|
354
|
-
stop=self._T_f - self._delta,
|
|
355
|
-
num=self._L)
|
|
356
|
-
self._update_Ps_from_last_state()
|
|
357
|
-
self._P_f = self._P[-1]
|
|
358
|
-
last_eta_norms += [np.linalg.norm(self._P_f)]
|
|
359
|
-
|
|
360
|
-
if len(last_eta_norms) > self.__eta:
|
|
361
|
-
last_eta_norms.pop(0)
|
|
362
|
-
|
|
363
|
-
if len(last_eta_norms) == self.__eta:
|
|
364
|
-
P_converged = all([abs(norm_i - norm_i1) < self.__epsilon_P for norm_i, norm_i1
|
|
365
|
-
in zip(last_eta_norms, last_eta_norms[1:])])
|
|
366
|
-
self._T_f -= self._delta
|
|
367
|
-
|
|
368
|
-
self._T_f = curr_iteration_T_f
|
|
369
|
-
|
|
370
|
-
if self._x_0 is not None:
|
|
371
|
-
curr_x_T = self._simulate_curr_x_T()
|
|
372
|
-
x_T_difference = x_T - curr_x_T
|
|
373
|
-
x_T_difference_norm = x_T_difference.T @ x_T_difference
|
|
374
|
-
curr_x_T_norm = curr_x_T.T @ curr_x_T
|
|
375
|
-
|
|
376
|
-
if curr_x_T_norm > 0:
|
|
377
|
-
convergence_ratio = x_T_difference_norm / curr_x_T_norm
|
|
378
|
-
x_converged = convergence_ratio < self.__epsilon_x
|
|
379
|
-
|
|
380
|
-
curr_iteration_T_f += 1
|
|
381
|
-
self._T_f = curr_iteration_T_f
|
|
382
|
-
self._forward_time = np.linspace(start=0,
|
|
383
|
-
stop=self._T_f,
|
|
384
|
-
num=self._L)
|
|
385
|
-
else:
|
|
386
|
-
x_converged = True
|
|
387
|
-
|
|
388
|
-
x_progress_bar.update(1)
|
|
389
|
-
|
|
390
|
-
def _post_convergence(f: Callable) -> Callable:
|
|
391
|
-
"""
|
|
392
|
-
A decorator static-method to apply on methods that need only be called after convergence
|
|
393
|
-
|
|
394
|
-
Parameters
|
|
395
|
-
----------
|
|
396
|
-
|
|
397
|
-
f: Callable
|
|
398
|
-
The method requiring convergence
|
|
399
|
-
|
|
400
|
-
Returns
|
|
401
|
-
----------
|
|
402
|
-
|
|
403
|
-
decorated_f: Callable
|
|
404
|
-
A decorated version of the method requiring convergence
|
|
405
|
-
"""
|
|
406
|
-
|
|
407
|
-
def decorated_f(self: PyDiffGame,
|
|
408
|
-
*args,
|
|
409
|
-
**kwargs):
|
|
410
|
-
if not self._converged:
|
|
411
|
-
raise RuntimeError('Must first simulate the differential game')
|
|
412
|
-
return f(self,
|
|
413
|
-
*args,
|
|
414
|
-
**kwargs)
|
|
415
|
-
|
|
416
|
-
return decorated_f
|
|
417
|
-
|
|
418
|
-
@_post_convergence
|
|
419
|
-
def __plot_temporal_variables(self,
|
|
420
|
-
t: np.array,
|
|
421
|
-
temporal_variables: np.array,
|
|
422
|
-
is_P: bool,
|
|
423
|
-
title: Optional[str] = None,
|
|
424
|
-
output_variables_names: Optional[Sequence[str]] = None,
|
|
425
|
-
two_state_spaces: Optional[bool] = False):
|
|
426
|
-
"""
|
|
427
|
-
Displays plots for the state variables with respect to time and the convergence of the values of P
|
|
428
|
-
|
|
429
|
-
Parameters
|
|
430
|
-
----------
|
|
431
|
-
|
|
432
|
-
t: np.array array of len(L)
|
|
433
|
-
The time axis information to plot
|
|
434
|
-
temporal_variables: np.arrays of shape(L, n)
|
|
435
|
-
The y-axis information to plot
|
|
436
|
-
is_P: bool
|
|
437
|
-
Indicates whether to accommodate plotting for all the values in P_i or just for x
|
|
438
|
-
title: str, optional
|
|
439
|
-
The plot title to display
|
|
440
|
-
two_state_spaces: bool, optional
|
|
441
|
-
Indicates whether to plot two state spaces
|
|
442
|
-
"""
|
|
443
|
-
|
|
444
|
-
self._fig = plt.figure(dpi=150)
|
|
445
|
-
self._fig.set_size_inches(8, 6)
|
|
446
|
-
plt.plot(t[:temporal_variables.shape[0]], temporal_variables)
|
|
447
|
-
plt.xlabel('Time $[s]$', fontsize=18)
|
|
448
|
-
plt.xticks(fontsize=18)
|
|
449
|
-
plt.yticks(fontsize=18)
|
|
450
|
-
plt.subplots_adjust(wspace=0)
|
|
451
|
-
|
|
452
|
-
if not is_P and max(np.max(temporal_variables, axis=0)) > 1e3:
|
|
453
|
-
plt.ticklabel_format(style='sci', axis='y', scilimits=(0, 0))
|
|
454
|
-
plt.gca().yaxis.get_offset_text().set_size(18)
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if title:
|
|
458
|
-
plt.title(title)
|
|
459
|
-
|
|
460
|
-
if self.__show_legend:
|
|
461
|
-
if is_P:
|
|
462
|
-
labels = ['${P' + str(i) + '}_{' + str(j) + str(k) + '}$' for i in range(1, self._N + 1)
|
|
463
|
-
for j in range(1, self._n + 1) for k in range(1, self._n + 1)]
|
|
464
|
-
elif output_variables_names is not None:
|
|
465
|
-
labels = output_variables_names
|
|
466
|
-
elif self.__state_variables_names is not None:
|
|
467
|
-
if two_state_spaces:
|
|
468
|
-
labels = [f'${name}_{1}$' for name in self.__state_variables_names] + \
|
|
469
|
-
[f'${name}_{2}$' for name in self.__state_variables_names]
|
|
470
|
-
else:
|
|
471
|
-
labels = [f'${name}$' for name in self.__state_variables_names]
|
|
472
|
-
else:
|
|
473
|
-
labels = ["${\mathbf{x}}_{" + str(j) + "}$" for j in range(1, self._n + 1)]
|
|
474
|
-
|
|
475
|
-
plt.legend(labels=labels,
|
|
476
|
-
loc='upper left' if is_P else 'right',
|
|
477
|
-
ncol=4 if self._n > 8 else 2,
|
|
478
|
-
prop={'size': 26},
|
|
479
|
-
bbox_to_anchor=(1, 0.55),
|
|
480
|
-
framealpha=0.3
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
plt.grid()
|
|
484
|
-
plt.show()
|
|
485
|
-
|
|
486
|
-
def __get_temporal_state_variables_title(self,
|
|
487
|
-
linear_system: Optional[bool] = True,
|
|
488
|
-
two_state_spaces: Optional[bool] = False) -> str:
|
|
489
|
-
"""
|
|
490
|
-
Returns the title of a temporal figure to plot
|
|
491
|
-
|
|
492
|
-
Parameters
|
|
493
|
-
----------
|
|
494
|
-
|
|
495
|
-
linear_system: bool, optional
|
|
496
|
-
Whether the system is linear
|
|
497
|
-
two_state_spaces: bool, optional
|
|
498
|
-
Whether the plot is of two state spaces
|
|
499
|
-
|
|
500
|
-
Returns
|
|
501
|
-
-------
|
|
502
|
-
|
|
503
|
-
title : str
|
|
504
|
-
The title of the figure
|
|
505
|
-
"""
|
|
506
|
-
|
|
507
|
-
if two_state_spaces:
|
|
508
|
-
title = f"{self._N}-Player Game"
|
|
509
|
-
else:
|
|
510
|
-
title = f"{('' if linear_system else 'Nonlinear, ')}" \
|
|
511
|
-
f"{('Continuous' if self.__continuous else 'Discrete')}, " \
|
|
512
|
-
f"{('Infinite' if self.__infinite_horizon else 'Finite')} Horizon" \
|
|
513
|
-
f"{(f', {self._N}-Player Game' if self._N > 1 else ' LQR')}" \
|
|
514
|
-
f"{f', {self._L} Sampling Points'}"
|
|
515
|
-
|
|
516
|
-
return title
|
|
517
|
-
|
|
518
|
-
def plot_state_variables(self,
|
|
519
|
-
state_variables: np.array,
|
|
520
|
-
linear_system: Optional[bool] = True,
|
|
521
|
-
output_variables_names: Optional[Sequence[str]] = None, ):
|
|
522
|
-
"""
|
|
523
|
-
Displays plots for the state variables with respect to time
|
|
524
|
-
|
|
525
|
-
Parameters
|
|
526
|
-
----------
|
|
527
|
-
|
|
528
|
-
state_variables: np.array of shape(L, n)
|
|
529
|
-
The temporal state variables y-axis information to plot
|
|
530
|
-
linear_system: bool, optional
|
|
531
|
-
Indicates whether the system is linear or not
|
|
532
|
-
output_variables_names: optional
|
|
533
|
-
Names of the output variables
|
|
534
|
-
"""
|
|
535
|
-
|
|
536
|
-
self.__plot_temporal_variables(t=self._forward_time,
|
|
537
|
-
temporal_variables=state_variables,
|
|
538
|
-
is_P=False,
|
|
539
|
-
# title=self.__get_temporal_state_variables_title(linear_system=linear_system),
|
|
540
|
-
output_variables_names=output_variables_names)
|
|
541
|
-
|
|
542
|
-
def __plot_x(self):
|
|
543
|
-
"""
|
|
544
|
-
Plots the state vector variables wth respect to time
|
|
545
|
-
"""
|
|
546
|
-
|
|
547
|
-
self.plot_state_variables(state_variables=self._x)
|
|
548
|
-
|
|
549
|
-
def plot_y(self,
|
|
550
|
-
C: np.array,
|
|
551
|
-
output_variables_names: Optional[Sequence[str]] = None,
|
|
552
|
-
save_figure: Optional[bool] = False,
|
|
553
|
-
figure_path: Optional[Union[str, Path]] = _default_figures_filename,
|
|
554
|
-
figure_filename: Optional[str] = _default_figures_filename):
|
|
555
|
-
"""
|
|
556
|
-
Plots an output vector y = C x^T wth respect to time
|
|
557
|
-
|
|
558
|
-
Parameters
|
|
559
|
-
----------
|
|
560
|
-
|
|
561
|
-
C: np.array array of shape(p, n)
|
|
562
|
-
The output coefficients with respect to state
|
|
563
|
-
output_variables_names: Sequence of len(p)
|
|
564
|
-
Names of output variables
|
|
565
|
-
save_figure: bool
|
|
566
|
-
Whether to save the figure or not
|
|
567
|
-
figure_path: str or Path, optional, default = figures sub-folder of the current directory
|
|
568
|
-
The desired saved figure's file path
|
|
569
|
-
figure_filename: str, optional
|
|
570
|
-
The desired saved figure's filename
|
|
571
|
-
"""
|
|
572
|
-
|
|
573
|
-
y = C @ self._x.T
|
|
574
|
-
self.plot_state_variables(state_variables=y.T,
|
|
575
|
-
output_variables_names=output_variables_names)
|
|
576
|
-
if save_figure:
|
|
577
|
-
self.__save_figure(figure_path=figure_path,
|
|
578
|
-
figure_filename=figure_filename)
|
|
579
|
-
|
|
580
|
-
@_post_convergence
|
|
581
|
-
def __save_figure(self,
|
|
582
|
-
figure_path: Optional[Union[str, Path]] = _default_figures_filename,
|
|
583
|
-
figure_filename: Optional[str] = _default_figures_filename):
|
|
584
|
-
"""
|
|
585
|
-
Saves the current figure
|
|
586
|
-
|
|
587
|
-
Parameters
|
|
588
|
-
----------
|
|
589
|
-
|
|
590
|
-
figure_path: str or Path, optional, default = figures sub-folder of the current directory
|
|
591
|
-
The desired saved figure's file path
|
|
592
|
-
figure_filename: str, optional
|
|
593
|
-
The desired saved figure's filename
|
|
594
|
-
"""
|
|
595
|
-
|
|
596
|
-
if self._x_0 is None:
|
|
597
|
-
raise RuntimeError('No parameter x_0 was defined to illustrate a figure')
|
|
598
|
-
|
|
599
|
-
if figure_filename == PyDiffGame._default_figures_filename:
|
|
600
|
-
figure_filename += '0'
|
|
601
|
-
|
|
602
|
-
if not figure_path.is_dir():
|
|
603
|
-
os.mkdir(figure_path)
|
|
604
|
-
|
|
605
|
-
figure_full_filename = Path(fr'{figure_path}/{figure_filename}.png')
|
|
606
|
-
|
|
607
|
-
while figure_full_filename.is_file():
|
|
608
|
-
curr_name = figure_full_filename.name
|
|
609
|
-
curr_num = int(curr_name.split('.png')[0][-1])
|
|
610
|
-
next_num = curr_num + 1
|
|
611
|
-
figure_full_filename = Path(fr'{figure_path}/image{next_num}.png')
|
|
612
|
-
|
|
613
|
-
self._fig.savefig(figure_full_filename)
|
|
614
|
-
|
|
615
|
-
def __augment_two_state_space_vectors(self,
|
|
616
|
-
other: PyDiffGame,
|
|
617
|
-
non_linear: bool = False) -> (np.array, np.array):
|
|
618
|
-
"""
|
|
619
|
-
Plots the state variables of two converged state spaces
|
|
620
|
-
|
|
621
|
-
Parameters
|
|
622
|
-
----------
|
|
623
|
-
|
|
624
|
-
other: PyDiffGame
|
|
625
|
-
Other converged differential game to augment the current one with
|
|
626
|
-
non_linear: bool, optional
|
|
627
|
-
Indicates whether to augment the non-linear states or not
|
|
628
|
-
|
|
629
|
-
Returns
|
|
630
|
-
----------
|
|
631
|
-
|
|
632
|
-
longer_period: np.array of len(max(self.L, other.L))
|
|
633
|
-
The longer time period of the two games
|
|
634
|
-
augmented_state_space: np.array of shape(max(self.L, other.L), self.n + other.n)
|
|
635
|
-
Augmented state space with interpolated and extrapolated values for both games
|
|
636
|
-
"""
|
|
637
|
-
|
|
638
|
-
f_self = sp.interpolate.interp1d(x=self._forward_time,
|
|
639
|
-
y=self._x if not non_linear else self._x_non_linear.T,
|
|
640
|
-
axis=0,
|
|
641
|
-
fill_value="extrapolate")
|
|
642
|
-
|
|
643
|
-
f_other = sp.interpolate.interp1d(x=other.forward_time,
|
|
644
|
-
y=other.x if not non_linear else other._x_non_linear.T,
|
|
645
|
-
axis=0,
|
|
646
|
-
fill_value="extrapolate")
|
|
647
|
-
|
|
648
|
-
longer_period = self._forward_time if max(self._forward_time) > max(other.forward_time) \
|
|
649
|
-
else other.forward_time
|
|
650
|
-
|
|
651
|
-
self_state_space = f_self(longer_period)
|
|
652
|
-
other_state_space = f_other(longer_period)
|
|
653
|
-
|
|
654
|
-
augmented_state_space = np.concatenate((self_state_space, other_state_space), axis=1)
|
|
655
|
-
|
|
656
|
-
return longer_period, augmented_state_space
|
|
657
|
-
|
|
658
|
-
def plot_two_state_spaces(self,
|
|
659
|
-
other: PyDiffGame,
|
|
660
|
-
non_linear: bool = False):
|
|
661
|
-
"""
|
|
662
|
-
Plots the state variables of two converged state spaces
|
|
663
|
-
|
|
664
|
-
Parameters
|
|
665
|
-
----------
|
|
666
|
-
|
|
667
|
-
other: PyDiffGame
|
|
668
|
-
Other converged differential game to plot along with the current one
|
|
669
|
-
non_linear: bool, optional
|
|
670
|
-
Indicates whether to plot the non-linear states or not
|
|
671
|
-
"""
|
|
672
|
-
|
|
673
|
-
longer_period, augmented_state_space = self.__augment_two_state_space_vectors(other=other,
|
|
674
|
-
non_linear=non_linear)
|
|
675
|
-
|
|
676
|
-
self_title = self.__get_temporal_state_variables_title(two_state_spaces=True)
|
|
677
|
-
other_title = other.__get_temporal_state_variables_title(two_state_spaces=True)
|
|
678
|
-
|
|
679
|
-
self.__plot_temporal_variables(t=longer_period,
|
|
680
|
-
temporal_variables=augmented_state_space,
|
|
681
|
-
is_P=False,
|
|
682
|
-
title=f"{self_title} " + "$(T_{f_1} = " +
|
|
683
|
-
f"{self._T_f})$ \nvs\n {other_title} " + "$(T_{f_2} = " + f"{other.T_f})$",
|
|
684
|
-
two_state_spaces=True
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
def __get_are_P_f(self) -> list[np.array]:
|
|
688
|
-
"""
|
|
689
|
-
Solves the uncoupled set of algebraic Riccati equations to use as initial guesses for fsolve and odeint
|
|
690
|
-
|
|
691
|
-
Returns
|
|
692
|
-
----------
|
|
693
|
-
|
|
694
|
-
P_f: list of numpy arrays, of len(N), of shape(n, n), solution of scipy's solve_are
|
|
695
|
-
Final condition for the Riccati equation matrix
|
|
696
|
-
"""
|
|
697
|
-
|
|
698
|
-
are_solver = sp.linalg.solve_continuous_are if self.__continuous else sp.linalg.solve_discrete_are
|
|
699
|
-
P_f = []
|
|
700
|
-
|
|
701
|
-
for B_i, Q_i, R_ii in zip(self._Bs, self._Qs, self._Rs):
|
|
702
|
-
try:
|
|
703
|
-
P_f_i = are_solver(self._A, B_i, Q_i, R_ii)
|
|
704
|
-
except (np.linalg.LinAlgError, ValueError):
|
|
705
|
-
P_f_i = Q_i
|
|
706
|
-
|
|
707
|
-
P_f += [P_f_i]
|
|
708
|
-
|
|
709
|
-
return P_f
|
|
710
|
-
|
|
711
|
-
@abstractmethod
|
|
712
|
-
def _update_K_from_last_state(self,
|
|
713
|
-
*args,
|
|
714
|
-
**kwargs):
|
|
715
|
-
"""
|
|
716
|
-
Updates the controllers K after forward propagation of the state through time
|
|
717
|
-
"""
|
|
718
|
-
|
|
719
|
-
pass
|
|
720
|
-
|
|
721
|
-
@abstractmethod
|
|
722
|
-
def _get_K_i(self,
|
|
723
|
-
*args,
|
|
724
|
-
**kwargs) -> np.array:
|
|
725
|
-
"""
|
|
726
|
-
Returns the i'th element of the currently calculated controllers
|
|
727
|
-
|
|
728
|
-
Returns
|
|
729
|
-
----------
|
|
730
|
-
|
|
731
|
-
K_i: numpy array of numpy arrays, of len(N), of shape(n, n), solution of scipy's solve_are
|
|
732
|
-
Final condition for the Riccati equation matrix
|
|
733
|
-
"""
|
|
734
|
-
|
|
735
|
-
pass
|
|
736
|
-
|
|
737
|
-
@abstractmethod
|
|
738
|
-
def _get_P_f_i(self,
|
|
739
|
-
i: int) -> np.array:
|
|
740
|
-
"""
|
|
741
|
-
Returns the i'th element of the final condition matrices P_f
|
|
742
|
-
|
|
743
|
-
Parameters
|
|
744
|
-
----------
|
|
745
|
-
|
|
746
|
-
i: int
|
|
747
|
-
The required index
|
|
748
|
-
|
|
749
|
-
Returns
|
|
750
|
-
----------
|
|
751
|
-
|
|
752
|
-
P_f_i: numpy array of shape(n, n)
|
|
753
|
-
i'th element of the final condition matrices P_f
|
|
754
|
-
"""
|
|
755
|
-
|
|
756
|
-
pass
|
|
757
|
-
|
|
758
|
-
def _update_A_cl_from_last_state(self,
|
|
759
|
-
k: Optional[int] = None):
|
|
760
|
-
"""
|
|
761
|
-
Updates the closed-loop dynamics with the updated controllers based on the relation:
|
|
762
|
-
A_cl = A - sum_{i=1}^N B_i K_i
|
|
763
|
-
|
|
764
|
-
Parameters
|
|
765
|
-
----------
|
|
766
|
-
|
|
767
|
-
k: int, optional
|
|
768
|
-
The current k'th sample index, in case the controller is time-dependant.
|
|
769
|
-
In this case the update rule is:
|
|
770
|
-
A_cl[k] = A - sum_{i=1}^N B_i K_i[k]
|
|
771
|
-
"""
|
|
772
|
-
|
|
773
|
-
A_cl = self._A - sum([self._Bs[i] @ (self._get_K_i(i, k) if k is not None else self._get_K_i(i))
|
|
774
|
-
for i in range(self._N)])
|
|
775
|
-
if k is not None:
|
|
776
|
-
self._A_cl[k] = A_cl
|
|
777
|
-
else:
|
|
778
|
-
self._A_cl = A_cl
|
|
779
|
-
|
|
780
|
-
@abstractmethod
|
|
781
|
-
def _update_Ps_from_last_state(self,
|
|
782
|
-
*args,
|
|
783
|
-
**kwargs):
|
|
784
|
-
"""
|
|
785
|
-
Updates the matrices {P_i}_{i=1}^N after forward propagation of the state through time
|
|
786
|
-
"""
|
|
787
|
-
|
|
788
|
-
pass
|
|
789
|
-
|
|
790
|
-
@abstractmethod
|
|
791
|
-
def is_A_cl_stable(self,
|
|
792
|
-
*args,
|
|
793
|
-
**kwargs) -> bool:
|
|
794
|
-
"""
|
|
795
|
-
Tests Lyapunov stability of the closed loop
|
|
796
|
-
|
|
797
|
-
Returns
|
|
798
|
-
----------
|
|
799
|
-
|
|
800
|
-
is_stable: bool
|
|
801
|
-
Indicates whether the system has Lyapunov stability or not
|
|
802
|
-
"""
|
|
803
|
-
|
|
804
|
-
pass
|
|
805
|
-
|
|
806
|
-
@abstractmethod
|
|
807
|
-
def _solve_finite_horizon(self):
|
|
808
|
-
"""
|
|
809
|
-
Solves for the finite horizon case
|
|
810
|
-
"""
|
|
811
|
-
|
|
812
|
-
pass
|
|
813
|
-
|
|
814
|
-
@_post_convergence
|
|
815
|
-
def __plot_finite_horizon_convergence(self):
|
|
816
|
-
"""
|
|
817
|
-
Plots the convergence of the values for the matrices P_i
|
|
818
|
-
"""
|
|
819
|
-
|
|
820
|
-
self.__plot_temporal_variables(t=self._backward_time,
|
|
821
|
-
temporal_variables=self._P,
|
|
822
|
-
is_P=True)
|
|
823
|
-
|
|
824
|
-
def __solve_and_plot_finite_horizon(self):
|
|
825
|
-
"""
|
|
826
|
-
Solves for the finite horizon case and plots the convergence of the values for the matrices P_i
|
|
827
|
-
"""
|
|
828
|
-
|
|
829
|
-
self._solve_finite_horizon()
|
|
830
|
-
self.__plot_finite_horizon_convergence()
|
|
831
|
-
|
|
832
|
-
@abstractmethod
|
|
833
|
-
def _solve_infinite_horizon(self):
|
|
834
|
-
"""
|
|
835
|
-
Solves for the infinite horizon case using the finite horizon case
|
|
836
|
-
"""
|
|
837
|
-
|
|
838
|
-
pass
|
|
839
|
-
|
|
840
|
-
@abstractmethod
|
|
841
|
-
@_post_convergence
|
|
842
|
-
def _solve_state_space(self):
|
|
843
|
-
"""
|
|
844
|
-
Propagates the game through time and solves for it
|
|
845
|
-
"""
|
|
846
|
-
|
|
847
|
-
pass
|
|
848
|
-
|
|
849
|
-
def __solve_game(self):
|
|
850
|
-
if self.__infinite_horizon:
|
|
851
|
-
self._solve_infinite_horizon()
|
|
852
|
-
else:
|
|
853
|
-
self.__solve_and_plot_finite_horizon()
|
|
854
|
-
|
|
855
|
-
self._converged = True
|
|
856
|
-
|
|
857
|
-
def __solve_game_and_simulate_state_space(self):
|
|
858
|
-
"""
|
|
859
|
-
Propagates the game through time, solves for it and plots the state with respect to time
|
|
860
|
-
"""
|
|
861
|
-
|
|
862
|
-
self.__solve_game()
|
|
863
|
-
|
|
864
|
-
if self._x_0 is not None:
|
|
865
|
-
self._solve_state_space()
|
|
866
|
-
|
|
867
|
-
def __solve_game_simulate_state_space_and_plot(self,
|
|
868
|
-
plot_Mx: Optional[bool] = False,
|
|
869
|
-
M: Optional[np.array] = None,
|
|
870
|
-
output_variables_names: Optional[Sequence[str]] = None):
|
|
871
|
-
"""
|
|
872
|
-
Propagates the game through time, solves for it and plots the state with respect to time
|
|
873
|
-
"""
|
|
874
|
-
|
|
875
|
-
self.__solve_game_and_simulate_state_space()
|
|
876
|
-
|
|
877
|
-
if self._x_0 is not None:
|
|
878
|
-
self.__plot_x()
|
|
879
|
-
|
|
880
|
-
if plot_Mx and self._n % self._m == 0:
|
|
881
|
-
C = np.kron(a=np.eye(int(self._n / self._m)),
|
|
882
|
-
b=M)
|
|
883
|
-
self.plot_y(C=C,
|
|
884
|
-
output_variables_names=output_variables_names)
|
|
885
|
-
|
|
886
|
-
def _simulate_curr_x_T(self) -> np.array:
|
|
887
|
-
"""
|
|
888
|
-
Evaluates the current value of the state space vector at the terminal time T_f
|
|
889
|
-
|
|
890
|
-
Returns
|
|
891
|
-
----------
|
|
892
|
-
|
|
893
|
-
x_T_f: numpy array of shape(n)
|
|
894
|
-
Evaluated terminal state vector x(T_f)
|
|
895
|
-
"""
|
|
896
|
-
|
|
897
|
-
pass
|
|
898
|
-
|
|
899
|
-
@_post_convergence
|
|
900
|
-
def __get_x_l_tilde(self,
|
|
901
|
-
x: np.array,
|
|
902
|
-
l: int) -> np.array:
|
|
903
|
-
"""
|
|
904
|
-
Returns the state difference at the lth segment
|
|
905
|
-
|
|
906
|
-
Parameters
|
|
907
|
-
----------
|
|
908
|
-
|
|
909
|
-
x: numpy array of shape(n)
|
|
910
|
-
The solved state vector to consider
|
|
911
|
-
l: int
|
|
912
|
-
The desired segment
|
|
913
|
-
|
|
914
|
-
Returns
|
|
915
|
-
----------
|
|
916
|
-
|
|
917
|
-
x_l_tilde: numpy array of shape(n)
|
|
918
|
-
State difference at the lth segment
|
|
919
|
-
"""
|
|
920
|
-
|
|
921
|
-
x_l = x[l]
|
|
922
|
-
x_l_tilde = self.x_T - x_l
|
|
923
|
-
|
|
924
|
-
return x_l_tilde
|
|
925
|
-
|
|
926
|
-
@_post_convergence
|
|
927
|
-
def __get_v_i_l_tilde(self,
|
|
928
|
-
x: np.array,
|
|
929
|
-
i: int,
|
|
930
|
-
l: int,
|
|
931
|
-
v_i_T: np.array) -> np.array:
|
|
932
|
-
"""
|
|
933
|
-
Returns the ith virtual input difference at the lth segment
|
|
934
|
-
|
|
935
|
-
Parameters
|
|
936
|
-
----------
|
|
937
|
-
|
|
938
|
-
x: numpy array of shape(n)
|
|
939
|
-
The solved state vector to consider
|
|
940
|
-
i: int
|
|
941
|
-
The desired input index
|
|
942
|
-
l: int
|
|
943
|
-
The desired segment
|
|
944
|
-
v_i_T: numpy array of shape(m_i)
|
|
945
|
-
The terminal value of the ith input
|
|
946
|
-
|
|
947
|
-
Returns
|
|
948
|
-
----------
|
|
949
|
-
|
|
950
|
-
v_i_l_tilde: numpy array of shape(m_i)
|
|
951
|
-
ith virtual input difference at the lth segment
|
|
952
|
-
"""
|
|
953
|
-
|
|
954
|
-
x_l = x[l]
|
|
955
|
-
|
|
956
|
-
K_i_l = self._get_K_i(i) if self.__continuous else self._get_K_i(i, l)
|
|
957
|
-
v_i_l = - K_i_l @ x_l
|
|
958
|
-
v_i_l_tilde = v_i_T - v_i_l
|
|
959
|
-
|
|
960
|
-
return v_i_l_tilde
|
|
961
|
-
|
|
962
|
-
@_post_convergence
|
|
963
|
-
def __get_u_l_tilde(self,
|
|
964
|
-
x: np.array,
|
|
965
|
-
l: int,
|
|
966
|
-
x_l_tilde: np.array) -> np.array:
|
|
967
|
-
"""
|
|
968
|
-
Returns the actual input difference at the lth segment
|
|
969
|
-
|
|
970
|
-
Parameters
|
|
971
|
-
----------
|
|
972
|
-
|
|
973
|
-
x: numpy array of shape(n)
|
|
974
|
-
The solved state vector to consider
|
|
975
|
-
l: int
|
|
976
|
-
The desired segment
|
|
977
|
-
x_l_tilde: numpy array of shape(n)
|
|
978
|
-
The state difference at the lth segment
|
|
979
|
-
|
|
980
|
-
Returns
|
|
981
|
-
----------
|
|
982
|
-
|
|
983
|
-
u_l_tilde: numpy array of shape(m)
|
|
984
|
-
Actual input difference at the lth segment
|
|
985
|
-
"""
|
|
986
|
-
|
|
987
|
-
if self.is_LQR():
|
|
988
|
-
k_T = self._get_K_i(0) if self.__continuous else self._get_K_i(0, self._L - 1)
|
|
989
|
-
u_l_tilde = - k_T @ x_l_tilde
|
|
990
|
-
else:
|
|
991
|
-
v_l_tilde = []
|
|
992
|
-
|
|
993
|
-
for i in range(self._N):
|
|
994
|
-
k_i_T = self._get_K_i(i) if self.__continuous else self._get_K_i(i, self._L - 1)
|
|
995
|
-
v_i_T = - k_i_T @ self.x_T
|
|
996
|
-
|
|
997
|
-
v_i_l_tilde = self.__get_v_i_l_tilde(x=x,
|
|
998
|
-
i=i,
|
|
999
|
-
l=l,
|
|
1000
|
-
v_i_T=v_i_T)
|
|
1001
|
-
|
|
1002
|
-
v_l_tilde += [v_i_l_tilde]
|
|
1003
|
-
|
|
1004
|
-
v_l_tilde = np.array(v_l_tilde)
|
|
1005
|
-
u_l_tilde = self._M_inv @ v_l_tilde
|
|
1006
|
-
|
|
1007
|
-
return u_l_tilde
|
|
1008
|
-
|
|
1009
|
-
@_post_convergence
|
|
1010
|
-
def __get_x_and_u_l_tilde(self,
|
|
1011
|
-
x: np.array,
|
|
1012
|
-
l: int) -> (np.array, np.array):
|
|
1013
|
-
"""
|
|
1014
|
-
Returns the state and actual input difference at the lth segment
|
|
1015
|
-
|
|
1016
|
-
Parameters
|
|
1017
|
-
----------
|
|
1018
|
-
|
|
1019
|
-
x: numpy array of shape(n)
|
|
1020
|
-
The solved state vector to consider
|
|
1021
|
-
l: int
|
|
1022
|
-
The desired segment
|
|
1023
|
-
|
|
1024
|
-
Returns
|
|
1025
|
-
----------
|
|
1026
|
-
|
|
1027
|
-
x_l_tilde: numpy array of shape(n)
|
|
1028
|
-
State difference at the lth segment
|
|
1029
|
-
u_l_tilde: numpy array of shape(m)
|
|
1030
|
-
Actual input difference at the lth segment
|
|
1031
|
-
"""
|
|
1032
|
-
|
|
1033
|
-
x_l_tilde = self.__get_x_l_tilde(x=x,
|
|
1034
|
-
l=l)
|
|
1035
|
-
u_l_tilde = self.__get_u_l_tilde(x=x,
|
|
1036
|
-
l=l,
|
|
1037
|
-
x_l_tilde=x_l_tilde)
|
|
1038
|
-
|
|
1039
|
-
return x_l_tilde, u_l_tilde
|
|
1040
|
-
|
|
1041
|
-
def __get_x(self,
|
|
1042
|
-
non_linear: bool) -> np.array:
|
|
1043
|
-
"""
|
|
1044
|
-
Returns the desired state vector to consider
|
|
1045
|
-
|
|
1046
|
-
Parameters
|
|
1047
|
-
----------
|
|
1048
|
-
|
|
1049
|
-
non_linear: bool
|
|
1050
|
-
Whether to return the non-linear state
|
|
1051
|
-
|
|
1052
|
-
Returns
|
|
1053
|
-
----------
|
|
1054
|
-
|
|
1055
|
-
x: numpy array of shape(n)
|
|
1056
|
-
Desired state vector
|
|
1057
|
-
"""
|
|
1058
|
-
|
|
1059
|
-
x = self._x if not non_linear else self._x_non_linear.T
|
|
1060
|
-
assert len(x) == self._L
|
|
1061
|
-
|
|
1062
|
-
return x
|
|
1063
|
-
|
|
1064
|
-
@_post_convergence
|
|
1065
|
-
def get_cost(self,
|
|
1066
|
-
lqr_objective: Objective,
|
|
1067
|
-
non_linear: Optional[bool] = False,
|
|
1068
|
-
x_only: Optional[bool] = False) -> float:
|
|
1069
|
-
"""
|
|
1070
|
-
Calculates the cost functionals values using the formula:
|
|
1071
|
-
J = int_{t=0}^T_f J(t) dt =
|
|
1072
|
-
int_{t=0}^T_f [ x_tilde(t)^T Q x_tilde(t) + u_tilde^T(t) R u_tilde(t) ) ] dt
|
|
1073
|
-
|
|
1074
|
-
and the corresponding Trapezoidal rule approximation:
|
|
1075
|
-
J ~ delta / 2 sum_{l=1}^{L} [ J(l) + J(l - 1) ]
|
|
1076
|
-
= delta * [ sum_{l=1}^{L-1} J(l) + 1/2 ( J(0) + J(L) ) ]
|
|
1077
|
-
|
|
1078
|
-
Parameters
|
|
1079
|
-
----------
|
|
1080
|
-
|
|
1081
|
-
lqr_objective: LQRObjective
|
|
1082
|
-
LQR objective to get Q and R from
|
|
1083
|
-
non_linear: bool, optional
|
|
1084
|
-
Indicates whether to calculate the cost with respect to the nonlinear state
|
|
1085
|
-
x_only: bool, optional
|
|
1086
|
-
Indicates whether to calculate the cost only with respect to x(t)
|
|
1087
|
-
|
|
1088
|
-
Returns
|
|
1089
|
-
----------
|
|
1090
|
-
|
|
1091
|
-
log_cost: float
|
|
1092
|
-
game cost
|
|
1093
|
-
"""
|
|
1094
|
-
|
|
1095
|
-
x = self.__get_x(non_linear=non_linear)
|
|
1096
|
-
|
|
1097
|
-
cost = 0
|
|
1098
|
-
Q = lqr_objective.Q
|
|
1099
|
-
R = lqr_objective.R
|
|
1100
|
-
|
|
1101
|
-
for l in range(0, self._L):
|
|
1102
|
-
x_l_tilde = self.__get_x_l_tilde(x=x,
|
|
1103
|
-
l=l)
|
|
1104
|
-
cost_l = float(x_l_tilde.T @ Q @ x_l_tilde)
|
|
1105
|
-
|
|
1106
|
-
if not x_only:
|
|
1107
|
-
u_l_tilde = self.__get_u_l_tilde(x=x,
|
|
1108
|
-
l=l,
|
|
1109
|
-
x_l_tilde=x_l_tilde)
|
|
1110
|
-
cost_l += float(u_l_tilde.T @ R @ u_l_tilde)
|
|
1111
|
-
|
|
1112
|
-
if l in [0, self._L - 1]:
|
|
1113
|
-
cost_l *= 0.5
|
|
1114
|
-
|
|
1115
|
-
cost += cost_l
|
|
1116
|
-
|
|
1117
|
-
return cost
|
|
1118
|
-
|
|
1119
|
-
@_post_convergence
|
|
1120
|
-
def get_agnostic_costs(self,
|
|
1121
|
-
non_linear: Optional[bool] = False) -> float:
|
|
1122
|
-
"""
|
|
1123
|
-
Calculates the agnostic functional value using the formula:
|
|
1124
|
-
J_agnostic = int_{t=0}^T_f J_agnostic(t) dt =
|
|
1125
|
-
int_{t=0}^T_f [ ||x_tilde(t)||^2 + ||u_tilde(t)||^2 ] dt =
|
|
1126
|
-
int_{t=0}^T_f [ x_tilde(t)^T x_tilde(t) + u_tilde(t)^T u_tilde(t) ] dt
|
|
1127
|
-
|
|
1128
|
-
and the corresponding Trapezoidal rule approximation:
|
|
1129
|
-
J ~ delta / 2 sum_{l=1}^L [ J(l) + J(l - 1) ] =
|
|
1130
|
-
delta * [ sum_{l=1}^{L-1} J(l) + 1/2 ( J(0) + J(L) ) ]
|
|
1131
|
-
|
|
1132
|
-
Parameters
|
|
1133
|
-
----------
|
|
1134
|
-
|
|
1135
|
-
non_linear: bool, optional
|
|
1136
|
-
Indicates whether to calculate the cost with respect to the nonlinear state
|
|
1137
|
-
|
|
1138
|
-
Returns
|
|
1139
|
-
----------
|
|
1140
|
-
|
|
1141
|
-
log_cost: float
|
|
1142
|
-
agnostic game cost
|
|
1143
|
-
"""
|
|
1144
|
-
|
|
1145
|
-
x = self.__get_x(non_linear=non_linear)
|
|
1146
|
-
|
|
1147
|
-
cost = sum((0.5 if l in [0, self._L - 1] else 1) * sum(y_l.T @ y_l
|
|
1148
|
-
for y_l in self.__get_x_and_u_l_tilde(x=x,
|
|
1149
|
-
l=l))
|
|
1150
|
-
for l in range(0, self._L)) * self._delta
|
|
1151
|
-
|
|
1152
|
-
return cost
|
|
1153
|
-
|
|
1154
|
-
def __call__(self,
|
|
1155
|
-
plot_state_space: Optional[bool] = True,
|
|
1156
|
-
plot_Mx: Optional[bool] = False,
|
|
1157
|
-
M: Optional[np.array] = None,
|
|
1158
|
-
output_variables_names: Optional[Sequence[str]] = None,
|
|
1159
|
-
save_figure: Optional[bool] = False,
|
|
1160
|
-
figure_path: Optional[Union[str, Path]] = _default_figures_path,
|
|
1161
|
-
figure_filename: Optional[str] = _default_figures_filename,
|
|
1162
|
-
print_characteristic_polynomials: Optional[bool] = False,
|
|
1163
|
-
print_eigenvalues: Optional[bool] = False):
|
|
1164
|
-
"""
|
|
1165
|
-
Runs the game simulation
|
|
1166
|
-
|
|
1167
|
-
Parameters
|
|
1168
|
-
----------
|
|
1169
|
-
|
|
1170
|
-
plot_state_space: bool, optional
|
|
1171
|
-
Whether to plot the solved game state space
|
|
1172
|
-
save_figure: bool, optional
|
|
1173
|
-
Whether to save the state space plot figure
|
|
1174
|
-
print_characteristic_polynomials: bool, optional
|
|
1175
|
-
Whether to print the characteristic polynomials of the matrices Q_i and R_ii for all 1 <= i <= N
|
|
1176
|
-
print_eigenvalues: bool, optional
|
|
1177
|
-
Whether to print the eigenvalues of the matrices Q_i and R_ii for all 1 <= i <= N
|
|
1178
|
-
|
|
1179
|
-
"""
|
|
1180
|
-
|
|
1181
|
-
if print_characteristic_polynomials:
|
|
1182
|
-
self.__print_characteristic_polynomials()
|
|
1183
|
-
if print_eigenvalues:
|
|
1184
|
-
self.__print_eigenvalues()
|
|
1185
|
-
|
|
1186
|
-
if plot_state_space:
|
|
1187
|
-
self.__solve_game_simulate_state_space_and_plot(plot_Mx=plot_Mx,
|
|
1188
|
-
M=M,
|
|
1189
|
-
output_variables_names=output_variables_names)
|
|
1190
|
-
if save_figure:
|
|
1191
|
-
self.__save_figure(figure_path=figure_path,
|
|
1192
|
-
figure_filename=figure_filename)
|
|
1193
|
-
else:
|
|
1194
|
-
self.__solve_game_and_simulate_state_space()
|
|
1195
|
-
|
|
1196
|
-
def __getitem__(self,
|
|
1197
|
-
i: int) -> Objective:
|
|
1198
|
-
"""
|
|
1199
|
-
Returns the i'th objective of the game
|
|
1200
|
-
|
|
1201
|
-
Parameters
|
|
1202
|
-
----------
|
|
1203
|
-
|
|
1204
|
-
i: int
|
|
1205
|
-
The objective index to consider
|
|
1206
|
-
|
|
1207
|
-
Returns
|
|
1208
|
-
----------
|
|
1209
|
-
|
|
1210
|
-
o_i: Objective
|
|
1211
|
-
i'th objective of the game
|
|
1212
|
-
"""
|
|
1213
|
-
|
|
1214
|
-
return self.__objectives[i]
|
|
1215
|
-
|
|
1216
|
-
def __len__(self) -> int:
|
|
1217
|
-
"""
|
|
1218
|
-
We define the length of a differential game to be its number of objectives (N)
|
|
1219
|
-
|
|
1220
|
-
Returns
|
|
1221
|
-
----------
|
|
1222
|
-
|
|
1223
|
-
len: int
|
|
1224
|
-
The length of the game
|
|
1225
|
-
"""
|
|
1226
|
-
|
|
1227
|
-
return self._N
|
|
1228
|
-
|
|
1229
|
-
@property
|
|
1230
|
-
def P(self) -> list[np.array]:
|
|
1231
|
-
return self._P
|
|
1232
|
-
|
|
1233
|
-
@property
|
|
1234
|
-
def K(self) -> list[np.array]:
|
|
1235
|
-
return self._K
|
|
1236
|
-
|
|
1237
|
-
@property
|
|
1238
|
-
def x_0(self) -> np.array:
|
|
1239
|
-
return self._x_0
|
|
1240
|
-
|
|
1241
|
-
@property
|
|
1242
|
-
def x_T(self) -> np.array:
|
|
1243
|
-
return self._x_T
|
|
1244
|
-
|
|
1245
|
-
@property
|
|
1246
|
-
def x(self) -> np.array:
|
|
1247
|
-
return self._x
|
|
1248
|
-
|
|
1249
|
-
@property
|
|
1250
|
-
def forward_time(self) -> np.array:
|
|
1251
|
-
return self._forward_time
|
|
1252
|
-
|
|
1253
|
-
@property
|
|
1254
|
-
def T_f(self) -> float:
|
|
1255
|
-
return self._T_f
|
|
1256
|
-
|
|
1257
|
-
@property
|
|
1258
|
-
def L(self) -> int:
|
|
1259
|
-
return self._L
|
|
1260
|
-
|
|
1261
|
-
@property
|
|
1262
|
-
def Bs(self) -> Sequence:
|
|
1263
|
-
return self._Bs
|
|
1264
|
-
|
|
1265
|
-
@property
|
|
1266
|
-
def M_inv(self) -> np.array:
|
|
1267
|
-
return self._M_inv
|
|
1268
|
-
|
|
1269
|
-
@property
|
|
1270
|
-
def epsilon(self) -> float:
|
|
1271
|
-
return self.__epsilon_x
|
|
1272
|
-
|
|
1273
|
-
_post_convergence = staticmethod(_post_convergence)
|