PyDiffGame 2.0.3__tar.gz → 2.0.4__tar.gz
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-2.0.3 → pydiffgame-2.0.4}/PKG-INFO +69 -12
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/README.md +68 -11
- pydiffgame-2.0.4/benchmarks/README.md +109 -0
- pydiffgame-2.0.4/benchmarks/__init__.py +0 -0
- pydiffgame-2.0.4/benchmarks/catalog.py +375 -0
- pydiffgame-2.0.4/benchmarks/harness.py +340 -0
- pydiffgame-2.0.4/benchmarks/pdg_design.py +47 -0
- pydiffgame-2.0.4/benchmarks/results/ROBUSTNESS_REPORT.md +33 -0
- pydiffgame-2.0.4/benchmarks/results/coupled_carts_anarchy.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/coupled_carts_anarchy.json +54 -0
- pydiffgame-2.0.4/benchmarks/results/masses_pdg_vs_lqr.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/masses_pdg_vs_lqr.json +54 -0
- pydiffgame-2.0.4/benchmarks/results/robust_DC_motor_servo.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_DC_motor_servo.json +32 -0
- pydiffgame-2.0.4/benchmarks/results/robust_PVTOL_aircraft.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_PVTOL_aircraft.json +54 -0
- pydiffgame-2.0.4/benchmarks/results/robust_active_suspension.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_active_suspension.json +34 -0
- pydiffgame-2.0.4/benchmarks/results/robust_aircraft_short-period.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_aircraft_short-period.json +30 -0
- pydiffgame-2.0.4/benchmarks/results/robust_car_cruise_control.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_car_cruise_control.json +30 -0
- pydiffgame-2.0.4/benchmarks/results/robust_car_lateral_(bicycle).gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_car_lateral_(bicycle).json +30 -0
- pydiffgame-2.0.4/benchmarks/results/robust_cart_(double_integrator).gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_cart_(double_integrator).json +30 -0
- pydiffgame-2.0.4/benchmarks/results/robust_cart_with_disturbance.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_cart_with_disturbance.json +31 -0
- pydiffgame-2.0.4/benchmarks/results/robust_flexible_two-mass.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_flexible_two-mass.json +34 -0
- pydiffgame-2.0.4/benchmarks/results/robust_gantry_crane.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_gantry_crane.json +34 -0
- pydiffgame-2.0.4/benchmarks/results/robust_inverted_pendulum.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_inverted_pendulum.json +34 -0
- pydiffgame-2.0.4/benchmarks/results/robust_mass-spring-damper.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_mass-spring-damper.json +30 -0
- pydiffgame-2.0.4/benchmarks/results/robust_quadrotor_(planar).gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_quadrotor_(planar).json +34 -0
- pydiffgame-2.0.4/benchmarks/results/robust_seismic_building.gif +0 -0
- pydiffgame-2.0.4/benchmarks/results/robust_seismic_building.json +34 -0
- pydiffgame-2.0.4/benchmarks/robust.py +37 -0
- pydiffgame-2.0.4/benchmarks/robust_compare.py +191 -0
- pydiffgame-2.0.4/benchmarks/run_anarchy.py +61 -0
- pydiffgame-2.0.4/benchmarks/run_masses.py +20 -0
- pydiffgame-2.0.4/benchmarks/run_robust_suite.py +111 -0
- pydiffgame-2.0.4/benchmarks/runner.py +93 -0
- pydiffgame-2.0.4/benchmarks/systems/__init__.py +0 -0
- pydiffgame-2.0.4/benchmarks/systems/masses.py +62 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/docs/README.md +68 -11
- pydiffgame-2.0.4/images/logo.gif +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/pyproject.toml +1 -1
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/__init__.py +3 -1
- pydiffgame-2.0.4/src/PyDiffGame/robust.py +287 -0
- pydiffgame-2.0.4/tests/test_robust.py +105 -0
- pydiffgame-2.0.4/tools/animate_logo.py +356 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/uv.lock +1 -1
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/.github/workflows/python-publish.yml +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/.github/workflows/tests.yml +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/.gitignore +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/.pre-commit-config.yaml +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/CITATIONS.bib +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/CLAUDE.md +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/CODE_OF_CONDUCT.md +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/CONTRIBUTING.md +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/LICENSE +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/_config.yml +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/Logo_ISTRC_Green_English.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/logo.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/logo_abc.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/logo_source.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/readme/masses_cost.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/readme/masses_game_vs_lqr.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/readme/masses_schematic.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/requirements.txt +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/_typing.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/base.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/comparison.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/continuous.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/discrete.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/InvertedPendulumComparison.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/MassesWithSpringsComparison.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/PVTOL.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/PVTOLComparison.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/QuadRotorControl.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/2-players_large_1.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/2-players_large_2.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/LQR_large_1.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/LQR_large_2.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/two_masses_tikz.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/4/4-players_large_1.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/4/4-players_large_2.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/4/LQR_large_1.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/4/LQR_large_2.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/8/8-players_large_1.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/8/8-players_large_2.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/8/LQR_large_1.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/8/LQR_large_2.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL/PVTOL10.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL/PVTOL100.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1000.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL0001.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL001.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL01.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL1.png +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/lqr.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/objective.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/plotting.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/conftest.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_discrete.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_examples.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_game.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_lqr.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_objective.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_simulation.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tools/bump_version.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tools/generate_readme_figures.py +0 -0
- {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tools/render_logo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyDiffGame
|
|
3
|
-
Version: 2.0.
|
|
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.
|
|
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
|
-
* [
|
|
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
|
-
#
|
|
336
|
+
# Robust control: H∞ as a game
|
|
336
337
|
|
|
337
|
-
The
|
|
338
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img alt="PyDiffGame logo" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/logo.
|
|
2
|
+
<img alt="PyDiffGame logo" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/logo.gif" width="420"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* [Quick start](#quick-start)
|
|
25
25
|
* [Input parameters](#input-parameters)
|
|
26
26
|
* [Tutorial: masses on springs](#tutorial-masses-on-springs)
|
|
27
|
-
* [
|
|
27
|
+
* [Robust control: H∞ as a game](#robust-control-h-as-a-game)
|
|
28
|
+
* [Examples — a walkthrough](#examples--a-walkthrough)
|
|
28
29
|
* [Testing and development](#testing-and-development)
|
|
29
30
|
* [Citing](#citing)
|
|
30
31
|
* [Acknowledgments](#acknowledgments)
|
|
@@ -273,17 +274,73 @@ player, without re-tuning one monolithic cost matrix.
|
|
|
273
274
|
> [`tools/generate_readme_figures.py`](https://github.com/krichelj/PyDiffGame/blob/master/tools/generate_readme_figures.py)
|
|
274
275
|
> (`uv run python tools/generate_readme_figures.py`), so they always match the current code.
|
|
275
276
|
|
|
276
|
-
#
|
|
277
|
+
# Robust control: H∞ as a game
|
|
277
278
|
|
|
278
|
-
The
|
|
279
|
-
|
|
279
|
+
A game ties the centralized LQR on a shared cost — it cannot beat it. The place a
|
|
280
|
+
differential game **provably wins** is *robustness*: classical **H∞** state feedback **is**
|
|
281
|
+
the saddle point of a two-player zero-sum game in which the controller minimises and an
|
|
282
|
+
adversarial disturbance maximises. PyDiffGame ships it as `ContinuousHInfinityControl`,
|
|
283
|
+
completing the family next to the LQR and the N-player Nash game:
|
|
280
284
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
```python
|
|
286
|
+
import numpy as np
|
|
287
|
+
from PyDiffGame import ContinuousHInfinityControl
|
|
288
|
+
|
|
289
|
+
A = np.array([[0.0, 1.0], [0.0, 0.0]]) # a cart: position, velocity
|
|
290
|
+
B = np.array([[0.0], [1.0]]) # control force
|
|
291
|
+
B_w = np.array([[0.0], [1.0]]) # disturbance force
|
|
292
|
+
Q, R = np.diag([1.0, 0.0]), np.array([[1.0]])
|
|
293
|
+
|
|
294
|
+
robust = ContinuousHInfinityControl(A, B, B_w, Q, R).solve() # picks gamma = 1.3 * gamma*
|
|
295
|
+
print(robust.K) # robust feedback gain, u = -K x
|
|
296
|
+
print(robust.worst_case_gain()[0]) # closed-loop ||G_zw||inf — provably below the LQR's
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
It solves the **game** algebraic Riccati equation whose quadratic term
|
|
300
|
+
`B R⁻¹ Bᵀ − γ⁻² B_w B_wᵀ` is *indefinite* — exactly what an ordinary LQR Riccati solver
|
|
301
|
+
cannot do — via the Hamiltonian/Schur method, auto-finds the optimal robustness level
|
|
302
|
+
`γ*`, and reports the formal worst-case L2 gain. Across a
|
|
303
|
+
[13-system benchmark](https://github.com/krichelj/PyDiffGame/tree/master/benchmarks)
|
|
304
|
+
(carts, vehicles, aircraft, drones, flexible structures) it reduces the worst-case
|
|
305
|
+
disturbance gain on every system, **practically significantly on 10/13** — most where the
|
|
306
|
+
LQR leaves a sharp resonant peak — each at a documented nominal-cost price:
|
|
307
|
+
|
|
308
|
+
<p align="center">
|
|
309
|
+
<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"/>
|
|
310
|
+
</p>
|
|
311
|
+
|
|
312
|
+
Left: the pendulum-angle response to the worst-case disturbance (H∞ roughly halves
|
|
313
|
+
it). Right: the `σmax(ω)` curves whose peak *is* the worst-case gain — the LQR's
|
|
314
|
+
resonant peak (≈1.92) flattened by the H∞ game to ≈1.25. The
|
|
315
|
+
[**robustness showcase**](https://github.com/krichelj/PyDiffGame/blob/master/benchmarks/README.md)
|
|
316
|
+
animates every system.
|
|
317
|
+
|
|
318
|
+
# Examples — a walkthrough
|
|
319
|
+
|
|
320
|
+
The package ships four worked **LQR-vs-game comparisons** under
|
|
321
|
+
[`src/PyDiffGame/examples`](https://github.com/krichelj/PyDiffGame/tree/master/src/PyDiffGame/examples);
|
|
322
|
+
each builds a system, designs an LQR and a decomposed game on it, runs both and reports the
|
|
323
|
+
costs. Run any of them with `uv run python -m PyDiffGame.examples.<name>`:
|
|
324
|
+
|
|
325
|
+
| Example | System | What it shows |
|
|
326
|
+
| --- | --- | --- |
|
|
327
|
+
| [`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 |
|
|
328
|
+
| [`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 |
|
|
329
|
+
| [`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 |
|
|
330
|
+
| [`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 |
|
|
331
|
+
|
|
332
|
+
For the **full study** — PyDiffGame vs [python-control](https://python-control.readthedocs.io/)
|
|
333
|
+
across 13 systems on both the *nominal* cost (lossless tie / price of anarchy) and the
|
|
334
|
+
*robustness* metric, with rendered GIFs, a metrics report and an adversarial review of the
|
|
335
|
+
methodology — see
|
|
336
|
+
[`benchmarks/`](https://github.com/krichelj/PyDiffGame/tree/master/benchmarks) and its
|
|
337
|
+
[README](https://github.com/krichelj/PyDiffGame/blob/master/benchmarks/README.md):
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
uv run --extra dev python -m benchmarks.run_masses # nominal: game == LQR (lossless)
|
|
341
|
+
uv run --extra dev python -m benchmarks.run_anarchy # nominal: the price of anarchy
|
|
342
|
+
uv run --extra dev python -m benchmarks.run_robust_suite # the 13-system robustness suite + report
|
|
343
|
+
```
|
|
287
344
|
|
|
288
345
|
# Testing and development
|
|
289
346
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# PyDiffGame benchmark study — where a differential game actually helps
|
|
2
|
+
|
|
3
|
+
This directory is an honest, reproducible head-to-head between **PyDiffGame** and
|
|
4
|
+
the **[python-control](https://python-control.readthedocs.io/)** package across a
|
|
5
|
+
catalogue of standard control systems (carts, vehicles, aircraft, drones and
|
|
6
|
+
flexible structures), with rendered GIFs and a formal verification of *where*
|
|
7
|
+
the differential-game approach beats classical optimal control — and where it
|
|
8
|
+
honestly does not.
|
|
9
|
+
|
|
10
|
+
## TL;DR (the honest scientific bottom line)
|
|
11
|
+
|
|
12
|
+
1. **On a single shared quadratic cost, a differential game cannot beat a
|
|
13
|
+
centralized LQR — at best it ties it.** This is not a tuning failure, it is
|
|
14
|
+
theory: the centralized LQR is the optimum of that cost, and a Nash game is a
|
|
15
|
+
*constrained* (decomposed) design, so its cost is `>= LQR` (price of anarchy),
|
|
16
|
+
with equality when the decomposition is lossless. We verify the *lossless*
|
|
17
|
+
case directly: on the masses-on-springs system PyDiffGame's modal game
|
|
18
|
+
reproduces the python-control LQR **to ~5e-12 on every metric**, disturbances
|
|
19
|
+
included. No boost — and we say so.
|
|
20
|
+
|
|
21
|
+
2. **The real, formally-verifiable win is robustness.** Robust (H-infinity)
|
|
22
|
+
control *is* a differential game — the saddle point of a controller-vs-
|
|
23
|
+
adversarial-disturbance zero-sum game. PyDiffGame now ships this as
|
|
24
|
+
`ContinuousHInfinityControl`, and it **provably reduces the worst-case
|
|
25
|
+
disturbance gain** an LQR leaves on the table — at a documented nominal-cost
|
|
26
|
+
price, and only when the plant has worst-case gain to recover.
|
|
27
|
+
|
|
28
|
+
## What is measured
|
|
29
|
+
|
|
30
|
+
For every system we design two state-feedback controllers on the **same**
|
|
31
|
+
weights `(Q, R)`:
|
|
32
|
+
|
|
33
|
+
| controller | what it optimizes |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `control.lqr` (python-control) | nominal cost (no disturbance) |
|
|
36
|
+
| `PyDiffGame.ContinuousHInfinityControl` | worst-case disturbance gain (the game) |
|
|
37
|
+
|
|
38
|
+
and report, on the same closed loop:
|
|
39
|
+
|
|
40
|
+
- **`‖G_zw‖∞`** — the closed-loop worst-case L2 gain from the disturbance to the
|
|
41
|
+
weighted performance output `z = [Q^{1/2}x; R^{1/2}u]` (the formal robustness
|
|
42
|
+
metric; lower is more robust). Computed slycot-free by a refined frequency
|
|
43
|
+
sweep.
|
|
44
|
+
- **time-domain peak** of the output under the single worst-case sinusoidal
|
|
45
|
+
disturbance (at the LQR's most vulnerable frequency).
|
|
46
|
+
- **nominal LQ cost penalty** — how much nominal performance the robust design
|
|
47
|
+
gives up (always `>= 0`, since the LQR is the nominal optimum; this is the
|
|
48
|
+
*price* of robustness, reported honestly alongside the gain).
|
|
49
|
+
|
|
50
|
+
## Nominal regime (two honest outcomes)
|
|
51
|
+
|
|
52
|
+
On the shared cost a game never beats the centralized LQR; it either ties it or
|
|
53
|
+
pays a small price. Both happen, and both are shown:
|
|
54
|
+
|
|
55
|
+
- **Lossless tie** — `run_masses.py`: with the *modal* decomposition the
|
|
56
|
+
objectives decouple, so PyDiffGame's Nash game reproduces the LQR to ~5e-12 on
|
|
57
|
+
every metric (`results/masses_pdg_vs_lqr.gif`).
|
|
58
|
+
- **Price of anarchy** — `run_anarchy.py`: two carts with *competing*
|
|
59
|
+
per-cart objectives and one actuator each. The decentralized Nash equilibrium
|
|
60
|
+
costs **+0.33%** on the joint objective vs the centralized LQR — while using
|
|
61
|
+
*less* control energy and overshoot (`results/coupled_carts_anarchy.gif`). The
|
|
62
|
+
tiny price buys decentralization and compositionality.
|
|
63
|
+
|
|
64
|
+
## Results (robustness)
|
|
65
|
+
|
|
66
|
+
See [`results/ROBUSTNESS_REPORT.md`](results/ROBUSTNESS_REPORT.md) for the full
|
|
67
|
+
auto-generated table, and `results/robust_<system>.gif` for each animation
|
|
68
|
+
(left: time response to the worst-case disturbance; right: the `σmax(ω)` curves
|
|
69
|
+
whose peak *is* `‖G_zw‖∞`).
|
|
70
|
+
|
|
71
|
+
<p align="center">
|
|
72
|
+
<img alt="H-infinity game vs LQR under worst-case disturbance (inverted pendulum)" src="results/robust_inverted_pendulum.gif" width="820"/>
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
*Inverted pendulum: the LQR leaves a sharp resonant peak (`σmax ≈ 1.92`) that the
|
|
76
|
+
H∞ game flattens to `≈ 1.25` (−35% worst-case gain), roughly halving the
|
|
77
|
+
pendulum-angle response to the worst-case disturbance — at a documented nominal-cost
|
|
78
|
+
price. The honest high-frequency trade-off (H∞ slightly higher past the peak) is
|
|
79
|
+
visible too.*
|
|
80
|
+
|
|
81
|
+
Headline (honest): all 13 systems show a *relative* worst-case-gain reduction,
|
|
82
|
+
but only the ones with a **non-negligible absolute gain** matter in practice —
|
|
83
|
+
**10/13 are practically significant** (inverted pendulum +35%, PVTOL/quadrotor
|
|
84
|
+
+26%, seismic building +24%, flexible two-mass / cart / DC motor ~+22%, gantry
|
|
85
|
+
crane / active suspension / aircraft ~+15%, ...), exactly the lightly-damped and
|
|
86
|
+
unstable plants where the LQR leaves a sharp resonant peak. The two cars
|
|
87
|
+
(cruise, bicycle) have an absolute worst-case gain of ~0 — the disturbance is
|
|
88
|
+
already rejected by *any* reasonable controller — so their real relative
|
|
89
|
+
reductions are **not practically meaningful**, and we say so rather than headline
|
|
90
|
+
a "10/10 win". (The bicycle's earlier apparent "tie" turned out to be a `γ*`
|
|
91
|
+
numerical artifact, caught by the methodology review and fixed; the corrected
|
|
92
|
+
result is a real-but-immaterial relative reduction.)
|
|
93
|
+
|
|
94
|
+
## Reproduce
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
uv run --extra dev python -m benchmarks.run_masses # nominal: game == LQR (lossless)
|
|
98
|
+
uv run --extra dev python -m benchmarks.robust_compare # one robust comparison + GIF
|
|
99
|
+
uv run --extra dev python -m benchmarks.run_robust_suite # the full 10-system suite + report
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Rigor
|
|
103
|
+
|
|
104
|
+
The catalogue models were verified entry-for-entry against the controls /
|
|
105
|
+
vehicle-dynamics / flight-dynamics literature, and the comparison methodology
|
|
106
|
+
(GARE solve, `γ*` search, the worst-case-gain metric, the nominal-cost
|
|
107
|
+
accounting, and the fairness of scoring both controllers on the same output) was
|
|
108
|
+
adversarially reviewed; the review hardened PyDiffGame's `γ*` search against a
|
|
109
|
+
boundary numerical instability (now regression-tested).
|
|
File without changes
|