PyDiffGame 2.0.0__tar.gz → 2.0.2__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 (71) hide show
  1. pydiffgame-2.0.2/.github/workflows/python-publish.yml +173 -0
  2. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/.github/workflows/tests.yml +2 -2
  3. pydiffgame-2.0.2/CLAUDE.md +42 -0
  4. pydiffgame-2.0.2/CONTRIBUTING.md +92 -0
  5. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/PKG-INFO +29 -16
  6. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/README.md +28 -15
  7. pydiffgame-2.0.2/docs/README.md +362 -0
  8. pydiffgame-2.0.2/images/logo.png +0 -0
  9. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/pyproject.toml +1 -1
  10. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/__init__.py +1 -1
  11. pydiffgame-2.0.2/tools/bump_version.py +78 -0
  12. pydiffgame-2.0.2/tools/render_logo.py +295 -0
  13. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/uv.lock +1 -1
  14. pydiffgame-2.0.0/.github/workflows/python-publish.yml +0 -71
  15. pydiffgame-2.0.0/CONTRIBUTING.md +0 -44
  16. pydiffgame-2.0.0/docs/README.md +0 -266
  17. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/.gitignore +0 -0
  18. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/.pre-commit-config.yaml +0 -0
  19. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/CITATIONS.bib +0 -0
  20. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/CODE_OF_CONDUCT.md +0 -0
  21. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/LICENSE +0 -0
  22. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/_config.yml +0 -0
  23. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/images/Logo_ISTRC_Green_English.png +0 -0
  24. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/images/logo_abc.png +0 -0
  25. /pydiffgame-2.0.0/images/logo.png → /pydiffgame-2.0.2/images/logo_source.png +0 -0
  26. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/images/readme/masses_cost.png +0 -0
  27. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/images/readme/masses_game_vs_lqr.png +0 -0
  28. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/images/readme/masses_schematic.png +0 -0
  29. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/requirements.txt +0 -0
  30. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/_typing.py +0 -0
  31. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/base.py +0 -0
  32. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/comparison.py +0 -0
  33. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/continuous.py +0 -0
  34. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/discrete.py +0 -0
  35. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/InvertedPendulumComparison.py +0 -0
  36. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/MassesWithSpringsComparison.py +0 -0
  37. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/PVTOL.py +0 -0
  38. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/PVTOLComparison.py +0 -0
  39. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/QuadRotorControl.py +0 -0
  40. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/2-players_large_1.png +0 -0
  41. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/2-players_large_2.png +0 -0
  42. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/LQR_large_1.png +0 -0
  43. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/LQR_large_2.png +0 -0
  44. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/two_masses_tikz.png +0 -0
  45. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/4-players_large_1.png +0 -0
  46. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/4-players_large_2.png +0 -0
  47. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/LQR_large_1.png +0 -0
  48. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/LQR_large_2.png +0 -0
  49. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/8-players_large_1.png +0 -0
  50. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/8-players_large_2.png +0 -0
  51. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/LQR_large_1.png +0 -0
  52. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/LQR_large_2.png +0 -0
  53. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1.png +0 -0
  54. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL/PVTOL10.png +0 -0
  55. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL/PVTOL100.png +0 -0
  56. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1000.png +0 -0
  57. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL0001.png +0 -0
  58. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL001.png +0 -0
  59. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL01.png +0 -0
  60. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL1.png +0 -0
  61. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/lqr.py +0 -0
  62. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/objective.py +0 -0
  63. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/src/PyDiffGame/plotting.py +0 -0
  64. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/tests/conftest.py +0 -0
  65. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/tests/test_discrete.py +0 -0
  66. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/tests/test_examples.py +0 -0
  67. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/tests/test_game.py +0 -0
  68. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/tests/test_lqr.py +0 -0
  69. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/tests/test_objective.py +0 -0
  70. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/tests/test_simulation.py +0 -0
  71. {pydiffgame-2.0.0 → pydiffgame-2.0.2}/tools/generate_readme_figures.py +0 -0
