PyDiffGame 0.1.2__tar.gz → 2.0.0__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 (82) hide show
  1. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/.github/workflows/python-publish.yml +8 -7
  2. pydiffgame-2.0.0/.github/workflows/tests.yml +51 -0
  3. pydiffgame-2.0.0/.gitignore +26 -0
  4. pydiffgame-2.0.0/.pre-commit-config.yaml +29 -0
  5. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/CODE_OF_CONDUCT.md +1 -1
  6. pydiffgame-2.0.0/CONTRIBUTING.md +44 -0
  7. pydiffgame-2.0.0/PKG-INFO +408 -0
  8. pydiffgame-2.0.0/README.md +349 -0
  9. pydiffgame-2.0.0/docs/README.md +266 -0
  10. pydiffgame-2.0.0/images/readme/masses_cost.png +0 -0
  11. pydiffgame-2.0.0/images/readme/masses_game_vs_lqr.png +0 -0
  12. pydiffgame-2.0.0/images/readme/masses_schematic.png +0 -0
  13. pydiffgame-2.0.0/pyproject.toml +94 -0
  14. pydiffgame-2.0.0/requirements.txt +8 -0
  15. pydiffgame-2.0.0/src/PyDiffGame/__init__.py +50 -0
  16. pydiffgame-2.0.0/src/PyDiffGame/_typing.py +25 -0
  17. pydiffgame-2.0.0/src/PyDiffGame/base.py +468 -0
  18. pydiffgame-2.0.0/src/PyDiffGame/comparison.py +121 -0
  19. pydiffgame-2.0.0/src/PyDiffGame/continuous.py +223 -0
  20. pydiffgame-2.0.0/src/PyDiffGame/discrete.py +211 -0
  21. pydiffgame-2.0.0/src/PyDiffGame/examples/InvertedPendulumComparison.py +232 -0
  22. pydiffgame-2.0.0/src/PyDiffGame/examples/MassesWithSpringsComparison.py +119 -0
  23. pydiffgame-2.0.0/src/PyDiffGame/examples/PVTOL.py +216 -0
  24. pydiffgame-2.0.0/src/PyDiffGame/examples/PVTOLComparison.py +117 -0
  25. pydiffgame-2.0.0/src/PyDiffGame/examples/QuadRotorControl.py +638 -0
  26. pydiffgame-2.0.0/src/PyDiffGame/lqr.py +30 -0
  27. pydiffgame-2.0.0/src/PyDiffGame/objective.py +108 -0
  28. pydiffgame-2.0.0/src/PyDiffGame/plotting.py +98 -0
  29. pydiffgame-2.0.0/tests/conftest.py +9 -0
  30. pydiffgame-2.0.0/tests/test_discrete.py +50 -0
  31. pydiffgame-2.0.0/tests/test_examples.py +75 -0
  32. pydiffgame-2.0.0/tests/test_game.py +45 -0
  33. pydiffgame-2.0.0/tests/test_lqr.py +66 -0
  34. pydiffgame-2.0.0/tests/test_objective.py +45 -0
  35. pydiffgame-2.0.0/tests/test_simulation.py +60 -0
  36. pydiffgame-2.0.0/tools/generate_readme_figures.py +336 -0
  37. pydiffgame-2.0.0/uv.lock +1298 -0
  38. pydiffgame-0.1.2/CONTRIBUTING.md +0 -18
  39. pydiffgame-0.1.2/PKG-INFO +0 -306
  40. pydiffgame-0.1.2/README.md +0 -292
  41. pydiffgame-0.1.2/docs/README.md +0 -277
  42. pydiffgame-0.1.2/pyproject.toml +0 -26
  43. pydiffgame-0.1.2/requirements.txt +0 -6
  44. pydiffgame-0.1.2/src/PyDiffGame/ContinuousPyDiffGame.py +0 -275
  45. pydiffgame-0.1.2/src/PyDiffGame/DiscretePyDiffGame.py +0 -359
  46. pydiffgame-0.1.2/src/PyDiffGame/LQR.py +0 -73
  47. pydiffgame-0.1.2/src/PyDiffGame/Objective.py +0 -62
  48. pydiffgame-0.1.2/src/PyDiffGame/PyDiffGame.py +0 -1273
  49. pydiffgame-0.1.2/src/PyDiffGame/PyDiffGameLQRComparison.py +0 -169
  50. pydiffgame-0.1.2/src/PyDiffGame/__init__.py +0 -0
  51. pydiffgame-0.1.2/src/PyDiffGame/examples/InvertedPendulumComparison.py +0 -257
  52. pydiffgame-0.1.2/src/PyDiffGame/examples/MassesWithSpringsComparison.py +0 -218
  53. pydiffgame-0.1.2/src/PyDiffGame/examples/PVTOL.py +0 -222
  54. pydiffgame-0.1.2/src/PyDiffGame/examples/PVTOLComparison.py +0 -111
  55. pydiffgame-0.1.2/src/PyDiffGame/examples/QuadRotorControl.py +0 -548
  56. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/CITATIONS.bib +0 -0
  57. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/LICENSE +0 -0
  58. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/_config.yml +0 -0
  59. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/images/Logo_ISTRC_Green_English.png +0 -0
  60. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/images/logo.png +0 -0
  61. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/images/logo_abc.png +0 -0
  62. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/2/2-players_large_1.png +0 -0
  63. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/2/2-players_large_2.png +0 -0
  64. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/2/LQR_large_1.png +0 -0
  65. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/2/LQR_large_2.png +0 -0
  66. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/2/two_masses_tikz.png +0 -0
  67. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/4/4-players_large_1.png +0 -0
  68. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/4/4-players_large_2.png +0 -0
  69. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/4/LQR_large_1.png +0 -0
  70. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/4/LQR_large_2.png +0 -0
  71. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/8/8-players_large_1.png +0 -0
  72. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/8/8-players_large_2.png +0 -0
  73. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/8/LQR_large_1.png +0 -0
  74. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/8/LQR_large_2.png +0 -0
  75. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1.png +0 -0
  76. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/PVTOL/PVTOL10.png +0 -0
  77. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/PVTOL/PVTOL100.png +0 -0
  78. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1000.png +0 -0
  79. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/PVTOL0001.png +0 -0
  80. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/PVTOL001.png +0 -0
  81. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/PVTOL01.png +0 -0
  82. {pydiffgame-0.1.2 → pydiffgame-2.0.0}/src/PyDiffGame/examples/figures/PVTOL1.png +0 -0
