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.
Files changed (118) hide show
  1. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/PKG-INFO +69 -12
  2. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/README.md +68 -11
  3. pydiffgame-2.0.4/benchmarks/README.md +109 -0
  4. pydiffgame-2.0.4/benchmarks/__init__.py +0 -0
  5. pydiffgame-2.0.4/benchmarks/catalog.py +375 -0
  6. pydiffgame-2.0.4/benchmarks/harness.py +340 -0
  7. pydiffgame-2.0.4/benchmarks/pdg_design.py +47 -0
  8. pydiffgame-2.0.4/benchmarks/results/ROBUSTNESS_REPORT.md +33 -0
  9. pydiffgame-2.0.4/benchmarks/results/coupled_carts_anarchy.gif +0 -0
  10. pydiffgame-2.0.4/benchmarks/results/coupled_carts_anarchy.json +54 -0
  11. pydiffgame-2.0.4/benchmarks/results/masses_pdg_vs_lqr.gif +0 -0
  12. pydiffgame-2.0.4/benchmarks/results/masses_pdg_vs_lqr.json +54 -0
  13. pydiffgame-2.0.4/benchmarks/results/robust_DC_motor_servo.gif +0 -0
  14. pydiffgame-2.0.4/benchmarks/results/robust_DC_motor_servo.json +32 -0
  15. pydiffgame-2.0.4/benchmarks/results/robust_PVTOL_aircraft.gif +0 -0
  16. pydiffgame-2.0.4/benchmarks/results/robust_PVTOL_aircraft.json +54 -0
  17. pydiffgame-2.0.4/benchmarks/results/robust_active_suspension.gif +0 -0
  18. pydiffgame-2.0.4/benchmarks/results/robust_active_suspension.json +34 -0
  19. pydiffgame-2.0.4/benchmarks/results/robust_aircraft_short-period.gif +0 -0
  20. pydiffgame-2.0.4/benchmarks/results/robust_aircraft_short-period.json +30 -0
  21. pydiffgame-2.0.4/benchmarks/results/robust_car_cruise_control.gif +0 -0
  22. pydiffgame-2.0.4/benchmarks/results/robust_car_cruise_control.json +30 -0
  23. pydiffgame-2.0.4/benchmarks/results/robust_car_lateral_(bicycle).gif +0 -0
  24. pydiffgame-2.0.4/benchmarks/results/robust_car_lateral_(bicycle).json +30 -0
  25. pydiffgame-2.0.4/benchmarks/results/robust_cart_(double_integrator).gif +0 -0
  26. pydiffgame-2.0.4/benchmarks/results/robust_cart_(double_integrator).json +30 -0
  27. pydiffgame-2.0.4/benchmarks/results/robust_cart_with_disturbance.gif +0 -0
  28. pydiffgame-2.0.4/benchmarks/results/robust_cart_with_disturbance.json +31 -0
  29. pydiffgame-2.0.4/benchmarks/results/robust_flexible_two-mass.gif +0 -0
  30. pydiffgame-2.0.4/benchmarks/results/robust_flexible_two-mass.json +34 -0
  31. pydiffgame-2.0.4/benchmarks/results/robust_gantry_crane.gif +0 -0
  32. pydiffgame-2.0.4/benchmarks/results/robust_gantry_crane.json +34 -0
  33. pydiffgame-2.0.4/benchmarks/results/robust_inverted_pendulum.gif +0 -0
  34. pydiffgame-2.0.4/benchmarks/results/robust_inverted_pendulum.json +34 -0
  35. pydiffgame-2.0.4/benchmarks/results/robust_mass-spring-damper.gif +0 -0
  36. pydiffgame-2.0.4/benchmarks/results/robust_mass-spring-damper.json +30 -0
  37. pydiffgame-2.0.4/benchmarks/results/robust_quadrotor_(planar).gif +0 -0
  38. pydiffgame-2.0.4/benchmarks/results/robust_quadrotor_(planar).json +34 -0
  39. pydiffgame-2.0.4/benchmarks/results/robust_seismic_building.gif +0 -0
  40. pydiffgame-2.0.4/benchmarks/results/robust_seismic_building.json +34 -0
  41. pydiffgame-2.0.4/benchmarks/robust.py +37 -0
  42. pydiffgame-2.0.4/benchmarks/robust_compare.py +191 -0
  43. pydiffgame-2.0.4/benchmarks/run_anarchy.py +61 -0
  44. pydiffgame-2.0.4/benchmarks/run_masses.py +20 -0
  45. pydiffgame-2.0.4/benchmarks/run_robust_suite.py +111 -0
  46. pydiffgame-2.0.4/benchmarks/runner.py +93 -0
  47. pydiffgame-2.0.4/benchmarks/systems/__init__.py +0 -0
  48. pydiffgame-2.0.4/benchmarks/systems/masses.py +62 -0
  49. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/docs/README.md +68 -11
  50. pydiffgame-2.0.4/images/logo.gif +0 -0
  51. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/pyproject.toml +1 -1
  52. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/__init__.py +3 -1
  53. pydiffgame-2.0.4/src/PyDiffGame/robust.py +287 -0
  54. pydiffgame-2.0.4/tests/test_robust.py +105 -0
  55. pydiffgame-2.0.4/tools/animate_logo.py +356 -0
  56. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/uv.lock +1 -1
  57. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/.github/workflows/python-publish.yml +0 -0
  58. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/.github/workflows/tests.yml +0 -0
  59. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/.gitignore +0 -0
  60. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/.pre-commit-config.yaml +0 -0
  61. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/CITATIONS.bib +0 -0
  62. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/CLAUDE.md +0 -0
  63. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/CODE_OF_CONDUCT.md +0 -0
  64. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/CONTRIBUTING.md +0 -0
  65. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/LICENSE +0 -0
  66. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/_config.yml +0 -0
  67. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/Logo_ISTRC_Green_English.png +0 -0
  68. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/logo.png +0 -0
  69. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/logo_abc.png +0 -0
  70. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/logo_source.png +0 -0
  71. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/readme/masses_cost.png +0 -0
  72. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/readme/masses_game_vs_lqr.png +0 -0
  73. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/images/readme/masses_schematic.png +0 -0
  74. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/requirements.txt +0 -0
  75. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/_typing.py +0 -0
  76. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/base.py +0 -0
  77. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/comparison.py +0 -0
  78. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/continuous.py +0 -0
  79. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/discrete.py +0 -0
  80. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/InvertedPendulumComparison.py +0 -0
  81. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/MassesWithSpringsComparison.py +0 -0
  82. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/PVTOL.py +0 -0
  83. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/PVTOLComparison.py +0 -0
  84. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/QuadRotorControl.py +0 -0
  85. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/2-players_large_1.png +0 -0
  86. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/2-players_large_2.png +0 -0
  87. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/LQR_large_1.png +0 -0
  88. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/LQR_large_2.png +0 -0
  89. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/2/two_masses_tikz.png +0 -0
  90. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/4/4-players_large_1.png +0 -0
  91. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/4/4-players_large_2.png +0 -0
  92. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/4/LQR_large_1.png +0 -0
  93. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/4/LQR_large_2.png +0 -0
  94. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/8/8-players_large_1.png +0 -0
  95. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/8/8-players_large_2.png +0 -0
  96. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/8/LQR_large_1.png +0 -0
  97. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/8/LQR_large_2.png +0 -0
  98. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1.png +0 -0
  99. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL/PVTOL10.png +0 -0
  100. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL/PVTOL100.png +0 -0
  101. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1000.png +0 -0
  102. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL0001.png +0 -0
  103. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL001.png +0 -0
  104. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL01.png +0 -0
  105. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/examples/figures/PVTOL1.png +0 -0
  106. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/lqr.py +0 -0
  107. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/objective.py +0 -0
  108. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/src/PyDiffGame/plotting.py +0 -0
  109. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/conftest.py +0 -0
  110. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_discrete.py +0 -0
  111. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_examples.py +0 -0
  112. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_game.py +0 -0
  113. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_lqr.py +0 -0
  114. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_objective.py +0 -0
  115. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tests/test_simulation.py +0 -0
  116. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tools/bump_version.py +0 -0
  117. {pydiffgame-2.0.3 → pydiffgame-2.0.4}/tools/generate_readme_figures.py +0 -0
  118. {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
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,5 +1,5 @@
1
1
  <p align="center">
2
- <img alt="PyDiffGame logo" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/logo.png" width="420"/>
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
- * [More examples](#more-examples)
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
- # More examples
277
+ # Robust control: H∞ as a game
277
278
 
278
- The [`src/PyDiffGame/examples`](https://github.com/krichelj/PyDiffGame/tree/master/src/PyDiffGame/examples)
279
- directory contains further worked comparisons:
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
- | Example | System |
282
- | --- | --- |
283
- | [`MassesWithSpringsComparison.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/MassesWithSpringsComparison.py) | Chain of masses coupled by springs (the tutorial above) |
284
- | [`InvertedPendulumComparison.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/InvertedPendulumComparison.py) | Inverted pendulum on a cart |
285
- | [`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 |
286
- | [`QuadRotorControl.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/QuadRotorControl.py) | Quadrotor attitude / position control |
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