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,15 +1,17 @@
|
|
|
1
|
-
PyDiffGame/
|
|
2
|
-
PyDiffGame/
|
|
3
|
-
PyDiffGame/
|
|
4
|
-
PyDiffGame/
|
|
5
|
-
PyDiffGame/
|
|
6
|
-
PyDiffGame/
|
|
7
|
-
PyDiffGame/
|
|
8
|
-
PyDiffGame/
|
|
9
|
-
PyDiffGame/
|
|
10
|
-
PyDiffGame/examples/
|
|
11
|
-
PyDiffGame/examples/
|
|
12
|
-
PyDiffGame/examples/
|
|
1
|
+
PyDiffGame/__init__.py,sha256=qvv_2OcKgr_pXiHeqPBnoYTH_qsggEmKsF7mJ_2wMrE,1657
|
|
2
|
+
PyDiffGame/_typing.py,sha256=HErUHmfzvkWLPWT1YJl_gpFYZctjkPqsnw3TeHuuggs,912
|
|
3
|
+
PyDiffGame/base.py,sha256=hurrmsjCRQ049rHQGDa78JUBkCbLs-ZufDF8eHW5oNk,17287
|
|
4
|
+
PyDiffGame/comparison.py,sha256=lJVhceZE7FqaNUGu8q1mA6GS0dbBwYFF1mHn0hzKuwM,4145
|
|
5
|
+
PyDiffGame/continuous.py,sha256=W0ZhLX5UT9MFdAKridf5jWsApkTo3WxfGw_l_t2reQk,8924
|
|
6
|
+
PyDiffGame/discrete.py,sha256=s62Fhx6H4okfalsopOwW2kp9onnnArHK7tkLnAUd3Zc,8127
|
|
7
|
+
PyDiffGame/lqr.py,sha256=tcmL2wY_PtNon0XxdWehPJRvLdc44UqzJY9fC-ZnaJ0,1057
|
|
8
|
+
PyDiffGame/objective.py,sha256=mnUPEauTZJl5HE3PScXdzDX5W0pm4fU-Jfi7Lvzgz1o,3910
|
|
9
|
+
PyDiffGame/plotting.py,sha256=gvcgd-T0Y5uTcq15lJX3V4l39YqjpA0jFJsP-RzWTR0,2777
|
|
10
|
+
PyDiffGame/examples/InvertedPendulumComparison.py,sha256=6jhtrHariZAblaWxfnrvOwjuGKKfTVhTyxDI5k3jam8,8615
|
|
11
|
+
PyDiffGame/examples/MassesWithSpringsComparison.py,sha256=JKUyaDfxKnK_N3DEUjWBIntijquZ59fEHLfmNPxSWPk,4626
|
|
12
|
+
PyDiffGame/examples/PVTOL.py,sha256=AQz_CFYw6oi9Qc2SXNFx1OIdtQTSDhqNSK02nPCDwuk,6542
|
|
13
|
+
PyDiffGame/examples/PVTOLComparison.py,sha256=6zbV2uVWdGdH3bksCmDKHkrFl0nU3CxslsGH3YsTqPU,3763
|
|
14
|
+
PyDiffGame/examples/QuadRotorControl.py,sha256=C5ihfYsp_6v8mIw1985W2_BCX4TlONDwR1y6iAAZu10,20440
|
|
13
15
|
PyDiffGame/examples/figures/PVTOL0001.png,sha256=hvXu1bRjOEACZ-b3DpHCukzxlqV9ziwg-8sBxgCPV6w,79438
|
|
14
16
|
PyDiffGame/examples/figures/PVTOL001.png,sha256=SThECsj1qoBJkhwGC4vTfM9jX9viJ1XxcfqWxTVrOps,77507
|
|
15
17
|
PyDiffGame/examples/figures/PVTOL01.png,sha256=q_FR9yMxRooXisIVN_ZWzAu3tNAYagvbUU4c94f6NtE,79290
|
|
@@ -31,7 +33,7 @@ PyDiffGame/examples/figures/PVTOL/PVTOL1.png,sha256=FBgKQSal5ZNyBsGz4RaVye0HBBQU
|
|
|
31
33
|
PyDiffGame/examples/figures/PVTOL/PVTOL10.png,sha256=XyoLMz5WKfg3ny2JxVxVd1Mxjz9WQQn7N87-8YDLisY,77894
|
|
32
34
|
PyDiffGame/examples/figures/PVTOL/PVTOL100.png,sha256=wkMdqrn9hSsIdU-Yhiki3X2fQ7dRRkfLyvBCFx5cMWI,75698
|
|
33
35
|
PyDiffGame/examples/figures/PVTOL/PVTOL1000.png,sha256=Hg_hsf9gnc1HqC4JVjATVay7uDlnIqBRkG8lzpcfiZ0,77630
|
|
34
|
-
pydiffgame-0.
|
|
35
|
-
pydiffgame-0.
|
|
36
|
-
pydiffgame-0.
|
|
37
|
-
pydiffgame-0.
|
|
36
|
+
pydiffgame-2.0.0.dist-info/METADATA,sha256=ZIqiy-9etdpsJTmpB_K3dfzs97q1NuLQmSQzG2iVuwE,19783
|
|
37
|
+
pydiffgame-2.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
38
|
+
pydiffgame-2.0.0.dist-info/licenses/LICENSE,sha256=IYfdsBdqL9SmCEWsXUBTYh303LOWQQRCOUOfZI7PWz8,1139
|
|
39
|
+
pydiffgame-2.0.0.dist-info/RECORD,,
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import contextlib
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
import numpy as np
|
|
5
|
-
from scipy.integrate import odeint
|
|
6
|
-
from numpy.linalg import eigvals, inv
|
|
7
|
-
from typing import Sequence, Optional, Union
|
|
8
|
-
|
|
9
|
-
from PyDiffGame.PyDiffGame import PyDiffGame
|
|
10
|
-
from PyDiffGame.Objective import Objective
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ContinuousPyDiffGame(PyDiffGame):
|
|
14
|
-
"""
|
|
15
|
-
Continuous differential game base class
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Considers control design according to the system:
|
|
19
|
-
dx(t)/dt = A x(t) + sum_{j=1}^N B_j v_j(t)
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def __init__(self,
|
|
23
|
-
A: np.array,
|
|
24
|
-
B: Optional[np.array] = None,
|
|
25
|
-
Bs: Optional[Sequence[np.array]] = None,
|
|
26
|
-
Qs: Optional[Sequence[np.array]] = None,
|
|
27
|
-
Rs: Optional[Sequence[np.array]] = None,
|
|
28
|
-
Ms: Optional[Sequence[np.array]] = None,
|
|
29
|
-
objectives: Optional[Sequence[Objective]] = None,
|
|
30
|
-
x_0: Optional[np.array] = None,
|
|
31
|
-
x_T: Optional[np.array] = None,
|
|
32
|
-
T_f: Optional[float] = None,
|
|
33
|
-
P_f: Optional[Union[Sequence[np.array], np.array]] = None,
|
|
34
|
-
show_legend: Optional[bool] = True,
|
|
35
|
-
state_variables_names: Optional[Sequence[str]] = None,
|
|
36
|
-
epsilon_x: Optional[float] = PyDiffGame._epsilon_x_default,
|
|
37
|
-
epsilon_P: Optional[float] = PyDiffGame._epsilon_P_default,
|
|
38
|
-
L: Optional[int] = PyDiffGame._L_default,
|
|
39
|
-
eta: Optional[int] = PyDiffGame._eta_default,
|
|
40
|
-
debug: Optional[bool] = False):
|
|
41
|
-
|
|
42
|
-
super().__init__(A=A,
|
|
43
|
-
B=B,
|
|
44
|
-
Bs=Bs,
|
|
45
|
-
Qs=Qs,
|
|
46
|
-
Rs=Rs,
|
|
47
|
-
Ms=Ms,
|
|
48
|
-
objectives=objectives,
|
|
49
|
-
x_0=x_0,
|
|
50
|
-
x_T=x_T,
|
|
51
|
-
T_f=T_f,
|
|
52
|
-
P_f=P_f,
|
|
53
|
-
show_legend=show_legend,
|
|
54
|
-
state_variables_names=state_variables_names,
|
|
55
|
-
epsilon_x=epsilon_x,
|
|
56
|
-
epsilon_P=epsilon_P,
|
|
57
|
-
L=L,
|
|
58
|
-
eta=eta,
|
|
59
|
-
debug=debug)
|
|
60
|
-
|
|
61
|
-
self.__dx_dt = None
|
|
62
|
-
|
|
63
|
-
def __solve_N_coupled_diff_riccati(_: float, P_t: np.array) -> np.array:
|
|
64
|
-
"""
|
|
65
|
-
odeint Coupled Differential Matrix Riccati Equations Solver function
|
|
66
|
-
|
|
67
|
-
Parameters
|
|
68
|
-
----------
|
|
69
|
-
_: float
|
|
70
|
-
Integration point in time
|
|
71
|
-
P_t: numpy array of numpy 2-d arrays, of len(N), of shape(n, n)
|
|
72
|
-
Current integrated solution
|
|
73
|
-
|
|
74
|
-
Returns
|
|
75
|
-
----------
|
|
76
|
-
dP_tdt: numpy array of numpy 2-d arrays, of len(N), of shape(n, n)
|
|
77
|
-
Current calculated value for the time-derivative of the matrices P_t
|
|
78
|
-
"""
|
|
79
|
-
P_t = [(P_t[i * self._P_size:(i + 1) * self._P_size]).reshape(self._n, self._n) for i in range(self._N)]
|
|
80
|
-
dP_tdt = np.zeros((self._P_size,))
|
|
81
|
-
A_cl_t = self._A - sum([(B_j @ inv(R_j) if R_j.ndim > 1 else B_j / R_j) @ B_j.T @ P_t_j
|
|
82
|
-
for B_j, R_j, P_t_j in zip(self._Bs, self._Rs, P_t)])
|
|
83
|
-
|
|
84
|
-
for i in range(self._N):
|
|
85
|
-
P_t_i = P_t[i]
|
|
86
|
-
B_i = self._Bs[i]
|
|
87
|
-
R_ii = self._Rs[i]
|
|
88
|
-
Q_i = self._Qs[i]
|
|
89
|
-
|
|
90
|
-
dP_t_idt = - A_cl_t.T @ P_t_i - P_t_i @ A_cl_t - Q_i - P_t_i @ (B_i @ inv(R_ii)
|
|
91
|
-
if R_ii.ndim > 1
|
|
92
|
-
else B_i / R_ii) @ B_i.T @ P_t_i
|
|
93
|
-
|
|
94
|
-
dP_t_idt = dP_t_idt.reshape(self._P_size)
|
|
95
|
-
dP_tdt = dP_t_idt if not i else np.concatenate((dP_tdt, dP_t_idt), axis=0)
|
|
96
|
-
|
|
97
|
-
dP_tdt = np.ravel(dP_tdt)
|
|
98
|
-
|
|
99
|
-
return dP_tdt
|
|
100
|
-
|
|
101
|
-
self._ode_func = __solve_N_coupled_diff_riccati
|
|
102
|
-
|
|
103
|
-
def _update_K_from_last_state(self, t: int):
|
|
104
|
-
"""
|
|
105
|
-
After the matrices P_i are obtained, this method updates the controllers at time t by the rule:
|
|
106
|
-
K_i(t) = R^{-1}_ii B^T_i P_i(t)
|
|
107
|
-
|
|
108
|
-
Parameters
|
|
109
|
-
----------
|
|
110
|
-
t: int
|
|
111
|
-
Current point in time
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
P_t = [(self._P[t][i * self._P_size:(i + 1) * self._P_size]).reshape(self._n, self._n) for i in range(self._N)]
|
|
115
|
-
self._K = [(inv(R_ii) @ B_i.T if R_ii.ndim > 1 else 1 / R_ii * B_i.T) @ P_i
|
|
116
|
-
for R_ii, B_i, P_i in zip(self._Rs, self._Bs, P_t)]
|
|
117
|
-
|
|
118
|
-
def _get_K_i(self, i: int) -> np.array:
|
|
119
|
-
return self._K[i]
|
|
120
|
-
|
|
121
|
-
def _get_P_f_i(self, i: int) -> np.array:
|
|
122
|
-
if len(self._P_f) == self._N:
|
|
123
|
-
return self._P_f[i]
|
|
124
|
-
|
|
125
|
-
return self._P_f[i * self._P_size:(i + 1) * self._P_size].reshape(self._n, self._n)
|
|
126
|
-
|
|
127
|
-
def _update_Ps_from_last_state(self):
|
|
128
|
-
"""
|
|
129
|
-
With P_f as the terminal condition, evaluates the matrices P_i by backwards-solving this set of
|
|
130
|
-
coupled time-continuous Riccati differential equations:
|
|
131
|
-
|
|
132
|
-
dP_idt = - A^T P_i(t) - P_i(t) A - Q_i + P_i(t) sum_{j=1}^N B_j R^{-1}_{jj} B^T_j P_j(t) +
|
|
133
|
-
[sum_{j=1, j!=i}^N P_j(t) B_j R^{-1}_{jj} B^T_j] P_i(t)
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
with stdout_redirected():
|
|
137
|
-
self._P = odeint(func=self._ode_func,
|
|
138
|
-
y0=np.ravel(self._P_f),
|
|
139
|
-
t=self._backward_time,
|
|
140
|
-
tfirst=True)
|
|
141
|
-
|
|
142
|
-
def _solve_finite_horizon(self):
|
|
143
|
-
"""
|
|
144
|
-
Considers the following set of finite-horizon cost functions:
|
|
145
|
-
J_i = x(T_f)^T F_i(T_f) x(T_f) + int_{t=0}^T_f [x(t)^T Q_i x(t) + sum_{j=1}^N u_j(t)^T R_{ij} u_j(t)] dt
|
|
146
|
-
|
|
147
|
-
In the continuous-time case, the matrices P_i can be solved for, unrelated to the controllers K_i.
|
|
148
|
-
Then when simulating the state space progression, the controllers K_i can be computed as functions of P_i
|
|
149
|
-
for each time interval
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
self._update_Ps_from_last_state()
|
|
153
|
-
self._converged = True
|
|
154
|
-
|
|
155
|
-
def _solve_infinite_horizon(self):
|
|
156
|
-
"""
|
|
157
|
-
Considers the following finite-horizon cost functions:
|
|
158
|
-
J_i = int_{t=0}^infty [x(t)^T Q_i x(t) + sum_{j=1}^N u_j(t)^T R_{ij} u_j(t)] dt
|
|
159
|
-
"""
|
|
160
|
-
|
|
161
|
-
self._converge_DREs_to_AREs()
|
|
162
|
-
|
|
163
|
-
def is_A_cl_stable(self) -> bool:
|
|
164
|
-
"""
|
|
165
|
-
Tests Lyapunov stability of the closed loop:
|
|
166
|
-
A continuous dynamic system governed by the matrix A_cl has Lyapunov stability iff:
|
|
167
|
-
Re(eig) <= 0 forall eigenvalues of A_cl and there is at most one real eigenvalue at the origin
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
closed_loop_eigenvalues = eigvals(self._A_cl)
|
|
171
|
-
closed_loop_eigenvalues_real_values = [eig.real for eig in closed_loop_eigenvalues]
|
|
172
|
-
num_of_zeros = [int(v) for v in closed_loop_eigenvalues_real_values].count(0)
|
|
173
|
-
non_positive_eigenvalues = all([eig_real_value <= 0 for eig_real_value in closed_loop_eigenvalues_real_values])
|
|
174
|
-
at_most_one_zero_eigenvalue = num_of_zeros <= 1
|
|
175
|
-
stability = non_positive_eigenvalues and at_most_one_zero_eigenvalue
|
|
176
|
-
|
|
177
|
-
return stability
|
|
178
|
-
|
|
179
|
-
@PyDiffGame._post_convergence
|
|
180
|
-
def _solve_state_space(self):
|
|
181
|
-
"""
|
|
182
|
-
Propagates the game forward through time and solves for it by solving the continuous differential equation:
|
|
183
|
-
dx(t)/dt = A_cl(t) x(t)
|
|
184
|
-
In each step, the controllers K_i are calculated with the values of P_i evaluated beforehand.
|
|
185
|
-
"""
|
|
186
|
-
|
|
187
|
-
t = 0
|
|
188
|
-
|
|
189
|
-
def state_diff_eqn(x_t: np.array,
|
|
190
|
-
_: float) -> np.array:
|
|
191
|
-
"""
|
|
192
|
-
Scipy's odeint State Variables Solver function
|
|
193
|
-
|
|
194
|
-
Parameters
|
|
195
|
-
----------
|
|
196
|
-
x_t: numpy 1-d array of shape(n)
|
|
197
|
-
Current integrated state variables
|
|
198
|
-
_: float
|
|
199
|
-
Current time
|
|
200
|
-
|
|
201
|
-
Returns
|
|
202
|
-
----------
|
|
203
|
-
dx_t_dt: numpy 1-d array of shape(n)
|
|
204
|
-
Current calculated value for the time-derivative of the state variable vector x_t
|
|
205
|
-
"""
|
|
206
|
-
|
|
207
|
-
nonlocal t
|
|
208
|
-
|
|
209
|
-
self._update_K_from_last_state(t=t)
|
|
210
|
-
self._update_A_cl_from_last_state()
|
|
211
|
-
dx_t_dt = self._A_cl @ ((x_t - self._x_T) if self._x_T is not None else x_t)
|
|
212
|
-
dx_t_dt = dx_t_dt.ravel()
|
|
213
|
-
|
|
214
|
-
if t < self._L - 1:
|
|
215
|
-
t += 1
|
|
216
|
-
|
|
217
|
-
return dx_t_dt
|
|
218
|
-
|
|
219
|
-
with stdout_redirected():
|
|
220
|
-
self._x = odeint(func=state_diff_eqn,
|
|
221
|
-
y0=self._x_0,
|
|
222
|
-
t=self._forward_time)
|
|
223
|
-
|
|
224
|
-
def _simulate_curr_x_T(self) -> np.array:
|
|
225
|
-
|
|
226
|
-
def state_diff_eqn(x_t: np.array,
|
|
227
|
-
_: float) -> np.array:
|
|
228
|
-
self._update_K_from_last_state(t=-1)
|
|
229
|
-
self._update_A_cl_from_last_state()
|
|
230
|
-
dx_t_dt = self._A_cl @ ((x_t - self._x_T) if self._x_T is not None else x_t)
|
|
231
|
-
|
|
232
|
-
return dx_t_dt.ravel()
|
|
233
|
-
|
|
234
|
-
with stdout_redirected():
|
|
235
|
-
simulated_x_T = odeint(func=state_diff_eqn,
|
|
236
|
-
y0=self._x_0,
|
|
237
|
-
t=self._forward_time)[-1]
|
|
238
|
-
|
|
239
|
-
return simulated_x_T
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def fileno(file_or_fd):
|
|
243
|
-
fd = getattr(file_or_fd, 'fileno', lambda: file_or_fd)()
|
|
244
|
-
if not isinstance(fd, int):
|
|
245
|
-
raise ValueError("Expected a file (`.fileno()`) or a file descriptor")
|
|
246
|
-
|
|
247
|
-
return fd
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
@contextlib.contextmanager
|
|
251
|
-
def stdout_redirected(to=os.devnull,
|
|
252
|
-
stdout=None):
|
|
253
|
-
"""
|
|
254
|
-
https://stackoverflow.com/a/22434262/190597 (J.F. Sebastian)
|
|
255
|
-
"""
|
|
256
|
-
if stdout is None:
|
|
257
|
-
stdout = sys.stdout
|
|
258
|
-
|
|
259
|
-
stdout_fd = fileno(stdout)
|
|
260
|
-
# copy stdout_fd before it is overwritten
|
|
261
|
-
# NOTE: `copied` is inheritable on Windows when duplicating a standard stream
|
|
262
|
-
with os.fdopen(os.dup(stdout_fd), 'wb') as copied:
|
|
263
|
-
stdout.flush() # flush library buffers that dup2 knows nothing about
|
|
264
|
-
try:
|
|
265
|
-
os.dup2(fileno(to), stdout_fd) # $ exec >&to
|
|
266
|
-
except ValueError: # filename
|
|
267
|
-
with open(to, 'wb') as to_file:
|
|
268
|
-
os.dup2(to_file.fileno(), stdout_fd) # $ exec > to
|
|
269
|
-
try:
|
|
270
|
-
yield stdout # allow code to be run with the redirected stdout
|
|
271
|
-
finally:
|
|
272
|
-
# restore stdout to its previous value
|
|
273
|
-
# NOTE: dup2 makes stdout_fd inheritable unconditionally
|
|
274
|
-
stdout.flush()
|
|
275
|
-
os.dup2(copied.fileno(), stdout_fd) # $ exec >&copied
|
PyDiffGame/DiscretePyDiffGame.py
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import scipy as sp
|
|
3
|
-
|
|
4
|
-
from typing import Sequence, Optional, Union
|
|
5
|
-
|
|
6
|
-
from PyDiffGame.PyDiffGame import PyDiffGame
|
|
7
|
-
from PyDiffGame.Objective import Objective
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class DiscretePyDiffGame(PyDiffGame):
|
|
11
|
-
"""
|
|
12
|
-
Discrete differential game base class
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Considers control design according to the system:
|
|
16
|
-
x[k+1] = A_tilda x[k] + sum_{j=1}^N B_j_tilda v_j[k]
|
|
17
|
-
|
|
18
|
-
where A_tilda and each B_j_tilda are in discrete form, meaning they correlate to a discrete system,
|
|
19
|
-
which, at the sampling points, is assumed to be equal to some equivalent continuous system of the form:
|
|
20
|
-
dx(t)/dt = A x(t) + sum_{j=1}^N B_j v_j(t)
|
|
21
|
-
|
|
22
|
-
Parameters
|
|
23
|
-
----------
|
|
24
|
-
is_input_discrete: boolean, optional, default = False
|
|
25
|
-
Indicates whether the input matrices A, B, Q, R are in discrete form or whether they need to be discretized
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
def __init__(self,
|
|
29
|
-
A: np.array,
|
|
30
|
-
B: Optional[np.array] = None,
|
|
31
|
-
Bs: Optional[Sequence[np.array]] = None,
|
|
32
|
-
Qs: Optional[Sequence[np.array]] = None,
|
|
33
|
-
Rs: Optional[Sequence[np.array]] = None,
|
|
34
|
-
Ms: Optional[Sequence[np.array]] = None,
|
|
35
|
-
objectives: Optional[Sequence[Objective]] = None,
|
|
36
|
-
is_input_discrete: Optional[bool] = False,
|
|
37
|
-
x_0: Optional[np.array] = None,
|
|
38
|
-
x_T: Optional[np.array] = None,
|
|
39
|
-
T_f: Optional[float] = None,
|
|
40
|
-
P_f: Optional[Union[Sequence[np.array], np.array]] = None,
|
|
41
|
-
show_legend: Optional[bool] = True,
|
|
42
|
-
state_variables_names: Optional[Sequence] = None,
|
|
43
|
-
epsilon_x: Optional[float] = PyDiffGame._epsilon_x_default,
|
|
44
|
-
epsilon_P: Optional[float] = PyDiffGame._epsilon_P_default,
|
|
45
|
-
L: Optional[int] = PyDiffGame._L_default,
|
|
46
|
-
eta: Optional[int] = PyDiffGame._eta_default,
|
|
47
|
-
debug: Optional[bool] = False):
|
|
48
|
-
|
|
49
|
-
super().__init__(A=A,
|
|
50
|
-
B=B,
|
|
51
|
-
Bs=Bs,
|
|
52
|
-
Qs=Qs,
|
|
53
|
-
Rs=Rs,
|
|
54
|
-
Ms=Ms,
|
|
55
|
-
objectives=objectives,
|
|
56
|
-
x_0=x_0,
|
|
57
|
-
x_T=x_T,
|
|
58
|
-
T_f=T_f,
|
|
59
|
-
P_f=P_f,
|
|
60
|
-
show_legend=show_legend,
|
|
61
|
-
state_variables_names=state_variables_names,
|
|
62
|
-
epsilon_x=epsilon_x,
|
|
63
|
-
epsilon_P=epsilon_P,
|
|
64
|
-
L=L,
|
|
65
|
-
eta=eta,
|
|
66
|
-
debug=debug
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
if not is_input_discrete:
|
|
70
|
-
self.__discretize_game()
|
|
71
|
-
|
|
72
|
-
self.__K_rows_num = sum([B_i.shape[1] for B_i in self._Bs])
|
|
73
|
-
|
|
74
|
-
def __solve_for_K_k(K_k_previous: np.array,
|
|
75
|
-
k_1: int) -> np.array:
|
|
76
|
-
"""
|
|
77
|
-
fsolve Controllers Solver function
|
|
78
|
-
|
|
79
|
-
Parameters
|
|
80
|
-
----------
|
|
81
|
-
K_k_previous: numpy array
|
|
82
|
-
The previous estimation for the controllers at the k'th sample point
|
|
83
|
-
k_1: int
|
|
84
|
-
The k+1 index
|
|
85
|
-
|
|
86
|
-
Returns
|
|
87
|
-
----------
|
|
88
|
-
dP_tdt: list of numpy 2-d arrays, of len(N), of shape(n, n)
|
|
89
|
-
Current calculated value for the time-derivative of the matrices P_t
|
|
90
|
-
"""
|
|
91
|
-
|
|
92
|
-
K_k_previous = K_k_previous.reshape((self._N,
|
|
93
|
-
self.__K_rows_num,
|
|
94
|
-
self._n))
|
|
95
|
-
P_k_1 = self._P[k_1]
|
|
96
|
-
K_k = np.zeros_like(K_k_previous)
|
|
97
|
-
|
|
98
|
-
for i in range(self._N):
|
|
99
|
-
i_indices = self.__get_indices_tuple(i)
|
|
100
|
-
K_k_previous_i = K_k_previous[i_indices]
|
|
101
|
-
P_k_1_i = P_k_1[i]
|
|
102
|
-
R_ii = self._R[i]
|
|
103
|
-
B_i = self._Bs[i]
|
|
104
|
-
B_i_T = B_i.T
|
|
105
|
-
|
|
106
|
-
A_cl_k_i = self._A
|
|
107
|
-
|
|
108
|
-
for j in range(self._N):
|
|
109
|
-
if j != i:
|
|
110
|
-
B_j = self._Bs[j]
|
|
111
|
-
j_indices = self.__get_indices_tuple(j)
|
|
112
|
-
K_k_previous_j = K_k_previous[j_indices]
|
|
113
|
-
A_cl_k_i = A_cl_k_i - B_j @ K_k_previous_j
|
|
114
|
-
|
|
115
|
-
K_k_i = K_k_previous_i - np.linalg.inv(R_ii + B_i_T @ P_k_1_i @ B_i) @ B_i_T @ P_k_1_i @ A_cl_k_i
|
|
116
|
-
K_k[i_indices] = K_k_i
|
|
117
|
-
|
|
118
|
-
return K_k.ravel()
|
|
119
|
-
|
|
120
|
-
self.f_solve_func = __solve_for_K_k
|
|
121
|
-
|
|
122
|
-
def __discretize_game(self):
|
|
123
|
-
"""
|
|
124
|
-
The input to the discrete game can either be given in continuous or discrete form.
|
|
125
|
-
If it is not in discrete form, the model gets discretized using this method in the following manner:
|
|
126
|
-
A = exp(delta_T A)
|
|
127
|
-
B_i = int_{t=0}^delta_T exp(tA) dt B_i
|
|
128
|
-
|
|
129
|
-
The performance index gets discretized in the following manner:
|
|
130
|
-
Q_i = Q_i * delta_T
|
|
131
|
-
R_i = Q_i / delta_T
|
|
132
|
-
"""
|
|
133
|
-
|
|
134
|
-
A_tilda = np.exp(self._A * self._delta)
|
|
135
|
-
e_AT = sp.integrate.quad(lambda T: np.array([
|
|
136
|
-
np.exp(t * self._A) for t in T]).swapaxes(0, 2).swapaxes(0, 1),
|
|
137
|
-
a=0,
|
|
138
|
-
b=self._delta)[0]
|
|
139
|
-
|
|
140
|
-
self._A = A_tilda
|
|
141
|
-
B_tilda = np.array(e_AT)
|
|
142
|
-
self._Bs = [B_tilda @ B_i for B_i in self._Bs]
|
|
143
|
-
self._Q = [Q_i * self._delta for Q_i in self._Q]
|
|
144
|
-
self._R = [R_i / self._delta for R_i in self._Rs]
|
|
145
|
-
|
|
146
|
-
def __simpson_integrate_Q(self):
|
|
147
|
-
"""
|
|
148
|
-
Use Simpson's approximation for the definite integral of the state cost function term:
|
|
149
|
-
x(t) ^T Q_f_i x(t) + int_{t=0}^T_f x(t) ^T Q_i x(t) ~ sum_{k=0)^K x(t) ^T Q_i_k x(t)
|
|
150
|
-
|
|
151
|
-
where:
|
|
152
|
-
Q_i_0 = 1 / 3 * Q_i
|
|
153
|
-
Q_i_1, ..., Q_i_K-1 = 4 / 3 * Q_i
|
|
154
|
-
Q_i_2, ..., Q_i_K-2 = 2 / 3 * Q_i
|
|
155
|
-
"""
|
|
156
|
-
Q = np.array(self._Q)
|
|
157
|
-
self._Q = np.zeros((self._L,
|
|
158
|
-
self._N,
|
|
159
|
-
self._n,
|
|
160
|
-
self._n))
|
|
161
|
-
self._Q[0] = 1 / 3 * Q
|
|
162
|
-
self._Q[1::2] = 4 / 3 * Q
|
|
163
|
-
self._Q[::2] = 2 / 3 * Q
|
|
164
|
-
|
|
165
|
-
def __initialize_finite_horizon(self):
|
|
166
|
-
"""
|
|
167
|
-
Initializes the calculated parameters for the finite horizon case by the following steps:
|
|
168
|
-
- The matrices P_i and controllers K_i are initialized randomly for all sample points
|
|
169
|
-
- The closed-loop dynamics matrix A_cl is first set to zero for all sample points
|
|
170
|
-
- The terminal conditions P_f_i are set to Q_i
|
|
171
|
-
- The resulting closed-loop dynamics matrix A_cl for the last sampling point is updated
|
|
172
|
-
- The state is initialized with its initial value, if given
|
|
173
|
-
"""
|
|
174
|
-
|
|
175
|
-
self._P = np.random.rand(self._L,
|
|
176
|
-
self._N,
|
|
177
|
-
self._n,
|
|
178
|
-
self._n)
|
|
179
|
-
self._K = np.random.rand(self._L,
|
|
180
|
-
self._N,
|
|
181
|
-
self.__K_rows_num,
|
|
182
|
-
self._n)
|
|
183
|
-
self._A_cl = np.zeros((self._L,
|
|
184
|
-
self._n,
|
|
185
|
-
self._n))
|
|
186
|
-
|
|
187
|
-
self._P[-1] = np.array(self._Q).reshape((self._N,
|
|
188
|
-
self._n,
|
|
189
|
-
self._n))
|
|
190
|
-
self._update_A_cl_from_last_state(k=-1)
|
|
191
|
-
self._x = np.zeros((self._L,
|
|
192
|
-
self._n))
|
|
193
|
-
self._x[0] = self._x_0
|
|
194
|
-
|
|
195
|
-
def __get_K_i_shape(self,
|
|
196
|
-
i: int) -> range:
|
|
197
|
-
"""
|
|
198
|
-
Returns the i'th controller shape indices
|
|
199
|
-
|
|
200
|
-
Parameters
|
|
201
|
-
----------
|
|
202
|
-
i: int
|
|
203
|
-
The desired controller index
|
|
204
|
-
"""
|
|
205
|
-
|
|
206
|
-
K_i_offset = sum([self._Bs[j].shape[1] for j in range(i)]) if i else 0
|
|
207
|
-
first_K_i_index = K_i_offset
|
|
208
|
-
second_K_i_index = first_K_i_index + self._Bs[i].shape[1]
|
|
209
|
-
|
|
210
|
-
return range(first_K_i_index, second_K_i_index)
|
|
211
|
-
|
|
212
|
-
def __get_indices_tuple(self, i: int):
|
|
213
|
-
return i, self.__get_K_i_shape(i)
|
|
214
|
-
|
|
215
|
-
def _update_K_from_last_state(self,
|
|
216
|
-
k_1: int):
|
|
217
|
-
"""
|
|
218
|
-
After initializing, in order to solve for the controllers K_i and matrices P_i,
|
|
219
|
-
we start by solving for the controllers by the update rule:
|
|
220
|
-
K_i[k] = [ R_ii + B_i^T P_i[k+1] B_i ] ^ {-1} B_i^T P_i[k+1] [ A - sum_{j=1, j!=i}^N B_j K_j[k] ]
|
|
221
|
-
|
|
222
|
-
Parameters
|
|
223
|
-
----------
|
|
224
|
-
k_1: int
|
|
225
|
-
The current k+1 sample index
|
|
226
|
-
"""
|
|
227
|
-
|
|
228
|
-
K_k_1 = self._K[k_1]
|
|
229
|
-
K_k_0 = np.random.rand(*K_k_1.shape)
|
|
230
|
-
K_k = sp.optimize.fsolve(func=self.f_solve_func,
|
|
231
|
-
x0=K_k_0,
|
|
232
|
-
args=tuple([k_1]))
|
|
233
|
-
K_k_reshaped = np.array(K_k).reshape((self._N,
|
|
234
|
-
self.__K_rows_num,
|
|
235
|
-
self._n))
|
|
236
|
-
|
|
237
|
-
k = k_1 - 1
|
|
238
|
-
|
|
239
|
-
if self._debug:
|
|
240
|
-
f_K_k = self.f_solve_func(K_k_previous=K_k, k_1=k)
|
|
241
|
-
close_to_zero_array = np.isclose(f_K_k, np.zeros_like(K_k))
|
|
242
|
-
is_close_to_zero = all(close_to_zero_array)
|
|
243
|
-
print(f"The current solution is {'NOT ' if not is_close_to_zero else ''}close to zero"
|
|
244
|
-
f"\nK_k:\n{K_k_reshaped}\nf_K_k:\n{f_K_k}")
|
|
245
|
-
|
|
246
|
-
self._K[k] = K_k_reshaped
|
|
247
|
-
|
|
248
|
-
def _get_K_i(self,
|
|
249
|
-
i: int,
|
|
250
|
-
k: int) -> np.array:
|
|
251
|
-
return self._K[k][self.__get_indices_tuple(i)]
|
|
252
|
-
|
|
253
|
-
def _get_P_f_i(self,
|
|
254
|
-
i: int) -> np.array:
|
|
255
|
-
return self._P_f[i]
|
|
256
|
-
|
|
257
|
-
def _update_Ps_from_last_state(self,
|
|
258
|
-
k_1: int):
|
|
259
|
-
"""
|
|
260
|
-
Updates the matrices P_i with
|
|
261
|
-
A_cl[k] = A - sum_{i=1}^N B_i K_i[k]
|
|
262
|
-
|
|
263
|
-
Parameters
|
|
264
|
-
----------
|
|
265
|
-
k_1: int
|
|
266
|
-
The current k'th sample index
|
|
267
|
-
"""
|
|
268
|
-
|
|
269
|
-
k = k_1 - 1
|
|
270
|
-
P_k_1 = self._P[k_1]
|
|
271
|
-
K_k = self._K[k]
|
|
272
|
-
A_cl_k = self._A_cl[k]
|
|
273
|
-
P_k = np.zeros_like(P_k_1)
|
|
274
|
-
|
|
275
|
-
for i in range(self._N):
|
|
276
|
-
i_indices = self.__get_indices_tuple(i)
|
|
277
|
-
K_k_i = K_k[i_indices]
|
|
278
|
-
K_k_i_T = K_k_i.T
|
|
279
|
-
P_k_1_i = P_k_1[i]
|
|
280
|
-
R_ii = self._R[i]
|
|
281
|
-
Q_i = self._Q[i]
|
|
282
|
-
|
|
283
|
-
P_k[i] = A_cl_k.T @ P_k_1_i @ A_cl_k + (K_k_i_T @ R_ii if R_ii.ndim > 1 else K_k_i_T * R_ii) @ K_k_i + Q_i
|
|
284
|
-
|
|
285
|
-
self._P[k] = P_k
|
|
286
|
-
|
|
287
|
-
def is_A_cl_stable(self,
|
|
288
|
-
k: int) -> bool:
|
|
289
|
-
"""
|
|
290
|
-
Tests Lyapunov stability of the closed loop:
|
|
291
|
-
A discrete dynamic system governed by the matrix A_cl has Lyapunov stability iff:
|
|
292
|
-
|eig| <= 1 forall eigenvalues of A_cl
|
|
293
|
-
"""
|
|
294
|
-
|
|
295
|
-
A_cl_k = self._A_cl[k]
|
|
296
|
-
A_cl_k_eigenvalues = np.linalg.eigvals(A_cl_k)
|
|
297
|
-
A_cl_k_eigenvalues_absolute_values = [abs(eig) for eig in A_cl_k_eigenvalues]
|
|
298
|
-
|
|
299
|
-
if self._debug:
|
|
300
|
-
print(A_cl_k_eigenvalues_absolute_values)
|
|
301
|
-
|
|
302
|
-
stability = all([eig_norm < 1 for eig_norm in A_cl_k_eigenvalues_absolute_values])
|
|
303
|
-
|
|
304
|
-
return stability
|
|
305
|
-
|
|
306
|
-
def _solve_finite_horizon(self):
|
|
307
|
-
"""
|
|
308
|
-
Solves the system with the finite-horizon cost functions:
|
|
309
|
-
J_i = sum_{k=1}^T_f [ x[k]^T Q_i x[k] + sum_{j=1}^N u_j[k]^T R_{ij} u_j[k] ]
|
|
310
|
-
|
|
311
|
-
In the discrete-time case, the matrices P_i have to be solved simultaneously with the controllers K_i
|
|
312
|
-
"""
|
|
313
|
-
|
|
314
|
-
pass
|
|
315
|
-
|
|
316
|
-
def _solve_infinite_horizon(self):
|
|
317
|
-
"""
|
|
318
|
-
Solves the system with the infinite-horizon cost functions:
|
|
319
|
-
J_i = sum_{k=1}^infty [ x[k]^T Q_i x[k] + sum_{j=1}^N u_j[k]^T R_{ij} u_j[k] ]
|
|
320
|
-
"""
|
|
321
|
-
|
|
322
|
-
self.__initialize_finite_horizon()
|
|
323
|
-
|
|
324
|
-
for backwards_index, t in enumerate(self._backward_time[:-1]):
|
|
325
|
-
k_1 = self._L - 1 - backwards_index
|
|
326
|
-
k = k_1 - 1
|
|
327
|
-
|
|
328
|
-
if self._debug:
|
|
329
|
-
print('#' * 30 + f' t = {round(t, 3)} ' + '#' * 30)
|
|
330
|
-
|
|
331
|
-
self._update_K_from_last_state(k_1=k_1)
|
|
332
|
-
|
|
333
|
-
if self._debug:
|
|
334
|
-
print(f'K_k+1:\n{self._K[k_1]}')
|
|
335
|
-
|
|
336
|
-
self._update_A_cl_from_last_state(k=k)
|
|
337
|
-
|
|
338
|
-
if self._debug:
|
|
339
|
-
print(f'A_cl_k:\n{self._A_cl[k]}')
|
|
340
|
-
stable_k = self.is_A_cl_stable(k)
|
|
341
|
-
print(f"The system is {'NOT ' if not stable_k else ''}stable")
|
|
342
|
-
|
|
343
|
-
self._update_Ps_from_last_state(k_1=k_1)
|
|
344
|
-
|
|
345
|
-
@PyDiffGame._post_convergence
|
|
346
|
-
def _solve_state_space(self):
|
|
347
|
-
"""
|
|
348
|
-
Propagates the game through time and solves for it by solving the discrete difference equation:
|
|
349
|
-
x[k+1] = A_cl[k] x[k]
|
|
350
|
-
In each step, the controllers K_i are calculated with the values of P_i evaluated beforehand.
|
|
351
|
-
"""
|
|
352
|
-
|
|
353
|
-
x_1_k = self._x_0
|
|
354
|
-
|
|
355
|
-
for k in range(1, self._L - 1):
|
|
356
|
-
A_cl_k = self._A_cl[k]
|
|
357
|
-
x_k = A_cl_k @ x_1_k
|
|
358
|
-
self._x[k] = x_k
|
|
359
|
-
x_1_k = x_k
|