From a8a3574636175452c409df8928eeb90a18a2a57e Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 25 Oct 2023 08:48:39 +0100 Subject: [PATCH] probs of actions at chance node normalized after an action is deleted --- src/games/game.h | 2 + src/games/gameagg.h | 1 + src/games/gamebagg.h | 1 + src/games/gametable.h | 1 + src/games/gametree.cc | 43 +++++++++++++++++++ src/games/gametree.h | 1 + src/pygambit/tests/test_actions.py | 30 +++++++++++++ ...nce_root_3_moves_only_one_nonzero_prob.efg | 9 ++++ ...e_root_5_moves_no_nonterm_player_nodes.efg | 9 ++++ 9 files changed, 97 insertions(+) create mode 100644 src/pygambit/tests/test_games/chance_root_3_moves_only_one_nonzero_prob.efg create mode 100644 src/pygambit/tests/test_games/chance_root_5_moves_no_nonterm_player_nodes.efg diff --git a/src/games/game.h b/src/games/game.h index 7d6d5a653..590cbb108 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -847,6 +847,8 @@ class GameRep : public BaseGameRep { //@{ /// Set the probability distribution of actions at a chance node virtual Game SetChanceProbs(const GameInfoset &, const Array &) = 0; + /// Normalize the probability distribution of actions at a chance node + virtual Game NormalizeChanceProbs(const GameInfoset &) = 0; //@} }; diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 0d3ba156e..82da5c2ee 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -147,6 +147,7 @@ class GameAggRep : public GameRep { /// @name Modification //@{ Game SetChanceProbs(const GameInfoset &, const Array &) override { throw UndefinedException(); } + Game NormalizeChanceProbs(const GameInfoset &) override { throw UndefinedException(); } //@} /// @name Writing data files diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 423519fde..1ed003f4b 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -154,6 +154,7 @@ class GameBagentRep : public GameRep { /// @name Modification //@{ Game SetChanceProbs(const GameInfoset &, const Array &) override { throw UndefinedException(); } + Game NormalizeChanceProbs(const GameInfoset &) override { throw UndefinedException(); } //@} }; diff --git a/src/games/gametable.h b/src/games/gametable.h index 3a2843c50..555718fa7 100644 --- a/src/games/gametable.h +++ b/src/games/gametable.h @@ -116,6 +116,7 @@ class GameTableRep : public GameExplicitRep { /// @name Modification //@{ Game SetChanceProbs(const GameInfoset &, const Array &) override { throw UndefinedException(); } + Game NormalizeChanceProbs(const GameInfoset &) override { throw UndefinedException(); } //@} PureStrategyProfile NewPureStrategyProfile() const override; diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 4f55ddd35..0212bdbb9 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -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(); } @@ -1054,6 +1058,45 @@ Game GameTreeRep::SetChanceProbs(const GameInfoset &p_infoset, const ArrayGetGame() != 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)); + std::cout << "action prob BEFORE NORM: " << action_prob << '\n'; + sum += action_prob; + } + Array m_probs(m_infoset->NumActions()); + std::cout << "COMPUTED SUM IS: " << sum << '\n'; + 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++) { + std::cout << "SUM 0\n"; + m_probs[act] = Rational(1, m_infoset->NumActions()); + std::cout << "action prob SET UNIFORM: " << (std::string) m_probs[act] << '\n'; + } + } + else { + for (int act = 1; act <= m_infoset->NumActions(); act++) { + Rational prob(m_infoset->GetActionProb(act)); + m_probs[act] = prob / sum; + std::cout << "action prob SET NORMALIZE: " << (std::string) m_probs[act] << '\n'; + } + } + m_infoset->GetGame()->SetChanceProbs(m_infoset, m_probs); + + for (int act = 1; act <= m_infoset->NumActions(); act++) { + Rational prob(m_infoset->GetActionProb(act)); + std::cout << "action prob NORMALIZED: " << prob << '\n'; + } + + return this; +} //------------------------------------------------------------------------ // GameTreeRep: Factory functions diff --git a/src/games/gametree.h b/src/games/gametree.h index 21826e436..040b25632 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -281,6 +281,7 @@ class GameTreeRep : public GameExplicitRep { /// @name Modification //@{ Game SetChanceProbs(const GameInfoset &, const Array &) override; + Game NormalizeChanceProbs(const GameInfoset &) override; //@} PureStrategyProfile NewPureStrategyProfile() const override; diff --git a/src/pygambit/tests/test_actions.py b/src/pygambit/tests/test_actions.py index 50070db3b..525c60f69 100644 --- a/src/pygambit/tests/test_actions.py +++ b/src/pygambit/tests/test_actions.py @@ -96,3 +96,33 @@ 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): + """ """ + chance_iset = game.players.chance.infosets[0] + while len(chance_iset.actions) > 1: + check_flag = False + if chance_iset.actions[0].prob == 0: + print("CHECK FLAG IS TRUE") + # the move we are about to delete has 0 prob so nothing should change + check_flag = True + # keep copy of old probs to check that nothing changed + old_probs = [a.prob for a in chance_iset.actions] + print("old_probs", old_probs) + print("chance_iset.actions[0]).prob", chance_iset.actions[0].prob) + game.delete_action(chance_iset.actions[0]) + # always check that new prob sum is 1 + assert sum([a.prob for a in chance_iset.actions]) == 1 + if check_flag: + new_probs = [a.prob for a in chance_iset.actions] + for p1, p2 in zip(old_probs[1:], new_probs): + assert p1 == p2 + with pytest.raises(gbt.UndefinedOperationError): + game.delete_action(chance_iset.actions[0]) diff --git a/src/pygambit/tests/test_games/chance_root_3_moves_only_one_nonzero_prob.efg b/src/pygambit/tests/test_games/chance_root_3_moves_only_one_nonzero_prob.efg new file mode 100644 index 000000000..f8c5e9ada --- /dev/null +++ b/src/pygambit/tests/test_games/chance_root_3_moves_only_one_nonzero_prob.efg @@ -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" 0 "B" 0 "C" 1 } 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 } diff --git a/src/pygambit/tests/test_games/chance_root_5_moves_no_nonterm_player_nodes.efg b/src/pygambit/tests/test_games/chance_root_5_moves_no_nonterm_player_nodes.efg new file mode 100644 index 000000000..545761413 --- /dev/null +++ b/src/pygambit/tests/test_games/chance_root_5_moves_no_nonterm_player_nodes.efg @@ -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 }