Skip to content

Commit

Permalink
probs of actions at chance node normalized after an action is deleted
Browse files Browse the repository at this point in the history
  • Loading branch information
rahulsavani committed Oct 29, 2023
1 parent d401f7c commit c7d8116
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 5 deletions.
6 changes: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [unreleased] - unreleased

### Fixed
- When an action at a chance node is deleted the probabilities for the remaining actions are
normalized.

## [16.1.0a4] - 2023-10-13

### Changed
Expand Down
38 changes: 35 additions & 3 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ void GameTreeActionRep::DeleteAction()
member->children[where]->DeleteTree();
member->children.Remove(where)->Invalidate();
}

if (m_infoset->IsChanceInfoset()) {
m_infoset->m_efg->NormalizeChanceProbs(m_infoset);
}
m_infoset->m_efg->ClearComputedValues();
m_infoset->m_efg->Canonicalize();
}
Expand Down Expand Up @@ -162,12 +166,12 @@ GameAction GameTreeInfosetRep::InsertAction(GameAction p_action /* =0 */)
void GameTreeInfosetRep::RemoveAction(int which)
{
m_actions.Remove(which)->Invalidate();
for (; which <= m_actions.Length(); which++)
m_actions[which]->m_number = which;

if (m_player->IsChance()) {
m_probs.Remove(which);
}
for (; which <= m_actions.Length(); which++) {
m_actions[which]->m_number = which;
}
}

void GameTreeInfosetRep::RemoveMember(GameTreeNodeRep *p_node)
Expand Down Expand Up @@ -1054,6 +1058,34 @@ Game GameTreeRep::SetChanceProbs(const GameInfoset &p_infoset, const Array<Numbe
return this;
}

Game GameTreeRep::NormalizeChanceProbs(const GameInfoset &m_infoset) {
if (m_infoset->GetGame() != this) {
throw MismatchException();
}
if (!m_infoset->IsChanceInfoset()) {
throw UndefinedException("Action probabilities can only be normalized for chance information sets");
}
Rational sum(0);
for (int act = 1; act <= m_infoset->NumActions(); act++) {
Rational action_prob(m_infoset->GetActionProb(act));
sum += action_prob;
}
Array <Number> m_probs(m_infoset->NumActions());
if (sum == Rational(0)) {
// all remaining moves have prob zero; split prob 1 equally among them
for (int act = 1; act <= m_infoset->NumActions(); act++) {
m_probs[act] = Rational(1, m_infoset->NumActions());
}
}
else {
for (int act = 1; act <= m_infoset->NumActions(); act++) {
Rational prob(m_infoset->GetActionProb(act));
m_probs[act] = prob / sum;
}
}
m_infoset->GetGame()->SetChanceProbs(m_infoset, m_probs);
return this;
}

//------------------------------------------------------------------------
// GameTreeRep: Factory functions
Expand Down
2 changes: 2 additions & 0 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ class GameTreeRep : public GameExplicitRep {
/// @name Private auxiliary functions
//@{
void NumberNodes(GameTreeNodeRep *, int &);
/// Normalize the probability distribution of actions at a chance node
Game NormalizeChanceProbs(const GameInfoset &);
//@}

/// @name Managing the representation
Expand Down
4 changes: 2 additions & 2 deletions src/pygambit/behav.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ class MixedBehaviorProfile:
"""
return self._is_defined_at(self.game._resolve_infoset(infoset, 'is_defined_at'))

def belief(self, node: Node):
def belief(self, node: typing.Union[Node, str]):
"""Returns the conditional probability that a node is reached, given that
its information set is reached.
Expand All @@ -271,7 +271,7 @@ class MixedBehaviorProfile:
"""
if node.game != self.game:
raise MismatchError("belief: node must be part of the same game as the profile")
return self._belief(node)
return self._belief(self.game._resolve_node(node, 'belief'))

def payoff(self, player: typing.Union[Player, str]):
"""Returns the expected payoff to a player if all players play
Expand Down
3 changes: 3 additions & 0 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,9 @@ class Game:
def delete_action(self, action: typing.Union[Action, str]) -> None:
"""Deletes `action` from its information set. The subtrees which
are rooted at nodes that follow the deleted action are also deleted.
If the action is at a chance node then the probabilities of any remaining actions
are normalized to sum to one; if all remaining actions previously had probability zero
then this normalization gives those remaining actions all equal probability.

Raises
------
Expand Down
29 changes: 29 additions & 0 deletions src/pygambit/tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,32 @@ def test_action_delete_last(game: gbt.Game):
game.delete_action(node.infoset.actions[0])
with pytest.raises(gbt.UndefinedOperationError):
game.delete_action(node.infoset.actions[0])


@pytest.mark.parametrize(
"game",
[gbt.Game.read_game("test_games/chance_root_3_moves_only_one_nonzero_prob.efg"),
gbt.Game.read_game("test_games/complicated_extensive_game.efg"),
gbt.Game.read_game("test_games/chance_root_5_moves_no_nonterm_player_nodes.efg")]
)
def test_action_delete_chance(game: gbt.Game):
"""Test the renormalization of action probabilities when an action is deleted at a chance
node
"""
chance_iset = game.players.chance.infosets[0]
while len(chance_iset.actions) > 1:
old_probs = [a.prob for a in chance_iset.actions]
game.delete_action(chance_iset.actions[0])
new_probs = [a.prob for a in chance_iset.actions]
assert sum(new_probs) == 1
if sum(old_probs[1:]) == 0:
for p in new_probs:
assert p == 1/len(new_probs)
else:
for p1, p2 in zip(old_probs[1:], new_probs):
if p1 == 0:
assert p2 == 0
else:
assert p2 == p1 / (1-old_probs[0])
with pytest.raises(gbt.UndefinedOperationError):
game.delete_action(chance_iset.actions[0])
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
EFG 2 R "A chance node with 3 moves, 2 with zero prob; one non-terminal player node" { "Player" }
""

c "" 1 "(0,1)" { "A" 1 "B" 0 "C" 0 } 0
p "" 1 1 "(1,1)" { "LEFT" "RIGHT" } 0
t "" 1 "Outcome 1" { 0 }
t "" 2 "Outcome 2" { 1 }
t "" 3 "Outcome 3" { 2 }
t "" 4 "Outcome 4" { 3 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
EFG 2 R "A chance node with 5 moves; no non-terminal player node" { "Player" }
""

c "" 1 "" { "A" 1/3 "B" 0 "C" 1/3 "D" 1/3 "E" 0 } 0
t "" 1 "A" { 0 }
t "" 2 "B" { 1 }
t "" 3 "C" { 3 }
t "" 4 "D" { 3 }
t "" 5 "E" { 4 }

0 comments on commit c7d8116

Please sign in to comment.