Skip to content

Commit

Permalink
Add initialisation of mixed behavior profiles by specified distributi…
Browse files Browse the repository at this point in the history
…ons.
  • Loading branch information
Konstantinos Varsos authored and tturocy committed Feb 8, 2024
1 parent 40966e7 commit d2e38f7
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 15 deletions.
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
68 changes: 68 additions & 0 deletions tests/test_behav.py
Original file line number Diff line number Diff line change
Expand Up @@ -1169,3 +1169,71 @@ 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(
"game,rational_flag,data",
[(games.create_mixed_behav_game_efg(), True, [[[0, 1]], [[0, 1]], [[1, 0]]]),
(games.create_mixed_behav_game_efg(), True, [[["1/5", "4/5"]], [["1/4", "3/4"]], [[1, 0]]]),
(games.create_myerson_2_card_poker_efg(), True, [[[1/5, 4/5], [3/5, 2/5]], [[1/4, 3/4]]]),
(games.create_mixed_behav_game_efg(), False, [[[0, 1]], [[1, 0]], [[1, 0]]]),
(games.create_mixed_behav_game_efg(), False, [[[1/5, 4/5]], [[1/4, 3/4]], [[1, 0]]]),
(games.create_myerson_2_card_poker_efg(), 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_efg(), True,
[[[0, 1, 0]], [[1, 0]], [["1/2", "1/2"]]]),
(games.create_mixed_behav_game_efg(), True,
[[[0, 1]], [[1, 0]], [[1, 0]], [[0, 1]]]),
(games.create_myerson_2_card_poker_efg(), True,
[[["1/5", "4/5"], ["3/5", "2/5"]], [["1/4", "3/4"], ["1/4", "3/4"]]]),
(games.create_el_farol_bar_game_efg(), True,
[[4/9, 5/9], [0], [1/2, 1/2], [11/12, 1/12], [1/2, 1/2]]),
(games.create_el_farol_bar_game_efg(), True,
[[1/2, 1/2]]),
(games.create_mixed_behav_game_efg(), False,
[[[0, 1, 0]], [[1, 0]], [[1, 0]]]),
(games.create_mixed_behav_game_efg(), False,
[[[0, 1]], [[1, 0]], [[1, 0]], [[0, 1]]]),
(games.create_myerson_2_card_poker_efg(), False,
[[[1/5, 4/5], [3/5, 2/5]], [[1/4, 3/4], [1/4, 3/4]]]),
(games.create_el_farol_bar_game_efg(), False,
[[4/9, 5/9], [0], [1/2, 1/2], [11/12, 1/12], [1/2, 1/2]]),
(games.create_el_farol_bar_game_efg(), 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_coord_4x4_nfg(), False,
[[1/5, 2/5, 0/5, 2/5], [1/4, 3/8, 1/4, 3/8]]),
]
)
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)

0 comments on commit d2e38f7

Please sign in to comment.