├── tests ├── __init__.py └── test_jlgametheory.py ├── docs ├── _templates │ └── autosummary │ │ └── function.rst ├── _static │ ├── autosummary_no_underline.css │ └── autosummary_fixes.js ├── index.rst └── conf.py ├── jlgametheory ├── juliapkg.json ├── __init__.py └── jlgametheory.py ├── .github ├── dependabot.yml └── workflows │ ├── release-pypi.yml │ ├── ci.yml │ └── docs.yml ├── pyproject.toml ├── LICENSE ├── README.md └── .gitignore /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/function.rst: -------------------------------------------------------------------------------- 1 | {{ objname }} 2 | {{ '=' * (objname | length) }} 3 | 4 | .. currentmodule:: {{ module }} 5 | 6 | .. autofunction:: {{ fullname }} 7 | -------------------------------------------------------------------------------- /jlgametheory/juliapkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "julia": "^1.10", 3 | "packages": { 4 | "GameTheory": { 5 | "uuid": "64a4ffa8-f47c-4a47-8dad-aee7aadc3b51", 6 | "version": "0.4" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /jlgametheory/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | jlgametheory: Python interface to GameTheory.jl 3 | 4 | """ 5 | from juliacall import Main as jl 6 | from importlib.metadata import PackageNotFoundError, version as _version 7 | 8 | jl.seval("using GameTheory") 9 | GameTheory = jl.GameTheory 10 | 11 | from .jlgametheory import ( 12 | lrsnash, hc_solve 13 | ) 14 | 15 | try: 16 | __version__ = _version("jlgametheory") 17 | except PackageNotFoundError: # pragma: no cover 18 | __version__ = "0+unknown" 19 | 20 | __all__ = [ 21 | "lrsnash", "hc_solve" 22 | ] 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Monitor Python packages from pyproject.toml 4 | - package-ecosystem: "pip" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "saturday" 9 | open-pull-requests-limit: 10 10 | labels: 11 | - "dependencies" 12 | - "python" 13 | 14 | # Monitor GitHub Actions workflows 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: "weekly" 19 | day: "saturday" 20 | open-pull-requests-limit: 5 21 | labels: 22 | - "dependencies" 23 | - "github-actions" 24 | -------------------------------------------------------------------------------- /docs/_static/autosummary_no_underline.css: -------------------------------------------------------------------------------- 1 | // Ensure autosummary links have no underline in sphinxdoc 2 | document.addEventListener('DOMContentLoaded', () => { 3 | const style = document.createElement('style'); 4 | style.textContent = ` 5 | /* Remove all underline-like effects inside autosummary tables */ 6 | table.autosummary a.reference, 7 | table.autosummary a.reference *, 8 | table.autosummary code.xref, 9 | table.autosummary code.literal { 10 | text-decoration: none !important; 11 | border-bottom: 0 !important; 12 | box-shadow: none !important; 13 | background-image: none !important; 14 | } 15 | `; 16 | document.head.appendChild(style); 17 | }); 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | jlgametheory — Documentation 2 | ============================ 3 | 4 | `jlgametheory `_ is a Python interface to 5 | `GameTheory.jl `_. 6 | 7 | It allows passing a `NormalFormGame` instance from 8 | `quantecon.game_theory `_ 9 | to GameTheory.jl functions via 10 | `juliacall `_. 11 | 12 | * For constructing `NormalFormGame`, 13 | see its `documentation `_. 14 | 15 | 16 | Implemented functions 17 | --------------------- 18 | 19 | .. autosummary:: 20 | :toctree: _autosummary 21 | :nosignatures: 22 | :template: function 23 | 24 | ~jlgametheory.lrsnash 25 | ~jlgametheory.hc_solve 26 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | sys.path.insert(0, os.path.abspath('..')) 3 | 4 | project = "jlgametheory" 5 | extensions = [ 6 | "sphinx.ext.autodoc", 7 | "sphinx.ext.autosummary", 8 | "sphinx.ext.viewcode", 9 | "sphinx_copybutton", 10 | ] 11 | autosummary_generate = True 12 | autodoc_mock_imports = ["juliacall"] 13 | add_module_names = False 14 | html_theme = "sphinxdoc" 15 | 16 | templates_path = ["_templates"] 17 | autodoc_typehints = "none" 18 | add_function_parentheses = False 19 | 20 | default_role = "code" 21 | 22 | html_show_sourcelink = False 23 | 24 | html_static_path = ["_static"] 25 | html_js_files = ["autosummary_fixes.js", "autosummary_no_underline.css"] 26 | 27 | copybutton_prompt_text = r">>> |\.\.\. " 28 | copybutton_prompt_is_regexp = True 29 | 30 | try: 31 | import jlgametheory as _pkg 32 | version = release = getattr(_pkg, "__version__", "") 33 | except Exception: 34 | version = release = "" 35 | -------------------------------------------------------------------------------- /.github/workflows/release-pypi.yml: -------------------------------------------------------------------------------- 1 | name: PyPI Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build-and-publish: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v6 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v6 23 | with: 24 | python-version: '3.13' 25 | 26 | - name: Install build dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install build 30 | 31 | - name: Build package 32 | run: python -m build 33 | 34 | - name: Publish to PyPI 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | password: ${{ secrets.PYPI_API_TOKEN }} 38 | 39 | - name: Create GitHub Release 40 | uses: softprops/action-gh-release@v2 41 | with: 42 | generate_release_notes: true 43 | allow_updates: true 44 | append_body: false 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | permissions: 13 | contents: read 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | python-version: ['3.14'] 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v6 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Set up Julia 28 | uses: julia-actions/setup-julia@v2 29 | with: 30 | version: "1" 31 | 32 | - name: "Cache Julia" 33 | uses: julia-actions/cache@v2 34 | 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install -e ".[test]" 39 | 40 | - name: Show installed dependencies 41 | run: python -m pip list 42 | 43 | - name: Run tests 44 | run: | 45 | pytest --cov=jlgametheory --cov-report=term-missing 46 | 47 | - name: Upload coverage to Coveralls 48 | if: runner.os == 'Linux' 49 | uses: coverallsapp/github-action@v2 50 | with: 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "jlgametheory" 7 | version = "0.1.0" 8 | description = "Python interface to GameTheory.jl" 9 | readme = "README.md" 10 | license = {file = "LICENSE"} 11 | authors = [ 12 | {name = "QuantEcon"} 13 | ] 14 | classifiers = [ 15 | "Development Status :: 3 - Alpha", 16 | "Intended Audience :: Science/Research", 17 | "License :: OSI Approved :: BSD License", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Topic :: Scientific/Engineering", 24 | ] 25 | requires-python = ">=3.10" 26 | dependencies = [ 27 | "numpy>=1.22", 28 | "quantecon>=0.8", 29 | "juliacall>=0.9.20", 30 | ] 31 | 32 | [project.optional-dependencies] 33 | # pip install jlgametheory[test] 34 | test = ["pytest>=7", "pytest-cov>=4"] 35 | # pip install jlgametheory[docs] 36 | docs = [ 37 | "sphinx>=8.0", 38 | "sphinx-copybutton", 39 | ] 40 | 41 | [project.urls] 42 | Homepage = "https://github.com/QuantEcon/jlgametheory" 43 | Repository = "https://github.com/QuantEcon/jlgametheory" 44 | 45 | [tool.coverage.run] 46 | omit = ["tests/test_*.py"] 47 | -------------------------------------------------------------------------------- /docs/_static/autosummary_fixes.js: -------------------------------------------------------------------------------- 1 | // Strip fragment (#...) from autosummary links so they go to the page, not the anchor 2 | document.addEventListener('DOMContentLoaded', () => { 3 | document.querySelectorAll('table.autosummary a.reference.internal[href*="#"]').forEach(a => { 4 | try { 5 | const href = a.getAttribute('href'); 6 | const idx = href.indexOf('#'); 7 | if (idx > -1) { 8 | a.setAttribute('href', href.slice(0, idx)); // keep only the page path 9 | } 10 | } catch (_) {} 11 | }); 12 | }); 13 | 14 | 15 | // Prefix function signatures with the top-level package name (project) 16 | // Works regardless of internal module structure. 17 | document.addEventListener('DOMContentLoaded', () => { 18 | const pkg = 19 | (window.DOCUMENTATION_OPTIONS && DOCUMENTATION_OPTIONS.PROJECT) || 20 | 'jlgametheory'; 21 | 22 | // Target Sphinx function signatures 23 | // (dl.py.function or dl.function depending on theme/Sphinx version) 24 | const sigNameSelectors = [ 25 | 'dl.py.function > dt .sig-name', 26 | 'dl.function > dt .sig-name' 27 | ]; 28 | 29 | document.querySelectorAll(sigNameSelectors.join(',')).forEach(el => { 30 | // Avoid double-prefixing on reloads 31 | if (el.dataset.pkgPrefixed === '1') return; 32 | 33 | // If it already starts with ".", skip 34 | const current = el.textContent.trim(); 35 | if (!current.startsWith(pkg + '.')) { 36 | el.textContent = `${pkg}.${current}`; 37 | } 38 | el.dataset.pkgPrefixed = '1'; 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, QuantEcon 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write 13 | 14 | # Allow one concurrent deployment 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build-and-deploy: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v6 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v6 29 | with: 30 | python-version: '3.13' 31 | 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | pip install -e ".[docs]" 36 | 37 | - name: Determine version path 38 | id: version 39 | run: | 40 | if [[ "${{ github.ref }}" == refs/tags/v* ]]; then 41 | VERSION="stable" 42 | echo "Building docs for tag: ${{ github.ref_name }}" 43 | else 44 | VERSION="latest" 45 | echo "Building docs for main branch" 46 | fi 47 | echo "version=$VERSION" >> $GITHUB_OUTPUT 48 | 49 | - name: Build documentation 50 | run: | 51 | cd docs 52 | sphinx-build -b html . _build/html 53 | 54 | - name: Deploy documentation 55 | uses: peaceiris/actions-gh-pages@v4 56 | with: 57 | github_token: ${{ secrets.GITHUB_TOKEN }} 58 | publish_branch: gh-pages 59 | publish_dir: ./docs/_build/html 60 | destination_dir: ${{ steps.version.outputs.version }} 61 | keep_files: true 62 | user_name: 'github-actions[bot]' 63 | user_email: 'github-actions[bot]@users.noreply.github.com' 64 | commit_message: 'Deploy ${{ steps.version.outputs.version }} docs from ${{ github.sha }}' 65 | 66 | - name: Deploy redirect page 67 | if: steps.version.outputs.version == 'stable' 68 | run: | 69 | mkdir -p _redirect 70 | cat > _redirect/index.html << 'EOF' 71 | 72 | 73 | 74 | 75 | Redirecting to stable documentation 76 | 77 | 78 | 79 | 80 |

If you are not redirected automatically, follow this link to the stable documentation.

81 | 82 | 83 | EOF 84 | 85 | - name: Update redirect 86 | if: steps.version.outputs.version == 'stable' 87 | uses: peaceiris/actions-gh-pages@v4 88 | with: 89 | github_token: ${{ secrets.GITHUB_TOKEN }} 90 | publish_branch: gh-pages 91 | publish_dir: ./_redirect 92 | keep_files: true 93 | user_name: 'github-actions[bot]' 94 | user_email: 'github-actions[bot]@users.noreply.github.com' 95 | commit_message: 'Update root redirect' 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jlgametheory 2 | 3 | [![Build Status](https://github.com/QuantEcon/jlgametheory/actions/workflows/ci.yml/badge.svg)](https://github.com/QuantEcon/jlgametheory/actions/workflows/ci.yml) 4 | [![Coverage Status](https://coveralls.io/repos/QuantEcon/jlgametheory/badge.svg)](https://coveralls.io/github/QuantEcon/jlgametheory) 5 | [![Documentation (stable)](https://img.shields.io/badge/docs-stable-blue.svg)](https://quantecon.github.io/jlgametheory/stable/) 6 | [![Documentation (latest)](https://img.shields.io/badge/docs-latest-blue.svg)](https://quantecon.github.io/jlgametheory/latest/) 7 | 8 | Python interface to GameTheory.jl 9 | 10 | `jlgametheory` is a Python package that allows passing 11 | a `NormalFormGame` instance from 12 | [`QuantEcon.py`](https://github.com/QuantEcon/QuantEcon.py) to 13 | [`GameTheory.jl`](https://github.com/QuantEcon/GameTheory.jl) functions 14 | via [`JuliaCall`](https://github.com/JuliaPy/PythonCall.jl). 15 | 16 | ## Installation 17 | 18 | ``` 19 | pip install jlgametheory 20 | ``` 21 | 22 | ## Implemented functions 23 | 24 | * [`lrsnash`](https://quantecon.github.io/jlgametheory/stable/_autosummary/jlgametheory.lrsnash.html): 25 | Compute in exact arithmetic all extreme mixed-action Nash equilibria of a 2-player normal form game with integer payoffs. 26 | * [`hc_solve`](https://quantecon.github.io/jlgametheory/stable/_autosummary/jlgametheory.hc_solve.html): 27 | Compute all isolated mixed-action Nash equilibria of an N-player normal form game. 28 | 29 | ## Example usage 30 | 31 | ```python 32 | import quantecon.game_theory as gt 33 | import jlgametheory as jgt 34 | ``` 35 | 36 | ### lrsnash 37 | 38 | `lrsnash` calls the Nash equilibrium computation routine in [lrslib](http://cgm.cs.mcgill.ca/~avis/C/lrs.html) 39 | (through its Julia wrapper [LRSLib.jl](https://github.com/JuliaPolyhedra/LRSLib.jl)): 40 | 41 | ```python 42 | bimatrix = [[(3, 3), (3, 2)], 43 | [(2, 2), (5, 6)], 44 | [(0, 3), (6, 1)]] 45 | g = gt.NormalFormGame(bimatrix) 46 | jgt.lrsnash(g) 47 | ``` 48 | 49 | ``` 50 | [(array([Fraction(4, 5), Fraction(1, 5), Fraction(0, 1)], dtype=object), 51 | array([Fraction(2, 3), Fraction(1, 3)], dtype=object)), 52 | (array([Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)], dtype=object), 53 | array([Fraction(1, 3), Fraction(2, 3)], dtype=object)), 54 | (array([Fraction(1, 1), Fraction(0, 1), Fraction(0, 1)], dtype=object), 55 | array([Fraction(1, 1), Fraction(0, 1)], dtype=object))] 56 | ``` 57 | 58 | ### hc_solve 59 | 60 | `hc_solve` computes all isolated Nash equilibria of an N-player game by using 61 | [HomotopyContinuation.jl](https://github.com/JuliaHomotopyContinuation/HomotopyContinuation.jl): 62 | 63 | ```python 64 | g = gt.NormalFormGame((2, 2, 2)) 65 | g[0, 0, 0] = 9, 8, 12 66 | g[1, 1, 0] = 9, 8, 2 67 | g[0, 1, 1] = 3, 4, 6 68 | g[1, 0, 1] = 3, 4, 4 69 | jgt.hc_solve(g) 70 | ``` 71 | 72 | ``` 73 | [(array([0., 1.]), array([0., 1.]), array([1., 0.])), 74 | (array([0.5, 0.5]), array([0.5, 0.5]), array([1.000e+00, 2.351e-38])), 75 | (array([1., 0.]), array([0., 1.]), array([-1.881e-37, 1.000e+00])), 76 | (array([0.25, 0.75]), array([0.5, 0.5]), array([0.333, 0.667])), 77 | (array([0.25, 0.75]), array([1.000e+00, 1.345e-43]), array([0.25, 0.75])), 78 | (array([0., 1.]), array([0.333, 0.667]), array([0.333, 0.667])), 79 | (array([1., 0.]), array([ 1.00e+00, -5.74e-42]), array([1., 0.])), 80 | (array([0., 1.]), array([1., 0.]), array([2.374e-66, 1.000e+00])), 81 | (array([0.5, 0.5]), array([0.333, 0.667]), array([0.25, 0.75]))] 82 | ``` 83 | 84 | ## Tutorials 85 | 86 | * [Tools for Game Theory in QuantEcon.py](https://nbviewer.jupyter.org/github/QuantEcon/game-theory-notebooks/blob/main/game_theory_py.ipynb) 87 | * [Tools for Game Theory in GameTheory.jl](https://nbviewer.jupyter.org/github/QuantEcon/game-theory-notebooks/blob/main/game_theory_jl.ipynb) 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[codz] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | docs/_autosummary/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # UV 99 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | #uv.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | #poetry.toml 111 | 112 | # pdm 113 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 114 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. 115 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control 116 | #pdm.lock 117 | #pdm.toml 118 | .pdm-python 119 | .pdm-build/ 120 | 121 | # pixi 122 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. 123 | #pixi.lock 124 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one 125 | # in the .venv directory. It is recommended not to include this directory in version control. 126 | .pixi 127 | 128 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 129 | __pypackages__/ 130 | 131 | # Celery stuff 132 | celerybeat-schedule 133 | celerybeat.pid 134 | 135 | # SageMath parsed files 136 | *.sage.py 137 | 138 | # Environments 139 | .env 140 | .envrc 141 | .venv 142 | env/ 143 | venv/ 144 | ENV/ 145 | env.bak/ 146 | venv.bak/ 147 | 148 | # Spyder project settings 149 | .spyderproject 150 | .spyproject 151 | 152 | # Rope project settings 153 | .ropeproject 154 | 155 | # mkdocs documentation 156 | /site 157 | 158 | # mypy 159 | .mypy_cache/ 160 | .dmypy.json 161 | dmypy.json 162 | 163 | # Pyre type checker 164 | .pyre/ 165 | 166 | # pytype static type analyzer 167 | .pytype/ 168 | 169 | # Cython debug symbols 170 | cython_debug/ 171 | 172 | # PyCharm 173 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 174 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 175 | # and can be added to the global gitignore or merged into this file. For a more nuclear 176 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 177 | #.idea/ 178 | 179 | # Abstra 180 | # Abstra is an AI-powered process automation framework. 181 | # Ignore directories containing user credentials, local state, and settings. 182 | # Learn more at https://abstra.io/docs 183 | .abstra/ 184 | 185 | # Visual Studio Code 186 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 187 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 188 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 189 | # you could uncomment the following to ignore the entire vscode folder 190 | # .vscode/ 191 | 192 | # Ruff stuff: 193 | .ruff_cache/ 194 | 195 | # PyPI configuration file 196 | .pypirc 197 | 198 | # Cursor 199 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 200 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 201 | # refer to https://docs.cursor.com/context/ignore-files 202 | .cursorignore 203 | .cursorindexingignore 204 | 205 | # Marimo 206 | marimo/_static/ 207 | marimo/_lsp/ 208 | __marimo__/ 209 | -------------------------------------------------------------------------------- /tests/test_jlgametheory.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | import numpy as np 3 | from numpy.testing import assert_, assert_raises 4 | from quantecon.game_theory import NormalFormGame 5 | from jlgametheory import lrsnash, hc_solve 6 | 7 | 8 | def compare_act_profs(operator, act_prof1, act_prof2, *args, **kwargs): 9 | if len(act_prof1) != len(act_prof2): 10 | return False 11 | for a1, a2 in zip(act_prof1, act_prof2): 12 | if not operator(a1, a2, *args, **kwargs): 13 | return False 14 | return True 15 | 16 | 17 | def compare_lists_act_profs(operator, list_act_profs1, list_act_profs2, 18 | *args, **kwargs): 19 | if len(list_act_profs1) != len(list_act_profs2): 20 | return False 21 | for prof2 in list_act_profs2: 22 | if not any(compare_act_profs(operator, prof1, prof2, *args, **kwargs) 23 | for prof1 in list_act_profs1): 24 | return False 25 | return True 26 | 27 | 28 | class TestLRSNash: 29 | def setup_method(self): 30 | self.game_dicts = [] 31 | 32 | # From von Stengel 2007 in Algorithmic Game Theory 33 | bimatrix = [[(3, 3), (3, 2)], 34 | [(2, 2), (5, 6)], 35 | [(0, 3), (6, 1)]] 36 | d = {'g': NormalFormGame(bimatrix), 37 | 'NEs': [([Fraction(1), Fraction(0), Fraction(0)], 38 | [Fraction(1), Fraction(0)]), 39 | ([Fraction(4, 5), Fraction(1, 5), Fraction(0)], 40 | [Fraction(2, 3), Fraction(1, 3)]), 41 | ([Fraction(0), Fraction(1, 3), Fraction(2, 3)], 42 | [Fraction(1, 3), Fraction(2, 3)])]} 43 | self.game_dicts.append(d) 44 | 45 | # Degenerate game 46 | bimatrix = [[(3, 3), (3, 3)], 47 | [(2, 2), (5, 6)], 48 | [(0, 3), (6, 1)]] 49 | d = {'g': NormalFormGame(bimatrix), 50 | 'NEs': [([Fraction(1), Fraction(0), Fraction(0)], 51 | [Fraction(1), Fraction(0)]), 52 | ([Fraction(1), Fraction(0), Fraction(0)], 53 | [Fraction(2, 3), Fraction(1, 3)]), 54 | ([Fraction(0), Fraction(1, 3), Fraction(2, 3)], 55 | [Fraction(1, 3), Fraction(2, 3)])]} 56 | self.game_dicts.append(d) 57 | 58 | def test_lrsnash(self): 59 | for d in self.game_dicts: 60 | NEs_computed = lrsnash(d['g']) 61 | assert_(compare_lists_act_profs( 62 | np.array_equal, NEs_computed, d['NEs'] 63 | ) 64 | ) 65 | 66 | def test_invalid_non_integer_g(self): 67 | g_float = NormalFormGame( 68 | self.game_dicts[0]['g'].payoff_profile_array.astype('float') 69 | ) 70 | assert_raises(NotImplementedError, lrsnash, g_float) 71 | 72 | 73 | class TestHCSolve: 74 | def setup_method(self): 75 | self.game_dicts = [] 76 | 77 | # From von Stengel 2007 in Algorithmic Game Theory 78 | bimatrix = [[(3, 3), (3, 2)], 79 | [(2, 2), (5, 6)], 80 | [(0, 3), (6, 1)]] 81 | d = {'g': NormalFormGame(bimatrix), 82 | 'NEs': [([1, 0, 0], [1, 0]), 83 | ([4/5, 1/5, 0], [2/3, 1/3]), 84 | ([0, 1/3, 2/3], [1/3, 2/3])]} 85 | self.game_dicts.append(d) 86 | 87 | # 2x2x2 game from McKelvey and McLennan 88 | g = NormalFormGame((2, 2, 2)) 89 | g[0, 0, 0] = 9, 8, 12 90 | g[1, 1, 0] = 9, 8, 2 91 | g[0, 1, 1] = 3, 4, 6 92 | g[1, 0, 1] = 3, 4, 4 93 | NEs = [ 94 | ([1, 0], [1, 0], [1, 0]), 95 | ([0, 1], [0, 1], [1, 0]), 96 | ([1, 0], [0, 1], [0, 1]), 97 | ([0, 1], [1, 0], [0, 1]), 98 | ([0, 1], [1/3, 2/3], [1/3, 2/3]), 99 | ([1/4, 3/4], [1, 0], [1/4, 3/4]), 100 | ([1/2, 1/2], [1/2, 1/2], [1, 0]), 101 | ([1/4, 3/4], [1/2, 1/2], [1/3, 2/3]), 102 | ([1/2, 1/2], [1/3, 2/3], [1/4, 3/4]) 103 | ] 104 | d = {'g': g, 105 | 'NEs': NEs} 106 | self.game_dicts.append(d) 107 | 108 | # 2x2x2 game from Nau, Canovas, and Hansen 109 | payoff_profile_array = [ 110 | [[(3, 0, 2), (1, 0, 0)], 111 | [(0, 2, 0), (0, 1, 0)]], 112 | [[(0, 1, 0), (0, 3, 0)], 113 | [(1, 0, 0), (2, 0, 3)]] 114 | ] 115 | q = (-13 + np.sqrt(601)) / 24 116 | p = (9*q - 1) / (7*q + 2) 117 | r = (-3*q + 2) / (q + 1) 118 | d = {'g': NormalFormGame(payoff_profile_array), 119 | 'NEs': [([p, 1-p], [q, 1-q], [r, 1-r])]} 120 | self.game_dicts.append(d) 121 | 122 | def test_hc_solve(self): 123 | for d in self.game_dicts: 124 | NEs_computed = hc_solve(d['g'], show_progress=False) 125 | assert_(compare_lists_act_profs( 126 | np.allclose, NEs_computed, d['NEs'], atol=1e-15 127 | ) 128 | ) 129 | 130 | def test_ntofind(self): 131 | g = self.game_dicts[1]['g'] 132 | NEs_computed = hc_solve(g, ntofind=1, show_progress=False) 133 | assert_(len(NEs_computed) == 1) 134 | assert_(g.is_nash(NEs_computed[0])) 135 | 136 | 137 | def test_invalid_1player_g(): 138 | g = NormalFormGame([[1], [2], [3]]) 139 | for func in [lrsnash, hc_solve]: 140 | assert_raises(NotImplementedError, func, g) 141 | 142 | 143 | def test_invalid_input(): 144 | bimatrix = [[(3, 3), (3, 2)], 145 | [(2, 2), (5, 6)], 146 | [(0, 3), (6, 1)]] 147 | for func in [lrsnash, hc_solve]: 148 | assert_raises(TypeError, func, bimatrix) 149 | -------------------------------------------------------------------------------- /jlgametheory/jlgametheory.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from . import GameTheory 3 | 4 | 5 | def to_jl_nfg(g): 6 | g_jl = GameTheory.NormalFormGame( 7 | *(GameTheory.Player(player.payoff_array) for player in g.players) 8 | ) 9 | return g_jl 10 | 11 | 12 | def _to_py_ne(NE_jl): 13 | return tuple(x.to_numpy() for x in NE_jl) 14 | 15 | 16 | def _to_py_nes(NEs_jl): 17 | return [_to_py_ne(NE) for NE in NEs_jl] 18 | 19 | 20 | def lrsnash(g): 21 | """ 22 | Compute in exact arithmetic all extreme mixed-action Nash equilibria 23 | of a 2-player normal form game with integer payoffs. This function 24 | calls the Nash equilibrium computation routine in `lrslib` (through 25 | its Julia wrapper `LRSLib.jl`) which is based on the "lexicographic 26 | reverse search" vertex enumeration algorithm [1]_. 27 | 28 | Parameters 29 | ---------- 30 | g : NormalFormGame 31 | 2-player NormalFormGame instance with integer payoffs. 32 | 33 | Returns 34 | ------- 35 | NEs : list(tuple(ndarray(object, ndim=1))) 36 | List containing tuples of Nash equilibrium mixed actions, where 37 | the values are represented by `fractions.Fraction`. 38 | 39 | Examples 40 | -------- 41 | A degenerate game example: 42 | 43 | >>> import quantecon.game_theory as gt 44 | >>> import jlgametheory as jgt 45 | >>> from pprint import pprint 46 | >>> bimatrix = [[(3, 3), (3, 3)], 47 | ... [(2, 2), (5, 6)], 48 | ... [(0, 3), (6, 1)]] 49 | >>> g = gt.NormalFormGame(bimatrix) 50 | >>> NEs = jgt.lrsnash(g) 51 | >>> pprint(NEs) 52 | [(array([Fraction(1, 1), Fraction(0, 1), Fraction(0, 1)], dtype=object), 53 | array([Fraction(1, 1), Fraction(0, 1)], dtype=object)), 54 | (array([Fraction(1, 1), Fraction(0, 1), Fraction(0, 1)], dtype=object), 55 | array([Fraction(2, 3), Fraction(1, 3)], dtype=object)), 56 | (array([Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)], dtype=object), 57 | array([Fraction(1, 3), Fraction(2, 3)], dtype=object))] 58 | 59 | The set of Nash equilibria of this degenerate game consists of an 60 | isolated equilibrium, the third output, and a non-singleton 61 | equilibrium component, the extreme points of which are given by the 62 | first two outputs. 63 | 64 | References 65 | ---------- 66 | .. [1] D. Avis, G. Rosenberg, R. Savani, and B. von Stengel, 67 | "Enumeration of Nash Equilibria for Two-Player Games," Economic 68 | Theory (2010), 9-37. 69 | 70 | """ 71 | try: 72 | N = g.N 73 | except AttributeError: 74 | raise TypeError('input must be a 2-player NormalFormGame') 75 | if N != 2: 76 | raise NotImplementedError('Implemented only for 2-player games') 77 | if not np.issubdtype(g.dtype, np.integer): 78 | raise NotImplementedError( 79 | 'Implemented only for games with integer payoffs' 80 | ) 81 | 82 | NEs_jl = GameTheory.lrsnash(to_jl_nfg(g)) 83 | return _to_py_nes(NEs_jl) 84 | 85 | 86 | def hc_solve(g, ntofind=float('inf'), **options): 87 | """ 88 | Compute all isolated mixed-action Nash equilibria of an N-player 89 | normal form game. 90 | 91 | This function solves a system of polynomial equations arising from 92 | the nonlinear complementarity problem representation of Nash 93 | equilibrium, by using `HomotopyContinuation.jl`. 94 | 95 | Parameters 96 | ---------- 97 | g : NormalFormGame 98 | N-player NormalFormGame instance. 99 | 100 | ntofind : scalar, optional(default=float('inf')) 101 | Number of Nash equilibria to find. 102 | 103 | options : 104 | Optional keyword arguments to pass to `HomotopyContinuation.solve`. 105 | For example, the option `seed` can set the random seed used 106 | during the computations. See the `documentation 107 | `_ 108 | for `HomotopyContinuation.solve` for details. 109 | 110 | Returns 111 | ------- 112 | NEs : list(tuple(ndarray(float, ndim=1))) 113 | List containing tuples of Nash equilibrium mixed actions. 114 | 115 | Examples 116 | -------- 117 | Consider the 3-player 2-action game with 9 Nash equilibria in 118 | McKelvey and McLennan (1996) "Computation of Equilibria in Finite 119 | Games": 120 | 121 | >>> import quantecon.game_theory as gt 122 | >>> import jlgametheory as jgt 123 | >>> from pprint import pprint 124 | >>> import numpy as np 125 | >>> np.set_printoptions(precision=3) # Reduce the digits printed 126 | >>> g = gt.NormalFormGame((2, 2, 2)) 127 | >>> g[0, 0, 0] = 9, 8, 12 128 | >>> g[1, 1, 0] = 9, 8, 2 129 | >>> g[0, 1, 1] = 3, 4, 6 130 | >>> g[1, 0, 1] = 3, 4, 4 131 | >>> print(g) 132 | 3-player NormalFormGame with payoff profile array: 133 | [[[[ 9., 8., 12.], [ 0., 0., 0.]], 134 | [[ 0., 0., 0.], [ 3., 4., 6.]]], 135 | 136 | [[[ 0., 0., 0.], [ 3., 4., 4.]], 137 | [[ 9., 8., 2.], [ 0., 0., 0.]]]] 138 | >>> NEs = jgt.hc_solve(g, show_progress=False) 139 | >>> len(NEs) 140 | 9 141 | >>> pprint(NEs) 142 | [(array([0., 1.]), array([0., 1.]), array([1., 0.])), 143 | (array([0.5, 0.5]), array([0.5, 0.5]), array([1.000e+00, 2.351e-38])), 144 | (array([1., 0.]), array([0., 1.]), array([-1.881e-37, 1.000e+00])), 145 | (array([0.25, 0.75]), array([0.5, 0.5]), array([0.333, 0.667])), 146 | (array([0.25, 0.75]), array([1.000e+00, 1.345e-43]), array([0.25, 0.75])), 147 | (array([0., 1.]), array([0.333, 0.667]), array([0.333, 0.667])), 148 | (array([1., 0.]), array([ 1.00e+00, -5.74e-42]), array([1., 0.])), 149 | (array([0., 1.]), array([1., 0.]), array([2.374e-66, 1.000e+00])), 150 | (array([0.5, 0.5]), array([0.333, 0.667]), array([0.25, 0.75]))] 151 | >>> all(g.is_nash(NE) for NE in NEs) 152 | True 153 | 154 | """ 155 | try: 156 | N = g.N 157 | except AttributeError: 158 | raise TypeError('g must be a NormalFormGame') 159 | if N < 2: 160 | raise NotImplementedError('Not implemented for 1-player games') 161 | 162 | NEs_jl = GameTheory.hc_solve(to_jl_nfg(g), ntofind=ntofind, **options) 163 | return _to_py_nes(NEs_jl) 164 | --------------------------------------------------------------------------------