PyDiffGame 2.0.2__py3-none-any.whl → 2.0.4__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 CHANGED
@@ -33,8 +33,11 @@ from PyDiffGame.continuous import ContinuousPyDiffGame
33
33
  from PyDiffGame.discrete import DiscretePyDiffGame
34
34
  from PyDiffGame.lqr import ContinuousLQR, DiscreteLQR
35
35
  from PyDiffGame.objective import GameObjective, LQRObjective, Objective
36
+ from PyDiffGame.robust import ContinuousHInfinityControl
36
37
 
37
- __version__ = "2.0.2"
38
+ __version__ = "2.0.4"
39
+ __author__ = "Joshua Shay Kricheli"
40
+ __url__ = "https://github.com/krichelj/PyDiffGame"
38
41
 
39
42
  __all__ = [
40
43
  "PyDiffGame",
@@ -42,9 +45,12 @@ __all__ = [
42
45
  "DiscretePyDiffGame",
43
46
  "ContinuousLQR",
44
47
  "DiscreteLQR",
48
+ "ContinuousHInfinityControl",
45
49
  "Objective",
46
50
  "LQRObjective",
47
51
  "GameObjective",
48
52
  "PyDiffGameLQRComparison",
49
53
  "__version__",
54
+ "__author__",
55
+ "__url__",
50
56
  ]
PyDiffGame/robust.py ADDED
@@ -0,0 +1,287 @@
1
+ r"""Robust (:math:`H_\infty`) state feedback as a controller-vs-disturbance game.
2
+
3
+ This completes the differential-games family in :mod:`PyDiffGame`: alongside the
4
+ single-objective :class:`~PyDiffGame.lqr.ContinuousLQR` and the :math:`N`-player
5
+ Nash :class:`~PyDiffGame.continuous.ContinuousPyDiffGame`, the *robust* design is
6
+ the two-player **zero-sum** differential game in which the controller minimises
7
+ and an adversarial disturbance maximises
8
+
9
+ .. math::
10
+ J = \int_0^\infty x^\top Q x + u^\top R u - \gamma^2 w^\top w \; dt ,
11
+ \qquad \dot x = A x + B u + B_w w .
12
+
13
+ Its saddle point is the :math:`H_\infty` state-feedback controller
14
+ :math:`u = -K x`, :math:`K = R^{-1} B^\top P`, where :math:`P` solves the game
15
+ algebraic Riccati equation
16
+
17
+ .. math::
18
+ A^\top P + P A + Q - P\,(B R^{-1} B^\top - \gamma^{-2} B_w B_w^\top)\,P = 0 .
19
+
20
+ The quadratic term is *indefinite* (the disturbance subtracts), which is exactly
21
+ why an ordinary LQR Riccati solver cannot produce it and the game formulation is
22
+ required. The controller minimises the worst-case :math:`L_2` gain from the
23
+ disturbance to the weighted performance output — the robustness an LQR does not
24
+ optimise.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import numpy as np
30
+ from scipy.linalg import schur
31
+
32
+ from PyDiffGame._typing import ArrayLike, FloatArray
33
+
34
+
35
+ def _as_matrix(value: ArrayLike) -> FloatArray:
36
+ arr = np.asarray(value, dtype=np.float64)
37
+ return arr.reshape(1, 1) if arr.ndim == 0 else arr
38
+
39
+
40
+ def _symmetric_sqrt(matrix: FloatArray) -> FloatArray:
41
+ """Symmetric positive-semidefinite square root of ``matrix``."""
42
+ eigenvalues, eigenvectors = np.linalg.eigh(0.5 * (matrix + matrix.T))
43
+ return (eigenvectors * np.sqrt(np.clip(eigenvalues, 0.0, None))) @ eigenvectors.T
44
+
45
+
46
+ class ContinuousHInfinityControl:
47
+ r"""Continuous-time :math:`H_\infty` state feedback via the disturbance game.
48
+
49
+ Parameters
50
+ ----------
51
+ A, B : array_like
52
+ State and control matrices of :math:`\dot x = A x + B u + B_w w`.
53
+ B_w : array_like
54
+ Disturbance input matrix :math:`B_w`.
55
+ Q, R : array_like
56
+ Symmetric weights of the performance output (``Q`` PSD, ``R`` PD).
57
+ gamma : float, optional
58
+ Robustness level. Smaller is more robust but harder; it must exceed the
59
+ optimal :math:`\gamma^\star`. If omitted, :math:`\gamma^\star` is found by
60
+ bisection and ``gamma = gamma_margin * gamma_star`` is used.
61
+ gamma_margin : float, default ``1.3``
62
+ Multiplier applied to :math:`\gamma^\star` when ``gamma`` is not given.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ A: ArrayLike,
68
+ B: ArrayLike,
69
+ B_w: ArrayLike,
70
+ Q: ArrayLike,
71
+ R: ArrayLike,
72
+ *,
73
+ gamma: float | None = None,
74
+ gamma_margin: float = 1.3,
75
+ ) -> None:
76
+ self._A = _as_matrix(A)
77
+ self._B = _as_matrix(B)
78
+ self._B_w = _as_matrix(B_w)
79
+ self._Q = _as_matrix(Q)
80
+ self._R = _as_matrix(R)
81
+ self._n = self._A.shape[0]
82
+ if self._A.shape != (self._n, self._n):
83
+ raise ValueError("A must be square")
84
+ for name, mat, rows in (
85
+ ("B", self._B, self._n),
86
+ ("B_w", self._B_w, self._n),
87
+ ("Q", self._Q, self._n),
88
+ ):
89
+ if mat.shape[0] != rows:
90
+ raise ValueError(f"{name} must have {rows} rows")
91
+ if gamma_margin <= 1.0:
92
+ raise ValueError("gamma_margin must be > 1")
93
+
94
+ self._gamma_margin = float(gamma_margin)
95
+ self._gamma_star: float | None = None
96
+ self._gamma = None if gamma is None else float(gamma)
97
+ self._P: FloatArray | None = None
98
+ self._K: FloatArray | None = None
99
+
100
+ # ------------------------------------------------------------------ #
101
+ # GARE solve (Hamiltonian / ordered Schur)
102
+ # ------------------------------------------------------------------ #
103
+ def _solve_gare(self, gamma: float) -> FloatArray:
104
+ R_inv = np.linalg.inv(self._R)
105
+ S = self._B @ R_inv @ self._B.T - (1.0 / gamma**2) * (self._B_w @ self._B_w.T)
106
+ hamiltonian = np.block([[self._A, -S], [-self._Q, -self._A.T]])
107
+ _, vectors, stable_dim = schur(hamiltonian, sort="lhp")
108
+ if stable_dim != self._n:
109
+ raise ValueError(
110
+ f"no stabilising H-infinity solution at gamma={gamma:.4g} "
111
+ f"(stable subspace dimension {stable_dim} != {self._n}); gamma is below gamma*"
112
+ )
113
+ basis = vectors[:, : self._n]
114
+ upper, lower = basis[: self._n, :], basis[self._n :, :]
115
+ if abs(np.linalg.det(upper)) < 1e-12:
116
+ raise ValueError(f"degenerate Schur basis at gamma={gamma:.4g}")
117
+ P = np.real(lower @ np.linalg.inv(upper))
118
+ return 0.5 * (P + P.T)
119
+
120
+ def _stabilizing_solution(self, gamma: float) -> tuple[FloatArray | None, bool]:
121
+ """Solve the GARE at ``gamma`` and validate a stabilising PSD solution.
122
+
123
+ Robust admissibility predicate: rather than trust the (boundary-brittle)
124
+ stable-subspace dimension count alone, ``gamma`` is admissible only if the
125
+ recovered ``P`` is symmetric PSD *and* the closed loop ``A - B K`` is
126
+ strictly Hurwitz with a small margin. This makes admissibility monotone
127
+ through :math:`\\gamma^\\star` and immune to the imaginary-axis eigenvalue
128
+ jitter of the Hamiltonian at the boundary.
129
+ """
130
+ try:
131
+ P = self._solve_gare(gamma)
132
+ except ValueError:
133
+ return None, False
134
+ if float(np.min(np.linalg.eigvalsh(P))) < -1e-7:
135
+ return P, False
136
+ K = np.linalg.inv(self._R) @ self._B.T @ P
137
+ spectral_abscissa = float(np.max(np.linalg.eigvals(self._A - self._B @ K).real))
138
+ return P, spectral_abscissa < -1e-7
139
+
140
+ def _is_admissible(self, gamma: float) -> bool:
141
+ return self._stabilizing_solution(gamma)[1]
142
+
143
+ def optimal_gamma(self, *, iterations: int = 100) -> float:
144
+ r"""Smallest :math:`\gamma` admitting a stabilising solution (:math:`\gamma^\star`).
145
+
146
+ Brackets :math:`\gamma^\star` from *both* sides adaptively — doubling the
147
+ upper bound up until admissible and halving the lower bound down until
148
+ inadmissible — so it never silently floors out on systems whose
149
+ :math:`\gamma^\star` is tiny, then bisects on the validated admissibility
150
+ predicate (:meth:`_stabilizing_solution`), which is monotone through the
151
+ boundary.
152
+ """
153
+ if self._gamma_star is not None:
154
+ return self._gamma_star
155
+ upper = 1.0
156
+ for _ in range(60):
157
+ if self._is_admissible(upper):
158
+ break
159
+ upper *= 2.0
160
+ else:
161
+ raise RuntimeError("could not bracket a feasible upper gamma")
162
+ lower = upper
163
+ for _ in range(60):
164
+ lower *= 0.5
165
+ if not self._is_admissible(lower):
166
+ break
167
+ else:
168
+ # admissible all the way down: gamma* is effectively zero
169
+ self._gamma_star = float(lower)
170
+ return self._gamma_star
171
+ for _ in range(iterations):
172
+ mid = np.sqrt(lower * upper)
173
+ if self._is_admissible(mid):
174
+ upper = mid
175
+ else:
176
+ lower = mid
177
+ if upper / lower < 1.0 + 1e-6:
178
+ break
179
+ self._gamma_star = float(upper)
180
+ return self._gamma_star
181
+
182
+ def solve(self) -> ContinuousHInfinityControl:
183
+ """Solve the disturbance game and store the controller. Returns ``self``."""
184
+ gamma = self._gamma if self._gamma is not None else self.optimal_gamma() * self._gamma_margin
185
+ self._gamma = float(gamma)
186
+ self._P = self._solve_gare(gamma)
187
+ self._K = np.linalg.inv(self._R) @ self._B.T @ self._P
188
+ return self
189
+
190
+ # ------------------------------------------------------------------ #
191
+ # Results
192
+ # ------------------------------------------------------------------ #
193
+ def _require_solved(self) -> None:
194
+ if self._K is None:
195
+ raise RuntimeError("call solve() first")
196
+
197
+ @property
198
+ def K(self) -> FloatArray:
199
+ self._require_solved()
200
+ assert self._K is not None
201
+ return self._K
202
+
203
+ @property
204
+ def P(self) -> FloatArray:
205
+ self._require_solved()
206
+ assert self._P is not None
207
+ return self._P
208
+
209
+ @property
210
+ def gamma(self) -> float:
211
+ self._require_solved()
212
+ assert self._gamma is not None
213
+ return self._gamma
214
+
215
+ @property
216
+ def A_cl(self) -> FloatArray:
217
+ return self._A - self._B @ self.K
218
+
219
+ def is_closed_loop_stable(self, tolerance: float = 1e-9) -> bool:
220
+ return bool(np.all(np.linalg.eigvals(self.A_cl).real < tolerance))
221
+
222
+ def gare_residual(self) -> float:
223
+ """Max-abs residual of the game algebraic Riccati equation (should be ~0)."""
224
+ self._require_solved()
225
+ P, R_inv = self.P, np.linalg.inv(self._R)
226
+ S = self._B @ R_inv @ self._B.T - (1.0 / self.gamma**2) * (self._B_w @ self._B_w.T)
227
+ res = self._A.T @ P + P @ self._A + self._Q - P @ S @ P
228
+ return float(np.max(np.abs(res)))
229
+
230
+ def worst_case_gain(self, *, n_grid: int = 6000) -> tuple[float, float]:
231
+ r"""Closed-loop worst-case :math:`L_2` gain of this :math:`H_\infty` design.
232
+
233
+ See :func:`worst_case_l2_gain`. Returns ``(peak_gain, peak_frequency)``.
234
+ """
235
+ self._require_solved()
236
+ return worst_case_l2_gain(self._A, self._B, self.K, self._Q, self._R, self._B_w, n_grid=n_grid)
237
+
238
+
239
+ def worst_case_l2_gain(
240
+ A: ArrayLike,
241
+ B: ArrayLike,
242
+ K: ArrayLike,
243
+ Q: ArrayLike,
244
+ R: ArrayLike,
245
+ B_w: ArrayLike,
246
+ *,
247
+ n_grid: int = 6000,
248
+ ) -> tuple[float, float]:
249
+ r"""Worst-case :math:`L_2` gain :math:`\lVert G_{zw}\rVert_\infty` of any state-feedback gain.
250
+
251
+ The induced gain from disturbance ``w`` to the weighted performance output
252
+ :math:`z = [Q^{1/2} x;\, R^{1/2} u]` for the closed loop ``A - B K``, i.e.
253
+ :math:`\max_\omega \sigma_{\max}(C (j\omega I - (A-BK))^{-1} B_w)`, by a
254
+ log-spaced frequency sweep refined around the peak (no slycot needed). Lower
255
+ is more robust; this is the formal robustness metric used to compare an
256
+ :math:`H_\infty` design against, say, an LQR. Returns ``inf`` if unstable.
257
+ """
258
+ A, B, K = _as_matrix(A), _as_matrix(B), _as_matrix(K)
259
+ Q, R, B_w = _as_matrix(Q), _as_matrix(R), _as_matrix(B_w)
260
+ A_cl = A - B @ K
261
+ eigenvalues = np.linalg.eigvals(A_cl)
262
+ if np.max(eigenvalues.real) >= 0:
263
+ return float("inf"), float("nan")
264
+ C = np.vstack([_symmetric_sqrt(Q), -_symmetric_sqrt(R) @ K])
265
+ eye = np.eye(A_cl.shape[0])
266
+
267
+ def sigma_max(omega: float) -> float:
268
+ transfer = C @ np.linalg.solve(1j * omega * eye - A_cl, B_w)
269
+ return float(np.linalg.svd(transfer, compute_uv=False)[0])
270
+
271
+ # Upper frequency derived from the fastest closed-loop mode so the sweep can
272
+ # never miss a high-frequency peak (with a generous safety factor and floor).
273
+ top = max(1.0e5, 50.0 * float(np.max(np.abs(eigenvalues))))
274
+ grid = np.concatenate([[0.0], np.logspace(-4, np.log10(top), n_grid)])
275
+ values = np.array([sigma_max(w) for w in grid])
276
+ peak = int(np.argmax(values))
277
+ lo = grid[max(peak - 1, 0)]
278
+ hi = grid[min(peak + 1, len(grid) - 1)]
279
+ fine = np.linspace(lo, hi, n_grid // 2)
280
+ fine_values = np.array([sigma_max(w) for w in fine])
281
+ best = int(np.argmax(fine_values))
282
+ if fine_values[best] >= values[peak]:
283
+ return float(fine_values[best]), float(fine[best])
284
+ return float(values[peak]), float(grid[peak])
285
+
286
+
287
+ __all__ = ["ContinuousHInfinityControl", "worst_case_l2_gain"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyDiffGame
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Nash-equilibrium solutions to linear-quadratic differential games, via a reduction of the Game Hamilton-Jacobi-Bellman equations to coupled algebraic and differential Riccati equations for multi-objective dynamical control systems.
5
5
  Project-URL: Homepage, https://krichelj.github.io/PyDiffGame/
6
6
  Project-URL: Repository, https://github.com/krichelj/PyDiffGame
@@ -58,7 +58,7 @@ Requires-Dist: control>=0.10; extra == 'examples'
58
58
  Description-Content-Type: text/markdown
59
59
 
60
60
  <p align="center">
61
- <img alt="PyDiffGame logo" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/logo.png" width="420"/>
61
+ <img alt="PyDiffGame logo" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/logo.gif" width="420"/>
62
62
  </p>
63
63
 
64
64
  <p align="center">
@@ -83,7 +83,8 @@ Description-Content-Type: text/markdown
83
83
  * [Quick start](#quick-start)
84
84
  * [Input parameters](#input-parameters)
85
85
  * [Tutorial: masses on springs](#tutorial-masses-on-springs)
86
- * [More examples](#more-examples)
86
+ * [Robust control: H∞ as a game](#robust-control-h-as-a-game)
87
+ * [Examples — a walkthrough](#examples--a-walkthrough)
87
88
  * [Testing and development](#testing-and-development)
88
89
  * [Citing](#citing)
89
90
  * [Acknowledgments](#acknowledgments)
@@ -332,17 +333,73 @@ player, without re-tuning one monolithic cost matrix.
332
333
  > [`tools/generate_readme_figures.py`](https://github.com/krichelj/PyDiffGame/blob/master/tools/generate_readme_figures.py)
333
334
  > (`uv run python tools/generate_readme_figures.py`), so they always match the current code.
334
335
 
335
- # More examples
336
+ # Robust control: H∞ as a game
336
337
 
337
- The [`src/PyDiffGame/examples`](https://github.com/krichelj/PyDiffGame/tree/master/src/PyDiffGame/examples)
338
- directory contains further worked comparisons:
338
+ A game ties the centralized LQR on a shared cost — it cannot beat it. The place a
339
+ differential game **provably wins** is *robustness*: classical **H∞** state feedback **is**
340
+ the saddle point of a two-player zero-sum game in which the controller minimises and an
341
+ adversarial disturbance maximises. PyDiffGame ships it as `ContinuousHInfinityControl`,
342
+ completing the family next to the LQR and the N-player Nash game:
339
343
 
340
- | Example | System |
341
- | --- | --- |
342
- | [`MassesWithSpringsComparison.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/MassesWithSpringsComparison.py) | Chain of masses coupled by springs (the tutorial above) |
343
- | [`InvertedPendulumComparison.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/InvertedPendulumComparison.py) | Inverted pendulum on a cart |
344
- | [`PVTOL.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/PVTOL.py) · [`PVTOLComparison.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/PVTOLComparison.py) | Planar vertical take-off & landing aircraft |
345
- | [`QuadRotorControl.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/QuadRotorControl.py) | Quadrotor attitude / position control |
344
+ ```python
345
+ import numpy as np
346
+ from PyDiffGame import ContinuousHInfinityControl
347
+
348
+ A = np.array([[0.0, 1.0], [0.0, 0.0]]) # a cart: position, velocity
349
+ B = np.array([[0.0], [1.0]]) # control force
350
+ B_w = np.array([[0.0], [1.0]]) # disturbance force
351
+ Q, R = np.diag([1.0, 0.0]), np.array([[1.0]])
352
+
353
+ robust = ContinuousHInfinityControl(A, B, B_w, Q, R).solve() # picks gamma = 1.3 * gamma*
354
+ print(robust.K) # robust feedback gain, u = -K x
355
+ print(robust.worst_case_gain()[0]) # closed-loop ||G_zw||inf — provably below the LQR's
356
+ ```
357
+
358
+ It solves the **game** algebraic Riccati equation whose quadratic term
359
+ `B R⁻¹ Bᵀ − γ⁻² B_w B_wᵀ` is *indefinite* — exactly what an ordinary LQR Riccati solver
360
+ cannot do — via the Hamiltonian/Schur method, auto-finds the optimal robustness level
361
+ `γ*`, and reports the formal worst-case L2 gain. Across a
362
+ [13-system benchmark](https://github.com/krichelj/PyDiffGame/tree/master/benchmarks)
363
+ (carts, vehicles, aircraft, drones, flexible structures) it reduces the worst-case
364
+ disturbance gain on every system, **practically significantly on 10/13** — most where the
365
+ LQR leaves a sharp resonant peak — each at a documented nominal-cost price:
366
+
367
+ <p align="center">
368
+ <img alt="H-infinity game vs LQR under worst-case disturbance (inverted pendulum)" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/bbd010f15ee13adc2bba5e7b99e1a3ecc0238583/benchmarks/results/robust_inverted_pendulum.gif" width="860"/>
369
+ </p>
370
+
371
+ Left: the pendulum-angle response to the worst-case disturbance (H∞ roughly halves
372
+ it). Right: the `σmax(ω)` curves whose peak *is* the worst-case gain — the LQR's
373
+ resonant peak (≈1.92) flattened by the H∞ game to ≈1.25. The
374
+ [**robustness showcase**](https://github.com/krichelj/PyDiffGame/blob/master/benchmarks/README.md)
375
+ animates every system.
376
+
377
+ # Examples — a walkthrough
378
+
379
+ The package ships four worked **LQR-vs-game comparisons** under
380
+ [`src/PyDiffGame/examples`](https://github.com/krichelj/PyDiffGame/tree/master/src/PyDiffGame/examples);
381
+ each builds a system, designs an LQR and a decomposed game on it, runs both and reports the
382
+ costs. Run any of them with `uv run python -m PyDiffGame.examples.<name>`:
383
+
384
+ | Example | System | What it shows |
385
+ | --- | --- | --- |
386
+ | [`MassesWithSpringsComparison`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/MassesWithSpringsComparison.py) | Chain of masses coupled by springs | The **lossless** modal decomposition (the tutorial above): the game reproduces the monolithic LQR optimum |
387
+ | [`InvertedPendulumComparison`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/InvertedPendulumComparison.py) | Inverted pendulum on a cart | An **unstable, underactuated** plant; the nonlinear closed loop can be simulated from the designed gains |
388
+ | [`PVTOLComparison`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/PVTOLComparison.py) · [`PVTOL`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/PVTOL.py) | Planar vertical take-off & landing aircraft | A 6-state aircraft with input decomposition across players |
389
+ | [`QuadRotorControl`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/QuadRotorControl.py) | Quadrotor attitude / position control | A larger nonlinear vehicle with a cascaded design |
390
+
391
+ For the **full study** — PyDiffGame vs [python-control](https://python-control.readthedocs.io/)
392
+ across 13 systems on both the *nominal* cost (lossless tie / price of anarchy) and the
393
+ *robustness* metric, with rendered GIFs, a metrics report and an adversarial review of the
394
+ methodology — see
395
+ [`benchmarks/`](https://github.com/krichelj/PyDiffGame/tree/master/benchmarks) and its
396
+ [README](https://github.com/krichelj/PyDiffGame/blob/master/benchmarks/README.md):
397
+
398
+ ```bash
399
+ uv run --extra dev python -m benchmarks.run_masses # nominal: game == LQR (lossless)
400
+ uv run --extra dev python -m benchmarks.run_anarchy # nominal: the price of anarchy
401
+ uv run --extra dev python -m benchmarks.run_robust_suite # the 13-system robustness suite + report
402
+ ```
346
403
 
347
404
  # Testing and development
348
405
 
@@ -1,4 +1,4 @@
1
- PyDiffGame/__init__.py,sha256=66tL7W30LVRZoWD76n0TPtv0omIrE9hy2TUaqYTiEVw,1657
1
+ PyDiffGame/__init__.py,sha256=3h1YwXc7Klt4KXljtH4ng9mYCeYqg5H6QtlaruYFvww,1868
2
2
  PyDiffGame/_typing.py,sha256=HErUHmfzvkWLPWT1YJl_gpFYZctjkPqsnw3TeHuuggs,912
3
3
  PyDiffGame/base.py,sha256=hurrmsjCRQ049rHQGDa78JUBkCbLs-ZufDF8eHW5oNk,17287
4
4
  PyDiffGame/comparison.py,sha256=lJVhceZE7FqaNUGu8q1mA6GS0dbBwYFF1mHn0hzKuwM,4145
@@ -7,6 +7,7 @@ PyDiffGame/discrete.py,sha256=s62Fhx6H4okfalsopOwW2kp9onnnArHK7tkLnAUd3Zc,8127
7
7
  PyDiffGame/lqr.py,sha256=tcmL2wY_PtNon0XxdWehPJRvLdc44UqzJY9fC-ZnaJ0,1057
8
8
  PyDiffGame/objective.py,sha256=mnUPEauTZJl5HE3PScXdzDX5W0pm4fU-Jfi7Lvzgz1o,3910
9
9
  PyDiffGame/plotting.py,sha256=gvcgd-T0Y5uTcq15lJX3V4l39YqjpA0jFJsP-RzWTR0,2777
10
+ PyDiffGame/robust.py,sha256=OrwHOnc1TJ1SnBO5F6ASTePJJmQLDi1HjsibXkTFkiM,11665
10
11
  PyDiffGame/examples/InvertedPendulumComparison.py,sha256=6jhtrHariZAblaWxfnrvOwjuGKKfTVhTyxDI5k3jam8,8615
11
12
  PyDiffGame/examples/MassesWithSpringsComparison.py,sha256=JKUyaDfxKnK_N3DEUjWBIntijquZ59fEHLfmNPxSWPk,4626
12
13
  PyDiffGame/examples/PVTOL.py,sha256=AQz_CFYw6oi9Qc2SXNFx1OIdtQTSDhqNSK02nPCDwuk,6542
@@ -33,7 +34,7 @@ PyDiffGame/examples/figures/PVTOL/PVTOL1.png,sha256=FBgKQSal5ZNyBsGz4RaVye0HBBQU
33
34
  PyDiffGame/examples/figures/PVTOL/PVTOL10.png,sha256=XyoLMz5WKfg3ny2JxVxVd1Mxjz9WQQn7N87-8YDLisY,77894
34
35
  PyDiffGame/examples/figures/PVTOL/PVTOL100.png,sha256=wkMdqrn9hSsIdU-Yhiki3X2fQ7dRRkfLyvBCFx5cMWI,75698
35
36
  PyDiffGame/examples/figures/PVTOL/PVTOL1000.png,sha256=Hg_hsf9gnc1HqC4JVjATVay7uDlnIqBRkG8lzpcfiZ0,77630
36
- pydiffgame-2.0.2.dist-info/METADATA,sha256=p2IQEz1MJ-C4DXrRHENghBjc-lYIfo-kOnDUuj4iusQ,20580
37
- pydiffgame-2.0.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
38
- pydiffgame-2.0.2.dist-info/licenses/LICENSE,sha256=IYfdsBdqL9SmCEWsXUBTYh303LOWQQRCOUOfZI7PWz8,1139
39
- pydiffgame-2.0.2.dist-info/RECORD,,
37
+ pydiffgame-2.0.4.dist-info/METADATA,sha256=Q1Bd3uLy5PI_yxcLJxOUlZoC_2rrbL-Xja2c2484jVw,24273
38
+ pydiffgame-2.0.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
39
+ pydiffgame-2.0.4.dist-info/licenses/LICENSE,sha256=IYfdsBdqL9SmCEWsXUBTYh303LOWQQRCOUOfZI7PWz8,1139
40
+ pydiffgame-2.0.4.dist-info/RECORD,,