Skip to content

Commit

Permalink
Refactor support profile implementation in pygambit.
Browse files Browse the repository at this point in the history
* Cleans up support profile constructor calling signature, implementation of operators in cython
* Test suite migrated to pytest style.
  • Loading branch information
tturocy committed Nov 15, 2023
1 parent 59469a7 commit 95b27fb
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 128 deletions.
2 changes: 1 addition & 1 deletion src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ class Game:
)

def support_profile(self):
return StrategySupportProfile(list(self.strategies), self)
return StrategySupportProfile(self)

def nodes(
self,
Expand Down
58 changes: 27 additions & 31 deletions src/pygambit/stratspt.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,27 @@ from libcpp.memory cimport unique_ptr
from deprecated import deprecated

@cython.cclass
class StrategySupportProfile(Collection):
class StrategySupportProfile:
"""A set-like object representing a subset of the strategies in game.
A StrategySupportProfile always contains at least one strategy for each player
in the game.
"""
support = cython.declare(unique_ptr[c_StrategySupportProfile])

def __init__(self, strategies, Game game not None):
if len(set([strat.player.number for strat in strategies])) != len(game.players):
def __init__(self,
game: Game,
strategies: typing.Optional[typing.Iterable[Strategy]] = None):
if (strategies is not None and
len(set([strat.player.number for strat in strategies])) != len(game.players)):
raise ValueError(
"A StrategySupportProfile must have at least one strategy for each player"
)
# There's at least one strategy for each player, so this forms a valid support profile
self.support.reset(new c_StrategySupportProfile((<Game>game).game))
for strategy in game.strategies:
if strategy not in strategies:
deref(self.support).RemoveStrategy((<Strategy>strategy).strategy)
self.support.reset(new c_StrategySupportProfile(game.game))
if strategies is not None:
for strategy in game.strategies:
if strategy not in strategies:
deref(self.support).RemoveStrategy(cython.cast(Strategy, strategy).strategy)

@property
def game(self) -> Game:
Expand All @@ -55,25 +59,17 @@ class StrategySupportProfile(Collection):
"""Returns the total number of strategies in the support profile."""
return deref(self.support).MixedProfileLength()

def __richcmp__(self, other: typing.Any, whichop: int) -> bool:
if isinstance(other, StrategySupportProfile):
if whichop == 1:
return self.issubset(other)
elif whichop == 2:
return deref(self.support) == deref((<StrategySupportProfile>other).support)
elif whichop == 3:
return deref(self.support) != deref((<StrategySupportProfile>other).support)
elif whichop == 5:
return self.issuperset(other)
else:
raise NotImplementedError
else:
if whichop == 2:
return False
elif whichop == 3:
return True
else:
raise NotImplementedError
def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, StrategySupportProfile) and
deref(self.support) == deref(cython.cast(StrategySupportProfile, other).support)
)

def __le__(self, other: StrategySupportProfile) -> bool:
return self.issubset(other)

def __ge__(self, other: StrategySupportProfile) -> bool:
return self.issuperset(other)

def __getitem__(self, index: int) -> Strategy:
for pl in range(len(self.game.players)):
Expand Down Expand Up @@ -131,7 +127,7 @@ class StrategySupportProfile(Collection):
)
strategies = list(self)
strategies.remove(strategy)
return StrategySupportProfile(strategies, self.game)
return StrategySupportProfile(self.game, strategies)

def difference(self, other: StrategySupportProfile) -> StrategySupportProfile:
"""Create a support profile which contains all strategies in this profile that
Expand All @@ -154,7 +150,7 @@ class StrategySupportProfile(Collection):
"""
if self.game != other.game:
raise MismatchError("difference(): support profiles are defined on different games")
return StrategySupportProfile(set(self) - set(other), self.game)
return StrategySupportProfile(self.game, set(self) - set(other))

