PyDiffGame 0.1.2__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,17 @@
1
- PyDiffGame/ContinuousPyDiffGame.py,sha256=31Rf7zUMQOWxVT23x1hR_LXqcrpI25W-IugRA4a3E6c,10462
2
- PyDiffGame/DiscretePyDiffGame.py,sha256=86cCk91ogzQsEMaE9aM5VBEhZ_09RdJjmiJgR9_y_H0,12952
3
- PyDiffGame/LQR.py,sha256=bIUDt9frlX2GNXrX1XfJarwqrjrVCm4AAAv6zrKumHA,2267
4
- PyDiffGame/Objective.py,sha256=Q6En2czCtJFHG3vRNUbsxb7V5ADIO-b03qqlq9Fy2fc,1201
5
- PyDiffGame/PyDiffGame.py,sha256=JsqY33WdPbiNIpN3m_xj8aooStf815XG2gEA67J3fFg,45084
6
- PyDiffGame/PyDiffGameLQRComparison.py,sha256=b2eobj3AyY42uElAp5abs7iI0rMy4op0jYybjBG0ad4,5850
7
- PyDiffGame/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- PyDiffGame/examples/InvertedPendulumComparison.py,sha256=z72VznBkml5Xv69kt8RizVsevj05tlmttd3CJYoaJUQ,10151
9
- PyDiffGame/examples/MassesWithSpringsComparison.py,sha256=saT6g7T4ZDlXYAPw5ApwKFCaxzvXixOPfXyzMX2p6v4,8647
10
- PyDiffGame/examples/PVTOL.py,sha256=eHi8nYnkSGgAtH5CVR_Cxakhapd8pDYYpsn6jkIwsN4,6649
11
- PyDiffGame/examples/PVTOLComparison.py,sha256=uFPVry2lfyWKOk8OR5harsqiV1X6Z2jVtkEtWGQYZ1Q,3567
12
- PyDiffGame/examples/QuadRotorControl.py,sha256=uAIWx25KQm8RemL5Wi0Zcvnbv-6umQp4Q1VAIeLdYfM,17909
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.1.2.dist-info/METADATA,sha256=2Ojsxsj5rdulRMJ_SgjP_wfooBdDoYH7jry2ZRIszXU,14281
35
- pydiffgame-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- pydiffgame-0.1.2.dist-info/licenses/LICENSE,sha256=IYfdsBdqL9SmCEWsXUBTYh303LOWQQRCOUOfZI7PWz8,1139
37
- pydiffgame-0.1.2.dist-info/RECORD,,
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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
@@ -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