@@ -11,6 +11,7 @@ name: Upload Python Package
11
11
  on:
12
12
  release:
13
13
  types: [published]
14
+ workflow_dispatch:
14
15
 
15
16
  permissions:
16
17
  contents: read
@@ -22,15 +23,11 @@ jobs:
22
23
  steps:
23
24
  - uses: actions/checkout@v4
24
25
 
25
- - uses: actions/setup-python@v5
26
- with:
27
- python-version: "3.10"
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v4
28
28
 
29
29
  - name: Build release distributions
30
- run: |
31
- # NOTE: put your own distribution build steps here.
32
- python -m pip install build
33
- python -m build
30
+ run: uv build
34
31
 
35
32
  - name: Upload distributions
36
33
  uses: actions/upload-artifact@v4
@@ -64,6 +61,10 @@ jobs:
64
61
  name: release-dists
65
62
  path: dist/
66
63
 
64
+ # Authentication is via PyPI Trusted Publishing (OIDC) — no token needed.
65
+ # Configure the trusted publisher once on PyPI (see below) with:
66
+ # owner: krichelj repo: PyDiffGame
67
+ # workflow: python-publish.yml environment: pypi
67
68
  - name: Publish release distributions to PyPI
68
69
  uses: pypa/gh-action-pypi-publish@release/v1
69
70
  with:
