Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/pygambit/nash.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -791,9 +791,7 @@ class LogitQREMixedBehaviorProfile:
@property
def game(self) -> Game:
"""The game on which this mixed strategy profile is defined."""
g = Game()
g.game = deref(self.thisptr).GetGame()
return g
return Game.wrap(deref(self.thisptr).GetGame())

@property
def lam(self) -> double:
Expand All @@ -808,11 +806,9 @@ class LogitQREMixedBehaviorProfile:
@property
def profile(self) -> MixedBehaviorProfileDouble:
"""The mixed strategy profile."""
profile = MixedBehaviorProfileDouble()
profile.profile = (
return MixedBehaviorProfileDouble.wrap(
make_shared[c_MixedBehaviorProfile[double]](deref(self.thisptr).GetProfile())
)
return profile


def _logit_behavior_estimate(profile: MixedBehaviorProfileDouble,
Expand Down
4 changes: 3 additions & 1 deletion src/pygambit/qre.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,9 @@ def _estimate_behavior_empirical(
for player in data.game.players:
for infoset in player.infosets:
node = next(iter(infoset.members))
profile[node] = [math.exp(next(log_probs)) for _ in infoset.actions]
profile[node] = {
a.label: math.exp(next(log_probs)) for a in infoset.actions
}
return LogitQREMixedBehaviorFitResult(
data, "empirical", res.x[0], profile, -res.fun
)
Expand Down
2 changes: 1 addition & 1 deletion src/solvers/logit/efglogit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class EquationSystem {
};

Array<std::shared_ptr<Equation>> m_equations;
const Game &m_game;
Game m_game;
};

EquationSystem::EquationSystem(const Game &p_game) : m_game(p_game)
Expand Down
2 changes: 1 addition & 1 deletion src/solvers/logit/nfglogit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class EquationSystem {

private:
std::vector<std::shared_ptr<Equation>> m_equations;
const Game &m_game;
Game m_game;
mutable MixedStrategyProfile<double> m_profile, m_logProfile;
mutable Vector<double> m_strategyValues;
mutable Matrix<double> m_strategyDerivs;
Expand Down
40 changes: 40 additions & 0 deletions tests/test_qre.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@

import pygambit as gbt

from . import games


def _asymmetric_2x2() -> gbt.Game:
return gbt.Game.from_arrays([[1, 2], [3, 4]], [[4, 3], [2, 1]])


def _asymmetric_poker_behavior_data() -> gbt.MixedBehaviorProfile:
game = games.create_stripped_down_poker_efg()
data = game.mixed_behavior_profile(rational=False)
for player in game.players:
for infoset in player.infosets:
node = next(iter(infoset.members))
data[node] = {a.label: float(i + 2) for i, a in enumerate(infoset.actions)}
return data


@pytest.mark.parametrize("use_empirical", [False, True])
def test_logit_estimate_strategy_accepts_rational_profile(use_empirical: bool):
"""logit_estimate() accepts a MixedStrategyProfileRational, converting it to
Expand Down Expand Up @@ -38,3 +50,31 @@ def test_logit_estimate_strategy_rational_and_float_data_agree():
rational_result.profile[player.label][strategy.label]
== pytest.approx(float_result.profile[player.label][strategy.label])
)


@pytest.mark.parametrize("use_empirical", [False, True])
@pytest.mark.parametrize("local_max", [False, True])
def test_logit_estimate_behavior_completes(use_empirical: bool, local_max: bool):
"""logit_estimate() on a MixedBehaviorProfile returns a well-formed result instead of
crashing or raising.

Regression coverage for three bugs found while implementing rational-profile support
for `liap_solve`/`logit_estimate` (gambitproject/gambit#721, #458):
- `_estimate_behavior_fixedpoint()` segfaulted, because `EquationSystem` (in both
efglogit.cc and nfglogit.cc) stored its `Game` by dangling reference rather than
by value; `LogitBehaviorEstimate()` bound that reference straight to a temporary.
- `LogitQREMixedBehaviorProfile.profile`/`.game` (nash.pxi) constructed their return
value by calling the blocked `MixedBehaviorProfileDouble()`/`Game()` constructors
directly instead of via their `.wrap()` factories, raising `ValueError`.
- `_estimate_behavior_empirical()` assigned a `list` into a `MixedBehaviorProfile`,
which requires a `Mapping` from action label to weight, raising `TypeError`.
"""
data = _asymmetric_poker_behavior_data()
result = gbt.qre.logit_estimate(data, use_empirical=use_empirical, local_max=local_max)
assert isinstance(result.profile, gbt.MixedBehaviorProfileDouble)
for player in data.game.players:
for infoset in player.infosets:
node = next(iter(infoset.members))
probs = dict(result.profile[node])
assert probs.keys() == {a.label for a in infoset.actions}
assert sum(probs.values()) == pytest.approx(1.0)
Loading