Skip to content
Open
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
26 changes: 13 additions & 13 deletions src/games/behavspt.cc
Original file line number Diff line number Diff line change
Expand Up @@ -223,39 +223,39 @@ size_t BehaviorSupportProfile::Sequences::size() const

BehaviorSupportProfile::Sequences::iterator BehaviorSupportProfile::Sequences::begin() const
{
return {m_support->GetSequenceMap(), false};
return {m_support->GetSequenceMap(), m_support->GetPlayers(), false};
}
BehaviorSupportProfile::Sequences::iterator BehaviorSupportProfile::Sequences::end() const
{
return {m_support->GetSequenceMap(), true};
return {m_support->GetSequenceMap(), m_support->GetPlayers(), true};
}

BehaviorSupportProfile::Sequences::iterator::iterator(
const std::shared_ptr<SequenceMap> p_sequences, bool p_end)
: m_sequences(p_sequences)
const std::shared_ptr<SequenceMap> p_sequences, const GameRep::Players &p_players, bool p_end)
: m_sequences(p_sequences), m_players(p_players)
{
if (p_end) {
m_currentPlayer = m_sequences->cend();
m_currentPlayer = m_players.end();
}
else {
m_currentPlayer = m_sequences->cbegin();
m_currentSequence = m_currentPlayer->second.cbegin();
m_currentPlayer = m_players.begin();
m_currentSequence = m_sequences->at(*m_currentPlayer).cbegin();
}
}