def intersection(self, other: StrategySupportProfile) -> StrategySupportProfile:
"""Create a support profile which contains all strategies that are in both this and
Expand All @@ -177,7 +173,7 @@ class StrategySupportProfile(Collection):
"""
if self.game != other.game:
raise MismatchError("intersection(): support profiles are defined on different games")
return StrategySupportProfile(set(self) & set(other), self.game)
return StrategySupportProfile(self.game, set(self) & set(other))

def union(self, other: StrategySupportProfile) -> StrategySupportProfile:
"""Create a support profile which contains all strategies that are in either this or
Expand All @@ -200,7 +196,7 @@ class StrategySupportProfile(Collection):
"""
if self.game != other.game:
raise MismatchError("union(): support profiles are defined on different games")
return StrategySupportProfile(set(self) | set(other), self.game)
return StrategySupportProfile(self.game, set(self) | set(other))

def issubset(self, other: StrategySupportProfile) -> bool:
"""Test for whether this support is contained in another.
Expand Down Expand Up @@ -286,6 +282,6 @@ class StrategySupportProfile(Collection):
def _undominated_strategies_solve(
profile: StrategySupportProfile, strict: bool, external: bool
) -> StrategySupportProfile:
result = StrategySupportProfile(list(profile), profile.game)
result = StrategySupportProfile(profile.game)
result.support.reset(new c_StrategySupportProfile(deref(profile.support).Undominated(strict, external)))
return result
178 changes: 82 additions & 96 deletions src/pygambit/tests/test_stratprofiles.py
Original file line number Diff line number Diff line change
@@ -1,103 +1,89 @@
import unittest
import pytest

import pygambit
import pygambit as gbt


class TestGambitStrategySupportProfile(unittest.TestCase):
def setUp(self):
self.game = pygambit.Game.read_game("test_games/mixed_strategy.nfg")
self.support_profile = self.game.support_profile()
self.restriction = self.support_profile.restrict()
def test_remove_strategy():
game = gbt.Game.read_game("test_games/mixed_strategy.nfg")
support_profile = game.support_profile()
strategy = support_profile[0]
new_profile = support_profile.remove(strategy)
assert len(support_profile) == len(new_profile) + 1
assert strategy not in new_profile

def tearDown(self):
del self.game
del self.support_profile
del self.restriction

def test_num_strategies(self):
"""Ensure the support profile of the full game still has all strategies"""
assert len(self.support_profile) == len(self.game.strategies)
assert len(self.support_profile) == len(self.restriction.strategies)

def test_remove(self):
"""Test removing strategies from a support profile"""
strategy = self.support_profile[0]
new_profile = self.support_profile.remove(strategy)
assert len(self.support_profile) == len(new_profile) + 1
def test_difference():
game = gbt.Game.read_game("test_games/mixed_strategy.nfg")
support_profile = game.support_profile()
strat_list = [support_profile[0], support_profile[4]]
dif_profile = gbt.StrategySupportProfile(game, strat_list)
new_profile = support_profile - dif_profile
assert len(new_profile) == 3
for strategy in strat_list:
assert strategy not in new_profile

def test_difference(self):
"""Test the subtraction of two support profiles"""
strat_list = [self.support_profile[0], self.support_profile[4]]
dif_profile = pygambit.StrategySupportProfile(
strat_list, self.game)
new_profile = self.support_profile - dif_profile
assert len(new_profile) == 3
for strategy in strat_list:
assert strategy not in new_profile

def test_difference_error(self):
"""Ensure an error is raised when the difference isn't a
valid support profile
"""
def foo():
strat_list = [self.support_profile[0], self.support_profile[4]]
dif_profile = pygambit.StrategySupportProfile(
strat_list, self.game
)
dif_profile - self.support_profile
self.assertRaises(ValueError, foo)

def test_intersection(self):
"""Test the intersection between two support profiles"""
strat_list = [self.support_profile[0], self.support_profile[2],
self.support_profile[4]]
fir_profile = pygambit.StrategySupportProfile(
strat_list, self.game)
sec_profile = self.support_profile.remove(self.support_profile[2])
new_profile = fir_profile & sec_profile
assert len(new_profile) == 2
assert new_profile <= sec_profile
assert new_profile <= fir_profile

def test_intersection_error(self):
"""Ensure an error is raised when the intersection isn't a
valid support profile.
"""
def foo():
strat_list = [self.support_profile[0], self.support_profile[2],
self.support_profile[4]]
fir_profile = pygambit.StrategySupportProfile(
strat_list, self.game
)
sec_profile = self.support_profile.remove(self.support_profile[4])
fir_profile & sec_profile
self.assertRaises(ValueError, foo)

def test_union(self):
"""Test the union between two support profiles"""
strat_list = [self.support_profile[0], self.support_profile[2],
self.support_profile[4]]
fir_profile = pygambit.StrategySupportProfile(
strat_list, self.game)
sec_profile = self.support_profile.remove(self.support_profile[4])
new_profile = fir_profile | sec_profile
assert new_profile == self.support_profile

def test_undominated(self):
"""Test removing undominated strategies from the support profile"""
new_profile = self.support_profile
loop_profile = pygambit.supports.undominated_strategies_solve(new_profile)
while loop_profile != new_profile:
new_profile = loop_profile
loop_profile = pygambit.supports.undominated_strategies_solve(new_profile)
assert len(loop_profile) == 2
assert loop_profile == pygambit.StrategySupportProfile(
[self.support_profile[0], self.support_profile[3]], self.game)

def test_remove_error(self):
"""Test removing the last strategy of a player"""
def foo():
profile = self.support_profile.remove(self.support_profile[3])
profile.remove(profile[3])
self.assertRaises(pygambit.UndefinedOperationError, foo)

def test_difference_error():
game = gbt.Game.read_game("test_games/mixed_strategy.nfg")
support_profile = game.support_profile()
strat_list = [support_profile[0], support_profile[4]]
dif_profile = gbt.StrategySupportProfile(game, strat_list)
with pytest.raises(ValueError):
dif_profile - support_profile


def test_intersection():
game = gbt.Game.read_game("test_games/mixed_strategy.nfg")
support_profile = game.support_profile()
strat_list = [support_profile[0], support_profile[2],
support_profile[4]]
fir_profile = gbt.StrategySupportProfile(game, strat_list)
sec_profile = support_profile.remove(support_profile[2])
new_profile = fir_profile & sec_profile
assert len(new_profile) == 2
assert new_profile <= sec_profile
assert new_profile <= fir_profile


def test_intersection_error():
game = gbt.Game.read_game("test_games/mixed_strategy.nfg")
support_profile = game.support_profile()
strat_list = [support_profile[0], support_profile[2],
support_profile[4]]
fir_profile = gbt.StrategySupportProfile(game, strat_list)
sec_profile = support_profile.remove(support_profile[4])
with pytest.raises(ValueError):
fir_profile & sec_profile


def test_union():
game = gbt.Game.read_game("test_games/mixed_strategy.nfg")
support_profile = game.support_profile()
strat_list = [support_profile[0], support_profile[2],
support_profile[4]]
fir_profile = gbt.StrategySupportProfile(game, strat_list)
sec_profile = support_profile.remove(support_profile[4])
new_profile = fir_profile | sec_profile
assert new_profile == support_profile


def test_undominated():
game = gbt.Game.read_game("test_games/mixed_strategy.nfg")
support_profile = game.support_profile()
new_profile = support_profile
loop_profile = gbt.supports.undominated_strategies_solve(new_profile)
while loop_profile != new_profile:
new_profile = loop_profile
loop_profile = gbt.supports.undominated_strategies_solve(new_profile)
assert len(loop_profile) == 2
assert loop_profile == gbt.StrategySupportProfile(
game, [support_profile[0], support_profile[3]]
)


def test_remove_error():
game = gbt.Game.read_game("test_games/mixed_strategy.nfg")
support_profile = game.support_profile()
profile = support_profile.remove(support_profile[3])
with pytest.raises(gbt.UndefinedOperationError):
profile.remove(profile[3])

0 comments on commit 95b27fb

Please sign in to comment.