From 966bec8ee39ae090180fb5b208658a6855e37507 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 24 Nov 2025 11:23:09 +0000 Subject: [PATCH 01/32] For mixed behaviors renamed `liap_value` -> `agent_liap_value` and `max_regret` -> `agent_max_regret` in Python --- doc/pygambit.api.rst | 4 ++-- doc/tutorials/03_poker.ipynb | 17 +++++++++-------- src/pygambit/behavmixed.pxi | 34 +++++++++++++++++++++------------- tests/test_behav.py | 20 +++++++++++--------- tests/test_game.py | 2 +- tests/test_nash.py | 8 ++++---- 6 files changed, 48 insertions(+), 37 deletions(-) diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 185f69335..7397ca6ef 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -254,8 +254,8 @@ Probability distributions over behavior MixedBehaviorProfile.infoset_prob MixedBehaviorProfile.belief MixedBehaviorProfile.is_defined_at - MixedBehaviorProfile.max_regret - MixedBehaviorProfile.liap_value + MixedBehaviorProfile.agent_max_regret + MixedBehaviorProfile.agent_liap_value MixedBehaviorProfile.as_strategy MixedBehaviorProfile.normalize MixedBehaviorProfile.copy diff --git a/doc/tutorials/03_poker.ipynb b/doc/tutorials/03_poker.ipynb index 14292df75..4c00e95d4 100644 --- a/doc/tutorials/03_poker.ipynb +++ b/doc/tutorials/03_poker.ipynb @@ -1070,7 +1070,7 @@ ], "source": [ "ls_eqm = logit_solve_result.equilibria[0]\n", - "ls_eqm.max_regret()" + "ls_eqm.agent_max_regret()" ] }, { @@ -1078,7 +1078,7 @@ "id": "a2ba06c4", "metadata": {}, "source": [ - "The value of `MixedBehaviorProfile.max_regret` of the computed profile exceeds $10^{-8}$ measured in payoffs of the game.\n", + "The value of `MixedBehaviorProfile.agent_max_regret` of the computed profile exceeds $10^{-8}$ measured in payoffs of the game.\n", "However, when considered relative to the scale of the game's payoffs, we see it is less than $10^{-8}$ of the payoff range, as requested:" ] }, @@ -1099,9 +1099,7 @@ "output_type": "execute_result" } ], - "source": [ - "ls_eqm.max_regret() / (g.max_payoff - g.min_payoff)" - ] + "source": "ls_eqm.agent_max_regret() / (g.max_payoff - g.min_payoff)" }, { "cell_type": "markdown", @@ -1131,7 +1129,10 @@ } ], "source": [ - "gbt.nash.logit_solve(g, maxregret=1e-4).equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)" + "(\n", + " gbt.nash.logit_solve(g, maxregret=1e-4).equilibria[0]\n", + " .agent_max_regret() / (g.max_payoff - g.min_payoff)\n", + ")" ] }, { @@ -1232,7 +1233,7 @@ "source": [ "(\n", " gbt.nash.liap_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)\n", - " .equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)\n", + " .equilibria[0].agent_max_regret() / (g.max_payoff - g.min_payoff)\n", ")" ] }, @@ -1268,7 +1269,7 @@ "\n", "(\n", " gbt.nash.liap_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)\n", - " .equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)\n", + " .equilibria[0].agent_max_regret() / (g.max_payoff - g.min_payoff)\n", ")" ] }, diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index e37d465f7..cb2e00975 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -811,39 +811,47 @@ class MixedBehaviorProfile: See Also -------- action_regret - max_regret + agent_max_regret """ self._check_validity() return self._infoset_regret(self.game._resolve_infoset(infoset, "infoset_regret")) - def max_regret(self) -> ProfileDType: - """Returns the maximum regret of any player. + def agent_max_regret(self) -> ProfileDType: + """Returns the maximum regret at any information set. A profile is an agent Nash equilibrium if and only if `max_regret()` is 0. - .. versionadded:: 16.2.0 + .. versionchanged:: 16.5.0 + + Renamed from `max_regret` to `agent_max_regret` to clarify the distinction between + per-player and per-agent concepts. See Also -------- action_regret infoset_regret - liap_value + agent_liap_value """ self._check_validity() - return self._max_regret() + return self._agent_max_regret() - def liap_value(self) -> ProfileDType: + def agent_liap_value(self) -> ProfileDType: """Returns the Lyapunov value (see [McK91]_) of the strategy profile. The Lyapunov value is a non-negative number which is zero exactly at agent Nash equilibria. + .. versionchanged:: 16.5.0 + + Renamed from `liap_value` to `agent_liap_value` to clarify the distinction between + per-player and per-agent concepts. + See Also -------- - max_regret + agent_max_regret """ self._check_validity() - return self._liap_value() + return self._agent_liap_value() def as_strategy(self) -> MixedStrategyProfile: """Returns a `MixedStrategyProfile` which is equivalent @@ -921,7 +929,7 @@ class MixedBehaviorProfileDouble(MixedBehaviorProfile): def _infoset_regret(self, infoset: Infoset) -> float: return deref(self.profile).GetRegret(infoset.infoset) - def _max_regret(self) -> float: + def _agent_max_regret(self) -> float: return deref(self.profile).GetMaxRegret() def __eq__(self, other: typing.Any) -> bool: @@ -940,7 +948,7 @@ class MixedBehaviorProfileDouble(MixedBehaviorProfile): deref(self.profile).ToMixedProfile() )) - def _liap_value(self) -> float: + def _agent_liap_value(self) -> float: return deref(self.profile).GetLiapValue() def _normalize(self) -> MixedBehaviorProfileDouble: @@ -1017,7 +1025,7 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile): def _infoset_regret(self, infoset: Infoset) -> Rational: return rat_to_py(deref(self.profile).GetRegret(infoset.infoset)) - def _max_regret(self) -> Rational: + def _agent_max_regret(self) -> Rational: return rat_to_py(deref(self.profile).GetMaxRegret()) def __eq__(self, other: typing.Any) -> bool: @@ -1036,7 +1044,7 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile): deref(self.profile).ToMixedProfile() )) - def _liap_value(self) -> Rational: + def _agent_liap_value(self) -> Rational: return rat_to_py(deref(self.profile).GetLiapValue()) def _normalize(self) -> MixedBehaviorProfileRational: diff --git a/tests/test_behav.py b/tests/test_behav.py index dbf096167..43d3025ad 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -941,16 +941,17 @@ def test_node_value_consistency(game: gbt.Game, rational_flag: bool): (games.create_stripped_down_poker_efg(), [1.0, 0.0, 1.0, 0.0, 1.0, 0.0], False, 1.0), ] ) -def test_liap_value_reference(game: gbt.Game, action_probs: typing.Union[None, list], - rational_flag: bool, expected_value: typing.Union[str, float]): - """Tests liap_value under profile given by action_probs +def test_agent_liap_value_reference(game: gbt.Game, action_probs: typing.Union[None, list], + rational_flag: bool, expected_value: typing.Union[str, float]): + """Tests agent_liap_value under profile given by action_probs (which will be uniform if action_probs is None) """ profile = game.mixed_behavior_profile(rational=rational_flag) if action_probs: _set_action_probs(profile, action_probs, rational_flag) assert ( - profile.liap_value() == (gbt.Rational(expected_value) if rational_flag else expected_value) + profile.agent_liap_value() == (gbt.Rational(expected_value) + if rational_flag else expected_value) ) @@ -1146,15 +1147,16 @@ def _get_and_check_answers(game: gbt.Game, action_probs1: tuple, action_probs2: lambda x, y: x.node_value(player=y[0], node=y[1]), lambda x: list(product(x.players, x.nodes))), ###################################################################################### - # liap_value (of profile, hence [1] for objects_to_test, any singleton collection would do) + # agent_liap_value (of profile, hence [1] for objects_to_test, + # any singleton collection would do) (games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False, - lambda x, y: x.liap_value(), lambda x: [1]), + lambda x, y: x.agent_liap_value(), lambda x: [1]), (games.create_mixed_behav_game_efg(), PROBS_1A_rat, PROBS_2A_rat, True, - lambda x, y: x.liap_value(), lambda x: [1]), + lambda x, y: x.agent_liap_value(), lambda x: [1]), (games.create_stripped_down_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False, - lambda x, y: x.liap_value(), lambda x: [1]), + lambda x, y: x.agent_liap_value(), lambda x: [1]), (games.create_stripped_down_poker_efg(), PROBS_1A_rat, PROBS_2A_rat, True, - lambda x, y: x.liap_value(), lambda x: [1]), + lambda x, y: x.agent_liap_value(), lambda x: [1]), ] ) def test_profile_order_consistency(game: gbt.Game, diff --git a/tests/test_game.py b/tests/test_game.py index 73137c1f8..c5d64aa60 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -191,4 +191,4 @@ def test_behavior_profile_invalidation(): with pytest.raises(gbt.GameStructureChangedError): profile.payoff(g.players[0]) with pytest.raises(gbt.GameStructureChangedError): - profile.liap_value() + profile.agent_liap_value() diff --git a/tests/test_nash.py b/tests/test_nash.py index 7fbf7c02b..1d118667c 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -154,7 +154,7 @@ def test_enumpoly_ordered_behavior( result = gbt.nash.enumpoly_solve(game, use_strategic=False) assert len(result.equilibria) == len(mixed_behav_prof_data) for eq, exp in zip(result.equilibria, mixed_behav_prof_data): - assert abs(eq.max_regret()) <= TOL + assert abs(eq.agent_max_regret()) <= TOL expected = game.mixed_behavior_profile(rational=True, data=exp) for p in game.players: for i in p.infosets: @@ -221,7 +221,7 @@ def are_the_same(game, found, candidate): return True for eq in result.equilibria: - assert abs(eq.max_regret()) <= TOL + assert abs(eq.agent_max_regret()) <= TOL found = False for exp in mixed_behav_prof_data[:]: expected = game.mixed_behavior_profile(rational=True, data=exp) @@ -365,7 +365,7 @@ def test_lcp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): result = gbt.nash.lcp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 eq = result.equilibria[0] - assert eq.max_regret() == 0 + assert eq.agent_max_regret() == 0 expected = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) assert eq == expected @@ -440,7 +440,7 @@ def test_lp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): result = gbt.nash.lp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 eq = result.equilibria[0] - assert eq.max_regret() == 0 + assert eq.agent_max_regret() == 0 expected = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) assert eq == expected From 89c182c82efc2b3bdc2bfa9dafabae5fba9c3b4e Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 24 Nov 2025 11:31:41 +0000 Subject: [PATCH 02/32] Rename agent functions in MixedBehaviorProfile --- src/games/behavmixed.cc | 4 ++-- src/games/behavmixed.h | 8 ++++---- src/games/stratmixed.h | 4 ++-- src/pygambit/behavmixed.pxi | 8 ++++---- src/pygambit/gambit.pxd | 4 ++-- src/solvers/enumpoly/efgpoly.cc | 2 +- src/solvers/liap/efgliap.cc | 2 +- src/solvers/logit/efglogit.cc | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/games/behavmixed.cc b/src/games/behavmixed.cc index cf11abb6d..014d40f52 100644 --- a/src/games/behavmixed.cc +++ b/src/games/behavmixed.cc @@ -261,7 +261,7 @@ template MixedBehaviorProfile MixedBehaviorProfile::ToFullSuppor // MixedBehaviorProfile: Interesting quantities //======================================================================== -template T MixedBehaviorProfile::GetLiapValue() const +template T MixedBehaviorProfile::GetAgentLiapValue() const { CheckVersion(); ComputeSolutionData(); @@ -364,7 +364,7 @@ template T MixedBehaviorProfile::GetRegret(const GameInfoset &p_inf return br_payoff - GetPayoff(p_infoset); } -template T MixedBehaviorProfile::GetMaxRegret() const +template T MixedBehaviorProfile::GetAgentMaxRegret() const { auto infosets = m_support.GetGame()->GetInfosets(); return std::accumulate( diff --git a/src/games/behavmixed.h b/src/games/behavmixed.h index 239d237db..8a0ad0b06 100644 --- a/src/games/behavmixed.h +++ b/src/games/behavmixed.h @@ -174,7 +174,7 @@ template class MixedBehaviorProfile { //@{ T GetPayoff(int p_player) const; T GetPayoff(const GamePlayer &p_player) const { return GetPayoff(p_player->GetNumber()); } - T GetLiapValue() const; + T GetAgentLiapValue() const; const T &GetRealizProb(const GameNode &node) const; T GetInfosetProb(const GameInfoset &iset) const; @@ -192,7 +192,7 @@ template class MixedBehaviorProfile { /// \p p_action. /// @param[in] p_action The action to compute the regret for. /// @sa GetRegret(const GameInfoset &) const; - /// GetMaxRegret() const + /// GetAgentMaxRegret() const const T &GetRegret(const GameAction &p_action) const; /// @brief Computes the regret at information set \p p_infoset @@ -202,14 +202,14 @@ template class MixedBehaviorProfile { /// the payoff to playing their specified mixed action. /// @param[in] p_infoset The information set to compute the regret at. /// @sa GetRegret(const GameAction &) const; - /// GetMaxRegret() const + /// GetAgentMaxRegret() const T GetRegret(const GameInfoset &p_infoset) const; /// @brief Computes the maximum regret at any information set in the profile /// @details Computes the maximum of the regrets of the information sets in the profile. /// @sa GetRegret(const GameInfoset &) const; /// GetRegret(const GameAction &) const - T GetMaxRegret() const; + T GetAgentMaxRegret() const; T DiffActionValue(const GameAction &action, const GameAction &oppAction) const; T DiffRealizProb(const GameNode &node, const GameAction &oppAction) const; diff --git a/src/games/stratmixed.h b/src/games/stratmixed.h index 7c7b13e08..2832dc05d 100644 --- a/src/games/stratmixed.h +++ b/src/games/stratmixed.h @@ -279,7 +279,7 @@ template class MixedStrategyProfile { /// \p p_strategy. /// @param[in] p_strategy The strategy to compute the regret for. /// @sa GetRegret(const GamePlayer &) const; - /// GetMaxRegret() const + /// GetAgentMaxRegret() const T GetRegret(const GameStrategy &p_strategy) const { CheckVersion(); @@ -293,7 +293,7 @@ template class MixedStrategyProfile { /// their specified mixed strategy. /// @param[in] p_player The player to compute the regret for. /// @sa GetRegret(const GameStrategy &) const; - /// GetMaxRegret() const + /// GetAgentMaxRegret() const T GetRegret(const GamePlayer &p_player) const { CheckVersion(); diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index cb2e00975..491382918 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -930,7 +930,7 @@ class MixedBehaviorProfileDouble(MixedBehaviorProfile): return deref(self.profile).GetRegret(infoset.infoset) def _agent_max_regret(self) -> float: - return deref(self.profile).GetMaxRegret() + return deref(self.profile).GetAgentMaxRegret() def __eq__(self, other: typing.Any) -> bool: return ( @@ -949,7 +949,7 @@ class MixedBehaviorProfileDouble(MixedBehaviorProfile): )) def _agent_liap_value(self) -> float: - return deref(self.profile).GetLiapValue() + return deref(self.profile).GetAgentLiapValue() def _normalize(self) -> MixedBehaviorProfileDouble: return MixedBehaviorProfileDouble.wrap( @@ -1026,7 +1026,7 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile): return rat_to_py(deref(self.profile).GetRegret(infoset.infoset)) def _agent_max_regret(self) -> Rational: - return rat_to_py(deref(self.profile).GetMaxRegret()) + return rat_to_py(deref(self.profile).GetAgentMaxRegret()) def __eq__(self, other: typing.Any) -> bool: return ( @@ -1045,7 +1045,7 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile): )) def _agent_liap_value(self) -> Rational: - return rat_to_py(deref(self.profile).GetLiapValue()) + return rat_to_py(deref(self.profile).GetAgentLiapValue()) def _normalize(self) -> MixedBehaviorProfileRational: return MixedBehaviorProfileRational.wrap( diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index a6c6bb40b..2d3702a0e 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -372,8 +372,8 @@ cdef extern from "games/behavmixed.h" namespace "Gambit": T GetPayoff(c_GameAction) except + T GetRegret(c_GameAction) except + T GetRegret(c_GameInfoset) except + - T GetMaxRegret() except + - T GetLiapValue() except + + T GetAgentMaxRegret() except + + T GetAgentLiapValue() except + c_MixedStrategyProfile[T] ToMixedProfile() # except + doesn't compile c_MixedBehaviorProfile(c_MixedStrategyProfile[T]) except +NotImplementedError c_MixedBehaviorProfile(c_Game) except + diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index e88cca353..921e94a37 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -213,7 +213,7 @@ EnumPolyBehaviorSolve(const Game &p_game, int p_stopAfter, double p_maxregret, for (auto solution : SolveSupport(support, isSingular, std::max(p_stopAfter - int(ret.size()), 0))) { const MixedBehaviorProfile fullProfile = solution.ToFullSupport(); - if (fullProfile.GetMaxRegret() < p_maxregret) { + if (fullProfile.GetAgentMaxRegret() < p_maxregret) { p_onEquilibrium(fullProfile); ret.push_back(fullProfile); } diff --git a/src/solvers/liap/efgliap.cc b/src/solvers/liap/efgliap.cc index 2add29501..af6d7363d 100644 --- a/src/solvers/liap/efgliap.cc +++ b/src/solvers/liap/efgliap.cc @@ -176,7 +176,7 @@ LiapBehaviorSolve(const MixedBehaviorProfile &p_start, double p_maxregre } auto p2 = EnforceNonnegativity(p); - if (p2.GetMaxRegret() * F.GetScale() < p_maxregret) { + if (p2.GetAgentMaxRegret() * F.GetScale() < p_maxregret) { p_callback(p2, "NE"); solutions.push_back(p2); } diff --git a/src/solvers/logit/efglogit.cc b/src/solvers/logit/efglogit.cc index 4b6c0d715..f81bd38ca 100644 --- a/src/solvers/logit/efglogit.cc +++ b/src/solvers/logit/efglogit.cc @@ -72,7 +72,7 @@ Vector ProfileToPoint(const LogitQREMixedBehaviorProfile &p_profile) bool RegretTerminationFunction(const Game &p_game, const Vector &p_point, double p_regret) { - return (p_point.back() < 0.0 || PointToProfile(p_game, p_point).GetMaxRegret() < p_regret); + return (p_point.back() < 0.0 || PointToProfile(p_game, p_point).GetAgentMaxRegret() < p_regret); } class EquationSystem { From 1b839d59ed1258369f2e0afa6d1cebc137cf8723 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 24 Nov 2025 11:40:24 +0000 Subject: [PATCH 03/32] Separated out `enumpure_agent_solve` function for clarity. --- doc/pygambit.api.rst | 1 + src/pygambit/nash.py | 68 ++++++++++++++++++++++++++++++-------------- tests/test_nash.py | 4 +-- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 7397ca6ef..a3282fa5c 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -293,6 +293,7 @@ Computation of Nash equilibria NashComputationResult enumpure_solve + enumpure_agent_solve enummixed_solve enumpoly_solve lp_solve diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py index 6579d11db..2efbe7a69 100644 --- a/src/pygambit/nash.py +++ b/src/pygambit/nash.py @@ -65,39 +65,65 @@ class NashComputationResult: parameters: dict = dataclasses.field(default_factory=dict) -def enumpure_solve(game: libgbt.Game, use_strategic: bool = True) -> NashComputationResult: +def enumpure_solve(game: libgbt.Game) -> NashComputationResult: """Compute all :ref:`pure-strategy Nash equilibria ` of game. + .. versionchanged:: 16.5.0 + + `use_strategic` parameter removed. The old behavior in the case + of `use_strategic=False` is now available as `enumpure_agent_solve`. + Parameters ---------- game : Game The game to compute equilibria in. - use_strategic : bool, default True - Whether to use the strategic form. If False, computes all agent-form - pure-strategy equilibria, which consider only unilateral deviations at each - individual information set. Returns ------- res : NashComputationResult The result represented as a ``NashComputationResult`` object. + + See also + -------- + enumpure_agent_solve """ - if not game.is_tree or use_strategic: - return NashComputationResult( - game=game, - method="enumpure", - rational=True, - use_strategic=True, - equilibria=libgbt._enumpure_strategy_solve(game) - ) - else: - return NashComputationResult( - game=game, - method="enumpure", - rational=True, - use_strategic=False, - equilibria=libgbt._enumpure_agent_solve(game) - ) + return NashComputationResult( + game=game, + method="enumpure", + rational=True, + use_strategic=True, + equilibria=libgbt._enumpure_strategy_solve(game) + ) + + +def enumpure_agent_solve(game: libgbt.Game) -> NashComputationResult: + """Compute all :ref:`pure-strategy agent Nash equilibria ` of game. + + .. versioncadded:: 16.5.0 + + Formerly implemented as `enumpure_solve` with `use_strategic=False`. + + Parameters + ---------- + game : Game + The game to compute agent-Nash equilibria in. + + Returns + ------- + res : NashComputationResult + The result represented as a ``NashComputationResult`` object. + + See also + -------- + enumpure_solve + """ + return NashComputationResult( + game=game, + method="enumpure-agent", + rational=True, + use_strategic=False, + equilibria=libgbt._enumpure_agent_solve(game) + ) def enummixed_solve( diff --git a/tests/test_nash.py b/tests/test_nash.py index 1d118667c..b16ccc0b8 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -20,13 +20,13 @@ def test_enumpure_strategy(): """Test calls of enumeration of pure strategies.""" game = games.read_from_file("stripped_down_poker.efg") - assert len(gbt.nash.enumpure_solve(game, use_strategic=True).equilibria) == 0 + assert len(gbt.nash.enumpure_solve(game).equilibria) == 0 def test_enumpure_agent(): """Test calls of enumeration of pure agent strategies.""" game = games.read_from_file("stripped_down_poker.efg") - assert len(gbt.nash.enumpure_solve(game, use_strategic=False).equilibria) == 0 + assert len(gbt.nash.enumpure_agent_solve(game).equilibria) == 0 def test_enummixed_double(): From a1f69464a907dfd74f726558a75cfd8955acfc85 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 24 Nov 2025 11:50:30 +0000 Subject: [PATCH 04/32] Add `liap_agent_solve` and remove that functionality from `liap_solve` for expositional clarity. --- doc/pygambit.api.rst | 1 + doc/tutorials/03_poker.ipynb | 6 +-- src/pygambit/gambit.pxd | 2 +- src/pygambit/nash.pxi | 2 +- src/pygambit/nash.py | 71 +++++++++++++++++++++++++++++------- src/solvers/liap/efgliap.cc | 6 +-- src/solvers/liap/liap.h | 4 +- src/tools/liap/liap.cc | 14 +++---- tests/test_nash.py | 6 +-- 9 files changed, 78 insertions(+), 34 deletions(-) diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index a3282fa5c..e84524c42 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -299,6 +299,7 @@ Computation of Nash equilibria lp_solve lcp_solve liap_solve + liap_agent_solve logit_solve simpdiv_solve ipa_solve diff --git a/doc/tutorials/03_poker.ipynb b/doc/tutorials/03_poker.ipynb index 4c00e95d4..fda248ddb 100644 --- a/doc/tutorials/03_poker.ipynb +++ b/doc/tutorials/03_poker.ipynb @@ -1210,7 +1210,7 @@ "source": [ "The convention of expressing `maxregret` scaled by the game's payoffs standardises the behavior of methods across games.\n", "\n", - "For example, consider solving the poker game instead using `liap_solve()`." + "For example, consider solving the poker game instead using `liap_agent_solve()`." ] }, { @@ -1232,7 +1232,7 @@ ], "source": [ "(\n", - " gbt.nash.liap_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)\n", + " gbt.nash.liap_agent_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)\n", " .equilibria[0].agent_max_regret() / (g.max_payoff - g.min_payoff)\n", ")" ] @@ -1268,7 +1268,7 @@ " outcome[\"Bob\"] = outcome[\"Bob\"] * 2\n", "\n", "(\n", - " gbt.nash.liap_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)\n", + " gbt.nash.liap_agent_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)\n", " .equilibria[0].agent_max_regret() / (g.max_payoff - g.min_payoff)\n", ")" ] diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 2d3702a0e..84216b53f 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -500,7 +500,7 @@ cdef extern from "solvers/liap/liap.h": stdlist[c_MixedStrategyProfile[double]] LiapStrategySolve( c_MixedStrategyProfile[double], double p_maxregret, int p_maxitsN ) except +RuntimeError - stdlist[c_MixedBehaviorProfile[double]] LiapBehaviorSolve( + stdlist[c_MixedBehaviorProfile[double]] LiapAgentSolve( c_MixedBehaviorProfile[double], double p_maxregret, int p_maxitsN ) except +RuntimeError diff --git a/src/pygambit/nash.pxi b/src/pygambit/nash.pxi index 9f8d73582..e0ce36f6d 100644 --- a/src/pygambit/nash.pxi +++ b/src/pygambit/nash.pxi @@ -125,7 +125,7 @@ def _liap_strategy_solve(start: MixedStrategyProfileDouble, def _liap_behavior_solve(start: MixedBehaviorProfileDouble, maxregret: float, maxiter: int) -> typing.List[MixedBehaviorProfileDouble]: - return _convert_mbpd(LiapBehaviorSolve(deref(start.profile), maxregret, maxiter)) + return _convert_mbpd(LiapAgentSolve(deref(start.profile), maxregret, maxiter)) def _simpdiv_strategy_solve( diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py index 2efbe7a69..f3298302e 100644 --- a/src/pygambit/nash.py +++ b/src/pygambit/nash.py @@ -304,7 +304,7 @@ def lp_solve( def liap_solve( - start: libgbt.MixedStrategyProfileDouble | libgbt.MixedBehaviorProfileDouble, + start: libgbt.MixedStrategyProfileDouble, maxregret: float = 1.0e-4, maxiter: int = 1000 ) -> NashComputationResult: @@ -317,9 +317,14 @@ def liap_solve( instead of a game. Implemented `maxregret` to specify acceptance criterion for approximation. + .. versionchanged:: 16.5.0 + + Computing agent Nash equilibria in the extensive game moved to + `liap_agent_solve` for clarity. + Parameters ---------- - start : MixedStrategyProfileDouble or MixedBehaviorProfileDouble + start : MixedStrategyProfileDouble The starting profile for function minimization. Up to one equilibrium will be found from any starting profile, and the equilibrium found may (and generally will) depend on the initial profile chosen. @@ -343,22 +348,60 @@ def liap_solve( """ if maxregret <= 0.0: raise ValueError("liap_solve(): maxregret argument must be positive") - if isinstance(start, libgbt.MixedStrategyProfileDouble): - equilibria = libgbt._liap_strategy_solve(start, - maxregret=maxregret, maxiter=maxiter) - elif isinstance(start, libgbt.MixedBehaviorProfileDouble): - equilibria = libgbt._liap_behavior_solve(start, - maxregret=maxregret, maxiter=maxiter) - else: - raise TypeError( - f"liap_solve(): start must be a MixedStrategyProfile or MixedBehaviorProfile, " - f"not {start.__class__.__name__}" - ) + equilibria = libgbt._liap_strategy_solve(start, + maxregret=maxregret, maxiter=maxiter) return NashComputationResult( game=start.game, method="liap", rational=False, - use_strategic=isinstance(start, libgbt.MixedStrategyProfileDouble), + use_strategic=True, + equilibria=equilibria, + parameters={"start": start, "maxregret": maxregret, "maxiter": maxiter} + ) + + +def liap_agent_solve( + start: libgbt.MixedBehaviorProfileDouble, + maxregret: float = 1.0e-4, + maxiter: int = 1000 +) -> NashComputationResult: + """Compute approximate agent Nash equilibria of a game using + :ref:`Lyapunov function minimization `. + + .. versionadded:: 16.5.0 + + Moved from `liap_solve` passing a `MixedBehaviorProfileDouble` for additional + clarity in the solution concept computed. + + Parameters + ---------- + start : MixedBehaviorProfileDouble + The starting profile for function minimization. Up to one equilibrium will be found + from any starting profile, and the equilibrium found may (and generally will) + depend on the initial profile chosen. + + maxregret : float, default 1e-4 + The acceptance criterion for approximate Nash equilibrium; the maximum + regret of any player must be no more than `maxregret` times the + difference of the maximum and minimum payoffs of the game + + maxiter : int, default 1000 + Maximum number of iterations in function minimization. + + Returns + ------- + res : NashComputationResult + The result represented as a ``NashComputationResult`` object. + """ + if maxregret <= 0.0: + raise ValueError("liap_solve(): maxregret argument must be positive") + equilibria = libgbt._liap_behavior_solve(start, + maxregret=maxregret, maxiter=maxiter) + return NashComputationResult( + game=start.game, + method="liap-agent", + rational=False, + use_strategic=False, equilibria=equilibria, parameters={"start": start, "maxregret": maxregret, "maxiter": maxiter} ) diff --git a/src/solvers/liap/efgliap.cc b/src/solvers/liap/efgliap.cc index af6d7363d..f6ec3b924 100644 --- a/src/solvers/liap/efgliap.cc +++ b/src/solvers/liap/efgliap.cc @@ -143,9 +143,9 @@ MixedBehaviorProfile EnforceNonnegativity(const MixedBehaviorProfile> -LiapBehaviorSolve(const MixedBehaviorProfile &p_start, double p_maxregret, int p_maxitsN, - BehaviorCallbackType p_callback) +std::list> LiapAgentSolve(const MixedBehaviorProfile &p_start, + double p_maxregret, int p_maxitsN, + BehaviorCallbackType p_callback) { if (!p_start.GetGame()->IsPerfectRecall()) { throw UndefinedException( diff --git a/src/solvers/liap/liap.h b/src/solvers/liap/liap.h index 7760a89ed..51ef13957 100644 --- a/src/solvers/liap/liap.h +++ b/src/solvers/liap/liap.h @@ -28,8 +28,8 @@ namespace Gambit::Nash { std::list> -LiapBehaviorSolve(const MixedBehaviorProfile &p_start, double p_maxregret, int p_maxitsN, - BehaviorCallbackType p_callback = NullBehaviorCallback); +LiapAgentSolve(const MixedBehaviorProfile &p_start, double p_maxregret, int p_maxitsN, + BehaviorCallbackType p_callback = NullBehaviorCallback); std::list> LiapStrategySolve(const MixedStrategyProfile &p_start, double p_maxregret, int p_maxitsN, diff --git a/src/tools/liap/liap.cc b/src/tools/liap/liap.cc index 4baebb0cc..d94f2dd80 100644 --- a/src/tools/liap/liap.cc +++ b/src/tools/liap/liap.cc @@ -241,13 +241,13 @@ int main(int argc, char *argv[]) for (size_t i = 1; i <= starts.size(); i++) { const std::shared_ptr> renderer( new BehavStrategyCSVRenderer(std::cout, numDecimals)); - LiapBehaviorSolve(starts[i], maxregret, maxitsN, - [renderer, verbose](const MixedBehaviorProfile &p_profile, - const std::string &p_label) { - if (p_label == "NE" || verbose) { - renderer->Render(p_profile, p_label); - } - }); + LiapAgentSolve(starts[i], maxregret, maxitsN, + [renderer, verbose](const MixedBehaviorProfile &p_profile, + const std::string &p_label) { + if (p_label == "NE" || verbose) { + renderer->Render(p_profile, p_label); + } + }); } } return 0; diff --git a/tests/test_nash.py b/tests/test_nash.py index b16ccc0b8..e364e440b 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -451,10 +451,10 @@ def test_liap_strategy(): _ = gbt.nash.liap_solve(game.mixed_strategy_profile()) -def test_liap_behavior(): - """Test calls of liap for mixed behavior equilibria.""" +def test_liap_agent(): + """Test calls of agent liap for mixed behavior equilibria.""" game = games.read_from_file("stripped_down_poker.efg") - _ = gbt.nash.liap_solve(game.mixed_behavior_profile()) + _ = gbt.nash.liap_agent_solve(game.mixed_behavior_profile()) def test_simpdiv_strategy(): From 8cca5082fe6ce93dadd9b07afcf15944ab236f07 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 24 Nov 2025 12:01:45 +0000 Subject: [PATCH 05/32] Change interface of `gambit-liap` to be more similar to `gambit-enumpure` --- doc/tools.enumpure.rst | 8 ++++---- doc/tools.liap.rst | 16 ++++++++++++++++ src/tools/liap/liap.cc | 12 ++++++++---- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/doc/tools.enumpure.rst b/doc/tools.enumpure.rst index 407461fd0..720ad6c87 100644 --- a/doc/tools.enumpure.rst +++ b/doc/tools.enumpure.rst @@ -38,10 +38,10 @@ pure-strategy Nash equilibria. .. versionadded:: 14.0.2 - Report agent form equilibria, that is, equilibria which consider - only deviations at one information set. Only has an effect for - extensive games, as strategic games have only one information set - per player. + Report agent Nash equilibria, that is, equilibria which consider + only deviations at a single information set at a time. Only has + an effect for extensive games, as strategic games have only + one information set per player. .. cmdoption:: -P diff --git a/doc/tools.liap.rst b/doc/tools.liap.rst index 561059d45..a9aa846fa 100644 --- a/doc/tools.liap.rst +++ b/doc/tools.liap.rst @@ -26,8 +26,24 @@ not guaranteed to find all, or even any, Nash equilibria. in terms of the maximum regret. This regret is interpreted as a fraction of the difference between the maximum and minimum payoffs in the game. +.. versionchanged:: 16.5.0 + + The `-A` switch has been introduced to be explicit in choosing to compute + agent Nash equilibria. The default is now to compute using the strategic + form even for extensive games. + + .. program:: gambit-liap +.. cmdoption:: -A + + .. versionadded:: 16.5.0 + + Report agent Nash equilibria, that is, equilibria which consider + only deviations at a single information set at a time. Only has + an effect for extensive games, as strategic games have only + one information set per player. + .. cmdoption:: -d Express all output using decimal representations with the diff --git a/src/tools/liap/liap.cc b/src/tools/liap/liap.cc index d94f2dd80..6d4171990 100644 --- a/src/tools/liap/liap.cc +++ b/src/tools/liap/liap.cc @@ -46,6 +46,7 @@ void PrintHelp(char *progname) std::cerr << "With no options, attempts to compute one equilibrium starting at centroid.\n"; std::cerr << "Options:\n"; + std::cerr << " -A compute agent form equilibria\n"; std::cerr << " -d DECIMALS print probabilities with DECIMALS digits\n"; std::cerr << " -h, --help print this help message\n"; std::cerr << " -n COUNT number of starting points to generate\n"; @@ -129,7 +130,7 @@ List> RandomBehaviorProfiles(const Game &p_game, in int main(int argc, char *argv[]) { opterr = 0; - bool quiet = false, useStrategic = false, verbose = false; + bool quiet = false, reportStrategic = false, solveAgent = false, verbose = false; const int numTries = 10; int maxitsN = 1000; int numDecimals = 6; @@ -142,7 +143,7 @@ int main(int argc, char *argv[]) {"verbose", 0, nullptr, 'V'}, {nullptr, 0, nullptr, 0}}; int c; - while ((c = getopt_long(argc, argv, "d:n:i:s:m:hqVvS", long_options, &long_opt_index)) != -1) { + while ((c = getopt_long(argc, argv, "d:n:i:s:m:hqVvAS", long_options, &long_opt_index)) != -1) { switch (c) { case 'v': PrintBanner(std::cerr); @@ -163,7 +164,10 @@ int main(int argc, char *argv[]) PrintHelp(argv[0]); break; case 'S': - useStrategic = true; + reportStrategic = true; + break; + case 'A': + solveAgent = true; break; case 'q': quiet = true; @@ -203,7 +207,7 @@ int main(int argc, char *argv[]) try { const Game game = ReadGame(*input_stream); - if (!game->IsTree() || useStrategic) { + if (!game->IsTree() || !solveAgent) { List> starts; if (!startFile.empty()) { std::ifstream startPoints(startFile.c_str()); From 8512263c964d667bfa02a8f967b6aeea0fcd6155 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 24 Nov 2025 12:33:17 +0000 Subject: [PATCH 06/32] Change GUI behavior to use only strategic for enumpure and liap --- src/gui/dlnash.cc | 15 ++++++++------- src/gui/dlnash.h | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index f38ca22a6..0bb2d9d65 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -104,7 +104,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, GameDocument *p_doc) SetSizer(topSizer); topSizer->Fit(this); topSizer->SetSizeHints(this); - Layout(); + wxTopLevelWindowBase::Layout(); CenterOnParent(); } @@ -140,9 +140,10 @@ void NashChoiceDialog::OnCount(wxCommandEvent &p_event) void NashChoiceDialog::OnMethod(wxCommandEvent &p_event) { - const wxString method = m_methodChoice->GetString(p_event.GetSelection()); - if (method == s_simpdiv || method == s_enummixed || method == s_gnm || method == s_ipa) { + if (const wxString method = m_methodChoice->GetString(p_event.GetSelection()); + method == s_enumpure || method == s_simpdiv || method == s_enummixed || method == s_liap || + method == s_gnm || method == s_ipa) { m_repChoice->SetSelection(1); m_repChoice->Enable(false); } @@ -233,9 +234,9 @@ std::shared_ptr NashChoiceDialog::GetCommand() const } } else if (method == s_enumpure) { - cmd = std::make_shared>(m_doc, useEfg); + cmd = std::make_shared>(m_doc, false); cmd->SetCommand(prefix + wxT("enumpure") + options); - cmd->SetDescription(count + wxT(" in pure strategies ") + game); + cmd->SetDescription(count + wxT(" in pure strategies in strategic game")); } else if (method == s_enummixed) { cmd = std::make_shared>(m_doc, false); @@ -268,9 +269,9 @@ std::shared_ptr NashChoiceDialog::GetCommand() const cmd->SetDescription(count + wxT(" by solving a linear complementarity program ") + game); } else if (method == s_liap) { - cmd = std::make_shared>(m_doc, useEfg); + cmd = std::make_shared>(m_doc, false); cmd->SetCommand(prefix + wxT("liap -d 10") + options); - cmd->SetDescription(count + wxT(" by function minimization ") + game); + cmd->SetDescription(count + wxT(" by function minimization in strategic game")); } else if (method == s_logit) { cmd = std::make_shared>(m_doc, useEfg); diff --git a/src/gui/dlnash.h b/src/gui/dlnash.h index 7939346aa..ba18110ad 100644 --- a/src/gui/dlnash.h +++ b/src/gui/dlnash.h @@ -43,4 +43,4 @@ class NashChoiceDialog final : public wxDialog { }; } // namespace Gambit::GUI -#endif // DLNFGNASH_H +#endif // GAMBIT_GUI_DLNASH_H From b8026c34aa2f6434d61a03b287180850afc30ea1 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 8 Dec 2025 21:56:33 +0000 Subject: [PATCH 07/32] Implement new (correct) max_regret and liap_value on mixed behaviour profiles --- doc/pygambit.api.rst | 2 ++ src/games/behavmixed.cc | 10 +++++++ src/games/behavmixed.h | 13 +++++++-- src/pygambit/behavmixed.pxi | 55 +++++++++++++++++++++++++++++++++++-- src/pygambit/gambit.pxd | 2 ++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 0eab2b52a..5549cc436 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -259,6 +259,8 @@ Probability distributions over behavior MixedBehaviorProfile.is_defined_at MixedBehaviorProfile.agent_max_regret MixedBehaviorProfile.agent_liap_value + MixedBehaviorProfile.max_regret + MixedBehaviorProfile.liap_value MixedBehaviorProfile.as_strategy MixedBehaviorProfile.normalize MixedBehaviorProfile.copy diff --git a/src/games/behavmixed.cc b/src/games/behavmixed.cc index 32b656f8b..76f375a2b 100644 --- a/src/games/behavmixed.cc +++ b/src/games/behavmixed.cc @@ -261,6 +261,11 @@ template MixedBehaviorProfile MixedBehaviorProfile::ToFullSuppor // MixedBehaviorProfile: Interesting quantities //======================================================================== +template T MixedBehaviorProfile::GetLiapValue() const +{ + return MixedStrategyProfile(*this).GetLiapValue(); +} + template T MixedBehaviorProfile::GetAgentLiapValue() const { CheckVersion(); @@ -364,6 +369,11 @@ template T MixedBehaviorProfile::GetRegret(const GameInfoset &p_inf return br_payoff - map_infosetValues[p_infoset]; } +template T MixedBehaviorProfile::GetMaxRegret() const +{ + return MixedStrategyProfile(*this).GetMaxRegret(); +} + template T MixedBehaviorProfile::GetAgentMaxRegret() const { return maximize_function(m_support.GetGame()->GetInfosets(), diff --git a/src/games/behavmixed.h b/src/games/behavmixed.h index 8a0ad0b06..f71b69fda 100644 --- a/src/games/behavmixed.h +++ b/src/games/behavmixed.h @@ -174,6 +174,7 @@ template class MixedBehaviorProfile { //@{ T GetPayoff(int p_player) const; T GetPayoff(const GamePlayer &p_player) const { return GetPayoff(p_player->GetNumber()); } + T GetLiapValue() const; T GetAgentLiapValue() const; const T &GetRealizProb(const GameNode &node) const; @@ -191,7 +192,7 @@ template class MixedBehaviorProfile { /// between the best-response payoff and the payoff to playing /// \p p_action. /// @param[in] p_action The action to compute the regret for. - /// @sa GetRegret(const GameInfoset &) const; + /// @sa GetRegret(const GameInfoset &) const /// GetAgentMaxRegret() const const T &GetRegret(const GameAction &p_action) const; @@ -201,16 +202,22 @@ template class MixedBehaviorProfile { /// as the difference between the payoff of the best response action and /// the payoff to playing their specified mixed action. /// @param[in] p_infoset The information set to compute the regret at. - /// @sa GetRegret(const GameAction &) const; + /// @sa GetRegret(const GameAction &) const /// GetAgentMaxRegret() const T GetRegret(const GameInfoset &p_infoset) const; /// @brief Computes the maximum regret at any information set in the profile /// @details Computes the maximum of the regrets of the information sets in the profile. - /// @sa GetRegret(const GameInfoset &) const; + /// @sa GetRegret(const GameInfoset &) const /// GetRegret(const GameAction &) const + /// GetMaxRegret() const T GetAgentMaxRegret() const; + /// @brief Computes the maximum regret for any player in the profile + /// @sa GetAgentMaxRegret() const + /// + T GetMaxRegret() const; + T DiffActionValue(const GameAction &action, const GameAction &oppAction) const; T DiffRealizProb(const GameNode &node, const GameAction &oppAction) const; T DiffNodeValue(const GameNode &node, const GamePlayer &player, diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index 8a834d915..28f9dad31 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -819,7 +819,7 @@ class MixedBehaviorProfile: def agent_max_regret(self) -> ProfileDType: """Returns the maximum regret at any information set. - A profile is an agent Nash equilibrium if and only if `max_regret()` is 0. + A profile is an agent Nash equilibrium if and only if `agent_max_regret()` is 0. .. versionchanged:: 16.5.0 @@ -830,6 +830,7 @@ class MixedBehaviorProfile: -------- action_regret infoset_regret + max_regret agent_liap_value """ self._check_validity() @@ -838,7 +839,7 @@ class MixedBehaviorProfile: def agent_liap_value(self) -> ProfileDType: """Returns the Lyapunov value (see [McK91]_) of the strategy profile. - The Lyapunov value is a non-negative number which is zero exactly at + The agent Lyapunov value is a non-negative number which is zero exactly at agent Nash equilibria. .. versionchanged:: 16.5.0 @@ -849,6 +850,44 @@ class MixedBehaviorProfile: See Also -------- agent_max_regret + liap_value + """ + self._check_validity() + return self._agent_liap_value() + + def max_regret(self) -> ProfileDType: + """Returns the maximum regret at any information set. + + A profile is a Nash equilibrium if and only if `max_regret()` is 0. + + .. versionchanged:: 16.5.0 + + New implementation of `max_regret` to clarify the distinction between + per-player and per-agent concepts. + + See Also + -------- + liap_value + agent_max_regret + """ + self._check_validity() + return self.__max_regret() + + def liap_value(self) -> ProfileDType: + """Returns the Lyapunov value (see [McK91]_) of the strategy profile. + + The Lyapunov value is a non-negative number which is zero exactly at + Nash equilibria. + + .. versionchanged:: 16.5.0 + + New implementation of `liap_value` to clarify the distinction between + per-player and per-agent concepts. + + See Also + -------- + max_regret + agent_liap_value """ self._check_validity() return self._agent_liap_value() @@ -932,6 +971,9 @@ class MixedBehaviorProfileDouble(MixedBehaviorProfile): def _agent_max_regret(self) -> float: return deref(self.profile).GetAgentMaxRegret() + def _max_regret(self) -> float: + return deref(self.profile).GetMaxRegret() + def __eq__(self, other: typing.Any) -> bool: return ( isinstance(other, MixedBehaviorProfileDouble) and @@ -951,6 +993,9 @@ class MixedBehaviorProfileDouble(MixedBehaviorProfile): def _agent_liap_value(self) -> float: return deref(self.profile).GetAgentLiapValue() + def _liap_value(self) -> float: + return deref(self.profile).GetLiapValue() + def _normalize(self) -> MixedBehaviorProfileDouble: return MixedBehaviorProfileDouble.wrap( make_shared[c_MixedBehaviorProfile[double]](deref(self.profile).Normalize()) @@ -1028,6 +1073,9 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile): def _agent_max_regret(self) -> Rational: return rat_to_py(deref(self.profile).GetAgentMaxRegret()) + def _max_regret(self) -> Rational: + return rat_to_py(deref(self.profile).GetMaxRegret()) + def __eq__(self, other: typing.Any) -> bool: return ( isinstance(other, MixedBehaviorProfileRational) and @@ -1047,6 +1095,9 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile): def _agent_liap_value(self) -> Rational: return rat_to_py(deref(self.profile).GetAgentLiapValue()) + def _liap_value(self) -> Rational: + return rat_to_py(deref(self.profile).GetLiapValue()) + def _normalize(self) -> MixedBehaviorProfileRational: return MixedBehaviorProfileRational.wrap( make_shared[c_MixedBehaviorProfile[c_Rational]](deref(self.profile).Normalize()) diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 0b97a5ef5..4d73ba838 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -378,6 +378,8 @@ cdef extern from "games/behavmixed.h" namespace "Gambit": T GetRegret(c_GameInfoset) except + T GetAgentMaxRegret() except + T GetAgentLiapValue() except + + T GetMaxRegret() except + + T GetLiapValue() except + c_MixedStrategyProfile[T] ToMixedProfile() # except + doesn't compile c_MixedBehaviorProfile(c_MixedStrategyProfile[T]) except +NotImplementedError c_MixedBehaviorProfile(c_Game) except + From 0db7bef1d1c18370cf8df4e4509a0d26a63504c2 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Thu, 11 Dec 2025 11:21:44 +0000 Subject: [PATCH 08/32] fixed typo: self.__max_regret -> self._max_regret --- src/pygambit/behavmixed.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index 28f9dad31..09e1861e1 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -871,7 +871,7 @@ class MixedBehaviorProfile: agent_max_regret """ self._check_validity() - return self.__max_regret() + return self._max_regret() def liap_value(self) -> ProfileDType: """Returns the Lyapunov value (see [McK91]_) of the strategy profile. From fef1ab144abe9ffd5306736cdbc7fb80f2a7ccb3 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Tue, 16 Dec 2025 18:55:36 +0000 Subject: [PATCH 09/32] tests for max_regret/infoset regret on mixed behavior profiles --- tests/test_behav.py | 148 ++++++++++++++++++++++++++++++++++++++++---- tests/test_mixed.py | 25 +++++++- tests/test_nash.py | 20 +++--- 3 files changed, 172 insertions(+), 21 deletions(-) diff --git a/tests/test_behav.py b/tests/test_behav.py index 91a64b79d..a271230c8 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -798,9 +798,11 @@ def test_action_value_by_label_reference(game: gbt.Game, label: str, (games.create_mixed_behav_game_efg(), True), (games.create_stripped_down_poker_efg(), False), (games.create_stripped_down_poker_efg(), True), + (games.create_kuhn_poker_efg(), False), + (games.create_kuhn_poker_efg(), True), ] ) -def test_regret_consistency(game: gbt.Game, rational_flag: bool): +def test_action_regret_consistency(game: gbt.Game, rational_flag: bool): profile = game.mixed_behavior_profile(rational=rational_flag) for player in game.players: for infoset in player.infosets: @@ -812,6 +814,60 @@ def test_regret_consistency(game: gbt.Game, rational_flag: bool): ) +@pytest.mark.parametrize( + "game,rational_flag", + [(games.create_mixed_behav_game_efg(), False), + (games.create_mixed_behav_game_efg(), True), + (games.create_stripped_down_poker_efg(), False), + (games.create_stripped_down_poker_efg(), True), + (games.create_kuhn_poker_efg(), False), + (games.create_kuhn_poker_efg(), True), + ] +) +def test_infoset_regret_consistency(game: gbt.Game, rational_flag: bool): + profile = game.mixed_behavior_profile(rational=rational_flag) + for player in game.players: + for infoset in player.infosets: + assert ( + profile.infoset_regret(infoset) == + max(profile.action_value(a) for a in infoset.actions) - + profile.infoset_value(infoset) + ) + + +@pytest.mark.parametrize( + "game,rational_flag", + [(games.create_mixed_behav_game_efg(), False), + (games.create_mixed_behav_game_efg(), True), + (games.create_stripped_down_poker_efg(), False), + (games.create_stripped_down_poker_efg(), True), + (games.create_kuhn_poker_efg(), False), + (games.create_kuhn_poker_efg(), True), + ] +) +def test_max_regret_consistency(game: gbt.Game, rational_flag: bool): + profile = game.mixed_behavior_profile(rational=rational_flag) + assert profile.max_regret() == profile.as_strategy().max_regret() + + +@pytest.mark.parametrize( + "game,rational_flag", + [(games.create_mixed_behav_game_efg(), False), + (games.create_mixed_behav_game_efg(), True), + (games.create_stripped_down_poker_efg(), False), + (games.create_stripped_down_poker_efg(), True), + (games.create_kuhn_poker_efg(), False), + (games.create_kuhn_poker_efg(), True), + ] +) +def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): + profile = game.mixed_behavior_profile(rational=rational_flag) + assert ( + profile.agent_max_regret() == + max([profile.infoset_regret(infoset) for infoset in game.infosets]) + ) + + @pytest.mark.parametrize( "game,player_idx,infoset_idx,action_idx,action_probs,rational_flag,tol,value", [ @@ -823,14 +879,14 @@ def test_regret_consistency(game: gbt.Game, rational_flag: bool): (games.create_mixed_behav_game_efg(), 2, 0, 0, None, False, TOL, 0), (games.create_mixed_behav_game_efg(), 2, 0, 1, None, False, TOL, 0.5), # 3.5 - 3 # U1 U2 U3 - (games.create_mixed_behav_game_efg(), 0, 0, 0, [1.0, 0.0, 1.0, 0.0, 1.0, 0.0], False, TOL, 0), - (games.create_mixed_behav_game_efg(), 0, 0, 0, ["1", "0", "1", "0", "1", "0"], True, ZERO, 0), - (games.create_mixed_behav_game_efg(), 0, 0, 1, [1.0, 0.0, 1.0, 0.0, 1.0, 0.0], False, TOL, 9), - (games.create_mixed_behav_game_efg(), 0, 0, 1, ["1", "0", "1", "0", "1", "0"], True, ZERO, 9), - (games.create_mixed_behav_game_efg(), 1, 0, 0, [1.0, 0.0, 1.0, 0.0, 1.0, 0.0], False, TOL, 0), - (games.create_mixed_behav_game_efg(), 1, 0, 0, ["1", "0", "1", "0", "1", "0"], True, ZERO, 0), - (games.create_mixed_behav_game_efg(), 1, 0, 1, [1.0, 0.0, 1.0, 0.0, 1.0, 0.0], False, TOL, 8), - (games.create_mixed_behav_game_efg(), 1, 0, 1, ["1", "0", "1", "0", "1", "0"], True, ZERO, 8), + (games.create_mixed_behav_game_efg(), 0, 0, 0, [1, 0, 1, 0, 1, 0], False, TOL, 0), + (games.create_mixed_behav_game_efg(), 0, 0, 0, [1, 0, 1, 0, 1, 0], True, ZERO, 0), + (games.create_mixed_behav_game_efg(), 0, 0, 1, [1, 0, 1, 0, 1, 0], False, TOL, 9), + (games.create_mixed_behav_game_efg(), 0, 0, 1, [1, 0, 1, 0, 1, 0], True, ZERO, 9), + (games.create_mixed_behav_game_efg(), 1, 0, 0, [1, 0, 1, 0, 1, 0], False, TOL, 0), + (games.create_mixed_behav_game_efg(), 1, 0, 0, [1, 0, 1, 0, 1, 0], True, ZERO, 0), + (games.create_mixed_behav_game_efg(), 1, 0, 1, [1, 0, 1, 0, 1, 0], False, TOL, 8), + (games.create_mixed_behav_game_efg(), 1, 0, 1, [1, 0, 1, 0, 1, 0], True, ZERO, 8), # Mixed Nash equilibrium (games.create_mixed_behav_game_efg(), 0, 0, 0, ["2/5", "3/5", "1/2", "1/2", "1/3", "2/3"], True, ZERO, 0), @@ -858,9 +914,9 @@ def test_regret_consistency(game: gbt.Game, rational_flag: bool): True, ZERO, "8/3"), # (2/3*2 + 1/3*1) - (-1) ] ) -def test_regret_reference(game: gbt.Game, player_idx: int, infoset_idx: int, action_idx: int, - action_probs: None | list, rational_flag: bool, - tol: gbt.Rational | float, value: str | float): +def test_action_regret_reference(game: gbt.Game, player_idx: int, infoset_idx: int, + action_idx: int, action_probs: None | list, rational_flag: bool, + tol: gbt.Rational | float, value: str | float): action = game.players[player_idx].infosets[infoset_idx].actions[action_idx] profile = game.mixed_behavior_profile(rational=rational_flag) if action_probs: @@ -955,6 +1011,41 @@ def test_agent_liap_value_reference(game: gbt.Game, action_probs: None | list, ) +@pytest.mark.parametrize( + "game,action_probs,rational_flag,max_regret,agent_max_regret,liap_value,agent_liap_value", + [ + # uniform (non-Nash): + (games.create_mixed_behav_game_efg(), None, True, "1/4", "1/4", "1/16", "1/16"), + (games.create_mixed_behav_game_efg(), None, False, 0.25, 0.25, 0.0625, 0.0625), + # Myerson fig 2.4 + pytest.param( + games.read_from_file("myerson_fig_4_2.efg"), [0, 1, 0, 1, 1, 0], True, 1, 0, 1, 0, + marks=pytest.mark.xfail(reason="Needs to be fixed now") + ), + ] +) +def test_agent_max_regret_versus_non_agent(game: gbt.Game, action_probs: None | list, + rational_flag: bool, + max_regret: str | float, + agent_max_regret: str | float, + agent_liap_value: str | float, + liap_value: str | float, + ): + profile = game.mixed_behavior_profile(rational=rational_flag) + if action_probs: + _set_action_probs(profile, action_probs, rational_flag) + assert (profile.max_regret() == (gbt.Rational(max_regret) if rational_flag else max_regret)) + assert ( + profile.agent_max_regret() == (gbt.Rational(agent_max_regret) + if rational_flag else agent_max_regret) + ) + assert (profile.liap_value() == (gbt.Rational(liap_value) if rational_flag else liap_value)) + assert ( + profile.agent_liap_value() == (gbt.Rational(agent_liap_value) + if rational_flag else agent_liap_value) + ) + + @pytest.mark.parametrize( "game,tol,probs,infoset_idx,member_idx,value,rational_flag", [(games.create_mixed_behav_game_efg(), TOL, [0.8, 0.2, 0.4, 0.6, 0.0, 1.0], 0, 0, 1.0, False), @@ -1157,6 +1248,39 @@ def _get_and_check_answers(game: gbt.Game, action_probs1: tuple, action_probs2: lambda x, y: x.agent_liap_value(), lambda x: [1]), (games.create_stripped_down_poker_efg(), PROBS_1A_rat, PROBS_2A_rat, True, lambda x, y: x.agent_liap_value(), lambda x: [1]), + ###################################################################################### + # liap_value (of profile, hence [1] for objects_to_test, + # any singleton collection would do) + (games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False, + lambda x, y: x.liap_value(), lambda x: [1]), + (games.create_mixed_behav_game_efg(), PROBS_1A_rat, PROBS_2A_rat, True, + lambda x, y: x.liap_value(), lambda x: [1]), + (games.create_stripped_down_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False, + lambda x, y: x.liap_value(), lambda x: [1]), + (games.create_stripped_down_poker_efg(), PROBS_1A_rat, PROBS_2A_rat, True, + lambda x, y: x.liap_value(), lambda x: [1]), + ###################################################################################### + # agent_max_regret (of profile, hence [1] for objects_to_test, + # any singleton collection would do) + (games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False, + lambda x, y: x.agent_max_regret(), lambda x: [1]), + (games.create_mixed_behav_game_efg(), PROBS_1A_rat, PROBS_2A_rat, True, + lambda x, y: x.agent_max_regret(), lambda x: [1]), + (games.create_stripped_down_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False, + lambda x, y: x.agent_max_regret(), lambda x: [1]), + (games.create_stripped_down_poker_efg(), PROBS_1A_rat, PROBS_2A_rat, True, + lambda x, y: x.agent_max_regret(), lambda x: [1]), + ###################################################################################### + # max_regret (of profile, hence [1] for objects_to_test, + # any singleton collection would do) + (games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False, + lambda x, y: x.max_regret(), lambda x: [1]), + (games.create_mixed_behav_game_efg(), PROBS_1A_rat, PROBS_2A_rat, True, + lambda x, y: x.max_regret(), lambda x: [1]), + (games.create_stripped_down_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False, + lambda x, y: x.max_regret(), lambda x: [1]), + (games.create_stripped_down_poker_efg(), PROBS_1A_rat, PROBS_2A_rat, True, + lambda x, y: x.max_regret(), lambda x: [1]), ] ) def test_profile_order_consistency(game: gbt.Game, diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 03d3da50e..591233a57 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -795,7 +795,7 @@ def _get_and_check_answers(game: gbt.Game, action_probs1: tuple, action_probs2: PROBS_2A_doub = (0.5, 0, 0.5, 0) PROBS_1A_rat = ("1/4", "1/4", "1/4", "1/4") PROBS_2A_rat = ("1/2", "0", "1/2", "0") -# For 2x2x2 nfg and Myserson 2-card poker efg (both have 6 strategies in total): +# For 2x2x2 nfg and stripped_down_poker efg (both have 6 strategies in total): PROBS_1B_doub = (0.5, 0.5, 0.5, 0.5, 0.5, 0.5) PROBS_2B_doub = (1.0, 0.0, 1.0, 0.0, 1.0, 0.0) PROBS_1B_rat = ("1/2", "1/2", "1/2", "1/2", "1/2", "1/2") @@ -933,6 +933,29 @@ def _get_and_check_answers(game: gbt.Game, action_probs1: tuple, action_probs2: pytest.param(games.create_stripped_down_poker_efg(), PROBS_1B_rat, PROBS_2B_rat, True, lambda profile, y: profile.liap_value(), lambda x: [1], id="liap_value_poker_rat"), + ################################################################################# + # max_regret (of profile, hence [1] for objects_to_test, any singleton collection would do) + # 4x4 coordination nfg + pytest.param(games.create_coord_4x4_nfg(), PROBS_1A_doub, PROBS_2A_doub, False, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_coord_doub"), + pytest.param(games.create_coord_4x4_nfg(), PROBS_1A_rat, PROBS_2A_rat, True, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_coord_rat"), + # 2x2x2 nfg + pytest.param(games.create_2x2x2_nfg(), PROBS_1B_doub, PROBS_2B_doub, False, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_2x2x2_doub"), + pytest.param(games.create_2x2x2_nfg(), PROBS_1B_rat, PROBS_2B_rat, True, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_2x2x2_rat"), + # stripped-down poker + pytest.param(games.create_stripped_down_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_poker_doub"), + pytest.param(games.create_stripped_down_poker_efg(), PROBS_1B_rat, PROBS_2B_rat, True, + lambda profile, y: profile.max_regret(), lambda x: [1], + id="max_regret_poker_rat"), ] ) def test_profile_order_consistency(game: gbt.Game, diff --git a/tests/test_nash.py b/tests/test_nash.py index 47bf12132..6db28928a 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -141,8 +141,8 @@ def test_enumpoly_ordered_behavior( game: gbt.Game, mixed_behav_prof_data: list, stop_after: None | int ): """Test calls of enumpoly for mixed behavior equilibria, - using max_regret (internal consistency); and comparison to a set of previously - computed equilibria using this function (regression test). + using max_regret and agent_max_regret (internal consistency); and + comparison to a set of previously computed equilibria with this function (regression test). This set will be the full set of all computed equilibria if stop_after is None, else the first stop_after-many equilibria. @@ -162,6 +162,7 @@ def test_enumpoly_ordered_behavior( result = gbt.nash.enumpoly_solve(game, use_strategic=False) assert len(result.equilibria) == len(mixed_behav_prof_data) for eq, exp in zip(result.equilibria, mixed_behav_prof_data, strict=True): + assert abs(eq.max_regret()) <= TOL assert abs(eq.agent_max_regret()) <= TOL expected = game.mixed_behavior_profile(rational=True, data=exp) for p in game.players: @@ -197,8 +198,8 @@ def test_enumpoly_unordered_behavior( game: gbt.Game, mixed_behav_prof_data: list, stop_after: None | int ): """Test calls of enumpoly for mixed behavior equilibria, - using max_regret (internal consistency); and comparison to a set of previously - computed equilibria using this function (regression test). + using max_regret and agent_max_regret (internal consistency); and + comparison to a set of previously computed equilibria using this function (regression test). This set will be the full set of all computed equilibria if stop_after is None, else the first stop_after-many equilibria. @@ -229,6 +230,7 @@ def are_the_same(game, found, candidate): return True for eq in result.equilibria: + assert abs(eq.max_regret()) <= TOL assert abs(eq.agent_max_regret()) <= TOL found = False for exp in mixed_behav_prof_data[:]: @@ -423,12 +425,13 @@ def test_lcp_behavior_double(): def test_lcp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): """Test calls of LCP for mixed behavior equilibria, rational precision. - using max_regret (internal consistency); and comparison to a previously - computed equilibrium using this function (regression test) + using max_regret and agent_max_regret (internal consistency); and + comparison to a previously computed equilibrium using this function (regression test). """ result = gbt.nash.lcp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 eq = result.equilibria[0] + assert eq.max_regret() == 0 assert eq.agent_max_regret() == 0 expected = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) assert eq == expected @@ -552,12 +555,13 @@ def test_lp_behavior_double(): ) def test_lp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): """Test calls of LP for mixed behavior equilibria, rational precision, - using max_regret (internal consistency); and comparison to a previously - computed equilibrium using this function (regression test) + using max_regret and agent_max_regret (internal consistency); and + comparison to a previously computed equilibrium using this function (regression test). """ result = gbt.nash.lp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 eq = result.equilibria[0] + assert eq.max_regret() == 0 assert eq.agent_max_regret() == 0 expected = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) assert eq == expected From 65f6a122c51b1343d6343e58885d06ae769bd3d7 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Tue, 16 Dec 2025 19:10:15 +0000 Subject: [PATCH 10/32] added tests/test_games/myerson_fig_4_2.efg --- tests/test_games/myerson_fig_4_2.efg | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/test_games/myerson_fig_4_2.efg diff --git a/tests/test_games/myerson_fig_4_2.efg b/tests/test_games/myerson_fig_4_2.efg new file mode 100644 index 000000000..d0ea81e8e --- /dev/null +++ b/tests/test_games/myerson_fig_4_2.efg @@ -0,0 +1,14 @@ +EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" } +"" + +p "" 1 1 "" { "A1" "B1" } 0 +p "" 2 1 "" { "W2" "X2" } 0 +p "" 1 2 "" { "Y1" "Z1" } 0 +t "" 1 "" { 3, 0 } +t "" 2 "" { 0, 0 } +p "" 1 2 "" { "Y1" "Z1" } 0 +t "" 3 "" { 2, 3 } +t "" 4 "" { 4, 1 } +p "" 2 1 "" { "W2" "X2" } 0 +t "" 5 "" { 2, 3 } +t "" 6 "" { 3, 2 } From ef1482b77ff0656455276f6f55201745b3ebe9de Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Mon, 15 Dec 2025 09:23:20 +0000 Subject: [PATCH 11/32] Added comprehensive tests for GameStructureChangedError (#700) --- tests/test_game.py | 111 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 12 deletions(-) diff --git a/tests/test_game.py b/tests/test_game.py index c5d64aa60..cebfa1b9f 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -159,36 +159,123 @@ def test_game_dereference_invalid(): _ = strategy.label -def test_strategy_profile_invalidation_table(): - """Test for invalidating mixed strategy profiles on tables when game changes.""" - g = gbt.Game.new_table([2, 2]) +def test_mixed_strategy_profile_game_structure_changed_no_tree(): + g = gbt.Game.from_arrays([[2, 2], [0, 0]], [[0, 0], [1, 1]]) profiles = [g.mixed_strategy_profile(rational=b) for b in [False, True]] - g.delete_strategy(g.players[0].strategies[0]) + g.outcomes[0][g.players[0]] = 3 for profile in profiles: with pytest.raises(gbt.GameStructureChangedError): - profile.payoff(g.players[0]) + profile.copy() with pytest.raises(gbt.GameStructureChangedError): profile.liap_value() + with pytest.raises(gbt.GameStructureChangedError): + profile.max_regret() + with pytest.raises(gbt.GameStructureChangedError): + # triggers error via __getitem__ + next(profile.mixed_strategies()) + with pytest.raises(gbt.GameStructureChangedError): + profile.normalize() + with pytest.raises(gbt.GameStructureChangedError): + profile.payoff(g.players[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.player_regret(g.players[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.strategy_regret(g.strategies[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.strategy_value(g.strategies[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.strategy_value_deriv(g.strategies[0], g.strategies[1]) + with pytest.raises(gbt.GameStructureChangedError): + # triggers error via __getitem__ + next(profile.__iter__()) + with pytest.raises(gbt.GameStructureChangedError): + profile.__setitem__(g.strategies[0], 0) + with pytest.raises(gbt.GameStructureChangedError): + profile.__getitem__(g.strategies[0]) -def test_strategy_profile_invalidation_payoff(): - g = gbt.Game.from_arrays([[2, 2], [0, 0]], [[0, 0], [1, 1]]) +def test_mixed_strategy_profile_game_structure_changed_tree(): + g = games.read_from_file("basic_extensive_game.efg") profiles = [g.mixed_strategy_profile(rational=b) for b in [False, True]] - g.outcomes[0][g.players[0]] = 3 + g.delete_action(g.players[0].infosets[0].actions[0]) for profile in profiles: with pytest.raises(gbt.GameStructureChangedError): - profile.payoff(g.players[0]) + profile.as_behavior() + with pytest.raises(gbt.GameStructureChangedError): + profile.copy() with pytest.raises(gbt.GameStructureChangedError): profile.liap_value() + with pytest.raises(gbt.GameStructureChangedError): + profile.max_regret() + with pytest.raises(gbt.GameStructureChangedError): + # triggers error via __getitem__ + next(profile.mixed_strategies()) + with pytest.raises(gbt.GameStructureChangedError): + profile.normalize() + with pytest.raises(gbt.GameStructureChangedError): + profile.payoff(g.players[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.player_regret(g.players[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.strategy_regret(g.strategies[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.strategy_value(g.strategies[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.strategy_value_deriv(g.strategies[0], g.strategies[1]) + with pytest.raises(gbt.GameStructureChangedError): + # triggers error via __getitem__ + next(profile.__iter__()) + with pytest.raises(gbt.GameStructureChangedError): + profile.__setitem__(g.strategies[0], 0) + with pytest.raises(gbt.GameStructureChangedError): + profile.__getitem__(g.strategies[0]) -def test_behavior_profile_invalidation(): - """Test for invalidating mixed strategy profiles on tables when game changes.""" +def test_mixed_behavior_profile_game_structure_changed(): g = games.read_from_file("basic_extensive_game.efg") profiles = [g.mixed_behavior_profile(rational=b) for b in [False, True]] g.delete_action(g.players[0].infosets[0].actions[0]) for profile in profiles: + with pytest.raises(gbt.GameStructureChangedError): + profile.action_regret(g.actions[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.action_value(g.actions[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.as_strategy() + with pytest.raises(gbt.GameStructureChangedError): + profile.belief(list(g.nodes)[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.copy() + with pytest.raises(gbt.GameStructureChangedError): + profile.infoset_prob(g.infosets[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.infoset_regret(g.infosets[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.infoset_value(g.infosets[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.is_defined_at(g.infosets[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.liap_value() + with pytest.raises(gbt.GameStructureChangedError): + profile.max_regret() + with pytest.raises(gbt.GameStructureChangedError): + # triggers error via __getitem__ + next(profile.mixed_actions()) + with pytest.raises(gbt.GameStructureChangedError): + # triggers error via __getitem__ + next(profile.mixed_behaviors()) + with pytest.raises(gbt.GameStructureChangedError): + profile.node_value(g.players[0], g.root) + with pytest.raises(gbt.GameStructureChangedError): + profile.normalize() with pytest.raises(gbt.GameStructureChangedError): profile.payoff(g.players[0]) with pytest.raises(gbt.GameStructureChangedError): - profile.agent_liap_value() + profile.realiz_prob(g.root) + with pytest.raises(gbt.GameStructureChangedError): + # triggers error via __getitem__ + next(profile.__iter__()) + with pytest.raises(gbt.GameStructureChangedError): + profile.__setitem__(g.infosets[0].actions[0], 0) + with pytest.raises(gbt.GameStructureChangedError): + profile.__getitem__(g.infosets[0]) From 8e92042c248cd77b448a911d2562aed2feb8e4a9 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 17 Dec 2025 06:00:32 +0000 Subject: [PATCH 12/32] agent_{liap_value,max_regret} added to tests for GameStructureChangedError --- tests/test_game.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_game.py b/tests/test_game.py index cebfa1b9f..3adc7672b 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -254,8 +254,12 @@ def test_mixed_behavior_profile_game_structure_changed(): profile.infoset_value(g.infosets[0]) with pytest.raises(gbt.GameStructureChangedError): profile.is_defined_at(g.infosets[0]) + with pytest.raises(gbt.GameStructureChangedError): + profile.agent_liap_value() with pytest.raises(gbt.GameStructureChangedError): profile.liap_value() + with pytest.raises(gbt.GameStructureChangedError): + profile.agent_max_regret() with pytest.raises(gbt.GameStructureChangedError): profile.max_regret() with pytest.raises(gbt.GameStructureChangedError): From cae6595cc05791be166889e70c8737b2b188385b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 17 Dec 2025 14:29:39 +0000 Subject: [PATCH 13/32] Fix incorrect function called in MixedBehaviorProfile.liap_value --- src/pygambit/behavmixed.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index 1c4ad0d83..950b65ac5 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -890,7 +890,7 @@ class MixedBehaviorProfile: agent_liap_value """ self._check_validity() - return self._agent_liap_value() + return self._liap_value() def as_strategy(self) -> MixedStrategyProfile: """Returns a `MixedStrategyProfile` which is equivalent From 8fb501aff7d8d98d61058f42fb4a5beb8abe4500 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 17 Dec 2025 14:54:32 +0000 Subject: [PATCH 14/32] remove xfail from test for Myerson fig 4.2 agent versus non-agent liap value --- tests/test_behav.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_behav.py b/tests/test_behav.py index a271230c8..6806dc63e 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -1018,11 +1018,8 @@ def test_agent_liap_value_reference(game: gbt.Game, action_probs: None | list, (games.create_mixed_behav_game_efg(), None, True, "1/4", "1/4", "1/16", "1/16"), (games.create_mixed_behav_game_efg(), None, False, 0.25, 0.25, 0.0625, 0.0625), # Myerson fig 2.4 - pytest.param( - games.read_from_file("myerson_fig_4_2.efg"), [0, 1, 0, 1, 1, 0], True, 1, 0, 1, 0, - marks=pytest.mark.xfail(reason="Needs to be fixed now") - ), - ] + (games.read_from_file("myerson_fig_4_2.efg"), [0, 1, 0, 1, 1, 0], True, 1, 0, 1, 0), + ] ) def test_agent_max_regret_versus_non_agent(game: gbt.Game, action_probs: None | list, rational_flag: bool, From a71091dbe01dc454b21eab1eb008282dfa8ffbc9 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 17 Dec 2025 16:07:12 +0000 Subject: [PATCH 15/32] title and comment in tests/test_games/myerson_fig_4_2.efg --- tests/test_behav.py | 2 +- tests/test_games/myerson_fig_4_2.efg | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_behav.py b/tests/test_behav.py index 6806dc63e..6fd5b2e20 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -1017,7 +1017,7 @@ def test_agent_liap_value_reference(game: gbt.Game, action_probs: None | list, # uniform (non-Nash): (games.create_mixed_behav_game_efg(), None, True, "1/4", "1/4", "1/16", "1/16"), (games.create_mixed_behav_game_efg(), None, False, 0.25, 0.25, 0.0625, 0.0625), - # Myerson fig 2.4 + # Myerson fig 4.2 (games.read_from_file("myerson_fig_4_2.efg"), [0, 1, 0, 1, 1, 0], True, 1, 0, 1, 0), ] ) diff --git a/tests/test_games/myerson_fig_4_2.efg b/tests/test_games/myerson_fig_4_2.efg index d0ea81e8e..713c4a34f 100644 --- a/tests/test_games/myerson_fig_4_2.efg +++ b/tests/test_games/myerson_fig_4_2.efg @@ -1,5 +1,6 @@ -EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" } -"" +EFG 2 R "Myerson (1991) Fig 4.2" { "Player 1" "Player 2" } +"An example from Myerson (1991) Fig 4.2 which has an agent Nash equilibrium that is +not a Nash equilibrium" p "" 1 1 "" { "A1" "B1" } 0 p "" 2 1 "" { "W2" "X2" } 0 From f4de44eccde390b4659ffa1ee94d912b1278c909 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 17 Dec 2025 17:05:00 +0000 Subject: [PATCH 16/32] notebook showing agent versus non-agent notions --- .../04_agent_versus_non_agent_regret.ipynb | 495 ++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 doc/tutorials/04_agent_versus_non_agent_regret.ipynb diff --git a/doc/tutorials/04_agent_versus_non_agent_regret.ipynb b/doc/tutorials/04_agent_versus_non_agent_regret.ipynb new file mode 100644 index 000000000..55503b3e0 --- /dev/null +++ b/doc/tutorials/04_agent_versus_non_agent_regret.ipynb @@ -0,0 +1,495 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "96dfe427-942a-47e9-8f1f-91854989b8c8", + "metadata": {}, + "source": [ + "# 3) Agent and standard notions of extensive form games\n", + "\n", + "The purpose of this tutorial is to explain the notions of `MixedBehaviorProfile.agent_max_regret` and `MixedBehaviorProfile.agent_liap_value`, and the corresponding solvers `Gambit.nash.enumpure_agent_solve` and `Gambit.nash.liap_agent_solve`. These notions are only relevant for *extensive-form games*, and so `agent_max_regret` and \n", + "`agent_liap_value` are only available for `MixedBehaviorProfile`s and not for `MixedStrategyProfile`s." + ] + }, + { + "cell_type": "markdown", + "id": "b87ebb4e-7080-4aa1-9920-67fba5a36114", + "metadata": {}, + "source": [ + "# Nash equilibria are profiles with maximum regret 0\n", + "\n", + "For either a `MixedBehaviorProfile` and a `MixedStrategyProfile`, the profile is a Nash equilibrium if and only if its maximum regret is zero.\n", + "The profiles maximum regret is the maximum over the players of the individual player regrets.\n", + "A player's regret is 0 if they are playing a mixed (including pure) best response; otherwise it is positive and \n", + "is the different between the best response payoff (achievable via a pure strategy) against the other players' and what the player actually gets as payoff in this profile.\n", + "\n", + "Let's see an example." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "5142d6ba-da13-4500-bca6-e68b608bfae9", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from draw_tree import draw_tree\n", + "\n", + "import pygambit as gbt\n", + "\n", + "g = gbt.read_efg(\"../../contrib/games/myerson_fig_4_2.efg\")\n", + "draw_tree(g)" + ] + }, + { + "cell_type": "markdown", + "id": "dabe6e40-509e-4454-b3ef-f7f0737cc9d8", + "metadata": {}, + "source": [ + "Let's use `enumpure_solve` to find all pure Nash equilibria of this game." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7882d327-ce04-43d3-bb5a-36cff6da6e96", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]]]\n", + "Max regret: 0\n" + ] + } + ], + "source": [ + "pure_Nash_equilibria = gbt.nash.enumpure_solve(g).equilibria\n", + "print(pure_Nash_equilibria)\n", + "print(\"Max regret:\", pure_Nash_equilibria[0].max_regret())" + ] + }, + { + "cell_type": "markdown", + "id": "98eb65d8", + "metadata": {}, + "source": [ + "We see that the game has only one pure Nash equilibrium and, its maximum regret is 0, which is what defines a Nash equilibrium.\n", + "\n", + "The `liap_value` which stands for \"Liapunov value\" is a related notion which sums the squared regrets of each pure strategy in the game. As with the maximum regret, the `liap_value` of a profile is 0 if and only if the profile is a Nash equilibrium, which we confirm now in our example:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "804345b9-d32b-4f60-b4a0-f9d69dca10a8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Liap value: 0\n" + ] + } + ], + "source": [ + "print(\"Liap value:\", pure_Nash_equilibria[0].liap_value())" + ] + }, + { + "cell_type": "markdown", + "id": "c88d08e2-33bf-48ad-b71f-4a0c19929fdc", + "metadata": {}, + "source": [ + "The method `Gambit.nash.liap_solve` essentially looks for *local* minima of the function from profiles to the Liapunov value. The set of Nash equilibria are exactly the *global* minima of this function, which is why `liap_solve` may not return a Nash equilibrium." + ] + }, + { + "cell_type": "markdown", + "id": "141a6c1f-7f3c-450b-8b2f-1d47671595de", + "metadata": {}, + "source": [ + "# Agent maximum regret versus standard maximum regret\n", + "\n", + "Now we can introduce the \"agent\" versions of these two notions, maximum regret and the Liapunov value.\n", + "\n", + "Both notions relate to what Myerson (1991) called the \"multi-agent representation\" of an extensive form game, in which each information set is treated as an individual \"agent\". The \"agent maximum regret\" is then either 0 or the largest over the \"action regrets\" if that is positive.\n", + "\n", + "The maximum regret of a profile is at least at large as the agent maximum regret. \n", + "The reason it can be larger is because under the standard notion a player may control multiple information sets and can deviate by changing actions at more than one of these information sets at once, whereas for agent maximum regret, only an \"agent\" deviation at a single information set is allowed.\n", + "Thus, **if the maximum regret is 0, then we have a Nash equilibrium, and the agent maximum regret will be 0 too**.\n", + "However, **there are examples where a profile has agent maximum regret of 0 but positive maximum regret**, so the profile is \n", + "not a Nash equilibrium.\n", + "\n", + "There is also an analagous distinction between `agent_liap_value` and `liap_value`, where the `liap_value` is at least as large as the `agent_liap_value` and there are examples where the former is positive (so we do not have a Nash equilibrium) but the latter is 0 (so we have an \"agent Nash equilibrium\".\n", + "\n", + "The game given above is such an example. It is taken from [Myerson (1991)](#references) figure 4.2. \n", + "\n", + "Gambit implements version of `enumpure_solve` and `liap_solve` called `enumpure_agent_solve` and `agent_liap_solve` that work only for the extensive form and use `agent_max_regret` and `agent_liap_value` respectively. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f46ce825-d2b7-492f-b0cf-6f213607e121", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n", + "[[[[Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(0, 1), Rational(1, 1)]]], [[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(1, 1), Rational(0, 1)]]]]\n" + ] + } + ], + "source": [ + "pure_agent_equilibria = gbt.nash.enumpure_agent_solve(g).equilibria\n", + "print(len(pure_agent_equilibria))\n", + "print(pure_agent_equilibria)" + ] + }, + { + "cell_type": "markdown", + "id": "912b9af6-a2e4-4bae-9594-41c8861a4d9d", + "metadata": {}, + "source": [ + "The first of the pure agent equilibria is the Nash equilibrium we found above, which we can check if we convert the agent equilibrium from a `MixedBehaviorProfile` to a `MixedStrategyProfile`:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "dbfa7035", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pure_Nash_equilibria[0] == pure_agent_equilibria[0].as_strategy()" + ] + }, + { + "cell_type": "markdown", + "id": "ec2a8564-5102-4847-8110-a26ee1f4f400", + "metadata": {}, + "source": [ + "The second agent equilibrium is not a Nash equilibrium, which we can confirm by showing that it's `max_regret` and `liap_value` are both positive, while the agent versions of these are 0 (which is why this profile was returned by `enumpure_agent_solve`:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "85760cec-5760-4f9d-8ca2-99fba79c7c3c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 1\n", + "0 0\n" + ] + } + ], + "source": [ + "aeq = pure_agent_equilibria[1]\n", + "print(aeq.max_regret(), aeq.liap_value())\n", + "print(aeq.agent_max_regret(), aeq.agent_liap_value())" + ] + }, + { + "cell_type": "markdown", + "id": "c4eeb65f", + "metadata": {}, + "source": [ + "For most use cases, the non-agent versions are probably what a user wants." + ] + }, + { + "cell_type": "markdown", + "id": "65def67e", + "metadata": {}, + "source": [ + "#### References\n", + "\n", + "Roger Myerson (1991) \"Game Theory: Analysis of Conflict.\" Harvard University Press. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 179422826dc44e0ac33285e43f32bb43b9221d6a Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 17 Dec 2025 17:26:15 +0000 Subject: [PATCH 17/32] missing file --- contrib/games/myerson_fig_4_2.efg | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 contrib/games/myerson_fig_4_2.efg diff --git a/contrib/games/myerson_fig_4_2.efg b/contrib/games/myerson_fig_4_2.efg new file mode 100644 index 000000000..713c4a34f --- /dev/null +++ b/contrib/games/myerson_fig_4_2.efg @@ -0,0 +1,15 @@ +EFG 2 R "Myerson (1991) Fig 4.2" { "Player 1" "Player 2" } +"An example from Myerson (1991) Fig 4.2 which has an agent Nash equilibrium that is +not a Nash equilibrium" + +p "" 1 1 "" { "A1" "B1" } 0 +p "" 2 1 "" { "W2" "X2" } 0 +p "" 1 2 "" { "Y1" "Z1" } 0 +t "" 1 "" { 3, 0 } +t "" 2 "" { 0, 0 } +p "" 1 2 "" { "Y1" "Z1" } 0 +t "" 3 "" { 2, 3 } +t "" 4 "" { 4, 1 } +p "" 2 1 "" { "W2" "X2" } 0 +t "" 5 "" { 2, 3 } +t "" 6 "" { 3, 2 } From f0cec0b11d12239469129425e9ff52a0d35eb324 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Thu, 18 Dec 2025 08:13:18 +0000 Subject: [PATCH 18/32] using liap_solve and enummixed_solv in 04_agent_versus_non_agent_regret.ipynb --- .../04_agent_versus_non_agent_regret.ipynb | 212 +++++++++++++++++- 1 file changed, 200 insertions(+), 12 deletions(-) diff --git a/doc/tutorials/04_agent_versus_non_agent_regret.ipynb b/doc/tutorials/04_agent_versus_non_agent_regret.ipynb index 55503b3e0..02bac1c60 100644 --- a/doc/tutorials/04_agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/04_agent_versus_non_agent_regret.ipynb @@ -302,30 +302,59 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]]]\n", + "Number of pure equilibria: 1\n", "Max regret: 0\n" ] } ], "source": [ "pure_Nash_equilibria = gbt.nash.enumpure_solve(g).equilibria\n", - "print(pure_Nash_equilibria)\n", - "print(\"Max regret:\", pure_Nash_equilibria[0].max_regret())" + "print(\"Number of pure equilibria:\", len(pure_Nash_equilibria))\n", + "pure_eq = pure_Nash_equilibria[0]\n", + "print(\"Max regret:\", pure_eq.max_regret())" ] }, { "cell_type": "markdown", - "id": "98eb65d8", + "id": "1b95e67d-a44e-4622-acb5-37bab18a30f4", "metadata": {}, "source": [ - "We see that the game has only one pure Nash equilibrium and, its maximum regret is 0, which is what defines a Nash equilibrium.\n", - "\n", - "The `liap_value` which stands for \"Liapunov value\" is a related notion which sums the squared regrets of each pure strategy in the game. As with the maximum regret, the `liap_value` of a profile is 0 if and only if the profile is a Nash equilibrium, which we confirm now in our example:" + "We see that the game has only one pure Nash equilibrium and, its maximum regret is 0, which is what defines a Nash equilibrium." ] }, { "cell_type": "code", "execution_count": 3, + "id": "6e3e9303-453a-4bac-a449-fa8fda2ba5ec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player 1 infoset: 0 behavior probabilities: [Rational(1, 1), Rational(0, 1)]\n", + "Player 1 infoset: 1 behavior probabilities: [Rational(0, 1), Rational(1, 1)]\n", + "Player 2 infoset: 0 behavior probabilities: [Rational(0, 1), Rational(1, 1)]\n" + ] + } + ], + "source": [ + "eq = pure_Nash_equilibria[0]\n", + "for infoset, probs in eq.as_behavior().mixed_actions():\n", + " print(infoset.player.label, \"infoset:\", infoset.number, \"behavior probabilities:\", probs)" + ] + }, + { + "cell_type": "markdown", + "id": "98eb65d8", + "metadata": {}, + "source": [ + "The `liap_value` which stands for \"Liapunov value\" is a related notion that sums the squared regrets of each pure strategy in the game. As with the maximum regret, the `liap_value` of a profile is 0 if and only if the profile is a Nash equilibrium, which we confirm now in our example:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, "id": "804345b9-d32b-4f60-b4a0-f9d69dca10a8", "metadata": {}, "outputs": [ @@ -338,7 +367,7 @@ } ], "source": [ - "print(\"Liap value:\", pure_Nash_equilibria[0].liap_value())" + "print(\"Liap value:\", pure_eq.liap_value())" ] }, { @@ -349,6 +378,165 @@ "The method `Gambit.nash.liap_solve` essentially looks for *local* minima of the function from profiles to the Liapunov value. The set of Nash equilibria are exactly the *global* minima of this function, which is why `liap_solve` may not return a Nash equilibrium." ] }, + { + "cell_type": "markdown", + "id": "4afdea13-0cbb-4430-9689-ecf9b6a4b18d", + "metadata": {}, + "source": [ + "Let's use the method which requires us to specify a starting profile. The method works only with floating point profiles. We will create two profiles, one in floating point and one in rationals, using the former as the starting point for the method and the latter to check the maximum regret and Liapunov value of the profile exactly." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9d18768b-db9b-41ef-aee7-5fe5f524a59e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Max regret of starting profile: 3\n", + "Liapunov value of starting profile: 14\n" + ] + } + ], + "source": [ + "starting_profile_double = g.mixed_strategy_profile(data=[[0,1,0],[1,0]], rational=False)\n", + "starting_profile_rational = g.mixed_strategy_profile(data=[[0,1,0],[1,0]], rational=True)\n", + "print(\"Max regret of starting profile:\", starting_profile_rational.max_regret())\n", + "print(\"Liapunov value of starting profile:\", starting_profile_rational.liap_value())" + ] + }, + { + "cell_type": "markdown", + "id": "e67d9926-d19d-4745-a406-a3c1198a8484", + "metadata": {}, + "source": [ + "Since the maximum regret and therefore Liapunov value are both positive, the starting profile is not a Nash equilibrium\n", + "and we expect `liap_solve` to return a different profile, which will hopefully, but not necessarily by a Nash equilibrium, depending on whether the solver finding a global minimum, or a non-global local minimum." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b885271f-7279-4d87-a0b9-bc28449b00ba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[4.2517925671604327e-07, 0.49999911111761514, 0.5000004637031282], [0.3333333517938241, 0.6666666482061759]]\n" + ] + } + ], + "source": [ + "candidate_eq = gbt.nash.liap_solve(start=starting_profile_double).equilibria[0]\n", + "print(candidate_eq)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f8a90a9c-393e-4812-9418-76e705880f6f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Liap value: 4.43446520109796e-14\n", + "Max regret: 1.694170896904268e-07\n" + ] + } + ], + "source": [ + "print(\"Liap value:\", candidate_eq.liap_value())\n", + "print(\"Max regret:\", candidate_eq.max_regret())" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "567e6a6a-fc8d-4142-806c-6510b2a4c624", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Liap value: 0\n", + "Max regret: 0\n" + ] + } + ], + "source": [ + "candidate_eq_rat = g.mixed_strategy_profile(data=[[0,\"1/2\",\"1/2\"],[\"1/3\",\"2/3\"]], rational=True)\n", + "print(\"Liap value:\", candidate_eq_rat.liap_value())\n", + "print(\"Max regret:\", candidate_eq_rat.max_regret())" + ] + }, + { + "cell_type": "markdown", + "id": "3b61364c-3e7f-4094-8ddf-a557863632e5", + "metadata": {}, + "source": [ + "Finally, before looking beyond Nash equilibria to \"agent Nash equilibria\", we will use Gambit's `enummixed_solve` to find all extreme mixed (including pure) Nash equilibria of this game." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "87a62c9e-b109-4f88-ac25-d0e0db3f27ea", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]]\n", + "[[Rational(1, 4), Rational(0, 1), Rational(3, 4)], [Rational(1, 2), Rational(1, 2)]]\n", + "[[Rational(0, 1), Rational(1, 2), Rational(1, 2)], [Rational(1, 3), Rational(2, 3)]]\n" + ] + } + ], + "source": [ + "all_extreme_Nash_equilibria = gbt.nash.enummixed_solve(g).equilibria\n", + "for eq in all_extreme_Nash_equilibria:\n", + " print(eq)" + ] + }, + { + "cell_type": "markdown", + "id": "e2e2e129-e7c7-41fe-8bf9-26e3ab889839", + "metadata": {}, + "source": [ + "The first of these is the pure equilibrium we found above with `enumpure_solve`. The last of these if the mixed equilibrium we just found with `liap_solve`. The middle of these is a new mixed equilibrium we haven't seen yet. Let's just confirm that it too, like the first and last, also have Liapunov value and maximum regret zero, as required for a Nash equilibrium:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2c8ed3df-958e-4ee9-aed6-a106547fbd37", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[Rational(0, 1), Rational(1, 2), Rational(1, 2)], [Rational(1, 3), Rational(2, 3)]]\n", + "Liap value: 0\n", + "Max regret: 0\n" + ] + } + ], + "source": [ + "print(all_extreme_Nash_equilibria[2])\n", + "print(\"Liap value:\", all_extreme_Nash_equilibria[2].liap_value())\n", + "print(\"Max regret:\", all_extreme_Nash_equilibria[2].max_regret())" + ] + }, { "cell_type": "markdown", "id": "141a6c1f-7f3c-450b-8b2f-1d47671595de", @@ -375,7 +563,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 11, "id": "f46ce825-d2b7-492f-b0cf-6f213607e121", "metadata": {}, "outputs": [ @@ -404,7 +592,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 12, "id": "dbfa7035", "metadata": {}, "outputs": [ @@ -414,7 +602,7 @@ "True" ] }, - "execution_count": 5, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -433,7 +621,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 13, "id": "85760cec-5760-4f9d-8ca2-99fba79c7c3c", "metadata": {}, "outputs": [ From 2ea0a224f90a68a8399b477a5e2c152806978b8b Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Thu, 18 Dec 2025 08:39:02 +0000 Subject: [PATCH 19/32] improve 04_agent_versus_non_agent_regret.ipynb --- .../04_agent_versus_non_agent_regret.ipynb | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/doc/tutorials/04_agent_versus_non_agent_regret.ipynb b/doc/tutorials/04_agent_versus_non_agent_regret.ipynb index 02bac1c60..a31b0e779 100644 --- a/doc/tutorials/04_agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/04_agent_versus_non_agent_regret.ipynb @@ -413,8 +413,15 @@ "id": "e67d9926-d19d-4745-a406-a3c1198a8484", "metadata": {}, "source": [ - "Since the maximum regret and therefore Liapunov value are both positive, the starting profile is not a Nash equilibrium\n", - "and we expect `liap_solve` to return a different profile, which will hopefully, but not necessarily by a Nash equilibrium, depending on whether the solver finding a global minimum, or a non-global local minimum." + "It could be a useful exercise to make sure that you can compute these values of the maximum regret and Liapunov value. For that, the starting point would be computing the reduced strategic form. " + ] + }, + { + "cell_type": "markdown", + "id": "e799eded-c6e1-4a3e-80cb-953c52627762", + "metadata": {}, + "source": [ + "Returning to `liap_solve`, since the maximum regret and therefore Liapunov value are both positive, the starting profile is not a Nash equilibrium and we expect `liap_solve` to return a different profile, which will hopefully, but not necessarily by a Nash equilibrium, depending on whether the solver finding a global minimum, or a non-global local minimum." ] }, { @@ -544,12 +551,13 @@ "source": [ "# Agent maximum regret versus standard maximum regret\n", "\n", - "Now we can introduce the \"agent\" versions of these two notions, maximum regret and the Liapunov value.\n", - "\n", - "Both notions relate to what Myerson (1991) called the \"multi-agent representation\" of an extensive form game, in which each information set is treated as an individual \"agent\". The \"agent maximum regret\" is then either 0 or the largest over the \"action regrets\" if that is positive.\n", + "Now we can introduce the \"agent\" versions of both of the notions, maximum regret and the Liapunov value. The \"agent\" versions relate to what [Myerson (1991)](#references) called the \"multi-agent representation\" of an extensive form game, in which each information set is treated as an individual \"agent\". The \"agent maximum regret\" is then either 0 (if every information set has regret 0, i.e. `infoset_regret` 0), or it is largest of the information set regrets, which is then necessarily positive.\n", "\n", "The maximum regret of a profile is at least at large as the agent maximum regret. \n", - "The reason it can be larger is because under the standard notion a player may control multiple information sets and can deviate by changing actions at more than one of these information sets at once, whereas for agent maximum regret, only an \"agent\" deviation at a single information set is allowed.\n", + "In short, the reason it cannot be smaller is that all possible deviations of a given player -- even those that require changing behavior at multiple information sets -- are considered.\n", + "In particular, that includes deviations at a single information set, or at more than one.\n", + "On the other hand, the agent maximum regret only considers deviations at a single information set at a time, by considering each such information set as an \"agent\".\n", + "\n", "Thus, **if the maximum regret is 0, then we have a Nash equilibrium, and the agent maximum regret will be 0 too**.\n", "However, **there are examples where a profile has agent maximum regret of 0 but positive maximum regret**, so the profile is \n", "not a Nash equilibrium.\n", @@ -572,14 +580,16 @@ "output_type": "stream", "text": [ "2\n", - "[[[[Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(0, 1), Rational(1, 1)]]], [[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(1, 1), Rational(0, 1)]]]]\n" + "[[[Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(0, 1), Rational(1, 1)]]]\n", + "[[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(1, 1), Rational(0, 1)]]]\n" ] } ], "source": [ "pure_agent_equilibria = gbt.nash.enumpure_agent_solve(g).equilibria\n", "print(len(pure_agent_equilibria))\n", - "print(pure_agent_equilibria)" + "for agent_eq in pure_agent_equilibria:\n", + " print(agent_eq)" ] }, { @@ -621,7 +631,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "id": "85760cec-5760-4f9d-8ca2-99fba79c7c3c", "metadata": {}, "outputs": [ @@ -629,15 +639,27 @@ "name": "stdout", "output_type": "stream", "text": [ - "1 1\n", - "0 0\n" + "Max regret: 1\n", + "Liapunov value: 1\n", + "Agent max regret 0\n", + "Agent Liapunov value: 0\n" ] } ], "source": [ "aeq = pure_agent_equilibria[1]\n", - "print(aeq.max_regret(), aeq.liap_value())\n", - "print(aeq.agent_max_regret(), aeq.agent_liap_value())" + "print(\"Max regret:\", aeq.max_regret())\n", + "print(\"Liapunov value:\", aeq.liap_value())\n", + "print(\"Agent max regret\", aeq.agent_max_regret())\n", + "print(\"Agent Liapunov value:\", aeq.agent_liap_value())" + ] + }, + { + "cell_type": "markdown", + "id": "a42f18d7-5fb4-4a45-9afd-76a63477ef1d", + "metadata": {}, + "source": [ + "It is a useful exercise to make sure you can confirm that the pure profile `pure_agent_equilibria[1]` indeed has these values of agent and standard maximum regret and Liapunov value." ] }, { @@ -645,7 +667,7 @@ "id": "c4eeb65f", "metadata": {}, "source": [ - "For most use cases, the non-agent versions are probably what a user wants." + "To conclude, we note that, for most use cases, the standard non-agent versions are probably what a user wants. The agent versions have applications in the area of \"equilibrium refinements\"; for more details see [Myerson (1991)](#references)." ] }, { From fd631353722ebe825ff5b6d14f69275a0513bc23 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Thu, 18 Dec 2025 11:57:42 +0000 Subject: [PATCH 20/32] move to advanced_tutorials/agent_versus_non_agent_regret.ipynb --- .../agent_versus_non_agent_regret.ipynb} | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) rename doc/tutorials/{04_agent_versus_non_agent_regret.ipynb => advanced_tutorials/agent_versus_non_agent_regret.ipynb} (98%) diff --git a/doc/tutorials/04_agent_versus_non_agent_regret.ipynb b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb similarity index 98% rename from doc/tutorials/04_agent_versus_non_agent_regret.ipynb rename to doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb index a31b0e779..f55dd5967 100644 --- a/doc/tutorials/04_agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -5,7 +5,7 @@ "id": "96dfe427-942a-47e9-8f1f-91854989b8c8", "metadata": {}, "source": [ - "# 3) Agent and standard notions of extensive form games\n", + "# Agent and standard notions of extensive form games\n", "\n", "The purpose of this tutorial is to explain the notions of `MixedBehaviorProfile.agent_max_regret` and `MixedBehaviorProfile.agent_liap_value`, and the corresponding solvers `Gambit.nash.enumpure_agent_solve` and `Gambit.nash.liap_agent_solve`. These notions are only relevant for *extensive-form games*, and so `agent_max_regret` and \n", "`agent_liap_value` are only available for `MixedBehaviorProfile`s and not for `MixedStrategyProfile`s." @@ -23,7 +23,7 @@ "A player's regret is 0 if they are playing a mixed (including pure) best response; otherwise it is positive and \n", "is the different between the best response payoff (achievable via a pure strategy) against the other players' and what the player actually gets as payoff in this profile.\n", "\n", - "Let's see an example." + "Let's see an example taken from [Myerson (1991)](#references)." ] }, { @@ -375,7 +375,9 @@ "id": "c88d08e2-33bf-48ad-b71f-4a0c19929fdc", "metadata": {}, "source": [ - "The method `Gambit.nash.liap_solve` essentially looks for *local* minima of the function from profiles to the Liapunov value. The set of Nash equilibria are exactly the *global* minima of this function, which is why `liap_solve` may not return a Nash equilibrium." + "As we have seen, both the maximum regret and Liapunov value of a profile are non-negative and zero if and only if the profile is a Nash equilibrium. When positive, one can think of both notions as describing how close one is to an equilibrium.\n", + "\n", + "Based on this idea, the method `Gambit.nash.liap_solve` looks for *local* minima of the function from profiles to the Liapunov value. The set of Nash equilibria are exactly the *global* minima of this function, where the value is 0, but `liap_solve` may terminate at a non-global, local minimum, which is not a Nash equilibrium." ] }, { @@ -667,7 +669,7 @@ "id": "c4eeb65f", "metadata": {}, "source": [ - "To conclude, we note that, for most use cases, the standard non-agent versions are probably what a user wants. The agent versions have applications in the area of \"equilibrium refinements\"; for more details see [Myerson (1991)](#references)." + "To conclude, we note that, for most use cases, the standard non-agent versions are probably what a user wants. The agent versions have applications in the area of \"equilibrium refinements\", in particular for \"sequential equilibria\"; for more details see Chapter 4, \"Sequential Equilibria of Extensive-Form Games\", in [Myerson (1991)](#references)." ] }, { From a4942683d05c811ad6e2cc467dd18b65d8baf988 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Thu, 18 Dec 2025 12:15:45 +0000 Subject: [PATCH 21/32] fix relative import --- .../advanced_tutorials/agent_versus_non_agent_regret.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb index f55dd5967..c22fc182d 100644 --- a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -280,7 +280,7 @@ "\n", "import pygambit as gbt\n", "\n", - "g = gbt.read_efg(\"../../contrib/games/myerson_fig_4_2.efg\")\n", + "g = gbt.read_efg(\"../../../contrib/games/myerson_fig_4_2.efg\")\n", "draw_tree(g)" ] }, @@ -633,7 +633,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 13, "id": "85760cec-5760-4f9d-8ca2-99fba79c7c3c", "metadata": {}, "outputs": [ From 698907b3632756fa3e7906613de5aee12a71e282 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Thu, 18 Dec 2025 12:19:36 +0000 Subject: [PATCH 22/32] agent -> non-agent max_regret/liap_solve in 03_stripped_down_poker.ipynb --- doc/tutorials/03_stripped_down_poker.ipynb | 262 +++++++++--------- .../agent_versus_non_agent_regret.ipynb | 2 +- 2 files changed, 133 insertions(+), 131 deletions(-) diff --git a/doc/tutorials/03_stripped_down_poker.ipynb b/doc/tutorials/03_stripped_down_poker.ipynb index 532a848cd..b5ca9565a 100644 --- a/doc/tutorials/03_stripped_down_poker.ipynb +++ b/doc/tutorials/03_stripped_down_poker.ipynb @@ -39,7 +39,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 1, "id": "69cbfe81", "metadata": {}, "outputs": [], @@ -59,7 +59,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 2, "id": "ad6a1119", "metadata": {}, "outputs": [], @@ -80,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 3, "id": "841f9f74", "metadata": {}, "outputs": [ @@ -116,7 +116,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 4, "id": "fe80c64c", "metadata": {}, "outputs": [], @@ -130,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 5, "id": "867cb1d8-7a5d-45d1-9349-9bbc2a4e2344", "metadata": {}, "outputs": [ @@ -230,7 +230,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -253,7 +253,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -284,7 +284,7 @@ "" ] }, - "execution_count": 41, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -309,7 +309,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 6, "id": "0e3bb5ef", "metadata": {}, "outputs": [], @@ -324,7 +324,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 7, "id": "0c522c2d-992e-48b6-a1f8-0696d33cdbe0", "metadata": {}, "outputs": [ @@ -488,7 +488,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -541,7 +541,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -609,7 +609,7 @@ "" ] }, - "execution_count": 43, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -638,7 +638,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 8, "id": "dbfa7035", "metadata": {}, "outputs": [], @@ -652,7 +652,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 9, "id": "e85b3346-2fea-4a73-aa72-9efb436c68c1", "metadata": {}, "outputs": [ @@ -820,7 +820,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -895,7 +895,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -986,7 +986,7 @@ "" ] }, - "execution_count": 45, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -1010,7 +1010,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 10, "id": "87c988be", "metadata": {}, "outputs": [], @@ -1031,7 +1031,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 11, "id": "29aa60a0", "metadata": {}, "outputs": [], @@ -1053,7 +1053,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 12, "id": "fdee7b53-7820-44df-9d17-d15d0b9667aa", "metadata": {}, "outputs": [ @@ -1198,7 +1198,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -1226,7 +1226,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -1246,7 +1246,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -1263,7 +1263,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -1294,7 +1294,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -1319,7 +1319,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -1344,7 +1344,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -1361,7 +1361,7 @@ "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", @@ -1392,7 +1392,7 @@ "" ] }, - "execution_count": 48, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -1414,7 +1414,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 13, "id": "4d92c8d9", "metadata": {}, "outputs": [ @@ -1424,7 +1424,7 @@ "NashComputationResult(method='lcp', rational=True, use_strategic=False, equilibria=[[[[Rational(1, 1), Rational(0, 1)], [Rational(1, 3), Rational(2, 3)]], [[Rational(2, 3), Rational(1, 3)]]]], parameters={'stop_after': 0, 'max_depth': 0})" ] }, - "execution_count": 49, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -1448,7 +1448,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 14, "id": "9967d6f7", "metadata": {}, "outputs": [ @@ -1475,7 +1475,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 15, "id": "3293e818", "metadata": {}, "outputs": [ @@ -1485,7 +1485,7 @@ "pygambit.gambit.MixedBehaviorProfileRational" ] }, - "execution_count": 51, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -1506,7 +1506,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 16, "id": "4cf38264", "metadata": {}, "outputs": [ @@ -1516,7 +1516,7 @@ "pygambit.gambit.MixedBehavior" ] }, - "execution_count": 52, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -1527,7 +1527,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 17, "id": "85e7fdda", "metadata": {}, "outputs": [ @@ -1540,7 +1540,7 @@ "[[Rational(1, 1), Rational(0, 1)], [Rational(1, 3), Rational(2, 3)]]" ] }, - "execution_count": 53, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -1565,7 +1565,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 18, "id": "f45a82b6", "metadata": {}, "outputs": [ @@ -1597,7 +1597,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 19, "id": "83bbd3e5", "metadata": {}, "outputs": [ @@ -1630,7 +1630,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 20, "id": "6bf51b38", "metadata": {}, "outputs": [ @@ -1643,7 +1643,7 @@ "[[Rational(2, 3), Rational(1, 3)]]" ] }, - "execution_count": 56, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -1666,7 +1666,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 21, "id": "2966e700", "metadata": {}, "outputs": [ @@ -1679,7 +1679,7 @@ "Rational(2, 3)" ] }, - "execution_count": 57, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -1698,7 +1698,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 22, "id": "f5a7f110", "metadata": {}, "outputs": [ @@ -1711,7 +1711,7 @@ "Rational(2, 3)" ] }, - "execution_count": 58, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -1732,7 +1732,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 23, "id": "a7d3816d", "metadata": {}, "outputs": [ @@ -1767,7 +1767,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 24, "id": "4a54b20c", "metadata": {}, "outputs": [ @@ -1800,7 +1800,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 25, "id": "b250c1cd", "metadata": {}, "outputs": [ @@ -1813,7 +1813,7 @@ "Rational(2, 3)" ] }, - "execution_count": 61, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -1832,7 +1832,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 26, "id": "6f01846b", "metadata": {}, "outputs": [ @@ -1864,7 +1864,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 27, "id": "5079d231", "metadata": {}, "outputs": [ @@ -1877,7 +1877,7 @@ "Rational(1, 3)" ] }, - "execution_count": 63, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -1888,7 +1888,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 28, "id": "c55f2c7a", "metadata": {}, "outputs": [ @@ -1901,7 +1901,7 @@ "Rational(-1, 3)" ] }, - "execution_count": 64, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1928,7 +1928,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 29, "id": "d4ecff88", "metadata": {}, "outputs": [ @@ -1938,7 +1938,7 @@ "['11', '12', '21', '22']" ] }, - "execution_count": 65, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -1962,7 +1962,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 30, "id": "24e4b6e8", "metadata": {}, "outputs": [ @@ -1972,7 +1972,7 @@ "NashComputationResult(method='gnm', rational=False, use_strategic=True, equilibria=[[[0.33333333333866677, 0.6666666666613335, 0.0, 0.0], [0.6666666666559997, 0.3333333333440004]]], parameters={'perturbation': [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0]], 'end_lambda': -10.0, 'steps': 100, 'local_newton_interval': 3, 'local_newton_maxits': 10})" ] }, - "execution_count": 66, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -1994,7 +1994,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 31, "id": "d9ffb4b8", "metadata": {}, "outputs": [ @@ -2004,7 +2004,7 @@ "pygambit.gambit.MixedStrategyProfileDouble" ] }, - "execution_count": 67, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -2026,7 +2026,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 32, "id": "56e2f847", "metadata": {}, "outputs": [ @@ -2079,7 +2079,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 33, "id": "d18a91f0", "metadata": {}, "outputs": [ @@ -2145,7 +2145,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 34, "id": "0c55f745", "metadata": {}, "outputs": [ @@ -2155,7 +2155,7 @@ "(Rational(2, 1), Rational(-2, 1))" ] }, - "execution_count": 70, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -2177,7 +2177,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 54, "id": "101598c6", "metadata": {}, "outputs": [ @@ -2187,36 +2187,36 @@ "1" ] }, - "execution_count": 71, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "logit_solve_result = gbt.nash.logit_solve(g, maxregret=1e-8)\n", + "logit_solve_result = gbt.nash.logit_solve(g, use_strategic=True, maxregret=1e-8)\n", "len(logit_solve_result.equilibria)" ] }, { "cell_type": "code", - "execution_count": 72, + "execution_count": 59, "id": "9b142728", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "3.987411578698641e-08" + "7.997619122512845e-08" ] }, - "execution_count": 72, + "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ls_eqm = logit_solve_result.equilibria[0]\n", - "ls_eqm.agent_max_regret()" + "ls_eqm.max_regret()" ] }, { @@ -2224,28 +2224,30 @@ "id": "a2ba06c4", "metadata": {}, "source": [ - "The value of `MixedBehaviorProfile.agent_max_regret` of the computed profile exceeds $10^{-8}$ measured in payoffs of the game.\n", - "However, when considered relative to the scale of the game's payoffs, we see it is less than $10^{-8}$ of the payoff range, as requested:" + "The value of `MixedBehaviorProfile.max_regret` of the computed profile exceeds `1e-8` measured in terms of payoffs of the game.\n", + "However, when considered relative to the scale of the game's payoffs, we see it is less than `1e-8` of the payoff range, as requested:" ] }, { "cell_type": "code", - "execution_count": 73, + "execution_count": 60, "id": "ff405409", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "9.968528946746602e-09" + "9.997023903141056e-09" ] }, - "execution_count": 73, + "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], - "source": "ls_eqm.agent_max_regret() / (g.max_payoff - g.min_payoff)" + "source": [ + "ls_eqm.max_regret() / (g.max_payoff - g.min_payoff)" + ] }, { "cell_type": "markdown", @@ -2254,30 +2256,30 @@ "source": [ "In general, for globally-convergent methods especially, there is a tradeoff between precision and running time.\n", "\n", - "We could instead ask only for an $\\varepsilon$-equilibrium with a (scaled) $\\varepsilon$ of no more than $10^{-4}$:" + "We could instead ask only for an $\\varepsilon$-equilibrium with a (scaled) $\\varepsilon$ of no more than `1e-4`:" ] }, { "cell_type": "code", - "execution_count": 74, + "execution_count": 62, "id": "31b0143c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "9.395259956013202e-05" + "9.434077820902331e-05" ] }, - "execution_count": 74, + "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(\n", - " gbt.nash.logit_solve(g, maxregret=1e-4).equilibria[0]\n", - " .agent_max_regret() / (g.max_payoff - g.min_payoff)\n", + " gbt.nash.logit_solve(g, use_strategic=True, maxregret=1e-4).equilibria[0]\n", + " .max_regret() / (g.max_payoff - g.min_payoff)\n", ")" ] }, @@ -2291,7 +2293,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": 63, "id": "7cfba34a", "metadata": {}, "outputs": [ @@ -2299,29 +2301,29 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 9.86 ms, sys: 91 μs, total: 9.95 ms\n", - "Wall time: 9.96 ms\n" + "CPU times: user 180 ms, sys: 2.62 ms, total: 183 ms\n", + "Wall time: 183 ms\n" ] }, { "data": { "text/plain": [ - "NashComputationResult(method='logit', rational=False, use_strategic=False, equilibria=[[[[1.0, 0.0], [0.3338351656285655, 0.666164834417892]], [[0.6670407651644307, 0.3329592348608147]]]], parameters={'first_step': 0.03, 'max_accel': 1.1})" + "NashComputationResult(method='logit', rational=False, use_strategic=True, equilibria=[[[0.3340897594396454, 0.6659102406666876, 0.0, 0.0], [0.667415735361018, 0.3325842647395834]]], parameters={'first_step': 0.03, 'max_accel': 1.1})" ] }, - "execution_count": 75, + "execution_count": 63, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", - "gbt.nash.logit_solve(g, maxregret=1e-4)" + "gbt.nash.logit_solve(g, use_strategic=True, maxregret=1e-4)" ] }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 64, "id": "6f1809a7", "metadata": {}, "outputs": [ @@ -2329,24 +2331,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 18.8 ms, sys: 148 μs, total: 19 ms\n", - "Wall time: 19 ms\n" + "CPU times: user 306 ms, sys: 3.42 ms, total: 310 ms\n", + "Wall time: 309 ms\n" ] }, { "data": { "text/plain": [ - "NashComputationResult(method='logit', rational=False, use_strategic=False, equilibria=[[[[1.0, 0.0], [0.33333338649882943, 0.6666666135011706]], [[0.6666667065407631, 0.3333332934592369]]]], parameters={'first_step': 0.03, 'max_accel': 1.1})" + "NashComputationResult(method='logit', rational=False, use_strategic=True, equilibria=[[[0.33333341330954375, 0.6666665866904562, 0.0, 0.0], [0.6666667466427941, 0.3333332533572059]]], parameters={'first_step': 0.03, 'max_accel': 1.1})" ] }, - "execution_count": 76, + "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", - "gbt.nash.logit_solve(g, maxregret=1e-8)" + "gbt.nash.logit_solve(g, use_strategic=True, maxregret=1e-8)" ] }, { @@ -2356,30 +2358,30 @@ "source": [ "The convention of expressing `maxregret` scaled by the game's payoffs standardises the behavior of methods across games.\n", "\n", - "For example, consider solving the poker game instead using `liap_agent_solve()`." + "For example, consider solving the poker game instead using `liap_solve()`." ] }, { "cell_type": "code", - "execution_count": 77, - "id": "414b6f65", + "execution_count": 80, + "id": "5998d609-8037-4814-ac78-1f5a288f4bdd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "5.509949805110326e-05" + "0.03211100728219732" ] }, - "execution_count": 77, + "execution_count": 80, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(\n", - " gbt.nash.liap_agent_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)\n", - " .equilibria[0].agent_max_regret() / (g.max_payoff - g.min_payoff)\n", + " gbt.nash.liap_solve(g.mixed_strategy_profile(), maxregret=1e-1)\n", + " .equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)\n", ")" ] }, @@ -2393,17 +2395,17 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 83, "id": "a892dc2b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "5.509949805110326e-05" + "0.03211100728219732" ] }, - "execution_count": 78, + "execution_count": 83, "metadata": {}, "output_type": "execute_result" } @@ -2414,8 +2416,8 @@ " outcome[\"Bob\"] = outcome[\"Bob\"] * 2\n", "\n", "(\n", - " gbt.nash.liap_agent_solve(g.mixed_behavior_profile(), maxregret=1.0e-4)\n", - " .equilibria[0].agent_max_regret() / (g.max_payoff - g.min_payoff)\n", + " gbt.nash.liap_solve(g.mixed_strategy_profile(), maxregret=1e-1)\n", + " .equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)\n", ")" ] }, @@ -2434,7 +2436,7 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 43, "id": "2f79695a", "metadata": {}, "outputs": [ @@ -2444,7 +2446,7 @@ "[Rational(1, 3), Rational(1, 3), Rational(1, 3)]" ] }, - "execution_count": 79, + "execution_count": 43, "metadata": {}, "output_type": "execute_result" } @@ -2468,7 +2470,7 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": 44, "id": "5de6acb2", "metadata": {}, "outputs": [ @@ -2478,7 +2480,7 @@ "[Rational(1, 4), Rational(1, 2), Rational(1, 4)]" ] }, - "execution_count": 80, + "execution_count": 44, "metadata": {}, "output_type": "execute_result" } @@ -2501,7 +2503,7 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": 45, "id": "c47d2ab6", "metadata": {}, "outputs": [ @@ -2511,7 +2513,7 @@ "[Decimal('0.25'), Decimal('0.50'), Decimal('0.25')]" ] }, - "execution_count": 81, + "execution_count": 45, "metadata": {}, "output_type": "execute_result" } @@ -2538,7 +2540,7 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": 46, "id": "04329084", "metadata": {}, "outputs": [ @@ -2548,7 +2550,7 @@ "[Rational(1, 4), Rational(1, 2), Rational(1, 4)]" ] }, - "execution_count": 82, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } @@ -2560,7 +2562,7 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": 47, "id": "9015e129", "metadata": {}, "outputs": [ @@ -2570,7 +2572,7 @@ "[Decimal('0.25'), Decimal('0.50'), Decimal('0.25')]" ] }, - "execution_count": 83, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } @@ -2595,7 +2597,7 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": 48, "id": "0a019aa5", "metadata": {}, "outputs": [ @@ -2605,7 +2607,7 @@ "[Decimal('0.25'), Decimal('0.5'), Decimal('0.25')]" ] }, - "execution_count": 84, + "execution_count": 48, "metadata": {}, "output_type": "execute_result" } @@ -2625,7 +2627,7 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": 49, "id": "1991d288", "metadata": {}, "outputs": [ @@ -2655,7 +2657,7 @@ }, { "cell_type": "code", - "execution_count": 86, + "execution_count": 50, "id": "b1dc37fd", "metadata": {}, "outputs": [ @@ -2665,7 +2667,7 @@ "1.0" ] }, - "execution_count": 86, + "execution_count": 50, "metadata": {}, "output_type": "execute_result" } @@ -2684,7 +2686,7 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": 51, "id": "dc1edea2", "metadata": {}, "outputs": [ @@ -2694,7 +2696,7 @@ "Decimal('0.3333333333333333')" ] }, - "execution_count": 87, + "execution_count": 51, "metadata": {}, "output_type": "execute_result" } @@ -2713,7 +2715,7 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": 52, "id": "1edd90d6", "metadata": {}, "outputs": [ @@ -2723,7 +2725,7 @@ "Decimal('0.9999999999999999')" ] }, - "execution_count": 88, + "execution_count": 52, "metadata": {}, "output_type": "execute_result" } @@ -2757,7 +2759,7 @@ ], "metadata": { "kernelspec": { - "display_name": "gambit310", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -2771,7 +2773,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.19" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb index c22fc182d..68b78ee11 100644 --- a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -423,7 +423,7 @@ "id": "e799eded-c6e1-4a3e-80cb-953c52627762", "metadata": {}, "source": [ - "Returning to `liap_solve`, since the maximum regret and therefore Liapunov value are both positive, the starting profile is not a Nash equilibrium and we expect `liap_solve` to return a different profile, which will hopefully, but not necessarily by a Nash equilibrium, depending on whether the solver finding a global minimum, or a non-global local minimum." + "Returning to `liap_solve`, since the maximum regret and therefore Liapunov value are both positive, the starting profile is not a Nash equilibrium and we expect `liap_solve` to return a different profile, which will hopefully, but not necessarily by a Nash equilibrium, depending on whether the solver finding a global minimum, or non-global local minimum, or nothing at all." ] }, { From cd2d9179cc73c4918f7da50849932c95e510a56a Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Thu, 18 Dec 2025 16:17:06 +0000 Subject: [PATCH 23/32] removed use_strategic from call to logit_solve in 03_stripped_down_poker.ipynb --- doc/tutorials/03_stripped_down_poker.ipynb | 62 +++++++++++----------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/doc/tutorials/03_stripped_down_poker.ipynb b/doc/tutorials/03_stripped_down_poker.ipynb index b5ca9565a..32413f7e1 100644 --- a/doc/tutorials/03_stripped_down_poker.ipynb +++ b/doc/tutorials/03_stripped_down_poker.ipynb @@ -2170,14 +2170,14 @@ "metadata": {}, "source": [ "`logit_solve` is a globally-convergent method, in that it computes a sequence of profiles which is guaranteed to have a subsequence that converges to a\n", - "Nash equilibrium.\n", + "Nash equilibrium. \n", "\n", - "The default value of `maxregret` for this method is set at $10^{-8}$:" + "The default value of `maxregret` for this method is set at `1e-8`:" ] }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 53, "id": "101598c6", "metadata": {}, "outputs": [ @@ -2187,29 +2187,29 @@ "1" ] }, - "execution_count": 54, + "execution_count": 53, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "logit_solve_result = gbt.nash.logit_solve(g, use_strategic=True, maxregret=1e-8)\n", + "logit_solve_result = gbt.nash.logit_solve(g, maxregret=1e-8)\n", "len(logit_solve_result.equilibria)" ] }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 54, "id": "9b142728", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "7.997619122512845e-08" + "5.0647885885268806e-08" ] }, - "execution_count": 59, + "execution_count": 54, "metadata": {}, "output_type": "execute_result" } @@ -2230,17 +2230,17 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 55, "id": "ff405409", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "9.997023903141056e-09" + "6.330985735658601e-09" ] }, - "execution_count": 60, + "execution_count": 55, "metadata": {}, "output_type": "execute_result" } @@ -2261,24 +2261,24 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 57, "id": "31b0143c", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "9.434077820902331e-05" + "6.566536354296604e-05" ] }, - "execution_count": 62, + "execution_count": 57, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(\n", - " gbt.nash.logit_solve(g, use_strategic=True, maxregret=1e-4).equilibria[0]\n", + " gbt.nash.logit_solve(g, maxregret=1e-4).equilibria[0]\n", " .max_regret() / (g.max_payoff - g.min_payoff)\n", ")" ] @@ -2293,7 +2293,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 58, "id": "7cfba34a", "metadata": {}, "outputs": [ @@ -2301,29 +2301,29 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 180 ms, sys: 2.62 ms, total: 183 ms\n", - "Wall time: 183 ms\n" + "CPU times: user 15.8 ms, sys: 429 μs, total: 16.3 ms\n", + "Wall time: 16.3 ms\n" ] }, { "data": { "text/plain": [ - "NashComputationResult(method='logit', rational=False, use_strategic=True, equilibria=[[[0.3340897594396454, 0.6659102406666876, 0.0, 0.0], [0.667415735361018, 0.3325842647395834]]], parameters={'first_step': 0.03, 'max_accel': 1.1})" + "NashComputationResult(method='logit', rational=False, use_strategic=False, equilibria=[[[[1.0, 0.0], [0.333859274697877, 0.6661407253531737]], [[0.6670586236711866, 0.3329413763565089]]]], parameters={'first_step': 0.03, 'max_accel': 1.1})" ] }, - "execution_count": 63, + "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", - "gbt.nash.logit_solve(g, use_strategic=True, maxregret=1e-4)" + "gbt.nash.logit_solve(g, maxregret=1e-4)" ] }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 59, "id": "6f1809a7", "metadata": {}, "outputs": [ @@ -2331,24 +2331,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 306 ms, sys: 3.42 ms, total: 310 ms\n", - "Wall time: 309 ms\n" + "CPU times: user 28.7 ms, sys: 883 μs, total: 29.6 ms\n", + "Wall time: 29.7 ms\n" ] }, { "data": { "text/plain": [ - "NashComputationResult(method='logit', rational=False, use_strategic=True, equilibria=[[[0.33333341330954375, 0.6666665866904562, 0.0, 0.0], [0.6666667466427941, 0.3333332533572059]]], parameters={'first_step': 0.03, 'max_accel': 1.1})" + "NashComputationResult(method='logit', rational=False, use_strategic=False, equilibria=[[[[1.0, 0.0], [0.3333333839812253, 0.6666666160187746]], [[0.6666667046525624, 0.33333329534743755]]]], parameters={'first_step': 0.03, 'max_accel': 1.1})" ] }, - "execution_count": 64, + "execution_count": 59, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%time\n", - "gbt.nash.logit_solve(g, use_strategic=True, maxregret=1e-8)" + "gbt.nash.logit_solve(g, maxregret=1e-8)" ] }, { @@ -2363,7 +2363,7 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": 60, "id": "5998d609-8037-4814-ac78-1f5a288f4bdd", "metadata": {}, "outputs": [ @@ -2373,7 +2373,7 @@ "0.03211100728219732" ] }, - "execution_count": 80, + "execution_count": 60, "metadata": {}, "output_type": "execute_result" } @@ -2395,7 +2395,7 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": 61, "id": "a892dc2b", "metadata": {}, "outputs": [ @@ -2405,7 +2405,7 @@ "0.03211100728219732" ] }, - "execution_count": 83, + "execution_count": 61, "metadata": {}, "output_type": "execute_result" } From c95c11c5e38dbf109d3c5c715af42fa026c9786b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 19 Dec 2025 11:36:26 +0000 Subject: [PATCH 24/32] Updated ChangeLog. --- ChangeLog | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog b/ChangeLog index d087d961d..2f6ae1e97 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,10 @@ to detect if an information is absent-minded. ### Changed +- Terminology for agent-form calculations on extensive games has been clarified. Mixed behavior profiles + distinguish "agent" regret and liap values from their strategy-based analogs. Methods which compute + using the agent-form - specifically `enumpure_solve` and `liap_solve`, now clarify this by being named + differently in `pygambit`. (#617) - In the graphical interface, removed option to configure information set link drawing; information sets are always drawn and indicators are always drawn if an information set spans multiple levels. - In `pygambit`, indexing the children of a node by a string inteprets the string as an action label, From e3b2fd09b38ee47c3703db157ca81112fa604b3e Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Fri, 19 Dec 2025 14:26:51 +0000 Subject: [PATCH 25/32] fixing up enumpoly tests --- tests/test_nash.py | 149 ++++++++++++++++++++++++++++++++------------- 1 file changed, 105 insertions(+), 44 deletions(-) diff --git a/tests/test_nash.py b/tests/test_nash.py index 15699b806..10ecd31e4 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -135,44 +135,25 @@ def test_enummixed_rational(game: gbt.Game, mixed_strategy_prof_data: list): # ], # 2, # 9 in total found by enumpoly (see unordered test) # ), + ############################################################################## + ############################################################################## ( games.create_3_player_with_internal_outcomes_efg(), [ - [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[0, 1], [1, 0]]], - [[[1, 0], [1, 0]], [[1, 0], [0, 1]], - [["1/3", "2/3"], [1, 0]]]], + [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]], + [[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], ["1/3", "2/3"]]], + ], 2, ), ( games.create_3_player_with_internal_outcomes_efg(nonterm_outcomes=True), [ - [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[0, 1], [1, 0]]], - [[[1, 0], [1, 0]], [[1, 0], [0, 1]], - [["1/3", "2/3"], [1, 0]]]], + [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]], + [[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], ["1/3", "2/3"]]]], 2, ), - ( - games.create_entry_accomodation_efg(), - [ - [[["2/3", "1/3"], [1, 0], [1, 0]], - [["2/3", "1/3"]]], - [[[0, 1], [0, 0], ["1/3", "2/3"]], - [[0, 1]]], - [[[0, 1], [0, 0], [1, 0]], [[1, 0]]], - [[[0, 1], [0, 0], [0, 0]], [[0, 1]]]], - 4, - ), - # ( - # games.create_entry_accomodation_efg(nonterm_outcomes=True), - # [ - # [[["2/3", "1/3"], [1, 0], [1, 0]], - # [["2/3", "1/3"]]], - # [[[0, 1], [0, 0], ["1/3", "2/3"]], - # [[0, 1]]], - # [[[0, 1], [0, 0], [1, 0]], [[1, 0]]], - # [[[0, 1], [0, 0], [0, 0]], [[0, 1]]]], - # 4, - # ), + ############################################################################## + ############################################################################## ( games.create_non_zero_sum_lacking_outcome_efg(), [[[["1/3", "2/3"]], [["1/2", "1/2"]]]], @@ -183,27 +164,21 @@ def test_enummixed_rational(game: gbt.Game, mixed_strategy_prof_data: list): [[[["1/3", "2/3"]], [["1/2", "1/2"]]]], 1, ), + ############################################################################## + ############################################################################## ( games.create_chance_in_middle_efg(), - [[[["3/11", "8/11"], - [1, 0], [1, 0], [1, 0], [1, 0]], - [[1, 0], ["6/11", "5/11"]]], - [[[1, 0], [1, 0], [1, 0], [0, 0], [0, 0]], - [[0, 1], [1, 0]]], - [[[0, 1], [0, 0], [0, 0], [1, 0], [1, 0]], - [[1, 0], [0, 1]]]], - 3, + [[[["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]], [[1, 0], ["6/11", "5/11"]]], + ], # [[[1, 0], [1, 0], [1, 0], [0, 0], [0, 0]], [[0, 1], [1, 0]]], + # [[[0, 1], [0, 0], [0, 0], [1, 0], [1, 0]], [[1, 0], [0, 1]]], + 1, # subsequent eqs have undefined infosets; include after #issue 660 ), ( games.create_chance_in_middle_efg(nonterm_outcomes=True), - [[[["3/11", "8/11"], - [1, 0], [1, 0], [1, 0], [1, 0]], - [[1, 0], ["6/11", "5/11"]]], - [[[1, 0], [1, 0], [1, 0], [0, 0], [0, 0]], - [[0, 1], [1, 0]]], - [[[0, 1], [0, 0], [0, 0], [1, 0], [1, 0]], - [[1, 0], [0, 1]]]], - 3, + [[[["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]], [[1, 0], ["6/11", "5/11"]]], + ], # [[[1, 0], [1, 0], [1, 0], [0, 0], [0, 0]], [[0, 1], [1, 0]]], + # [[[0, 1], [0, 0], [0, 0], [1, 0], [1, 0]], [[1, 0], [0, 1]]], + 1, ), ], ) @@ -232,9 +207,79 @@ def test_enumpoly_ordered_behavior( result = gbt.nash.enumpoly_solve(game, use_strategic=False) assert len(result.equilibria) == len(mixed_behav_prof_data) for eq, exp in zip(result.equilibria, mixed_behav_prof_data, strict=True): + print("FOUND EQ:", eq) + print(eq.max_regret()) + print(eq.agent_max_regret()) + assert abs(eq.max_regret()) <= TOL + assert abs(eq.agent_max_regret()) <= TOL + expected = game.mixed_behavior_profile(rational=True, data=exp) + # print(expected) + # print(eq) + for p in game.players: + for i in p.infosets: + for a in i.actions: + assert abs(eq[p][i][a] - expected[p][i][a]) <= TOL + + +@pytest.mark.nash +@pytest.mark.nash_enumpoly_behavior +@pytest.mark.parametrize( + "game,mixed_behav_prof_data,stop_after", + [ + ############################################################################## + ############################################################################## + ( + games.create_3_player_with_internal_outcomes_efg(), + [ + [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]], + [[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], ["1/3", "2/3"]]], + ], + 2, + ), + ( + games.create_3_player_with_internal_outcomes_efg(nonterm_outcomes=True), + [ + [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]], + [[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], ["1/3", "2/3"]]]], + 2, + ), + ############################################################################## + ############################################################################## + ], +) +def test_enumpoly_ordered_behavior2( + game: gbt.Game, mixed_behav_prof_data: list, stop_after: None | int +): + """Test calls of enumpoly for mixed behavior equilibria, + using max_regret and agent_max_regret (internal consistency); and + comparison to a set of previously computed equilibria with this function (regression test). + This set will be the full set of all computed equilibria if stop_after is None, + else the first stop_after-many equilibria. + + This is the "ordered" version where we test for the outputs coming in a specific + order; there is also an "unordered" version. The game 2x2x2.nfg, for example, + has a point at which the Jacobian is singular. As a result, the order in which it + returns the two totally-mixed equilbria is system-dependent due, essentially, + to inherent numerical instability near that point. + """ + if stop_after: + result = gbt.nash.enumpoly_solve( + game, use_strategic=False, stop_after=stop_after, maxregret=0.00001 + ) + assert len(result.equilibria) == stop_after + else: + # compute all + result = gbt.nash.enumpoly_solve(game, use_strategic=False) + assert len(result.equilibria) == len(mixed_behav_prof_data) + for eq, exp in zip(result.equilibria, mixed_behav_prof_data, strict=True): + print("FOUND EQ:", eq) + print("found max regret:", eq.max_regret()) + print("found agent max regret:", eq.agent_max_regret()) assert abs(eq.max_regret()) <= TOL assert abs(eq.agent_max_regret()) <= TOL expected = game.mixed_behavior_profile(rational=True, data=exp) + print("exp max regret:", eq.max_regret()) + print("exp agent max regret:", eq.agent_max_regret()) for p in game.players: for i in p.infosets: for a in i.actions: @@ -845,3 +890,19 @@ def test_logit_solve_lambda(): game = games.read_from_file("const_sum_game.nfg") assert len(gbt.qre.logit_solve_lambda( game=game, lam=[1, 2, 3], first_step=0.2, max_accel=1)) > 0 + + +def test_regrets_tmp(): + + g = games.create_3_player_with_internal_outcomes_efg() + prof_data = [] + prof_data.append([[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]]) + prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], ["1/3", "2/3"]]]) + prof_data.append([[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[0, 1], [1, 0]]]) + prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [["1/3", "2/3"], [1, 0]]]) + + for p in prof_data: + prof = g.mixed_behavior_profile(rational=True, data=p) + print(prof) + print(prof.max_regret()) + print(prof.agent_max_regret()) From c03048cb7214e34db7c40da5dbf199b861934fe5 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Fri, 19 Dec 2025 14:55:59 +0000 Subject: [PATCH 26/32] problem cases shown in tests --- tests/test_nash.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_nash.py b/tests/test_nash.py index 10ecd31e4..19b9687ee 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -247,7 +247,7 @@ def test_enumpoly_ordered_behavior( ############################################################################## ], ) -def test_enumpoly_ordered_behavior2( +def test_enumpoly_ordered_behavior_PROBLEM_CASE( game: gbt.Game, mixed_behav_prof_data: list, stop_after: None | int ): """Test calls of enumpoly for mixed behavior equilibria, @@ -896,13 +896,25 @@ def test_regrets_tmp(): g = games.create_3_player_with_internal_outcomes_efg() prof_data = [] + prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]]) + prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], [0.33333, 0.6666]]]) + prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[0, 1], [1, 0]]]) + prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[0.33333, 0.6666], [1, 0]]]) + print("==================") + for p in prof_data: + prof = g.mixed_behavior_profile(rational=False, data=p) + # print(prof) + print(prof.max_regret()) + print(prof.agent_max_regret()) + + print("==================") + prof_data = [] prof_data.append([[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]]) prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], ["1/3", "2/3"]]]) prof_data.append([[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[0, 1], [1, 0]]]) prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [["1/3", "2/3"], [1, 0]]]) - for p in prof_data: prof = g.mixed_behavior_profile(rational=True, data=p) - print(prof) + # print(prof) print(prof.max_regret()) print(prof.agent_max_regret()) From 00da58dc0da8876734f2b4bece54e37a6b93c831 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Sun, 21 Dec 2025 14:44:25 +0000 Subject: [PATCH 27/32] test_regrets_tmp --- tests/games.py | 1 + tests/test_behav.py | 2 + tests/test_nash.py | 97 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/tests/games.py b/tests/games.py index 1a382079d..8e1f925d3 100644 --- a/tests/games.py +++ b/tests/games.py @@ -352,6 +352,7 @@ def create_3_player_with_internal_outcomes_efg(nonterm_outcomes: bool = False) - o = g.add_outcome([0, 0, 0]) g.set_outcome(g.root.children[0].children[0].children[1].children[0], o) g.set_outcome(g.root.children[0].children[0].children[1].children[1], o) + g.to_efg(f"TEST_{nonterm_outcomes}.efg") return g diff --git a/tests/test_behav.py b/tests/test_behav.py index 6fd5b2e20..c3468ef0c 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -843,6 +843,8 @@ def test_infoset_regret_consistency(game: gbt.Game, rational_flag: bool): (games.create_stripped_down_poker_efg(), True), (games.create_kuhn_poker_efg(), False), (games.create_kuhn_poker_efg(), True), + (games.create_3_player_with_internal_outcomes_efg(), False), + (games.create_3_player_with_internal_outcomes_efg(), True) ] ) def test_max_regret_consistency(game: gbt.Game, rational_flag: bool): diff --git a/tests/test_nash.py b/tests/test_nash.py index 19b9687ee..3b7ae25f0 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -894,27 +894,94 @@ def test_logit_solve_lambda(): def test_regrets_tmp(): + prof_data_doub = [] + prof_data_doub.append([[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]]) + # prof_data_doub.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], [0.33333, 0.6666]]]) + # prof_data_doub.append([[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[0, 1], [1, 0]]]) + # prof_data_doub.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[0.33333, 0.6666], [1, 0]]]) + + prof_data_rat = [] + prof_data_rat.append([[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]]) + # prof_data_rat.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], ["1/3", "2/3"]]]) + # prof_data_rat.append([[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[0, 1], [1, 0]]]) + # prof_data_rat.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [["1/3", "2/3"], [1, 0]]]) + g = games.create_3_player_with_internal_outcomes_efg() - prof_data = [] - prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]]) - prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], [0.33333, 0.6666]]]) - prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[0, 1], [1, 0]]]) - prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[0.33333, 0.6666], [1, 0]]]) + + print() print("==================") - for p in prof_data: + for p in prof_data_doub: prof = g.mixed_behavior_profile(rational=False, data=p) - # print(prof) print(prof.max_regret()) print(prof.agent_max_regret()) - print("==================") - prof_data = [] - prof_data.append([[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]]) - prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [[1, 0], ["1/3", "2/3"]]]) - prof_data.append([[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[0, 1], [1, 0]]]) - prof_data.append([[[1, 0], [1, 0]], [[1, 0], [0, 1]], [["1/3", "2/3"], [1, 0]]]) - for p in prof_data: + for p in prof_data_rat: prof = g.mixed_behavior_profile(rational=True, data=p) - # print(prof) print(prof.max_regret()) print(prof.agent_max_regret()) + print("==================") + for p in prof_data_doub: + prof = g.mixed_behavior_profile(rational=False, data=p) + print(prof.max_regret()) + print(prof.agent_max_regret()) + + +def test_regrets_tmp2(): + + g = games.create_3_player_with_internal_outcomes_efg() + + prof_data_rat = [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]] + profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) + print() + print(profile_rat.max_regret()) # 3/2 + # print(profile1.max_regret()) # same + profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) + print(profile_rat.max_regret()) # now different! 0 + print("=======================================") + + prof_data_doub = [[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]] + profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) + print() + print(profile_doub.max_regret()) + # print(profile1.max_regret()) # same + profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) + print(profile_doub.max_regret()) + print("=======================================") + + prof_data_rat = [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]] + profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) + print() + print(profile_rat.max_regret()) + # print(profile1.max_regret()) # same + profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) + print(profile_rat.max_regret()) + print("=======================================") + + +def test_regrets_tmp3(): + + g = games.create_3_player_with_internal_outcomes_efg() + + prof_data_doub = [[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]] + profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) + print() + print(profile_doub.max_regret()) # 1.5 + profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) + print(profile_doub.max_regret()) # now different! 0 + print("=======================================") + + prof_data_rat = [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]] + profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) + print() + print(profile_rat.max_regret()) + profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) + print(profile_rat.max_regret()) + print("=======================================") + + prof_data_doub = [[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]] + profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) + print() + print(profile_doub.max_regret()) + profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) + print(profile_doub.max_regret()) + print("=======================================") From edcd647eded65c5903c36e2dd3e15c7105bcbdfa Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Sun, 21 Dec 2025 15:03:13 +0000 Subject: [PATCH 28/32] clearer example of (one) problem --- tests/test_nash.py | 51 ---------------------------------------------- 1 file changed, 51 deletions(-) diff --git a/tests/test_nash.py b/tests/test_nash.py index 3b7ae25f0..58625e8b4 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -927,61 +927,10 @@ def test_regrets_tmp(): def test_regrets_tmp2(): - g = games.create_3_player_with_internal_outcomes_efg() - prof_data_rat = [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]] profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) print() print(profile_rat.max_regret()) # 3/2 - # print(profile1.max_regret()) # same profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) print(profile_rat.max_regret()) # now different! 0 - print("=======================================") - - prof_data_doub = [[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]] - profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) - print() - print(profile_doub.max_regret()) - # print(profile1.max_regret()) # same - profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) - print(profile_doub.max_regret()) - print("=======================================") - - prof_data_rat = [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]] - profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) - print() - print(profile_rat.max_regret()) - # print(profile1.max_regret()) # same - profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) - print(profile_rat.max_regret()) - print("=======================================") - - -def test_regrets_tmp3(): - - g = games.create_3_player_with_internal_outcomes_efg() - - prof_data_doub = [[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]] - profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) - print() - print(profile_doub.max_regret()) # 1.5 - profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) - print(profile_doub.max_regret()) # now different! 0 - print("=======================================") - - prof_data_rat = [[[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], [[1, 0], [0, 1]]] - profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) - print() - print(profile_rat.max_regret()) - profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) - print(profile_rat.max_regret()) - print("=======================================") - - prof_data_doub = [[[1, 0], [1, 0]], [[1, 0], [0.5, 0.5]], [[1, 0], [0, 1]]] - profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) - print() - print(profile_doub.max_regret()) - profile_doub = g.mixed_behavior_profile(rational=False, data=prof_data_doub) - print(profile_doub.max_regret()) - print("=======================================") From a5de7d48c77b01c143f40cd60615eed8de82cdd0 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Mon, 22 Dec 2025 08:49:59 +0000 Subject: [PATCH 29/32] test demonstrating issue is to do with infoset order --- tests/games.py | 107 +++++++++++++++++++++++++++++++++++++++++++-- tests/test_nash.py | 45 +++++++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/tests/games.py b/tests/games.py index 8e1f925d3..1376f2cdc 100644 --- a/tests/games.py +++ b/tests/games.py @@ -352,7 +352,7 @@ def create_3_player_with_internal_outcomes_efg(nonterm_outcomes: bool = False) - o = g.add_outcome([0, 0, 0]) g.set_outcome(g.root.children[0].children[0].children[1].children[0], o) g.set_outcome(g.root.children[0].children[0].children[1].children[1], o) - g.to_efg(f"TEST_{nonterm_outcomes}.efg") + # g.sort_infosets() return g @@ -652,7 +652,7 @@ def create_kuhn_poker_efg(nonterm_outcomes: bool = False) -> gbt.Game: g = _create_kuhn_poker_efg_only_term_outcomes() # Ensure infosets are in the same order as if game was written to efg and read back in - g.sort_infosets() + # g.sort_infosets() return g @@ -850,6 +850,107 @@ def create_reduction_both_players_payoff_ties_efg() -> gbt.Game: return g +def create_problem_example_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2"], title="") + g.append_move(g.root, player="1", actions=["L", "R"]) + # do the second child first on purpose to diverge from sort infosets order + g.append_move(g.root.children[1], "2", actions=["l2", "r2"]) + g.append_move(g.root.children[0], "2", actions=["l1", "r1"]) + g.set_outcome(g.root.children[0].children[0], outcome=g.add_outcome(payoffs=[5, -5])) + g.set_outcome(g.root.children[0].children[1], outcome=g.add_outcome(payoffs=[2, -2])) + g.set_outcome(g.root.children[1].children[0], outcome=g.add_outcome(payoffs=[-5, 5])) + g.set_outcome(g.root.children[1].children[1], outcome=g.add_outcome(payoffs=[-2, 2])) + return g + + +def create_STOC_simplified() -> gbt.Game: + """ + """ + g = gbt.Game.new_tree(players=["1", "2"], title="") + g.append_move(g.root, g.players.chance, actions=["1", "2"]) + g.set_chance_probs(g.root.infoset, [0.2, 0.8]) + g.append_move(g.root.children[0], player="1", actions=["l", "r"]) + g.append_move(g.root.children[1], player="1", actions=["c", "d"]) + g.append_move(g.root.children[0].children[1], player="2", actions=["p", "q"]) + g.append_move( + g.root.children[0].children[1].children[0], player="1", actions=["L", "R"] + ) + g.append_infoset( + g.root.children[0].children[1].children[1], + g.root.children[0].children[1].children[0].infoset, + ) + g.set_outcome( + g.root.children[0].children[0], + outcome=g.add_outcome(payoffs=[5, -5], label="l"), + ) + g.set_outcome( + g.root.children[0].children[1].children[0].children[0], + outcome=g.add_outcome(payoffs=[10, -10], label="rpL"), + ) + g.set_outcome( + g.root.children[0].children[1].children[0].children[1], + outcome=g.add_outcome(payoffs=[15, -15], label="rpR"), + ) + g.set_outcome( + g.root.children[0].children[1].children[1].children[0], + outcome=g.add_outcome(payoffs=[20, -20], label="rqL"), + ) + g.set_outcome( + g.root.children[0].children[1].children[1].children[1], + outcome=g.add_outcome(payoffs=[-5, 5], label="rqR"), + ) + g.set_outcome( + g.root.children[1].children[0], + outcome=g.add_outcome(payoffs=[10, -10], label="c"), + ) + g.set_outcome( + g.root.children[1].children[1], + outcome=g.add_outcome(payoffs=[20, -20], label="d"), + ) + # g.sort_infosets() + return g + + +def create_STOC_simplified2() -> gbt.Game: + """ + """ + g = gbt.Game.new_tree(players=["1", "2"], title="") + g.append_move(g.root, g.players.chance, actions=["1", "2"]) + g.set_chance_probs(g.root.infoset, [0.2, 0.8]) + g.append_move(g.root.children[0], player="1", actions=["r"]) + g.append_move(g.root.children[1], player="1", actions=["c"]) + g.append_move(g.root.children[0].children[0], player="2", actions=["p", "q"]) + g.append_move( + g.root.children[0].children[0].children[0], player="1", actions=["L", "R"] + ) + g.append_infoset( + g.root.children[0].children[0].children[1], + g.root.children[0].children[0].children[0].infoset, + ) + g.set_outcome( + g.root.children[0].children[0].children[0].children[0], + outcome=g.add_outcome(payoffs=[10, -10], label="rpL"), + ) + g.set_outcome( + g.root.children[0].children[0].children[0].children[1], + outcome=g.add_outcome(payoffs=[15, -15], label="rpR"), + ) + g.set_outcome( + g.root.children[0].children[0].children[1].children[0], + outcome=g.add_outcome(payoffs=[20, -20], label="rqL"), + ) + g.set_outcome( + g.root.children[0].children[0].children[1].children[1], + outcome=g.add_outcome(payoffs=[-5, 5], label="rqR"), + ) + g.set_outcome( + g.root.children[1].children[0], + outcome=g.add_outcome(payoffs=[10, -10], label="c"), + ) + # g.sort_infosets() + return g + + def create_seq_form_STOC_paper_zero_sum_2_player_efg() -> gbt.Game: """ Example from @@ -928,7 +1029,7 @@ def create_seq_form_STOC_paper_zero_sum_2_player_efg() -> gbt.Game: g.root.children[0].children[1].infoset.label = "01" g.root.children[2].children[0].infoset.label = "20" g.root.children[0].children[1].children[0].infoset.label = "010" - + # g.sort_infosets() return g diff --git a/tests/test_nash.py b/tests/test_nash.py index 58625e8b4..e9dd58afa 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -934,3 +934,48 @@ def test_regrets_tmp2(): print(profile_rat.max_regret()) # 3/2 profile_rat = g.mixed_behavior_profile(rational=True, data=prof_data_rat) print(profile_rat.max_regret()) # now different! 0 + + +@pytest.mark.nash +@pytest.mark.nash_lp_behavior +@pytest.mark.parametrize( + "game,mixed_behav_prof_data", + [ + ( + games.create_seq_form_STOC_paper_zero_sum_2_player_efg(), + [ + [[0, 1], ["1/3", "2/3"], ["2/3", "1/3"]], + [["5/6", "1/6"], ["5/9", "4/9"]], + ], + ), + ( + games.create_3_player_with_internal_outcomes_efg(), + [ + [[1, 0], [1, 0]], [[1, 0], ["1/2", "1/2"]], + [[1, 0], [0, 1]] + ], + ), + ( + games.create_STOC_simplified(), + [ + [[0, 1], ["1/3", "2/3"], ["2/3", "1/3"]], + [["5/6", "1/6"]], + ], + ), + # ( + # games.create_STOC_simplified2(), + # [ + # [[1], [1], ["1/3", "2/3"]], + # [["5/6", "1/6"]], + # ], + # ), + ], +) +def test_repeat_max_regret(game: gbt.Game, mixed_behav_prof_data: list): + profile1 = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) + mr1 = profile1.max_regret() + profile2 = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) + mr2 = profile2.max_regret() + print() + print(mr1, mr2) + assert mr1 == mr2 From 7d5628ec2539c2483932469e39cf682bad59e4b1 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Mon, 22 Dec 2025 08:58:15 +0000 Subject: [PATCH 30/32] test demonstrating issue is to do with infoset order --- tests/test_nash.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_nash.py b/tests/test_nash.py index e9dd58afa..ae2eedd7e 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -936,8 +936,6 @@ def test_regrets_tmp2(): print(profile_rat.max_regret()) # now different! 0 -@pytest.mark.nash -@pytest.mark.nash_lp_behavior @pytest.mark.parametrize( "game,mixed_behav_prof_data", [ From c7b36280f930e04f4acf804a835f4458d668430e Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Tue, 23 Dec 2025 12:03:18 +0000 Subject: [PATCH 31/32] Previous test failures were due to not calling `.sort_infosets()` prior to analysis. We are going to remove `.sort_infosets()` separately; for the moment this adjusts the tests to add an explicit call to clear the errors for clarity. --- tests/games.py | 2 +- tests/test_nash.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/games.py b/tests/games.py index 1376f2cdc..30a683205 100644 --- a/tests/games.py +++ b/tests/games.py @@ -1029,7 +1029,7 @@ def create_seq_form_STOC_paper_zero_sum_2_player_efg() -> gbt.Game: g.root.children[0].children[1].infoset.label = "01" g.root.children[2].children[0].infoset.label = "20" g.root.children[0].children[1].children[0].infoset.label = "010" - # g.sort_infosets() + g.sort_infosets() return g diff --git a/tests/test_nash.py b/tests/test_nash.py index ae2eedd7e..026199302 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -607,6 +607,7 @@ def test_lcp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): using max_regret and agent_max_regret (internal consistency); and comparison to a previously computed equilibrium using this function (regression test). """ + game.sort_infosets() result = gbt.nash.lcp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 eq = result.equilibria[0] @@ -731,7 +732,7 @@ def test_lp_behavior_double(): ( games.create_seq_form_STOC_paper_zero_sum_2_player_efg(), [ - [[0, 1], ["1/3", "2/3"], ["2/3", "1/3"]], + [[0, 1], ["2/3", "1/3"], ["1/3", "2/3"]], [["5/6", "1/6"], ["5/9", "4/9"]], ], ), @@ -784,12 +785,15 @@ def test_lp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): using max_regret and agent_max_regret (internal consistency); and comparison to a previously computed equilibrium using this function (regression test). """ + game.sort_infosets() result = gbt.nash.lp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 eq = result.equilibria[0] assert eq.max_regret() == 0 assert eq.agent_max_regret() == 0 expected = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) + print(expected) + print(eq) assert eq == expected @@ -970,6 +974,7 @@ def test_regrets_tmp2(): ], ) def test_repeat_max_regret(game: gbt.Game, mixed_behav_prof_data: list): + game.sort_infosets() profile1 = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) mr1 = profile1.max_regret() profile2 = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) From 5f782ce814c1826f6ccdfb6545dae7ccdd758691 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 2 Jan 2026 09:31:47 +0000 Subject: [PATCH 32/32] Tidy up tests now that information set sorting is obsolete. --- tests/games.py | 9 --------- tests/test_nash.py | 7 ------- 2 files changed, 16 deletions(-) diff --git a/tests/games.py b/tests/games.py index e993bce34..5fccf5e38 100644 --- a/tests/games.py +++ b/tests/games.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod import numpy as np -import pytest import pygambit as gbt @@ -353,7 +352,6 @@ def create_3_player_with_internal_outcomes_efg(nonterm_outcomes: bool = False) - o = g.add_outcome([0, 0, 0]) g.set_outcome(g.root.children[0].children[0].children[1].children[0], o) g.set_outcome(g.root.children[0].children[0].children[1].children[1], o) - # g.sort_infosets() return g @@ -651,10 +649,6 @@ def create_kuhn_poker_efg(nonterm_outcomes: bool = False) -> gbt.Game: g = _create_kuhn_poker_efg_nonterm_outcomes() else: g = _create_kuhn_poker_efg_only_term_outcomes() - - # Ensure infosets are in the same order as if game was written to efg and read back in - with pytest.warns(FutureWarning): - g.sort_infosets() return g @@ -909,7 +903,6 @@ def create_STOC_simplified() -> gbt.Game: g.root.children[1].children[1], outcome=g.add_outcome(payoffs=[20, -20], label="d"), ) - # g.sort_infosets() return g @@ -949,7 +942,6 @@ def create_STOC_simplified2() -> gbt.Game: g.root.children[1].children[0], outcome=g.add_outcome(payoffs=[10, -10], label="c"), ) - # g.sort_infosets() return g @@ -1031,7 +1023,6 @@ def create_seq_form_STOC_paper_zero_sum_2_player_efg() -> gbt.Game: g.root.children[0].children[1].infoset.label = "01" g.root.children[2].children[0].infoset.label = "20" g.root.children[0].children[1].children[0].infoset.label = "010" - g.sort_infosets() return g diff --git a/tests/test_nash.py b/tests/test_nash.py index 242830132..4683aa7bd 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -608,7 +608,6 @@ def test_lcp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): using max_regret and agent_max_regret (internal consistency); and comparison to a previously computed equilibrium using this function (regression test). """ - game.sort_infosets() result = gbt.nash.lcp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 eq = result.equilibria[0] @@ -786,15 +785,12 @@ def test_lp_behavior_rational(game: gbt.Game, mixed_behav_prof_data: list): using max_regret and agent_max_regret (internal consistency); and comparison to a previously computed equilibrium using this function (regression test). """ - game.sort_infosets() result = gbt.nash.lp_solve(game, use_strategic=False, rational=True) assert len(result.equilibria) == 1 eq = result.equilibria[0] assert eq.max_regret() == 0 assert eq.agent_max_regret() == 0 expected = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) - print(expected) - print(eq) assert eq == expected @@ -975,11 +971,8 @@ def test_regrets_tmp2(): ], ) def test_repeat_max_regret(game: gbt.Game, mixed_behav_prof_data: list): - game.sort_infosets() profile1 = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) mr1 = profile1.max_regret() profile2 = game.mixed_behavior_profile(rational=True, data=mixed_behav_prof_data) mr2 = profile2.max_regret() - print() - print(mr1, mr2) assert mr1 == mr2