Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialise the MixedBehaviorProfile on creation. Add test to the new … #435

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
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
various dict-like ways
- `gnm_solve`/`gambit-gnm` now exposes several parameters which control the behavior of the
path-following procedure
- The MixedBehaviorProfile object can now be initialized on creation by a given distribution.

### Changed
- Gambit now requires a compiler that supports C++17.
Expand Down
74 changes: 59 additions & 15 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -747,15 +747,20 @@ class Game:
mspr[s] = Rational(v)
return mspr

def mixed_behavior_profile(self, rational=False) -> MixedBehaviorProfile:
"""Create a behavior strategy profile over the game.
def mixed_behavior_profile(self, data=None, rational=False) -> MixedBehaviorProfile:
"""Create a mixed behavior profile over the game.

The profile is initialized to uniform randomization for each player
over their actions at each information set.
If `data` is not specified, the profile is initialized to uniform randomization
at each information set.

Parameters
----------
rational
data : array_like of array_like of array_like, optional
A nested list (or compatible type) with the
same dimension as the action set of the game,
specifying the probabilities of the actions.

rational : bool, optional
If True, probabilities are represented using rational numbers; otherwise
double-precision floating point numbers are used.

Expand All @@ -764,19 +769,58 @@ class Game:
UndefinedOperationError
If the game does not have a tree representation.
"""
if self.is_tree:
if not rational:
mbpd = MixedBehaviorProfileDouble()
mbpd.profile = make_shared[c_MixedBehaviorProfileDouble](self.game)
return mbpd
else:
mbpr = MixedBehaviorProfileRational()
mbpr.profile = make_shared[c_MixedBehaviorProfileRational](self.game)
return mbpr
else:
if not self.is_tree:
raise UndefinedOperationError(
"Game must have a tree representation to create a mixed behavior profile"
)
if not rational:
mbpd = MixedBehaviorProfileDouble()
mbpd.profile = make_shared[c_MixedBehaviorProfileDouble](self.game)
if data is None:
return mbpd
if len(data) != len(self.players):
raise ValueError(
"Number of elements does not match number of players"
)
for (p, d) in zip(self.players, data):
if len(p.infosets) != len(d):
raise ValueError(
f"Number of elements does not match number of "
f"infosets for {p}"
)
for (i, v) in zip(p.infosets, d):
if len(i.actions) != len(v):
raise ValueError(
f"Number of elements does not match number of "
f"actions for the infoset {i} for {p}"
)
for (a, u) in zip(i.actions, v):
mbpd[a] = float(u)
return mbpd
else:
mbpr = MixedBehaviorProfileRational()
mbpr.profile = make_shared[c_MixedBehaviorProfileRational](self.game)
if data is None:
return mbpr
if len(data) != len(self.players):
raise ValueError(
"Number of elements does not match number of players"
)
for (p, d) in zip(self.players, data):
if len(p.infosets) != len(d):
raise ValueError(
f"Number of elements does not match number of "
f"infosets for {p}"
)
for (i, v) in zip(p.infosets, d):
if len(i.actions) != len(v):
raise ValueError(
f"Number of elements does not match number of "
f"actions for the infoset {i} for {p}"
)
for (a, u) in zip(i.actions, v):
mbpr[a] = Rational(u)
return mbpr

def support_profile(self):
return StrategySupportProfile(self)
Expand Down
72 changes: 72 additions & 0 deletions tests/test_behav.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,3 +1169,75 @@ def test_profile_order_consistency(game: gbt.Game,
objects_to_test: typing.Callable):
_get_and_check_answers(game, action_probs1, action_probs2, rational_flag, func_to_test,
objects_to_test(game))


@pytest.mark.parametrize(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only tests with data that has the correct "shape". There are no tests to ensure that bad input data is handled appropriate (with an error).

"game,rational_flag,data",
[(games.create_mixed_behav_game(), True, [[[0, 1]], [[0, 1]], [[1, 0]]]),
(games.create_mixed_behav_game(), True, [[["1/5", "4/5"]], [["1/4", "3/4"]], [[1, 0]]]),
(games.create_myerson_2_card_poker(), True, [[[1/5, 4/5], [3/5, 2/5]], [[1/4, 3/4]]]),
(games.create_mixed_behav_game(), False, [[[0, 1]], [[1, 0]], [[1, 0]]]),
(games.create_mixed_behav_game(), False, [[[1/5, 4/5]], [[1/4, 3/4]], [[1, 0]]]),
(games.create_myerson_2_card_poker(), False, [[[1/5, 4/5], [3/5, 2/5]], [[1/4, 3/4]]])
]
)
def test_specific_profile(game: gbt.Game, rational_flag: bool, data: list):
"""Test that the mixed behavior profile is initialized from a specific distribution
for each player over his actions.
"""
profile = game.mixed_behavior_profile(rational=rational_flag, data=data)
for (action, prob) in zip(game.actions, [k for i in data for j in i for k in j]):
assert profile[action] == (gbt.Rational(prob) if rational_flag else prob)


@pytest.mark.parametrize(
"game,rational_flag,data",
[(games.create_mixed_behav_game(), True,
[[[0, 1, 0]], [[1, 0]], [["1/2", "1/2"]]]),
(games.create_mixed_behav_game(), True,
[[[0, 1]], [[1, 0]], [[1, 0]], [[0, 1]]]),
(games.create_myerson_2_card_poker(), True,
[[["1/5", "4/5"], ["3/5", "2/5"]], [["1/4", "3/4"], ["1/4", "3/4"]]]),
(games.create_el_farol_bar_game(), True,
[[4/9, 5/9], [0], [1/2, 1/2], [11/12, 1/12], [1/2, 1/2]]),
(games.create_el_farol_bar_game(), True,
[[1/2, 1/2]]),
(games.create_mixed_behav_game(), False,
[[[0, 1, 0]], [[1, 0]], [[1, 0]]]),
(games.create_mixed_behav_game(), False,
[[[0, 1]], [[1, 0]], [[1, 0]], [[0, 1]]]),
(games.create_myerson_2_card_poker(), False,
[[[1/5, 4/5], [3/5, 2/5]], [[1/4, 3/4], [1/4, 3/4]]]),
(games.create_el_farol_bar_game(), False,
[[4/9, 5/9], [0], [1/2, 1/2], [11/12, 1/12], [1/2, 1/2]]),
(games.create_el_farol_bar_game(), False,
[[1/2, 1/2]])
]
)
def test_profile_data_error(game: gbt.Game, rational_flag: bool, data: list):
"""Test to ensure a pygambit.ValueError is raised when the data do not
match with the number of players, the number of the infosets, and the
number of actions per infoset.
"""
with pytest.raises(ValueError):
game.mixed_behavior_profile(rational=rational_flag, data=data)


@pytest.mark.parametrize(
"game,rational_flag,data",
[(games.create_coord_4x4_nfg(), True,
[["1/5", "2/5", 0, "2/5"], ["1/4", "3/8", "1/4", "3/8"]]),
(games.create_strategic_game(), True,
[["4/9", "5/9"], ["1/3", "2/3"]]),
(games.create_coord_4x4_nfg(), False,
[[1/5, 2/5, 0/5, 2/5], [1/4, 3/8, 1/4, 3/8]]),
(games.create_strategic_game(), False,
[[4/9, 5/9], [1/3, 2/3]])
]
)
def test_tree_representation_error(game: gbt.Game, rational_flag: bool, data: list):
"""Test to ensure a pygambit.UndefinedOperationError is raised when the game
to create a mixed behavior profile does not have a tree representation.
"""
with pytest.raises(gbt.UndefinedOperationError):
game.mixed_behavior_profile(rational=rational_flag, data=data)
Loading