@@ -0,0 +1,173 @@
1
+ # Publishes the package to PyPI and creates the matching GitHub Release,
2
+ # auto-incrementing the version when source files change on master.
3
+ #
4
+ # Triggers:
5
+ # - push to master that touches SOURCE files (the package or its packaging
6
+ # metadata) — docs / tests / tooling / CI changes do not cut a release.
7
+ # - workflow_dispatch: manual trigger remains available for on-demand re-runs
8
+ # (use this if you ever need to publish despite no source-file changes).
9
+ #
10
+ # Loop prevention: the workflow itself commits a "chore: bump version" commit
11
+ # back to master. Three independent guards stop that commit from re-triggering
12
+ # the workflow:
13
+ # 1. GitHub Actions natively skips push events whose head commit message
14
+ # contains "[skip ci]" (which the bump commit always does).
15
+ # 2. The bump-version job is guarded with an explicit `if:` so it only runs
16
+ # for human commits / manual dispatch, never for github-actions[bot].
17
+ # 3. The bump commit message itself contains "[skip ci]" so even if (1) and
18
+ # (2) ever break, the message still signals "do not republish".
19
+ #
20
+ # Concurrency: serialize runs so two pushes close together don't race on the
21
+ # bump commit's git push.
22
+ #
23
+ # Flow per run:
24
+ # 1. bump-version : increment the version (carry-at-9 via tools/bump_version.py),
25
+ # commit it back to master with [skip ci].
26
+ # 2. release-build : build the dists from the bumped master.
27
+ # 3. pypi-publish : upload to PyPI via Trusted Publishing (OIDC, no tokens).
28
+ # 4. github-release: create the v<version> GitHub Release with notes + dists.
29
+ #
30
+ # Versions roll over at 9: 2.0.9 -> 2.1.0, 2.9.9 -> 3.0.0.
31
+
32
+ name: Upload Python Package
33
+
34
+ on:
35
+ push:
36
+ branches: [master]
37
+ # Only publish when source files change. Docs/tests/tooling commits do not
38
+ # cut a release; mixed commits do (the filter matches if ANY changed file
39
+ # matches).
40
+ paths:
41
+ - 'src/PyDiffGame/**'
42
+ - 'pyproject.toml'
43
+ workflow_dispatch:
44
+
45
+ permissions:
46
+ contents: read
47
+
48
+ concurrency:
49
+ group: publish-master
50
+ cancel-in-progress: false
51
+
52
+ jobs:
53
+ bump-version:
54
+ runs-on: ubuntu-latest
55
+ # Defense-in-depth loop guard: only release for human commits or manual
56
+ # dispatch — never for the workflow's own [skip ci] bump commit.
57
+ if: >-
58
+ ${{
59
+ github.event_name == 'workflow_dispatch' ||
60
+ (
61
+ github.actor != 'github-actions[bot]' &&
62
+ !contains(github.event.head_commit.message, '[skip ci]')
63
+ )
64
+ }}
65
+ permissions:
66
+ contents: write
67
+ outputs:
68
+ version: ${{ steps.bump.outputs.version }}
69
+ steps:
70
+ - uses: actions/checkout@v5
71
+ with:
72
+ ref: master
73
+
74
+ - name: Increment the version (carry-at-9)
75
+ id: bump
76
+ run: |
77
+ new="$(python3 tools/bump_version.py)"
78
+ echo "version=$new" >> "$GITHUB_OUTPUT"
79
+ echo "Bumped to $new"
80
+
81
+ - name: Commit and push the bump
82
+ run: |
83
+ git config user.name "github-actions[bot]"
84
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
85
+ git add pyproject.toml src/PyDiffGame/__init__.py
86
+ git commit -m "chore: bump version to ${{ steps.bump.outputs.version }} [skip ci]"
87
+ git push origin HEAD:master
88
+
89
+ release-build:
90
+ needs: bump-version
91
+ runs-on: ubuntu-latest
92
+ steps:
93
+ - uses: actions/checkout@v5
94
+ with:
95
+ ref: master # the bumped commit
96
+
97
+ - name: Install uv
98
+ uses: astral-sh/setup-uv@v6
99
+
100
+ - name: Build release distributions
101
+ run: uv build
102
+
103
+ - name: Upload distributions
104
+ uses: actions/upload-artifact@v5
105
+ with:
106
+ name: release-dists
107
+ path: dist/
108
+
109
+ pypi-publish:
110
+ needs:
111
+ - release-build
112
+ runs-on: ubuntu-latest
113
+ permissions:
114
+ # IMPORTANT: this permission is mandatory for trusted publishing
115
+ id-token: write
116
+
117
+ environment:
118
+ name: pypi
119
+ url: https://pypi.org/project/PyDiffGame/
120
+
121
+ steps:
122
+ - name: Retrieve release distributions
123
+ uses: actions/download-artifact@v5
124
+ with:
125
+ name: release-dists
126
+ path: dist/
127
+
128
+ # Authentication is via PyPI Trusted Publishing (OIDC) — no token needed.
129
+ # Trusted publisher on PyPI: owner krichelj, repo PyDiffGame,
130
+ # workflow python-publish.yml, environment pypi.
131
+ - name: Publish release distributions to PyPI
132
+ uses: pypa/gh-action-pypi-publish@release/v1
133
+ with:
134
+ packages-dir: dist/
135
+ # Don't fail if this version is already on PyPI (idempotent re-runs).
136
+ skip-existing: true
137
+
138
+ github-release:
139
+ needs:
140
+ - bump-version
141
+ - pypi-publish
142
+ runs-on: ubuntu-latest
143
+ permissions:
144
+ # Needed to create the release and its tag.
145
+ contents: write
146
+
147
+ steps:
148
+ - uses: actions/checkout@v5
149
+ with:
150
+ ref: master
151
+
152
+ - name: Retrieve release distributions
153
+ uses: actions/download-artifact@v5
154
+ with:
155
+ name: release-dists
156
+ path: dist/
157
+
158
+ - name: Create or update the GitHub Release
159
+ env:
160
+ GH_TOKEN: ${{ github.token }}
161
+ VERSION: ${{ needs.bump-version.outputs.version }}
162
+ run: |
163
+ tag="v${VERSION}"
164
+ echo "Releasing ${tag}"
165
+ if gh release view "$tag" >/dev/null 2>&1; then
166
+ echo "Release ${tag} already exists - refreshing its assets."
167
+ gh release upload "$tag" dist/* --clobber
168
+ else
169
+ gh release create "$tag" dist/* \
170
+ --title "$tag" \
171
+ --generate-notes \
172
+ --target master
173
+ fi
@@ -23,10 +23,10 @@ jobs:
23
23
  python-version: ["3.11", "3.12", "3.13", "3.14"]
24
24
 
25
25
  steps:
26
- - uses: actions/checkout@v4
26
+ - uses: actions/checkout@v5
27
27
 
28
28
  - name: Install uv
29
- uses: astral-sh/setup-uv@v4
29
+ uses: astral-sh/setup-uv@v6
30
30
  with:
31
31
  enable-cache: true
32
32
 
@@ -0,0 +1,42 @@
1
+ # PyDiffGame — repository guide for Claude
2
+
3
+ ## Python tooling (uv-first)
4
+ - Use **uv** for everything; pip is only a documented fallback.
5
+ - `uv sync --extra dev`, `uv run pytest`, `uv run ruff check`, `uv run ruff format`,
6
+ `uv run mypy src/PyDiffGame`, `uv build`.
7
+ - Keep the quality gates green before committing: ruff format, ruff check, mypy on
8
+ `src/PyDiffGame`, and the pytest suite — all via `uv run`.
9
+
10
+ ## Versioning (carry-at-9)
11
+ - The version is `X.Y.Z` with single-digit components that roll over at 9:
12
+ `2.0.9 -> 2.1.0`, `2.9.9 -> 3.0.0` (the major keeps growing).
13
+ - Increment **only** via `tools/bump_version.py`, which updates the version in both
14
+ `pyproject.toml` and `src/PyDiffGame/__init__.py`. Never hand-edit version strings.
15
+ - The version is bumped **automatically** by the publish workflow on each release, so
16
+ do not bump it in ordinary PRs — the release run does it.
17
+
18
+ ## Releasing
19
+ - **Continuous deployment, gated on source changes.** Every commit to `master`
20
+ that touches `src/PyDiffGame/**` or `pyproject.toml` automatically triggers
21
+ the publish workflow. Docs / tests / tooling / CI changes do **not** trigger
22
+ a release on their own; mixed commits do.
23
+ - The workflow auto-increments the version (`tools/bump_version.py`), commits
24
+ the bump to `master` as `chore: bump version to X.Y.Z [skip ci]`, builds with
25
+ `uv build`, publishes to PyPI via Trusted Publishing (OIDC, no tokens), and
26
+ creates the matching `v<version>` GitHub Release with notes and the built
27
+ dists attached. It is idempotent (`skip-existing`).
28
+ - Manual on-demand publish stays available via
29
+ `Actions -> Upload Python Package -> Run workflow` (`workflow_dispatch`).
30
+ - Three independent guards prevent the bump commit from re-triggering the
31
+ workflow (an infinite loop): `[skip ci]` in the message (which GitHub Actions
32
+ natively honors), an explicit job-level `if:` filtering out
33
+ `github-actions[bot]`, and `paths:` filter scoping to source files.
34
+
35
+ ## Docs
36
+ - `README.md` is the single canonical readme and is also the PyPI long-description
37
+ (`pyproject.toml: readme = "README.md"`), so its image/file links must be **absolute**
38
+ (`raw.githubusercontent.com/.../master/...` for images, `github.com/.../blob/master/...`
39
+ for files) so they render on PyPI.
40
+ - Keep `docs/README.md` identical to `README.md`.
41
+ - README figures are generated from the live solver:
42
+ `uv run python tools/generate_readme_figures.py`.
@@ -0,0 +1,92 @@
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
+ ## Releasing
43
+
44
+ The project is on **continuous deployment for source changes**: every commit
45
+ to `master` that touches source files automatically cuts a new release.
46
+
47
+ A commit triggers a release when it changes any of:
48
+
49
+ - `src/PyDiffGame/**` — the package itself
50
+ - `pyproject.toml` — packaging metadata, dependencies, classifiers
51
+
52
+ Docs (`*.md`, `docs/**`), tests (`tests/**`), tooling (`tools/**`, `.github/**`,
53
+ `.pre-commit-config.yaml`), images and the lock file do **not** trigger a
54
+ release on their own. A mixed commit (e.g. `src/foo.py` + `README.md`) does
55
+ trigger one — the path filter matches if any changed file matches.
56
+
57
+ When a release-triggering commit lands on `master`, the publish workflow:
58
+
59
+ 1. **Increments the version** with `tools/bump_version.py`, which rolls each
60
+ component over at 9 (`2.0.9 -> 2.1.0`, `2.9.9 -> 3.0.0`), updates both
61
+ `pyproject.toml` and `src/PyDiffGame/__init__.py`, and commits the bump back
62
+ to `master` as `chore: bump version to X.Y.Z [skip ci]`.
63
+ 2. Builds the distributions with `uv build`.
64
+ 3. Uploads them to PyPI via
65
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no
66
+ tokens or secrets).
67
+ 4. Creates a `v<version>` GitHub Release with auto-generated notes and the built
68
+ wheels/sdist attached.
69
+
70
+ The workflow can also be triggered manually from **Actions -> Upload Python
71
+ Package -> Run workflow** if you ever need to re-run it.
72
+
73
+ You normally never edit the version by hand. To bump it locally (e.g. to test),
74
+ run `uv run python tools/bump_version.py` (`--dry-run` to preview, `--current`
75
+ to print the current version). The PyPI upload is idempotent (`skip-existing`),
76
+ so re-running the workflow is safe.
77
+
78
+ ### What stops an infinite loop?
79
+
80
+ The publish workflow itself commits the version bump back to `master`. Three
81
+ independent guards stop that commit from re-triggering the workflow:
82
+
83
+ 1. The bump commit message contains `[skip ci]`, which GitHub Actions natively
84
+ honors by **not creating a workflow run at all** for that push.
85
+ 2. The `bump-version` job has an explicit `if:` that skips when the actor is
86
+ `github-actions[bot]` or the head-commit message contains `[skip ci]`.
87
+ 3. `bump_version.py` is the single source of truth for the version, so a
88
+ tampered bump commit still wouldn't double-bump.
89
+
90
+ Thank you for your contribution!
91
+
92
+ Joshua Shay Kricheli
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyDiffGame
3
- Version: 2.0.0
3
+ Version: 2.0.2
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="images/logo.png" width="420"/>
61
+ <img alt="PyDiffGame logo" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/logo.png" width="420"/>
62
62
  </p>
63
63
 
64
64
  <p align="center">
@@ -127,22 +127,32 @@ accounting on a common yardstick, and ready-made plotting.
127
127
 
128
128
  # Installation
129
129
 
130
- Install the latest release from PyPI:
130
+ PyDiffGame is published on [PyPI](https://pypi.org/project/PyDiffGame/) and is
131
+ managed with [**uv**](https://docs.astral.sh/uv/). Add it to your project:
131
132
 
132
133
  ```bash
133
- pip install PyDiffGame
134
+ uv add PyDiffGame
134
135
  ```
135
136
 
136
137
  To run the bundled examples (which additionally need
137
138
  [`python-control`](https://python-control.readthedocs.io/)):
138
139
 
139
140
  ```bash
140
- pip install "PyDiffGame[examples]"
141
+ uv add "PyDiffGame[examples]"
141
142
  ```
142
143
 
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:
144
+ <details>
145
+ <summary><b>Prefer pip?</b> It works as a fallback.</summary>
146
+
147
+ ```bash
148
+ pip install PyDiffGame
149
+ pip install "PyDiffGame[examples]" # with the examples extra
150
+ ```
151
+
152
+ </details>
153
+
154
+ To work on the package itself, clone it and sync the locked development
155
+ environment with uv:
146
156
 
147
157
  ```bash
148
158
  git clone https://github.com/krichelj/PyDiffGame.git
@@ -151,7 +161,10 @@ uv sync --extra dev # creates .venv with the exact locked dependencies
151
161
  uv run pre-commit install # enable the formatting / lint / type-check hooks
152
162
  ```
153
163
 
154
- Then run anything through `uv run` (`uv run pytest`, `uv run python -m PyDiffGame.examples.MassesWithSpringsComparison`, …).
164
+ Then run anything through `uv run` (`uv run pytest`,
165
+ `uv run python -m PyDiffGame.examples.MassesWithSpringsComparison`, …). Pip users
166
+ can instead `pip install -e ".[dev]"`, though uv is recommended for the exact
167
+ locked environment.
155
168
 
156
169
  # Quick start
157
170
 
@@ -242,7 +255,7 @@ To show the package in action we compare a differential game against an LQR on a
242
255
  masses connected by springs — a textbook coupled, oscillatory system:
243
256
 
244
257
  <p align="center">
245
- <img alt="Two masses connected by springs between two walls" src="images/readme/masses_schematic.png" width="760"/>
258
+ <img alt="Two masses connected by springs between two walls" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_schematic.png" width="760"/>
246
259
  </p>
247
260
 
248
261
  The physical input space is decomposed along the **modal** directions of $M^{-1}K$, so each
@@ -304,11 +317,11 @@ monolithic optimum **to numerical precision**: the two state trajectories coinci
304
317
  (they differ by ~10⁻⁷) and the costs are equal:
305
318
 
306
319
  <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"/>
320
+ <img alt="State trajectories: the decomposed game reproduces the monolithic LQR" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_game_vs_lqr.png" width="860"/>
308
321
  </p>
309
322
 
310
323
  <p align="center">
311
- <img alt="Cost comparison: the modal game recovers the LQR optimum" src="images/readme/masses_cost.png" width="440"/>
324
+ <img alt="Cost comparison: the modal game recovers the LQR optimum" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_cost.png" width="440"/>
312
325
  </p>
313
326
 
314
327
  For this modally-decoupled system the decomposition is **lossless** — and it buys
@@ -316,7 +329,7 @@ For this modally-decoupled system the decomposition is **lossless** — and it b
316
329
  player, without re-tuning one monolithic cost matrix.
317
330
 
318
331
  > The figures above are regenerated from the live solver by
319
- > [`tools/generate_readme_figures.py`](tools/generate_readme_figures.py)
332
+ > [`tools/generate_readme_figures.py`](https://github.com/krichelj/PyDiffGame/blob/master/tools/generate_readme_figures.py)
320
333
  > (`uv run python tools/generate_readme_figures.py`), so they always match the current code.
321
334
 
322
335
  # More examples
@@ -347,7 +360,7 @@ uv run mypy src/PyDiffGame # type-check
347
360
  ```
348
361
 
349
362
  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.
363
+ Python 3.11–3.14. See [CONTRIBUTING.md](https://github.com/krichelj/PyDiffGame/blob/master/CONTRIBUTING.md) for details.
351
364
 
352
365
  # Citing
353
366
 
@@ -363,7 +376,7 @@ If you use this work, please cite our paper:
363
376
  doi={10.1109/MED51440.2021.9480269}}
364
377
  ```
365
378
 
366
- Further details can be found in the [citation document](CITATIONS.bib).
379
+ Further details can be found in the [citation document](https://github.com/krichelj/PyDiffGame/blob/master/CITATIONS.bib).
367
380
 
368
381
  # Acknowledgments
369
382
 
@@ -375,7 +388,7 @@ and Bar-Ilan Universities, Israel.
375
388
 
376
389
  <p align="center">
377
390
  <a href="https://istrc.net.technion.ac.il/">
378
- <img src="images/Logo_ISTRC_Green_English.png" height="80" alt="ISTRC"/>
391
+ <img src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/Logo_ISTRC_Green_English.png" height="80" alt="ISTRC"/>
379
392
  </a>
380
393
  &emsp;
381
394
  <a href="https://in.bgu.ac.il/en/Pages/default.aspx">
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img alt="PyDiffGame logo" src="images/logo.png" width="420"/>
2
+ <img alt="PyDiffGame logo" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/logo.png" width="420"/>
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -68,22 +68,32 @@ accounting on a common yardstick, and ready-made plotting.
68
68
 
69
69
  # Installation
70
70
 
71
- Install the latest release from PyPI:
71
+ PyDiffGame is published on [PyPI](https://pypi.org/project/PyDiffGame/) and is
72
+ managed with [**uv**](https://docs.astral.sh/uv/). Add it to your project:
72
73
 
73
74
  ```bash
74
- pip install PyDiffGame
75
+ uv add PyDiffGame
75
76
  ```
76
77
 
77
78
  To run the bundled examples (which additionally need
78
79
  [`python-control`](https://python-control.readthedocs.io/)):
79
80
 
80
81
  ```bash
81
- pip install "PyDiffGame[examples]"
82
+ uv add "PyDiffGame[examples]"
82
83
  ```
83
84
 
84
- To work on the package itself, this project is managed with
85
- [**uv**](https://docs.astral.sh/uv/). Clone it and sync the locked development
86
- environment:
85
+ <details>
86
+ <summary><b>Prefer pip?</b> It works as a fallback.</summary>
87
+
88
+ ```bash
89
+ pip install PyDiffGame
90
+ pip install "PyDiffGame[examples]" # with the examples extra
91
+ ```
92
+
93
+ </details>
94
+
95
+ To work on the package itself, clone it and sync the locked development
96
+ environment with uv:
87
97
 
88
98
  ```bash
89
99
  git clone https://github.com/krichelj/PyDiffGame.git
@@ -92,7 +102,10 @@ uv sync --extra dev # creates .venv with the exact locked dependencies
92
102
  uv run pre-commit install # enable the formatting / lint / type-check hooks
93
103
  ```
94
104
 
95
- Then run anything through `uv run` (`uv run pytest`, `uv run python -m PyDiffGame.examples.MassesWithSpringsComparison`, …).
105
+ Then run anything through `uv run` (`uv run pytest`,
106
+ `uv run python -m PyDiffGame.examples.MassesWithSpringsComparison`, …). Pip users
107
+ can instead `pip install -e ".[dev]"`, though uv is recommended for the exact
108
+ locked environment.
96
109
 
97
110
  # Quick start
98
111
 
@@ -183,7 +196,7 @@ To show the package in action we compare a differential game against an LQR on a
183
196
  masses connected by springs — a textbook coupled, oscillatory system:
184
197
 
185
198
  <p align="center">
186
- <img alt="Two masses connected by springs between two walls" src="images/readme/masses_schematic.png" width="760"/>
199
+ <img alt="Two masses connected by springs between two walls" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_schematic.png" width="760"/>
187
200
  </p>
188
201
 
189
202
  The physical input space is decomposed along the **modal** directions of $M^{-1}K$, so each
@@ -245,11 +258,11 @@ monolithic optimum **to numerical precision**: the two state trajectories coinci
245
258
  (they differ by ~10⁻⁷) and the costs are equal:
246
259
 
247
260
  <p align="center">
248
- <img alt="State trajectories: the decomposed game reproduces the monolithic LQR" src="images/readme/masses_game_vs_lqr.png" width="860"/>
261
+ <img alt="State trajectories: the decomposed game reproduces the monolithic LQR" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_game_vs_lqr.png" width="860"/>
249
262
  </p>
250
263
 
251
264
  <p align="center">
252
- <img alt="Cost comparison: the modal game recovers the LQR optimum" src="images/readme/masses_cost.png" width="440"/>
265
+ <img alt="Cost comparison: the modal game recovers the LQR optimum" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_cost.png" width="440"/>
253
266
  </p>
254
267
 
255
268
  For this modally-decoupled system the decomposition is **lossless** — and it buys
@@ -257,7 +270,7 @@ For this modally-decoupled system the decomposition is **lossless** — and it b
257
270
  player, without re-tuning one monolithic cost matrix.
258
271
 
259
272
  > The figures above are regenerated from the live solver by
260
- > [`tools/generate_readme_figures.py`](tools/generate_readme_figures.py)
273
+ > [`tools/generate_readme_figures.py`](https://github.com/krichelj/PyDiffGame/blob/master/tools/generate_readme_figures.py)
261
274
  > (`uv run python tools/generate_readme_figures.py`), so they always match the current code.
262
275
 
263
276
  # More examples
@@ -288,7 +301,7 @@ uv run mypy src/PyDiffGame # type-check
288
301
  ```
289
302
 
290
303
  Continuous integration runs the formatter check, linter, type checker and full suite on
291
- Python 3.11–3.14. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
304
+ Python 3.11–3.14. See [CONTRIBUTING.md](https://github.com/krichelj/PyDiffGame/blob/master/CONTRIBUTING.md) for details.
292
305
 
293
306
  # Citing
294
307
 
@@ -304,7 +317,7 @@ If you use this work, please cite our paper:
304
317
  doi={10.1109/MED51440.2021.9480269}}
305
318
  ```
306
319
 
307
- Further details can be found in the [citation document](CITATIONS.bib).
320
+ Further details can be found in the [citation document](https://github.com/krichelj/PyDiffGame/blob/master/CITATIONS.bib).
308
321
 
309
322
  # Acknowledgments
310
323
 
@@ -316,7 +329,7 @@ and Bar-Ilan Universities, Israel.
316
329
 
317
330
  <p align="center">
318
331
  <a href="https://istrc.net.technion.ac.il/">
319
- <img src="images/Logo_ISTRC_Green_English.png" height="80" alt="ISTRC"/>
332
+ <img src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/Logo_ISTRC_Green_English.png" height="80" alt="ISTRC"/>
320
333
  </a>
321
334
  &emsp;
322
335
  <a href="https://in.bgu.ac.il/en/Pages/default.aspx">