Skip to content

Commit

Permalink
Merge branch 'd-kad-add-to-arrays-function'
Browse files Browse the repository at this point in the history
  • Loading branch information
tturocy committed Aug 30, 2024
2 parents cc7c97e + 195829c commit 32f5190
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 8 deletions.
2 changes: 2 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
and agent versions, and added a corresponding section in the user guide.
- `enumpoly_solve` has been returned to being fully supported from temporarily being experimental;
now available in `pygambit`.
- `to_arrays` converts a `Game` to a list of `numpy` arrays containing its reduced strategic form.
(#461)


## [16.1.2] - unreleased
Expand Down
1 change: 1 addition & 0 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Creating, reading, and writing games
Game.new_tree
Game.new_table
Game.from_arrays
Game.to_arrays
Game.from_dict
Game.read_game
Game.parse_game
Expand Down
23 changes: 23 additions & 0 deletions doc/pygambit.user.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,29 @@ the top level index is the choice of the first player, the second level index of
and so on. Therefore, to create a two-player symmetric game, as in this example, the payoff matrix
for the second player is transposed before passing to :py:meth:`.Game.from_arrays`.

There is a reverse function :py:meth:`.Game.to_arrays` that produces
the players' payoff tables given a strategic game. The output is the list of ``numpy`` arrays,
where the number of produced arrays is equal to the number of players.

.. ipython:: python
m, m_transposed = g.to_arrays()
m
The optional parameter `dtype`` controls the data type of the payoffs in the generated arrays.

.. ipython:: python
m, m_transposed = g.to_arrays(dtype=float)
m
The function supports any type which can convert from Python's `fractions.Fraction` type.
For example, to convert the payoffs to their string representations via `str`:

.. ipython:: python
m, m_transposed = g.to_arrays(dtype=str)
m
.. _pygambit.user.numbers:

Expand Down
34 changes: 34 additions & 0 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ class Game:
See Also
--------
from_dict : Create strategic game and set player labels
to_array: Generate the payoff tables for players represented as numpy arrays
"""
g = cython.declare(Game)
arrays = [np.array(a) for a in arrays]
Expand All @@ -336,6 +337,39 @@ class Game:
g.title = title
return g

def to_arrays(self, dtype: typing.Type = Rational) -> typing.List[np.array]:
"""Generate the payoff tables for players represented as numpy arrays.

Parameters
----------
dtype : type
The type to which payoff values will be converted and
the resulting arrays will be of that dtype

Returns
-------
list of np.array

See Also
--------
from_arrays : Create game from list-like of array-like
"""
arrays = []

shape = tuple(len(player.strategies) for player in self.players)
for player in self.players:
array = np.zeros(shape=shape, dtype=object)
for profile in itertools.product(*(range(s) for s in shape)):
try:
array[profile] = dtype(self[profile][player])
except (ValueError, TypeError, IndexError, KeyError):
raise ValueError(
f"Payoff '{self[profile][player]}' cannot be "
f"converted to requested type '{dtype}'"
) from None
arrays.append(array)
return arrays

@classmethod
def from_dict(cls, payoffs, title: str = "Untitled strategic game") -> Game:
"""Create a new ``Game`` with a strategic representation.
Expand Down
77 changes: 69 additions & 8 deletions tests/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,72 @@ def test_from_arrays():
assert len(game.players[1].strategies) == 2


def test_empty_array_to_arrays():
game = gbt.Game.from_arrays([])
a = game.to_arrays()
assert len(a) == 1
assert (a[0] == np.array([])).all()


def test_to_arrays_wrong_type():
m = np.array([[8, 2], [10, 5]])
game = gbt.Game.from_arrays(m, m.transpose())
with pytest.raises(ValueError):
_ = game.to_arrays(dtype=dict)


def test_different_num_representations_to_arrays_fraction():
game = gbt.Game.from_arrays([1, 2 / 1, "6/2", 0.25, ".99"])
A = game.to_arrays()[0]
correct_output = [gbt.Rational(1, 1), gbt.Rational(2, 1), gbt.Rational(3, 1),
gbt.Rational(1, 4), gbt.Rational(99, 100)]
assert (correct_output == A).all()


def test_different_num_representations_to_arrays_float():
game = gbt.Game.from_arrays([1, 2 / 1, "6/2", 0.25, ".99"])
A = game.to_arrays(dtype=float)[0]
correct_output = [1.0, 2.0, 3.0, 0.25, 0.99]
assert (correct_output == A).all()


def test_2d_to_arrays():
m = np.array([[8, 2], [10, 5]])
game = gbt.Game.from_arrays(m, m.transpose())
payoff, payoff_t = game.to_arrays()
assert (m == payoff).all()
assert (m.transpose() == payoff_t).all()


def test_3d_to_arrays():
a = np.array(
[
[[1, -1], [4, -4], [100, -100]],
[[2, -2], [5, -5], [101, -101]],
[[3, -3], [6, -6], [102, -102]],
]
)
b = np.array(
[
[[7, -7], [10, -10], [103, -103]],
[[8, -8], [11, -11], [104, -104]],
[[9, -9], [12, -12], [105, -105]],
]
)
c = np.array(
[
[[13, -13], [16, -16], [106, -106]],
[[14, -14], [17, -17], [107, -107]],
[[15, -15], [18, -18], [108, -108]],
]
)
game = gbt.Game.from_arrays(a, b, c)
a_, b_, c_ = game.to_arrays()
assert (a == a_).all()
assert (b == b_).all()
assert (c == c_).all()


def test_from_dict():
m = np.array([[8, 2], [10, 5]])
game = gbt.Game.from_dict({"a": m, "b": m.transpose()})
Expand Down Expand Up @@ -70,10 +136,7 @@ def test_game_get_outcome_unmatched_label():

def test_game_get_outcome_with_strategies():
game = gbt.Game.new_table([2, 2])
assert (
game[game.players[0].strategies[0], game.players[1].strategies[0]] ==
game.outcomes[0]
)
assert game[game.players[0].strategies[0], game.players[1].strategies[0]] == game.outcomes[0]


def test_game_get_outcome_with_bad_strategies():
Expand All @@ -92,8 +155,7 @@ def test_game_dereference_invalid():


def test_strategy_profile_invalidation_table():
"""Test for invalidating mixed strategy profiles on tables when game changes.
"""
"""Test for invalidating mixed strategy profiles on tables when game changes."""
g = gbt.Game.new_table([2, 2])
profiles = [g.mixed_strategy_profile(rational=b) for b in [False, True]]
g.delete_strategy(g.players[0].strategies[0])
Expand All @@ -116,8 +178,7 @@ def test_strategy_profile_invalidation_payoff():


def test_behavior_profile_invalidation():
"""Test for invalidating mixed strategy profiles on tables when game changes.
"""
"""Test for invalidating mixed strategy profiles on tables when game changes."""
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])
Expand Down

0 comments on commit 32f5190

Please sign in to comment.