diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index df04d9957..f380bff25 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -30,7 +30,7 @@ jobs: cd dist sdist=$(ls pygambit-*.tar.gz) pip install -v "${sdist}[test,doc]" - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.6.1" + pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" - name: Run tests run: pytest --run-tutorials @@ -53,7 +53,7 @@ jobs: - name: Build extension run: | python -m pip install -v .[test,doc] - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.6.1" + pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" - name: Run tests run: pytest --run-tutorials @@ -76,7 +76,7 @@ jobs: - name: Build extension run: | python -m pip install -v .[test,doc] - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.6.1" + pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" - name: Run tests run: pytest --run-tutorials @@ -99,6 +99,6 @@ jobs: - name: Build extension run: | python -m pip install -v .[test,doc] - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.6.1" + pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" - name: Run tests run: pytest --run-tutorials diff --git a/.gitignore b/.gitignore index f46c8bff9..e70893578 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ catalog/img/**/*.pdf catalog/img/**/*.png catalog/img/**/*.tex catalog/img/**/*.ef +catalog/img/**/*.svg diff --git a/.readthedocs.yml b/.readthedocs.yml index 4f24ce64f..16e06bb4c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -14,9 +14,10 @@ build: - pandoc - texlive-full - imagemagick + - pdf2svg jobs: post_install: - - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.6.1" + - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" # Create RST for catalog table in docs - $READTHEDOCS_VIRTUALENV_PATH/bin/python build_support/catalog/update.py diff --git a/Makefile.am b/Makefile.am index 703049248..f07dd5034 100644 --- a/Makefile.am +++ b/Makefile.am @@ -213,6 +213,10 @@ EXTRA_DIST = \ catalog/jakobsen2016/fig3.efg \ catalog/myerson1991/fig2_1.efg \ catalog/myerson1991/fig4_2.efg \ + catalog/nau2004/sec3.nfg \ + catalog/nau2004/sec4.nfg \ + catalog/nau2004/sec5.nfg \ + catalog/nau2004/sec6.nfg \ catalog/reiley2008/fig1.efg \ catalog/selten1975/fig1.efg \ catalog/selten1975/fig2.efg \ diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index de6a65cbf..0d44b3cc6 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -1,9 +1,8 @@ import argparse -import re from pathlib import Path import pandas as pd -from draw_tree import generate_pdf, generate_png, generate_tex +from draw_tree import generate_pdf, generate_png, generate_svg, generate_tex import pygambit as gbt @@ -30,128 +29,95 @@ def catalog_draw_tree_settings(slug: str) -> dict: return settings -def _write_efg_table(df: pd.DataFrame, f, tikz_re, regenerate_images: bool): - """Write the EFG games list-table to file handle f.""" - f.write(".. list-table::\n") - f.write(" :header-rows: 1\n") - f.write(" :widths: 100\n") - f.write(" :class: tight-table\n") - f.write("\n") - f.write(" * - **Extensive form games**\n") - - efg_df = df[df["Format"] == "efg"] - for _, row in efg_df.iterrows(): - slug = row["Game"] - title = str(row.get("Title", "")).strip() - description = str(row.get("Description", "")).strip() - if description: - tex_path = CATALOG_DIR / "img" / f"{slug}.tex" - png_path = CATALOG_DIR / "img" / f"{slug}.png" - pdf_path = CATALOG_DIR / "img" / f"{slug}.pdf" - ef_path = CATALOG_DIR / "img" / f"{slug}.ef" - - missing_any = not all(p.exists() for p in [tex_path, png_path, pdf_path, ef_path]) - - if regenerate_images or missing_any: - g = gbt.catalog.load(slug) - viz_path = CATALOG_DIR / "img" / f"{slug}" - viz_path.parent.mkdir(parents=True, exist_ok=True) - for func in [generate_tex, generate_png, generate_pdf]: - func(g, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) - - with open(tex_path, encoding="utf-8") as tex_f: - tex_content = tex_f.read() - match = tikz_re.search(tex_content) - tikz = ( - match.group(1).strip() - if match - else "% Could not extract tikzpicture from tex file" - ) - - # Main dropdown - f.write(f" * - .. dropdown:: {title}\n") - f.write(" \n") - - for line in description.splitlines(): - f.write(f" {line}\n") - f.write(" \n") - f.write(" **Load in PyGambit:**\n") - f.write(" \n") - f.write(" .. code-block:: python\n") - f.write(" \n") - f.write(f' pygambit.catalog.load("{slug}")\n') - f.write(" \n") - - # Download links - download_links = [row["Download"]] - for ext in ["ef", "tex", "png", "pdf"]: - download_links.append(f":download:`{slug}.{ext} <../catalog/img/{slug}.{ext}>`") - f.write(" **Download game and image files:**\n") - f.write(" \n") - f.write(f" {' '.join(download_links)}\n") - f.write(" \n") - - # TiKZ image - f.write(" .. tikz::\n") - f.write(" :align: center\n") - f.write(" \n") - for line in tikz.splitlines(): - f.write(f" {line}\n") - f.write(" \n") - - -# def _write_nfg_table(df: pd.DataFrame, f): -# """Write the NFG games list-table to file handle f.""" -# f.write(".. list-table::\n") -# f.write(" :header-rows: 1\n") -# f.write(" :widths: 100\n") -# f.write(" :class: tight-table\n") -# f.write("\n") -# f.write(" * - **Strategic form games**\n") - -# nfg_df = df[df["Format"] == "nfg"] -# for _, row in nfg_df.iterrows(): -# slug = row["Game"] - -# # Title as plain text header -# f.write(" * - \n") -# f.write(" \n") - -# # Jupyter-execute block (no dropdown) -# f.write(" .. jupyter-execute::\n") -# f.write(" \n") -# f.write(" import pygambit\n") -# f.write(f' pygambit.catalog.load("{slug}")\n') -# f.write(" \n") - -# # Download link (plain, no dropdown) -# f.write(f" :download:`{slug}.nfg <../catalog/{slug}.nfg>`\n") -# f.write(" \n") - - def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool = False): - """Generate RST output with two list-tables: one for EFG and one for NFG games.""" - tikz_re = re.compile(r"\\begin\{document\}(.*?)\\end\{document\}", re.DOTALL) - + """Generate RST output with a list-table for games.""" with open(rst_path, "w", encoding="utf-8") as f: # TOC linking to both sections - # f.write(".. contents::\n") - # f.write(" :local:\n") - # f.write(" :depth: 1\n") - # f.write("\n") - - # # EFG section - # f.write("Extensive form games\n") - # f.write("--------------------\n") - # f.write("\n") - _write_efg_table(df, f, tikz_re, regenerate_images) - # f.write("\n") - - # # NFG section - # f.write("Strategic form games\n") - # f.write("--------------------\n") - # f.write("\n") - # _write_nfg_table(df, f) + f.write(".. contents::\n") + f.write(" :local:\n") + f.write(" :depth: 1\n") + f.write("\n") + f.write(".. list-table::\n") + f.write(" :header-rows: 1\n") + f.write(" :widths: 100\n") + f.write(" :class: tight-table\n") + f.write("\n") + f.write(" * - **Catalog of games**\n") + + for _, row in df.iterrows(): + slug = row["Game"] + title = str(row.get("Title", "")).strip() + description = str(row.get("Description", "")).strip() + # Skip any games which lack a description + if description: + all_exts = [] + all_paths = [] + if row["Format"] == "efg": + ef_path = CATALOG_DIR / "img" / f"{slug}.ef" + all_exts.append("ef") + all_paths.append(ef_path) + all_exts = all_exts + ["tex", "png", "pdf", "svg"] + tex_path = CATALOG_DIR / "img" / f"{slug}.tex" + all_paths.append(tex_path) + all_paths.append(CATALOG_DIR / "img" / f"{slug}.png") + all_paths.append(CATALOG_DIR / "img" / f"{slug}.pdf") + all_paths.append(CATALOG_DIR / "img" / f"{slug}.svg") + missing_any = not all(p.exists() for p in all_paths) + + if regenerate_images or missing_any: + g = gbt.catalog.load(slug) + viz_path = CATALOG_DIR / "img" / f"{slug}" + viz_path.parent.mkdir(parents=True, exist_ok=True) + for func in [generate_tex, generate_png, generate_pdf, generate_svg]: + func(g, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) + + # Main dropdown + f.write(f" * - .. dropdown:: {title}\n") + f.write(" :open:\n") + f.write(" \n") + for line in description.splitlines(): + f.write(f" {line}\n") + + f.write(" \n") + f.write(" **Load in PyGambit:**\n") + f.write(" \n") + f.write(" .. code-block:: python\n") + f.write(" \n") + f.write(f' pygambit.catalog.load("{slug}")\n') + f.write(" \n") + + # Download links + download_links = [row["Download"]] + for ext in all_exts: + download_links.append( + f":download:`{slug}.{ext} <../catalog/img/{slug}.{ext}>`" + ) + f.write(" **Download game and image files:**\n") + f.write(" \n") + f.write(f" {' '.join(download_links)}\n") + f.write(" \n") + + # Draw image + f.write(" .. jupyter-execute::\n") + f.write(" :hide-code:\n") + f.write(" \n") + f.write(" import pygambit\n") + f.write(" from draw_tree import draw_tree\n") + if row["Format"] == "efg": + settings = catalog_draw_tree_settings(slug) + settings_str = ", ".join(f"{k}={v!r}" for k, v in settings.items()) + f.write( + f" draw_tree(" + f'pygambit.catalog.load("{slug}"), ' + f"{settings_str})\n" + ) + elif row["Format"] == "nfg": + f.write( + f" draw_tree(" + f'pygambit.catalog.load("{slug}"), ' + f'save_to="../catalog/img/{slug}.png")\n' + ) + f.write(" \n") def update_makefile(): diff --git a/catalog/nau2004/sec3.nfg b/catalog/nau2004/sec3.nfg new file mode 100644 index 000000000..bc203d6b2 --- /dev/null +++ b/catalog/nau2004/sec3.nfg @@ -0,0 +1,14 @@ +NFG 1 R "Battle of the Sexes" { "Player 1" "Player 2" } + +{ { "Top" "Bottom" } +{ "Left" "Right" } +} +"The coordination game known as Battle of the Sexes (section 3 of `Nau2004 `_). Has three Nash equilibria: two pure-strategy (TL and BR) and one completely mixed." + +{ +{ "" 3, 2 } +{ "" 0, 0 } +{ "" 0, 0 } +{ "" 2, 3 } +} +1 2 3 4 diff --git a/catalog/nau2004/sec4.nfg b/catalog/nau2004/sec4.nfg new file mode 100644 index 000000000..57b754b6f --- /dev/null +++ b/catalog/nau2004/sec4.nfg @@ -0,0 +1,19 @@ +NFG 1 R "Three-player game with a unique Nash solution in irrational strategies" { "Player 1" "Player 2" "Player 3" } + +{ { "Top" "Bottom" } +{ "Left" "Right" } +{ "1" "2" } +} +"A three-player game with a unique Nash equilibrium in irrational mixed strategies (section 4 of `Nau2004 `_). The correlated equilibrium polytope is seven-dimensional with 33 vertices." + +{ +{ "" 3, 0, 2 } +{ "" 0, 1, 0 } +{ "" 0, 2, 0 } +{ "" 1, 0, 0 } +{ "" 1, 0, 0 } +{ "" 0, 3, 0 } +{ "" 0, 1, 0 } +{ "" 2, 0, 3 } +} +1 2 3 4 5 6 7 8 diff --git a/catalog/nau2004/sec5.nfg b/catalog/nau2004/sec5.nfg new file mode 100644 index 000000000..fa9f40d53 --- /dev/null +++ b/catalog/nau2004/sec5.nfg @@ -0,0 +1,19 @@ +NFG 1 R "Game with a continuum of completely mixed-strategy Nash equilibria" { "Player 1" "Player 2" "Player 3" } + +{ { "Top" "Bottom" } +{ "Left" "Right" } +{ "1" "2" } +} +"A three-player 2x2x2 game with 3 pure, 2 incompletely mixed, and a continuum of completely mixed Nash equilibria (section 5 of `Nau2004 `_). The correlated equilibrium polytope is seven-dimensional with 8 vertices." + +{ +{ "" 0, 0, 2 } +{ "" 3, 0, 0 } +{ "" 0, 3, 0 } +{ "" 0, 0, 0 } +{ "" 1, 1, 0 } +{ "" 0, 0, 0 } +{ "" 0, 0, 0 } +{ "" 0, 0, 3 } +} +1 2 3 4 5 6 7 8 diff --git a/catalog/nau2004/sec6.nfg b/catalog/nau2004/sec6.nfg new file mode 100644 index 000000000..bb378500a --- /dev/null +++ b/catalog/nau2004/sec6.nfg @@ -0,0 +1,27 @@ +NFG 1 R "2x2x4 game with Nash equilibria in the relative interior of the correlated equilibrium polytope" { "Player 1" "Player 2" "Player 3" } + +{ { "Top" "Bottom" } +{ "Left" "Right" } +{ "1" "2" "3" "4" } +} +"A three-player 2x2x4 game (section 6 of `Nau2004 `_). The correlated equilibrium polytope is four-dimensional with six vertices. The set of Nash equilibria is a line segment in the relative interior of the polytope." + +{ +{ "" 2, 0, 0 } +{ "" 0, 2, 1 } +{ "" 0, 1, 1 } +{ "" 1, 0, 2 } +{ "" 1, 0, 2 } +{ "" 0, 1, 1 } +{ "" 0, 2, 1 } +{ "" 2, 0, 0 } +{ "" 1, 0, 1 } +{ "" 0, 2, 0 } +{ "" 0, 1, 2 } +{ "" 2, 0, 1 } +{ "" 2, 0, 1 } +{ "" 0, 1, 2 } +{ "" 0, 2, 0 } +{ "" 1, 0, 1 } +} +1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 diff --git a/contrib/games/2x2x2-nau.nfg b/contrib/games/2x2x2-nau.nfg deleted file mode 100644 index 0e9ca9eec..000000000 --- a/contrib/games/2x2x2-nau.nfg +++ /dev/null @@ -1,24 +0,0 @@ -NFG 1 R "2x2x2 example with 3 pure, 2 incompletely mixed, and a continuum of completely mixed NE" { "" "" "" } - -{ { "1" "2" } -{ "1" "2" } -{ "1" "2" } -} -"Example game from Nau et al [^Nau2004]. - -[^Nau2004]: Nau, Robert, Gomez Canovas, Sabrina, and Hansen, Pierre (2004). - On the geometry of Nash equilibria and correlated equilibria. - International Journal of Game Theory 32(4): 443-453 -" - -{ -{ "" 0, 0, 2 } -{ "" 3, 0, 0 } -{ "" 0, 3, 0 } -{ "" 0, 0, 0 } -{ "" 1, 1, 0 } -{ "" 0, 0, 0 } -{ "" 0, 0, 0 } -{ "" 0, 0, 3 } -} -1 2 3 4 5 6 7 8 diff --git a/doc/biblio.rst b/doc/biblio.rst index b256f566b..28c4e7ce6 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -52,6 +52,10 @@ Articles on computation of Nash equilibria D. and Rust, J. (eds), *Handbook of Computational Economics*, Elsevier, pp. 87-142. +.. [Nau2004] Nau, Robert, Gomez Canovas, Sabrina, and Hansen, Pierre (2004). + On the geometry of Nash equilibria and correlated equilibria. + International Journal of Game Theory 32(4): 443-453 + .. [PNS04] Porter, R., Nudelman, E. and Shoham, Y. 2004, 'Simple search methods for finding a Nash equilibrium', *Games and Economic Behavior*, pp. 664-669. diff --git a/doc/conf.py b/doc/conf.py index 0eb914202..f3926f47d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -31,6 +31,7 @@ "nbsphinx", "sphinxcontrib.tikz", "jupyter_sphinx", + "jupyter_sphinx", ] # IPython directive configuration diff --git a/pyproject.toml b/pyproject.toml index f2432645b..601a91599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ [project.optional-dependencies] test = ["pytest", "pytest-subtests", "nbformat", "nbclient", "ipykernel"] doc = [ + "sphinx>=7.0", "pydata-sphinx-theme", "sphinx_design", "sphinx-autobuild", @@ -42,6 +43,7 @@ doc = [ "matplotlib", "pickleshare", "jupyter", + "jupyter-sphinx", "open_spiel; sys_platform != 'win32'", "sphinxcontrib-tikz", "jupyter_sphinx",