Skip to content

Commit

Permalink
Add to_arrays method to Game class that takes as its input an array-l…
Browse files Browse the repository at this point in the history
…ike object of array-like objects produces a Gambit game object with a strategic form representation.

Add tests for the above method
Add docstring for the new function

index on add-to-arrays-function: b1b801f Add to_arrays function
  • Loading branch information
tturocy authored and d-kad committed Aug 29, 2024
2 parents cd80351 + 20139fd commit c6fbded
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repos:
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.3
rev: v0.6.1
hooks:
- id: ruff
- repo: https://github.com/pycqa/flake8
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
24 changes: 24 additions & 0 deletions doc/pygambit.user.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,30 @@ 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
There is a parameter `dtype`` passed to :py:meth:`.Game.to_arrays` that configures the data type of
payoffs in the generated arrays. Supports any type which can convert from Python's `fractions.Fraction`
type.

.. ipython:: python
m, m_transposed = g.to_arrays(dtype=float)
m
The type passed can be also `str` since it can be converted from `fractions.Fraction`:

.. 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
3 changes: 2 additions & 1 deletion tests/test_actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pygambit as gbt
import pytest

import pygambit as gbt

from . import games


Expand Down
3 changes: 2 additions & 1 deletion tests/test_behav.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import typing
from itertools import product

import pygambit as gbt
import pytest

import pygambit as gbt

from . import games

TOL = 1e-13 # tolerance for floating point assertions
Expand Down
3 changes: 2 additions & 1 deletion tests/test_extensive.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import typing

import pygambit as gbt
import pytest

import pygambit as gbt

from . import games


Expand Down
80 changes: 71 additions & 9 deletions tests/test_game.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import numpy as np
import pygambit as gbt
import pytest

import pygambit as gbt

from . import games


Expand All @@ -13,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 @@ -69,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 @@ -91,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 @@ -115,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
3 changes: 2 additions & 1 deletion tests/test_infosets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pygambit as gbt
import pytest

import pygambit as gbt

from . import games


Expand Down
3 changes: 2 additions & 1 deletion tests/test_mixed.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import typing
from itertools import product

import pygambit as gbt
import pytest

import pygambit as gbt

from . import games

TOL = 1e-13 # tolerance for floating point assertions
Expand Down
3 changes: 2 additions & 1 deletion tests/test_outcomes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pygambit as gbt
import pytest

import pygambit as gbt


@pytest.mark.parametrize(
"game", [gbt.Game.new_table([2, 2]), gbt.Game.new_tree()]
Expand Down
3 changes: 2 additions & 1 deletion tests/test_players.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pygambit as gbt
import pytest

import pygambit as gbt

from . import games


Expand Down
3 changes: 2 additions & 1 deletion tests/test_strategic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pygambit as gbt
import pytest

import pygambit as gbt

from . import games


Expand Down
3 changes: 2 additions & 1 deletion tests/test_stratprofiles.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pygambit as gbt
import pytest

import pygambit as gbt

from . import games


Expand Down

0 comments on commit c6fbded

Please sign in to comment.