@@ -0,0 +1,51 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [master, main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: tests-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ test:
18
+ name: Python ${{ matrix.python-version }}
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
24
+
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v4
30
+ with:
31
+ enable-cache: true
32
+
33
+ - name: Set up Python ${{ matrix.python-version }}
34
+ run: uv python install ${{ matrix.python-version }}
35
+
36
+ - name: Sync the environment (locked)
37
+ run: uv sync --extra dev --python ${{ matrix.python-version }} --frozen
38
+
39
+ - name: Lint with ruff
40
+ run: uv run ruff check src/PyDiffGame tests --exclude src/PyDiffGame/examples
41
+
42
+ - name: Check formatting with ruff
43
+ run: uv run ruff format --check src/PyDiffGame tests
44
+
45
+ - name: Type-check with mypy
46
+ run: uv run mypy src/PyDiffGame --exclude 'examples'
47
+
48
+ - name: Run the test suite
49
+ env:
50
+ MPLBACKEND: Agg
51
+ run: uv run pytest -q
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ .eggs/
7
+ build/
8
+ dist/
9
+ *.so
10
+
11
+ # Virtual environments
12
+ .venv*/
13
+ venv/
14
+ env/
15
+
16
+ # Test / coverage
17
+ .pytest_cache/
18
+ .coverage
19
+ htmlcov/
20
+ .mypy_cache/
21
+ .ruff_cache/
22
+
23
+ # OS / editors
24
+ .DS_Store
25
+ .idea/
26
+ .vscode/
@@ -0,0 +1,29 @@
1
+ # Pre-commit hooks for PyDiffGame.
2
+ # Install once with: pre-commit install
3
+ # Run on everything: pre-commit run --all-files
4
+ repos:
5
+ - repo: https://github.com/pre-commit/pre-commit-hooks
6
+ rev: v5.0.0
7
+ hooks:
8
+ - id: trailing-whitespace
9
+ - id: end-of-file-fixer
10
+ - id: check-yaml
11
+ - id: check-toml
12
+ - id: check-added-large-files
13
+ - id: check-merge-conflict
14
+
15
+ - repo: https://github.com/astral-sh/ruff-pre-commit
16
+ rev: v0.8.4
17
+ hooks:
18
+ # Linter (with autofix), then the black-compatible formatter.
19
+ - id: ruff
20
+ args: [--fix]
21
+ - id: ruff-format
22
+
23
+ - repo: https://github.com/pre-commit/mirrors-mypy
24
+ rev: v1.13.0
25
+ hooks:
26
+ - id: mypy
27
+ files: ^src/PyDiffGame/
28
+ exclude: ^src/PyDiffGame/examples/
29
+ additional_dependencies: [numpy]
@@ -60,7 +60,7 @@ representative at an online or offline event.
60
60
 
61
61
  Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
62
  reported to the community leaders responsible for enforcement at
63
- .
63
+ skricheli2@gmail.com.
64
64
  All complaints will be reviewed and investigated promptly and fairly.
65
65
 
66
66
  All community leaders are obligated to respect the privacy and security of the
@@ -0,0 +1,44 @@
1
+ # Contribution Guidelines
2
+
3
+ This repo is part of a research conducted at Ben Gurion University,
4
+ and is thus open source. We would love to receive your input!
5
+
6
+ Please ensure your pull request adheres to the following guidelines:
7
+
8
+ - Search previous suggestions before making a new one, as yours may be a duplicate.
9
+ - Make an individual pull request for each suggestion.
10
+ - New categories or improvements to the existing categorization are welcome.
11
+ - Check your spelling and grammar.
12
+ - Make sure your text editor is set to remove trailing whitespace.
13
+ - The pull request and commit should have a useful title.
14
+
15
+ ## Development setup
16
+
17
+ PyDiffGame requires **Python >= 3.11** and uses [uv](https://docs.astral.sh/uv/)
18
+ for environment and dependency management. Install uv, sync the locked
19
+ development environment, and enable the pre-commit hooks so the code-quality
20
+ tools run automatically on every commit:
21
+
22
+ ```bash
23
+ # install uv: https://docs.astral.sh/uv/getting-started/installation/
24
+ uv sync --extra dev
25
+ uv run pre-commit install
26
+ ```
27
+
28
+ `uv sync` creates the virtual environment and installs the exact, locked
29
+ dependencies. Run the tooling through `uv run`:
30
+
31
+ ```bash
32
+ uv run ruff format src/PyDiffGame tests # auto-format (black-compatible)
33
+ uv run ruff check src/PyDiffGame tests # lint
34
+ uv run mypy src/PyDiffGame # type-check
35
+ uv run pytest # test suite
36
+ ```
37
+
38
+ Continuous integration runs the formatter check, the linter, the type checker
39
+ and the full suite (all via uv) across Python 3.11–3.14, so please make sure
40
+ they pass locally.
41
+
42
+ Thank you for your contribution!
43
+
44
+ Joshua Shay Kricheli
@@ -0,0 +1,408 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyDiffGame
3
+ Version: 2.0.0
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
+ Project-URL: Homepage, https://krichelj.github.io/PyDiffGame/
6
+ Project-URL: Repository, https://github.com/krichelj/PyDiffGame
7
+ Project-URL: Bug Tracker, https://github.com/krichelj/PyDiffGame/issues
8
+ Author-email: Joshua Shay Kricheli <skricheli2@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2021-2024 Joshua Shay Kricheli, Dr. Aviran Sadon, Dr. Shai Arogeti and Prof. Gera Weiss
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: control-theory,differential-games,lqr,nash-equilibrium,optimal-control,riccati
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3.14
41
+ Classifier: Topic :: Scientific/Engineering
42
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
43
+ Classifier: Typing :: Typed
44
+ Requires-Python: >=3.11
45
+ Requires-Dist: matplotlib>=3.7
46
+ Requires-Dist: numpy>=1.24
47
+ Requires-Dist: scipy>=1.10
48
+ Requires-Dist: tqdm>=4.63
49
+ Provides-Extra: dev
50
+ Requires-Dist: control>=0.10; extra == 'dev'
51
+ Requires-Dist: mypy>=1.10; extra == 'dev'
52
+ Requires-Dist: pre-commit>=3.7; extra == 'dev'
53
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
54
+ Requires-Dist: pytest>=8.0; extra == 'dev'
55
+ Requires-Dist: ruff>=0.6; extra == 'dev'
56
+ Provides-Extra: examples
57
+ Requires-Dist: control>=0.10; extra == 'examples'
58
+ Description-Content-Type: text/markdown
59
+
60
+ <p align="center">
61
+ <img alt="PyDiffGame logo" src="images/logo.png" width="420"/>
62
+ </p>
63
+
64
+ <p align="center">
65
+ <i>Nash-equilibrium control for multi-objective dynamical systems, built on coupled Riccati equations.</i>
66
+ </p>
67
+
68
+ <p align="center">
69
+ <a href="https://github.com/krichelj/PyDiffGame/actions/workflows/tests.yml"><img alt="Tests" src="https://github.com/krichelj/PyDiffGame/actions/workflows/tests.yml/badge.svg?branch=master"></a>
70
+ <a href="https://pypi.org/project/PyDiffGame/"><img alt="PyPI" src="https://img.shields.io/pypi/v/PyDiffGame.svg"></a>
71
+ <a href="https://www.python.org/"><img alt="Python" src="https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue.svg"></a>
72
+ <a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg"></a>
73
+ <a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"></a>
74
+ <a href="https://mypy-lang.org/"><img alt="Checked with mypy" src="https://img.shields.io/badge/mypy-checked-2a6db2.svg"></a>
75
+ <a href="https://docs.astral.sh/uv/"><img alt="uv" src="https://img.shields.io/badge/managed%20with-uv-261230.svg"></a>
76
+ </p>
77
+
78
+ ---
79
+
80
+ * [What is this?](#what-is-this)
81
+ * [Why differential games?](#why-differential-games)
82
+ * [Installation](#installation)
83
+ * [Quick start](#quick-start)
84
+ * [Input parameters](#input-parameters)
85
+ * [Tutorial: masses on springs](#tutorial-masses-on-springs)
86
+ * [More examples](#more-examples)
87
+ * [Testing and development](#testing-and-development)
88
+ * [Citing](#citing)
89
+ * [Acknowledgments](#acknowledgments)
90
+ * [Star history](#star-history)
91
+
92
+ # What is this?
93
+
94
+ [`PyDiffGame`](https://github.com/krichelj/PyDiffGame) is a Python implementation of a
95
+ **Nash-equilibrium solution to differential games**. It reduces the Game
96
+ Hamilton–Jacobi–Bellman (GHJB) equations to a set of *coupled* Game Algebraic and
97
+ Differential Riccati equations, and solves them to synthesize feedback controllers for
98
+ multi-objective dynamical control systems.
99
+
100
+ In one sentence: where a classical Linear-Quadratic Regulator (LQR) folds every control
101
+ task into **one** quadratic cost and solves **one** Riccati equation, `PyDiffGame` keeps
102
+ each task as a separate *player*, and solves the coupled Riccati system whose fixed point
103
+ is their Nash equilibrium. A plain LQR is simply the one-player special case.
104
+
105
+ The method follows the formulation in:
106
+
107
+ - The thesis _“Differential Games for Compositional Handling of Competing Control Tasks”_
108
+ ([ResearchGate](https://www.researchgate.net/publication/359819808_Differential_Games_for_Compositional_Handling_of_Competing_Control_Tasks))
109
+ - The conference paper _“Composition of Dynamic Control Objectives Based on Differential Games”_
110
+ ([IEEE](https://ieeexplore.ieee.org/document/9480269) ·
111
+ [ResearchGate](https://www.researchgate.net/publication/353452024_Composition_of_Dynamic_Control_Objectives_Based_on_Differential_Games))
112
+
113
+ The package requires **Python ≥ 3.11** and is tested on CPython 3.11, 3.12, 3.13 and 3.14.
114
+
115
+ # Why differential games?
116
+
117
+ | Classical LQR | `PyDiffGame` |
118
+ | --- | --- |
119
+ | A single, hand-blended cost $\int x^\top Q x + u^\top R u$ | One objective **per task / player**, each with its own $Q_i, R_i$ |
120
+ | One Algebraic Riccati Equation | A **coupled** system of Riccati equations solved to its Nash fixed point |
121
+ | Re-tune the whole weight matrix to add a task | **Compose** tasks by adding a player — the others keep their own cost |
122
+ | Continuous time only, by convention | Continuous **and** discrete time, finite or infinite horizon |
123
+
124
+ `PyDiffGame` ships both regulation (drive the state to the origin) and **signal tracking**
125
+ (drive the state to a target $x_T$), a one-call **LQR-vs-game comparison** harness, cost
126
+ accounting on a common yardstick, and ready-made plotting.
127
+
128
+ # Installation
129
+
130
+ Install the latest release from PyPI:
131
+
132
+ ```bash
133
+ pip install PyDiffGame
134
+ ```
135
+
136
+ To run the bundled examples (which additionally need
137
+ [`python-control`](https://python-control.readthedocs.io/)):
138
+
139
+ ```bash
140
+ pip install "PyDiffGame[examples]"
141
+ ```
142
+
143
+ To work on the package itself, this project is managed with
144
+ [**uv**](https://docs.astral.sh/uv/). Clone it and sync the locked development
145
+ environment:
146
+
147
+ ```bash
148
+ git clone https://github.com/krichelj/PyDiffGame.git
149
+ cd PyDiffGame
150
+ uv sync --extra dev # creates .venv with the exact locked dependencies
151
+ uv run pre-commit install # enable the formatting / lint / type-check hooks
152
+ ```
153
+
154
+ Then run anything through `uv run` (`uv run pytest`, `uv run python -m PyDiffGame.examples.MassesWithSpringsComparison`, …).
155
+
156
+ # Quick start
157
+
158
+ A plain Linear-Quadratic Regulator is just a one-objective game. The continuous solver
159
+ matches `scipy.linalg.solve_continuous_are` exactly:
160
+
161
+ ```python
162
+ import numpy as np
163
+ from PyDiffGame import ContinuousLQR
164
+
165
+ A = np.array([[0.0, 1.0],
166
+ [0.0, 0.0]])
167
+ B = np.array([[0.0],
168
+ [1.0]])
169
+
170
+ lqr = ContinuousLQR(A=A, B=B, Q=np.eye(2), R=1.0).solve()
171
+
172
+ print(lqr.K[0]) # optimal feedback gain
173
+ print(lqr.is_closed_loop_stable()) # True
174
+ ```
175
+
176
+ For a multi-player differential game, give one [`Objective`](#input-parameters) per
177
+ player. Each player owns a slice of the physical input through a decomposition matrix `M`:
178
+
179
+ ```python
180
+ import numpy as np
181
+ from PyDiffGame import ContinuousPyDiffGame, GameObjective
182
+
183
+ A = np.array([[0.0, 1.0, 0.0, 0.0],
184
+ [0.0, 0.0, 0.0, 0.0],
185
+ [0.0, 0.0, 0.0, 1.0],
186
+ [0.0, 0.0, 0.0, 0.0]])
187
+ B = np.eye(4)[:, [1, 3]]
188
+
189
+ objectives = [
190
+ GameObjective(Q=np.diag([1.0, 0.1, 0.0, 0.0]), R=1.0, M=np.array([[1.0, 0.0]])),
191
+ GameObjective(Q=np.diag([0.0, 0.0, 1.0, 0.1]), R=1.0, M=np.array([[0.0, 1.0]])),
192
+ ]
193
+
194
+ game = ContinuousPyDiffGame(A=A, objectives=objectives, B=B).solve()
195
+
196
+ # Each converged P_i drives its coupled algebraic Riccati residual to ~0:
197
+ print(max(np.max(np.abs(r)) for r in game.algebraic_riccati_residuals())) # ~1e-14
198
+ print(game.is_closed_loop_stable()) # True
199
+ ```
200
+
201
+ # Input parameters
202
+
203
+ A game is described by a system matrix `A`, a set of
204
+ [`Objective`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/objective.py)
205
+ objects (one per player), and an input description (`B` together with each objective's
206
+ decomposition matrix `M`, or per-player matrices `Bs`). Construct one of the concrete
207
+ solvers —
208
+ [`ContinuousPyDiffGame`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/continuous.py)
209
+ or
210
+ [`DiscretePyDiffGame`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/discrete.py)
211
+ — with the following parameters:
212
+
213
+ | Parameter | Type | Meaning |
214
+ | --- | --- | --- |
215
+ | `A` | `np.ndarray` $(n, n)$ | System dynamics matrix |
216
+ | `objectives` | `Sequence[Objective]` of length $N$ | One objective per player; each carries $Q_i$, $R_{ii}$ and optionally $M_i$ |
217
+ | `B` | `np.ndarray` $(n, m)$, optional | Full input matrix, used with each objective's decomposition matrix $M_i$ |
218
+ | `Bs` | `Sequence[np.ndarray]`, optional | Per-player input matrices $B_i$ of shape $(n, m_i)$ — an alternative to `B` + `M` |
219
+ | `x_0` | `np.ndarray` $(n,)$, optional | Initial state vector |
220
+ | `x_T` | `np.ndarray` $(n,)$, optional | Final (target) state for signal tracking |
221
+ | `T_f` | positive `float`, optional | Finite-horizon length; omit (or `None`) for the infinite-horizon problem |
222
+ | `P_f` | `Sequence[np.ndarray]`, optional | Terminal Riccati condition (default: uncoupled algebraic Riccati solutions) |
223
+ | `L` | positive `int`, default `1000` | Number of time samples |
224
+ | `eta` | positive `int`, default `5` | Number of trailing matrix norms inspected for convergence |
225
+ | `epsilon_x`, `epsilon_P` | `float` in $(0, 1)$, optional | Convergence tolerances for the state and the Riccati matrices |
226
+ | `state_variables_names` | `Sequence[str]` of length $n$, optional | LaTeX names (without `$`) for state variables, used in plots |
227
+ | `show_legend` | `bool`, default `True` | Whether plots include a legend |
228
+ | `debug` | `bool`, default `False` | Emit verbose diagnostics while solving |
229
+
230
+ An [`Objective`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/objective.py) takes:
231
+
232
+ * `Q` — `np.ndarray` $(n, n)$, symmetric positive **semi**-definite state weight
233
+ * `R` — `np.ndarray` $(m_i, m_i)$ (or a scalar), symmetric positive definite input weight
234
+ * `M` — `np.ndarray` $(m_i, m)$, optional decomposition matrix (`None` for a plain LQR objective)
235
+
236
+ The helpers `LQRObjective(Q, R)` and `GameObjective(Q, R, M)` are thin constructors for
237
+ the two common cases.
238
+
239
+ # Tutorial: masses on springs
240
+
241
+ To show the package in action we compare a differential game against an LQR on a chain of
242
+ masses connected by springs — a textbook coupled, oscillatory system:
243
+
244
+ <p align="center">
245
+ <img alt="Two masses connected by springs between two walls" src="images/readme/masses_schematic.png" width="760"/>
246
+ </p>
247
+
248
+ The physical input space is decomposed along the **modal** directions of $M^{-1}K$, so each
249
+ vibration mode becomes one player of a game. The full example lives in
250
+ [`MassesWithSpringsComparison.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/MassesWithSpringsComparison.py);
251
+ here is its essence:
252
+
253
+ ```python
254
+ import numpy as np
255
+ from PyDiffGame import GameObjective, LQRObjective, PyDiffGameLQRComparison
256
+
257
+ N, m, k, r = 2, 50.0, 10.0, 1.0
258
+ q = [500.0, 2000.0] # per-mode state weights
259
+
260
+ I_N, Z_N = np.eye(N), np.zeros((N, N))
261
+ mass_inv_stiffness = (1.0 / m) * k * (2 * I_N - np.eye(N, k=1) - np.eye(N, k=-1))
262
+
263
+ # Modal decomposition (orthonormal, so the modal transform is orthogonal):
264
+ _, eigenvectors = np.linalg.eigh(mass_inv_stiffness)
265
+ Ms = [eigenvectors[:, i].reshape(1, N) for i in range(N)]
266
+ modal_to_state = np.kron(np.eye(2), np.concatenate(Ms, axis=0))
267
+
268
+ A = np.block([[Z_N, I_N], [-mass_inv_stiffness, Z_N]])
269
+ B = np.block([[Z_N], [(1.0 / m) * I_N]])
270
+
271
+ game_objectives = []
272
+ for i, (q_i, M_i) in enumerate(zip(q, Ms)):
273
+ modal_weight = np.diag([0.0] * i + [q_i] + [0.0] * (N - 1) + [q_i] + [0.0] * (N - i - 1))
274
+ game_objectives.append(GameObjective(Q=modal_to_state.T @ modal_weight @ modal_to_state, R=r, M=M_i))
275
+
276
+ # The LQR baseline optimizes exactly the aggregate of the game's objectives
277
+ # (sum of the per-mode Q_i, and the matching physical input weight r * I), so
278
+ # it is the genuine monolithic optimum to compare the decomposed game against:
279
+ modal_weight_total = np.diag(q + q)
280
+ lqr_objective = [LQRObjective(Q=modal_to_state.T @ modal_weight_total @ modal_to_state, R=r * I_N)]
281
+
282
+ x_0 = np.array([10.0, 20.0, 0.0, 0.0])
283
+ comparison = PyDiffGameLQRComparison(
284
+ A=A, B=B,
285
+ games_objectives=[lqr_objective, game_objectives],
286
+ x_0=x_0, x_T=x_0 * 10.0, T_f=25.0, L=300,
287
+ )
288
+
289
+ comparison.run(plot_state_spaces=True)
290
+ lqr_cost, game_cost = comparison.costs()
291
+ print(f"LQR cost = {lqr_cost:.4g}, game cost = {game_cost:.4g}")
292
+ ```
293
+
294
+ Run the bundled example end-to-end:
295
+
296
+ ```bash
297
+ uv run python -m PyDiffGame.examples.MassesWithSpringsComparison
298
+ ```
299
+
300
+ The monolithic LQR is, by construction, the optimal controller for the *aggregate* of the
301
+ game's objectives. The **fully decomposed** modal game — where each vibration mode is
302
+ solved as an independent player, coupled only through the shared dynamics — reproduces that
303
+ monolithic optimum **to numerical precision**: the two state trajectories coincide
304
+ (they differ by ~10⁻⁷) and the costs are equal:
305
+
306
+ <p align="center">
307
+ <img alt="State trajectories: the decomposed game reproduces the monolithic LQR" src="images/readme/masses_game_vs_lqr.png" width="860"/>
308
+ </p>
309
+
310
+ <p align="center">
311
+ <img alt="Cost comparison: the modal game recovers the LQR optimum" src="images/readme/masses_cost.png" width="440"/>
312
+ </p>
313
+
314
+ For this modally-decoupled system the decomposition is **lossless** — and it buys
315
+ **compositionality**: you can add, drop or re-weight a control task by editing a single
316
+ player, without re-tuning one monolithic cost matrix.
317
+
318
+ > The figures above are regenerated from the live solver by
319
+ > [`tools/generate_readme_figures.py`](tools/generate_readme_figures.py)
320
+ > (`uv run python tools/generate_readme_figures.py`), so they always match the current code.
321
+
322
+ # More examples
323
+
324
+ The [`src/PyDiffGame/examples`](https://github.com/krichelj/PyDiffGame/tree/master/src/PyDiffGame/examples)
325
+ directory contains further worked comparisons:
326
+
327
+ | Example | System |
328
+ | --- | --- |
329
+ | [`MassesWithSpringsComparison.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/MassesWithSpringsComparison.py) | Chain of masses coupled by springs (the tutorial above) |
330
+ | [`InvertedPendulumComparison.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/InvertedPendulumComparison.py) | Inverted pendulum on a cart |
331
+ | [`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 |
332
+ | [`QuadRotorControl.py`](https://github.com/krichelj/PyDiffGame/blob/master/src/PyDiffGame/examples/QuadRotorControl.py) | Quadrotor attitude / position control |
333
+
334
+ # Testing and development
335
+
336
+ The package ships with a `pytest` suite that validates the solvers against analytical
337
+ ground truth — LQR solutions are checked against `scipy`'s algebraic Riccati solvers, and
338
+ the games against their coupled-Riccati residuals and closed-loop stability. All tooling
339
+ runs through uv:
340
+
341
+ ```bash
342
+ uv sync --extra dev
343
+ uv run pytest # run the test suite
344
+ uv run ruff format src/PyDiffGame tests # auto-format (black-compatible)
345
+ uv run ruff check src/PyDiffGame tests # lint
346
+ uv run mypy src/PyDiffGame # type-check
347
+ ```
348
+
349
+ Continuous integration runs the formatter check, linter, type checker and full suite on
350
+ Python 3.11–3.14. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
351
+
352
+ # Citing
353
+
354
+ If you use this work, please cite our paper:
355
+
356
+ ```bibtex
357
+ @inproceedings{pydiffgame_paper,
358
+ author={Kricheli, Joshua Shay and Sadon, Aviran and Arogeti, Shai and Regev, Shimon and Weiss, Gera},
359
+ booktitle={29th Mediterranean Conference on Control and Automation (MED 2021)},
360
+ title={{Composition of Dynamic Control Objectives Based on Differential Games}},
361
+ year={2021},
362
+ pages={298-304},
363
+ doi={10.1109/MED51440.2021.9480269}}
364
+ ```
365
+
366
+ Further details can be found in the [citation document](CITATIONS.bib).
367
+
368
+ # Acknowledgments
369
+
370
+ This research was supported in part by the Leona M. and Harry B. Helmsley Charitable Trust
371
+ through the _‘Agricultural, Biological and Cognitive Robotics Initiative’_ (‘ABC’) and by
372
+ the Marcus Endowment Fund, both at Ben-Gurion University of the Negev, Israel. It was also
373
+ supported by The _‘Israeli Smart Transportation Research Center’_ (‘ISTRC’) by The Technion
374
+ and Bar-Ilan Universities, Israel.
375
+
376
+ <p align="center">
377
+ <a href="https://istrc.net.technion.ac.il/">
378
+ <img src="images/Logo_ISTRC_Green_English.png" height="80" alt="ISTRC"/>
379
+ </a>
380
+ &emsp;
381
+ <a href="https://in.bgu.ac.il/en/Pages/default.aspx">
382
+ <picture>
383
+ <source media="(prefers-color-scheme: dark)" srcset="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZ8GbtJiX8lNUygX7-inRBuWESK438jWbRjQ&s">
384
+ <source media="(prefers-color-scheme: light)" srcset="https://tamrur.bgu.ac.il/restore/BGU.sig.png">
385
+ <img alt="Ben-Gurion University of the Negev" src="https://tamrur.bgu.ac.il/restore/BGU.sig.png" height="80">
386
+ </picture>
387
+ </a>
388
+ &emsp;
389
+ <a href="https://helmsleytrust.org/">
390
+ <picture>
391
+ <source media="(prefers-color-scheme: dark)" srcset="https://helmsleytrust.org/wp-content/themes/helmsley/assets/img/helmsley-charitable-trust-logo-white.png">
392
+ <source media="(prefers-color-scheme: light)" srcset="https://helmsleytrust.org/wp-content/themes/helmsley/assets/img/helmsley-charitable-trust-logo-blue.png">
393
+ <img alt="Helmsley Charitable Trust" src="https://helmsleytrust.org/wp-content/themes/helmsley/assets/img/helmsley-charitable-trust-logo-blue.png" height="80">
394
+ </picture>
395
+ </a>
396
+ </p>
397
+
398
+ # Star history
399
+
400
+ <p align="center">
401
+ <a href="https://star-history.com/#krichelj/PyDiffGame&Date">
402
+ <picture>
403
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=krichelj/PyDiffGame&type=Date&theme=dark"/>
404
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=krichelj/PyDiffGame&type=Date"/>
405
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=krichelj/PyDiffGame&type=Date" width="600"/>
406
+ </picture>
407
+ </a>
408
+ </p>