BehaviorSupportProfile::Sequences::iterator &
BehaviorSupportProfile::Sequences::iterator::operator++()
{
if (m_currentPlayer == m_sequences->cend()) {
if (m_currentPlayer == m_players.end()) {
return *this;
}
m_currentSequence++;
if (m_currentSequence != m_currentPlayer->second.cend()) {
if (m_currentSequence != m_sequences->at(*m_currentPlayer).cend()) {
return *this;
}
m_currentPlayer++;
if (m_currentPlayer != m_sequences->cend()) {
m_currentSequence = m_currentPlayer->second.cbegin();
++m_currentPlayer;
if (m_currentPlayer != m_players.end()) {
m_currentSequence = m_sequences->at(*m_currentPlayer).cbegin();
}
return *this;
}
Expand All @@ -265,7 +265,7 @@ bool BehaviorSupportProfile::Sequences::iterator::operator==(const iterator &it)
if (m_sequences != it.m_sequences || m_currentPlayer != it.m_currentPlayer) {
return false;
}
if (m_currentPlayer == m_sequences->end()) {
if (m_currentPlayer == m_players.end()) {
return true;
}
return (m_currentSequence == it.m_currentSequence);
Expand Down
12 changes: 10 additions & 2 deletions src/games/behavspt.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,20 @@ class BehaviorSupportProfile {

public:
class iterator {
// Sequences are visited grouped by player, in the game's canonical player order
// (GameRep::Players), rather than in SequenceMap's own key order. SequenceMap is
// keyed by GamePlayer, whose ordering is based on object addresses; iterating the
// map directly would therefore make the visitation order (and hence, downstream,
// any polynomial variable numbering derived from it) depend on heap layout, which
// can differ from run to run.
const std::shared_ptr<SequenceMap> m_sequences;
SequenceMap::const_iterator m_currentPlayer;
GameRep::Players m_players;
GameRep::Players::iterator m_currentPlayer;
std::vector<GameSequence>::const_iterator m_currentSequence;

public:
iterator(const std::shared_ptr<SequenceMap> p_sequences, bool p_end);
iterator(const std::shared_ptr<SequenceMap> p_sequences, const GameRep::Players &p_players,
bool p_end);

GameSequence operator*() const { return *m_currentSequence; }
GameSequence operator->() const { return *m_currentSequence; }
Expand Down
11 changes: 11 additions & 0 deletions src/solvers/enumpoly/polysolver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ bool PolynomialSystemSolver::NewtonRootInRectangle(const Rectangle<double> &r,
newpoint = NewtonStep(point);
}
catch (SingularMatrixException &) {
bool perturbed = false;
for (int i = 1; i <= point.size(); i++) {
Vector<double> perturbed_point(point);
if (r.Side(i).UpperBound() > point[i]) {
Expand All @@ -82,11 +83,21 @@ bool PolynomialSystemSolver::NewtonRootInRectangle(const Rectangle<double> &r,
}
try {
newpoint = point + (NewtonStep(perturbed_point) - perturbed_point);
perturbed = true;
break;
}
catch (SingularMatrixException &) {
}
}
if (!perturbed) {
// The Jacobian is singular at this point, and remains singular under every
// single-coordinate perturbation tried. This is a local degeneracy of the
// search at this point/rectangle, not evidence that the system of equations
// for the support as a whole is degenerate -- so we treat it the same as any
// other failure to confirm a root here, leaving it to the caller to subdivide
// the rectangle further.
return false;
}
}

if (!r.SameCenterDoubleLengths().Contains(newpoint)) {
Expand Down
104 changes: 24 additions & 80 deletions tests/test_nash.py
Original file line number Diff line number Diff line change
Expand Up @@ -2443,18 +2443,31 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s
marks=pytest.mark.nash_enumpoly_behavior,
id="test_enumpoly_behavior_13",
),
# 3-player game equivalent to a simultaneous-move 2x2x2 game (nine equilibria total,
# including two totally-mixed ones); this previously required an unordered check due
# to run-to-run variation in the order equilibria were found (see #589)
pytest.param(
EquilibriumTestCase(
factory=functools.partial(games.read_from_file, "mixed_behavior_game.efg"),
solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None),
expected=[
[[d("1/2", "1/2")], [d("2/5", "3/5")], [d("1/4", "3/4")]],
[[d("2/5", "3/5")], [d("1/2", "1/2")], [d("1/3", "2/3")]],
[[d("1/2", "1/2")], [d("1/2", "1/2")], [d(1, 0)]],
[[d("1/3", "2/3")], [d(1, 0)], [d("1/4", "3/4")]],
[[d(1, 0)], [d(1, 0)], [d(1, 0)]],
[[d(1, 0)], [d(0, 1)], [d(0, 1)]],
[[d(0, 1)], [d("1/4", "3/4")], [d("1/3", "2/3")]],
[[d(0, 1)], [d(1, 0)], [d(0, 1)]],
[[d(0, 1)], [d(0, 1)], [d(1, 0)]],
],
regret_tol=TOL,
prob_tol=TOL,
),
marks=pytest.mark.nash_enumpoly_behavior,
id="test_enumpoly_behavior_14",
),
]
# ##############################################################################
# 3-player game
# (
# games.read_from_file("mixed_behavior_game.efg"),
# [
# [[["1/2", "1/2"]], [["2/5", "3/5"]], [["1/4", "3/4"]]],
# [[["2/5", "3/5"]], [["1/2", "1/2"]], [["1/3", "2/3"]]],
# ],
# 2, # 9 in total found by enumpoly (see unordered test)
# ),
# ##############################################################################


LOGIT_BEHAVIOR_CASES = [
Expand Down Expand Up @@ -2525,75 +2538,6 @@ def test_nash_behavior_solver(test_case: EquilibriumTestCase, subtests) -> None:
assert abs(eq[action] - expected[action]) <= test_case.prob_tol


##################################################################################################
# BEHVAIOR SOLVER -- UNORDERED
##################################################################################################

ENUMPOLY_BEHAVIOR_UNORDERED_CASES = [
pytest.param(
EquilibriumTestCase(
factory=functools.partial(games.read_from_file, "mixed_behavior_game.efg"),
solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=9),
expected=[
[[["2/5", "3/5"]], [["1/2", "1/2"]], [["1/3", "2/3"]]],
[[["1/2", "1/2"]], [["2/5", "3/5"]], [["1/4", "3/4"]]],
[[["1/2", "1/2"]], [["1/2", "1/2"]], [[1, 0]]],
[[["1/3", "2/3"]], [[1, 0]], [["1/4", "3/4"]]],
[[[1, 0]], [[1, 0]], [[1, 0]]],
[[[1, 0]], [[0, 1]], [[0, 1]]],
[[[0, 1]], [["1/4", "3/4"]], [["1/3", "2/3"]]],
[[[0, 1]], [[1, 0]], [[0, 1]]],
[[[0, 1]], [[0, 1]], [[1, 0]]],
],
regret_tol=TOL,
prob_tol=TOL,
),
marks=pytest.mark.nash_enumpoly_behavior,
id="test_enumpoly_behavior_unordered_1",
),
]


@pytest.mark.nash
@pytest.mark.parametrize("test_case", ENUMPOLY_BEHAVIOR_UNORDERED_CASES, ids=lambda c: c.label)
def test_nash_behavior_solver_unordered(test_case: EquilibriumTestCase, subtests) -> None:
"""Test calls of Nash solvers in EFGs in mixed behaviors -- UNORDERED

Subtests:
- Agent max regret no more than `test_case.regret_tol`
- Agent max regret no more than max regret (+ `test_case.regret_tol`)
- Equilibria that are output are distinct and all appear in the expected set
Equilibria are deemed to match if the maximum difference in probabilities is no more
than `test_case.prob_tol`
"""

def are_the_same(game, found, candidate):
for p in game.players:
for a in p.actions:
if not abs(found[a] - candidate[a]) <= TOL:
return False
return True

game = test_case.factory()
result = test_case.solver(game)
with subtests.test("number of equilibria found"):
assert len(result.equilibria) == len(test_case.expected)
for i, eq in enumerate(result.equilibria):
with subtests.test(eq=i, check="agent_max_regret"):
assert eq.max_regret() <= test_case.regret_tol
with subtests.test(eq=i, check="max_regret"):
assert eq.agent_max_regret() <= eq.max_regret() + test_case.regret_tol
with subtests.test(eq=i, check="strategy_profile"):
found = False
for exp in test_case.expected[:]:
expected = game.mixed_behavior_profile(rational=True, data=exp)
if are_the_same(game, eq, expected):
test_case.expected.remove(exp)
found = True
break
assert found


##################################################################################################
# AGENTS NASH SOLVERS (IN MIXED BEHAVIORS)
##################################################################################################
Expand Down
Loading