Normalise payoffs in vertex enumeration - #244
Open
choosen23 wants to merge 2 commits into
Open
Conversation
choosen23
force-pushed
the
fix-vertex-enumeration-payoff-scale
branch
from
August 26, 2026 09:40
2deb3a7 to
99ee7f1
Compare
Two checks in the tox env currently fail on main, so CI stops before the tests run. This affects every open pull request. `black --check src/` fails because recent releases collapse these short `.format` calls onto a single line. `mypy` fails on the return annotation of `imitation_dynamics`, which declares `Tuple[float, float]` but yields a pair of arrays. No behaviour change.
Author
|
A note on the red CI here: the run fails at the first tox command, Both of those, plus a Once #245 is in I will rebase this branch and CI should be green. Happy to reorder or combine them if you would rather. |
The best response polytope is {x >= 0, Mx <= 1}, so its vertex coordinates
scale as 1 / payoff. `non_trivial_vertices` discards the origin using
`np.isclose(v, 0)`, which falls back to an absolute tolerance of 1e-8. Once
payoffs are large enough every legitimate vertex falls below that tolerance
and is discarded, so `vertex_enumeration` yields nothing at all.
Rock-Paper-Scissors and Battle of the Sexes are both affected at a payoff
scale of 1e9, having been solved correctly at 1e6. Neither game is
degenerate, and multiplying every payoff by a constant cannot change the
equilibria.
Nash equilibria are invariant under a positive rescaling of each player's
payoffs, so normalising before the polytopes are built keeps them well
conditioned without changing the result.
choosen23
force-pushed
the
fix-vertex-enumeration-payoff-scale
branch
from
August 26, 2026 09:46
99ee7f1 to
fd0607b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits: the first unbreaks CI on current tooling, the second is the actual fix. Details on the
first are at the bottom.
The problem
vertex_enumerationreturns an empty generator — no exception, no warning — for games it solvescorrectly when the payoffs are smaller.
This is not degeneracy
Flagging this up front, since #39 and #66 were both reports of empty results that turned out to be
degeneracy. This one isn't:
Degeneracy is invariant under a positive rescaling of payoffs, so it cannot explain a failure that
appears only at ×1e9. And since every finite game has at least one equilibrium, an empty return is
never a correct answer here.
Root cause
The best response polytope is
{x >= 0, Mx <= 1}. Because the right hand side is fixed at 1, thevertex coordinates scale as
1 / payoff.non_trivial_verticesdiscards the origin withand
np.isclose(v, 0)reduces to its absolute toleranceatol=1e-8when comparing against zero. Soonce payoffs are large enough, every legitimate vertex falls under
atoland is thrown away as thetrivial vertex:
The fix
Normalise in
vertex_enumeration, just after the existing shift to non-negative payoffs. Nashequilibria are invariant under a positive rescaling of each player's payoffs, so this does not change
the result — it just keeps the polytope well conditioned so the existing tolerance is meaningful.
I first tried the smaller change, making the zero test in
polytope.pyscale-relative, and it isnot a good fix: it lets through vertices whose labels are then computed incorrectly, returning 49
spurious "equilibria" with a normalised regret of 1.0 at ×1e9, and crashing in
find_feasible_pointat ×1e15. Fixing the conditioning at the source avoids that.
Test plan
test_with_large_utilities, checking all three equilibria of Battle of theSexes at scales 1, 1e6, 1e9 and 1e12. It fails on
mainwith0 = len([])and passes here.normalised regret at float precision (2.8e-17 or lower) for scales up to ×1e18.
Full tox command set locally:
black --check src/ tests/mypy --ignore-missing-imports src/nashpyinterrogate --fail-under 100pytest . --cov=nashpy --cov-fail-under=100+ doctestsflake8 src/ tests/The first commit: unbreaking CI
Two tox commands fail on
mainunder current tooling releases, and both run before the tests, so CIis currently red on every open pull request:
black --check src/— recent black releases collapse these short.formatcalls onto a singleline (
game.py,support_enumeration.py).mypy—imitation_dynamicsis annotatedGenerator[Tuple[float, float], Any, None]but yields apair of arrays. Corrected to
Tuple[npt.NDArray, npt.NDArray].Formatting and one annotation, no behaviour change. Kept as a separate first commit so it can be
dropped or cherry-picked independently if you would rather handle it another way.