def get_3node_cbn() -> CausalBayesianNetwork: cbn = CausalBayesianNetwork([("S", "D"), ("S", "U"), ("D", "U")]) cpd_s = UniformRandomCPD("S", [-1, 1]) cpd_u = FunctionCPD("U", lambda s, d: s * d) # type: ignore cpd_d = FunctionCPD("D", lambda s: s + 1) # type: ignore cbn.add_cpds(cpd_d, cpd_s, cpd_u) return cbn
def get_sequential_cid() -> CID: """ This CID is a subtle case of sufficient recall, as the decision rule for D1 influences the expected utility of D2, but D2 can still be chosen without knowing D1, since D1 does not influence any utility nodes descending from D2. """ cid = CID( [ ("S1", "D1"), ("D1", "U1"), ("S1", "U1"), ("D1", "S2"), ("S2", "D2"), ("D2", "U2"), ("S2", "U2"), ], decisions=["D1", "D2"], utilities=["U1", "U2"], ) cid.add_cpds( UniformRandomCPD("S1", [0, 1]), DecisionDomain("D1", [0, 1]), FunctionCPD("U1", lambda s1, d1: int(s1 == d1)), # type: ignore FunctionCPD("S2", lambda d1: d1), # type: ignore DecisionDomain("D2", [0, 1]), FunctionCPD("U2", lambda s2, d2: int(s2 == d2)), # type: ignore ) return cid
def get_introduced_bias() -> CID: cid = CID( [ ("A", "X"), # defining the graph's nodes and edges ("Z", "X"), ("Z", "Y"), ("X", "D"), ("X", "Y"), ("D", "U"), ("Y", "U"), ], decisions=["D"], utilities=["U"], ) cpd_a = UniformRandomCPD("A", [0, 1]) cpd_z = UniformRandomCPD("Z", [0, 1]) cpd_x = FunctionCPD("X", lambda a, z: a * z) # type: ignore cpd_d = DecisionDomain("D", [0, 1]) cpd_y = FunctionCPD("Y", lambda x, z: x + z) # type: ignore cpd_u = FunctionCPD("U", lambda d, y: -((d - y) ** 2)) # type: ignore cid.add_cpds(cpd_a, cpd_d, cpd_z, cpd_x, cpd_y, cpd_u) return cid
def basic_different_dec_cardinality() -> MACID: """A basic MACIM where the cardinality of each agent's decision node is different. It has one subgame perfect NE. """ macid = MACID( [("D1", "D2"), ("D1", "U1"), ("D1", "U2"), ("D2", "U2"), ("D2", "U1")], agent_decisions={ 0: ["D1"], 1: ["D2"] }, agent_utilities={ 0: ["U1"], 1: ["U2"] }, ) cpd_d1 = DecisionDomain("D1", [0, 1]) cpd_d2 = DecisionDomain("D2", [0, 1, 2]) agent1_payoff = np.array([[3, 1, 0], [1, 2, 3]]) agent2_payoff = np.array([[1, 2, 1], [1, 0, 3]]) cpd_u1 = FunctionCPD("U1", lambda d1, d2: agent1_payoff[d1, d2]) # type: ignore cpd_u2 = FunctionCPD("U2", lambda d1, d2: agent2_payoff[d1, d2]) # type: ignore macid.add_cpds(cpd_d1, cpd_d2, cpd_u1, cpd_u2) return macid
def modified_taxi_competition() -> MACID: """Modifying the payoffs in the taxi competition example so that there is a tie break (if taxi 1 chooses to stop in front of the expensive hotel, taxi 2 is indifferent between their choices.) - There are now two SPNE D1 +----------+----------+----------+ | taxi 1 | expensive| cheap | +----------+----------+----------+ |expensive | 2 | 3 | D2 +----------+----------+----------+ | cheap | 5 | 1 | +----------+----------+----------+ D1 +----------+----------+----------+ | taxi 2 | expensive| cheap | +----------+----------+----------+ |expensive | 2 | 5 | D2 +----------+----------+----------+ | cheap | 3 | 5 | +----------+----------+----------+ """ macid = MACID( [("D1", "D2"), ("D1", "U1"), ("D1", "U2"), ("D2", "U2"), ("D2", "U1")], agent_decisions={ 1: ["D1"], 2: ["D2"] }, agent_utilities={ 1: ["U1"], 2: ["U2"] }, ) d1_domain = ["e", "c"] d2_domain = ["e", "c"] agent1_payoff = np.array([[2, 3], [5, 1]]) agent2_payoff = np.array([[2, 5], [3, 5]]) macid.add_cpds( DecisionDomain("D1", d1_domain), DecisionDomain("D2", d2_domain), FunctionCPD( "U1", lambda d1, d2: agent1_payoff[d2_domain.index(d2), d1_domain.index(d1)]), # type: ignore FunctionCPD( "U2", lambda d1, d2: agent2_payoff[d2_domain.index(d2), d1_domain.index(d1)]), # type: ignore ) return macid
def taxi_competition() -> MACID: """MACIM representation of the Taxi Competition game. "Taxi Competition" is an example introduced in "Equilibrium Refinements for Multi-Agent Influence Diagrams: Theory and Practice" by Hammond, Fox, Everitt, Abate & Wooldridge, 2021: D1 +----------+----------+----------+ | taxi 1 | expensive| cheap | +----------+----------+----------+ |expensive | 2 | 3 | D2 +----------+----------+----------+ | cheap | 5 | 1 | +----------+----------+----------+ D1 +----------+----------+----------+ | taxi 2 | expensive| cheap | +----------+----------+----------+ |expensive | 2 | 5 | D2 +----------+----------+----------+ | cheap | 3 | 1 | +----------+----------+----------+ There are 3 pure startegy NE and 1 pure SPE. """ macid = MACID( [("D1", "D2"), ("D1", "U1"), ("D1", "U2"), ("D2", "U2"), ("D2", "U1")], agent_decisions={ 1: ["D1"], 2: ["D2"] }, agent_utilities={ 1: ["U1"], 2: ["U2"] }, ) d1_domain = ["e", "c"] d2_domain = ["e", "c"] agent1_payoff = np.array([[2, 3], [5, 1]]) agent2_payoff = np.array([[2, 5], [3, 1]]) macid.add_cpds( DecisionDomain("D1", d1_domain), DecisionDomain("D2", d2_domain), FunctionCPD( "U1", lambda d1, d2: agent1_payoff[d2_domain.index(d2), d1_domain.index(d1)]), # type: ignore FunctionCPD( "U2", lambda d1, d2: agent2_payoff[d2_domain.index(d2), d1_domain.index(d1)]), # type: ignore ) return macid
def robot_warehouse() -> MACID: r""" Implementation of AAMAS robot warehouse example - Robot 1 collects packages, and can choose to hurry or not (D1) - Hurrying can be quicker (Q) but lead to breakages (B) - Robot 2 tidies up, and can choose to repair (R) breakages or not (D2) - Conducting repairs can obstruct (O) robot 1 - Robot 1 rewarded for speed and lack of breakages (U1), robot 2 is rewarded for things being in a state of repair (U2) """ macid = MACID( [ ("D1", "Q"), ("D1", "B"), ("Q", "U1"), ("B", "U1"), ("B", "R"), ("B", "D2"), ("D2", "R"), ("D2", "O"), ("O", "U1"), ("R", "U2"), ], agent_decisions={ 1: ["D1"], 2: ["D2"], }, agent_utilities={ 1: ["U1"], 2: ["U2"], }, ) macid.add_cpds( DecisionDomain("D1", domain=[0, 1]), DecisionDomain("D2", domain=[0, 1]), # Q copies the value of D1 with 90% probability StochasticFunctionCPD("Q", lambda d1: {d1: 0.9}, domain=[0, 1]), # B copies the value of D1 with 30% probability StochasticFunctionCPD("B", lambda d1: {d1: 0.3}, domain=[0, 1]), # R = not B or D2 FunctionCPD("R", lambda b, d2: int(not b or d2)), # O copies the value of D2 with 60% probability StochasticFunctionCPD("O", lambda d2: {d2: 0.6}, domain=[0, 1]), # U1 = (Q and not O) - B FunctionCPD("U1", lambda q, b, o: int(q and not o) - int(b)), # U2 = R FunctionCPD("U2", lambda r: r), # type: ignore ) return macid
def test_introduced_bias_reversed_sign(self) -> None: cbn = CausalBayesianNetwork([("A", "D"), ("A", "Y")]) cbn.add_cpds( UniformRandomCPD("A", [0, 1]), FunctionCPD("D", lambda a: 0), FunctionCPD("Y", lambda a: a), ) assert introduced_total_effect(cbn, "A", "D", "Y") == pytest.approx(-1) cbn.add_cpds(FunctionCPD("Y", lambda a: -a)) assert introduced_total_effect( cbn, "A", "D", "Y", adapt_marginalized=True) == pytest.approx(-1)
def get_2dec_cid() -> CID: cid = CID( [("S1", "S2"), ("S1", "D1"), ("D1", "S2"), ("S2", "U"), ("S2", "D2"), ("D2", "U")], decisions=["D1", "D2"], utilities=["U"], ) cpd_s1 = UniformRandomCPD("S1", [0, 1]) cpd_d1 = DecisionDomain("D1", [0, 1]) cpd_d2 = DecisionDomain("D2", [0, 1]) cpd_s2 = FunctionCPD("S2", lambda s1, d1: int(s1 == d1)) # type: ignore cpd_u = FunctionCPD("U", lambda s2, d2: int(s2 == d2)) # type: ignore cid.add_cpds(cpd_s1, cpd_d1, cpd_s2, cpd_d2, cpd_u) return cid
def battle_of_the_sexes() -> MACID: """MACIM representation of the battle of the sexes game. The battle of the sexes game (also known as Bach or Stravinsky) is a simultaneous symmetric two-player game with payoffs corresponding to the following normal form game - the row player is Female and the column player is Male: +----------+----------+----------+ | |Opera | Football | +----------+----------+----------+ | Opera | 3, 2 | 0, 0 | +----------+----------+----------+ | Football | 0, 0 | 2, 3 | +----------+----------+----------+ This game has two pure NE: (Opera, Football) and (Football, Opera) """ macid = MACID( [("D_F", "U_F"), ("D_F", "U_M"), ("D_M", "U_M"), ("D_M", "U_F")], agent_decisions={ "M": ["D_F"], "F": ["D_M"] }, agent_utilities={ "M": ["U_F"], "F": ["U_M"] }, ) d_f_domain = ["O", "F"] d_m_domain = ["O", "F"] agent_f_payoff = np.array([[3, 0], [0, 2]]) agent_m_payoff = np.array([[2, 0], [0, 3]]) macid.add_cpds( DecisionDomain("D_F", d_f_domain), DecisionDomain("D_M", d_m_domain), FunctionCPD( "U_F", lambda d_f, d_m: agent_f_payoff[d_f_domain.index( d_f), d_m_domain.index(d_m)] # type: ignore ), FunctionCPD( "U_M", lambda d_f, d_m: agent_m_payoff[d_f_domain.index( d_f), d_m_domain.index(d_m)] # type: ignore ), ) return macid
def get_5node_cid_with_scaled_utility() -> CID: cid = CID( [("S1", "D"), ("S1", "U1"), ("S2", "D"), ("S2", "U2"), ("D", "U1"), ("D", "U2")], decisions=["D"], utilities=["U1", "U2"], ) cpd_s1 = UniformRandomCPD("S1", [0, 1]) cpd_s2 = UniformRandomCPD("S2", [0, 1]) cpd_u1 = FunctionCPD("U1", lambda s1, d: 10 * int(s1 == d)) # type: ignore cpd_u2 = FunctionCPD("U2", lambda s2, d: 2 * int(s2 == d)) # type: ignore cpd_d = DecisionDomain("D", [0, 1]) cid.add_cpds(cpd_d, cpd_s1, cpd_s2, cpd_u1, cpd_u2) return cid
def matching_pennies() -> MACID: """MACIM representation of the matching pennies game. The matching pennies game is a symmetric two-player game with payoffs corresponding to the following normal form game - the row player is agent 1 and the column player is agent 2: +----------+----------+----------+ | |Heads | Tails | +----------+----------+----------+ | Heads | +1, -1 | -1, +1 | +----------+----------+----------+ | Tails | -1, +1 | +1, -1 | +----------+----------+----------+ This game has no pure NE, but has a mixed NE where each player chooses Heads or Tails with equal probability. """ macid = MACID( [("D1", "U1"), ("D1", "U2"), ("D2", "U2"), ("D2", "U1")], agent_decisions={ 1: ["D1"], 2: ["D2"] }, agent_utilities={ 1: ["U1"], 2: ["U2"] }, ) d1_domain = ["H", "T"] d2_domain = ["H", "T"] agent1_payoff = np.array([[1, -1], [-1, 1]]) agent2_payoff = np.array([[-1, 1], [1, -1]]) macid.add_cpds( DecisionDomain("D1", d1_domain), DecisionDomain("D2", d2_domain), FunctionCPD( "U1", lambda d1, d2: agent1_payoff[d1_domain.index(d1), d2_domain.index(d2)]), # type: ignore FunctionCPD( "U2", lambda d1, d2: agent2_payoff[d1_domain.index(d1), d2_domain.index(d2)]), # type: ignore ) return macid
def pure_decision_rules(self, decision: str) -> Iterator[FunctionCPD]: """Return a list of the decision rules available at the given decision""" domain = self.get_cpds(decision).domain parents = self.get_parents(decision) parent_cardinalities = [ self.get_cardinality(parent) for parent in parents ] # We begin by representing each possible decision rule as a tuple of outcomes, with # one element for each possible decision context number_of_decision_contexts = int(np.product(parent_cardinalities)) functions_as_tuples = itertools.product( domain, repeat=number_of_decision_contexts) def arg2idx(pv: Dict[str, Outcome]) -> int: """Convert a decision context into an index for the function list""" idx = 0 for i, parent in enumerate(parents): name_to_no: Dict[Outcome, int] = self.get_cpds( parent).name_to_no[parent] idx += name_to_no[pv[parent.lower()]] * int( np.product(parent_cardinalities[:i])) assert 0 <= idx <= number_of_decision_contexts return idx for func_list in functions_as_tuples: def produce_function( early_eval_func_list: tuple = func_list) -> Callable: # using a default argument is a trick to get func_list to evaluate early return lambda **parent_values: early_eval_func_list[arg2idx( parent_values)] yield FunctionCPD(decision, produce_function(), domain=domain)
def get_fork_cbn() -> CausalBayesianNetwork: cbn = CausalBayesianNetwork([("A", "C"), ("B", "C")]) cpd_a = UniformRandomCPD("A", [1, 2]) cpd_b = UniformRandomCPD("B", [3, 4]) cpd_c = FunctionCPD("C", lambda a, b: a * b) # type: ignore cbn.add_cpds(cpd_a, cpd_b, cpd_c) return cbn
def prisoners_dilemma() -> MACID: """MACIM representation of the canonical prisoner's dilemma. The prisoner's dilemma is a simultaneous symmetric two-player game with payoffs corresponding to the following normal form game - the row player is agent 1 and the column player is agent 2: +----------+----------+----------+ | |Cooperate | Defect | +----------+----------+----------+ |Cooperate | -1, -1 | -3, 0 | +----------+----------+----------+ | Defect | 0, -3 | -2, -2 | +----------+----------+----------+ This game has one pure NE: (defect, defect) """ macid = MACID( [("D1", "U1"), ("D1", "U2"), ("D2", "U2"), ("D2", "U1")], agent_decisions={ 1: ["D1"], 2: ["D2"] }, agent_utilities={ 1: ["U1"], 2: ["U2"] }, ) d1_domain = ["c", "d"] d2_domain = ["c", "d"] agent1_payoff = np.array([[-1, -3], [0, -2]]) agent2_payoff = np.transpose(agent1_payoff) macid.add_cpds( DecisionDomain("D1", d1_domain), DecisionDomain("D2", d2_domain), FunctionCPD( "U1", lambda d1, d2: agent1_payoff[d1_domain.index(d1), d2_domain.index(d2)]), # type: ignore FunctionCPD( "U2", lambda d1, d2: agent2_payoff[d1_domain.index(d1), d2_domain.index(d2)]), # type: ignore ) return macid
def test_introduced_bias_y_nodep_x( imputed_cid_introduced_bias: CID) -> None: # Modified model where Y doesn't depend on X cid = imputed_cid_introduced_bias cid.add_cpds(FunctionCPD("Y", lambda x, z: z)) # type: ignore cid.impute_conditional_expectation_decision("D", "Y") assert introduced_total_effect(cid, "A", "D", "Y", 0, 1) == pytest.approx(1 / 3)
def get_insufficient_recall_cid() -> CID: cid = CID([("A", "U"), ("B", "U")], decisions=["A", "B"], utilities=["U"]) cid.add_cpds( DecisionDomain("A", [0, 1]), DecisionDomain("B", [0, 1]), FunctionCPD("U", lambda a, b: a * b), # type: ignore ) return cid
def two_agents_three_actions() -> MACID: """This macim is a representation of a game where two players must decide between threee different actions simultaneously - the row player is agent 1 and the column player is agent 2 - the normal form representation of the payoffs is as follows: +----------+----------+----------+----------+ | | L | C | R | +----------+----------+----------+----------+ | T | 4, 3 | 5, 1 | 6, 2 | +----------+----------+----------+----------+ | M | 2, 1 | 8, 4 | 3, 6 | +----------+----------+----------+----------+ | B | 3, 0 | 9, 6 | 2, 8 | +----------+----------+----------+----------+ - The game has one pure NE (T,L) """ macid = MACID( [("D1", "U1"), ("D1", "U2"), ("D2", "U2"), ("D2", "U1")], agent_decisions={ 1: ["D1"], 2: ["D2"] }, agent_utilities={ 1: ["U1"], 2: ["U2"] }, ) d1_domain = ["T", "M", "B"] d2_domain = ["L", "C", "R"] cpd_d1 = DecisionDomain("D1", d1_domain) cpd_d2 = DecisionDomain("D2", d2_domain) agent1_payoff = np.array([[4, 5, 6], [2, 8, 3], [3, 9, 2]]) agent2_payoff = np.array([[3, 1, 2], [1, 4, 6], [0, 6, 8]]) cpd_u1 = FunctionCPD("U1", lambda d1, d2: agent1_payoff[d1_domain.index( d1), d2_domain.index(d2)]) # type: ignore cpd_u2 = FunctionCPD("U2", lambda d1, d2: agent2_payoff[d1_domain.index( d1), d2_domain.index(d2)]) # type: ignore macid.add_cpds(cpd_d1, cpd_d2, cpd_u1, cpd_u2) return macid
def get_3node_cid() -> CID: cid = CID([("S", "D"), ("S", "U"), ("D", "U")], decisions=["D"], utilities=["U"]) cpd_s = UniformRandomCPD("S", [-1, 1]) cpd_u = FunctionCPD("U", lambda s, d: s * d) # type: ignore cpd_d = DecisionDomain("D", [-1, 1]) cid.add_cpds(cpd_d, cpd_s, cpd_u) return cid
def get_quantitative_voi_cid() -> CID: cid = CID([("S", "X"), ("X", "D"), ("D", "U"), ("S", "U")], decisions=["D"], utilities=["U"]) cpd_s = UniformRandomCPD("S", [-1, 1]) # X takes the value of S with probability 0.8 cpd_x = StochasticFunctionCPD("X", lambda s: {s: 0.8}, domain=[-1, 1]) cpd_d = DecisionDomain("D", [-1, 0, 1]) cpd_u = FunctionCPD("U", lambda s, d: int(s) * int(d)) # type: ignore cid.add_cpds(cpd_s, cpd_x, cpd_d, cpd_u) return cid
def two_agent_one_pne() -> MACID: """This macim is a simultaneous two player game and has a parameterisation that corresponds to the following normal form game - where the row player is agent 1, and the column player is agent 2 +----------+----------+----------+ | | Act(0) | Act(1) | +----------+----------+----------+ | Act(0) | 1, 2 | 3, 0 | +----------+----------+----------+ | Act(1) | 0, 3 | 2, 2 | +----------+----------+----------+ """ macid = MACID( [("D1", "U1"), ("D1", "U2"), ("D2", "U2"), ("D2", "U1")], agent_decisions={ 1: ["D1"], 2: ["D2"] }, agent_utilities={ 1: ["U1"], 2: ["U2"] }, ) cpd_d1 = DecisionDomain("D1", [0, 1]) cpd_d2 = DecisionDomain("D2", [0, 1]) agent1_payoff = np.array([[1, 3], [0, 2]]) agent2_payoff = np.array([[2, 0], [3, 2]]) cpd_u1 = FunctionCPD("U1", lambda d1, d2: agent1_payoff[d1, d2]) # type: ignore cpd_u2 = FunctionCPD("U2", lambda d1, d2: agent2_payoff[d1, d2]) # type: ignore macid.add_cpds(cpd_d1, cpd_d2, cpd_u1, cpd_u2) return macid
def impute_conditional_expectation_decision(self, decision: str, y: str) -> None: """Imputes a policy for decision = the expectation of y conditioning on d's parents""" # TODO: Move to analyze, as this is not really a core feature? copy = self.copy() @lru_cache(maxsize=1000) def cond_exp_policy(**pv: Outcome) -> float: if y.lower() in pv: return pv[y.lower()] # type: ignore else: return copy.expected_value([y], pv)[0] self.add_cpds( FunctionCPD(decision, cond_exp_policy, label="cond_exp({})".format(y)))
def intervene(self, intervention: Dict[str, Outcome]) -> None: """Given a dictionary of interventions, replace the CPDs for the relevant nodes. Soft interventions can be achieved by using self.add_cpds() directly. Parameters ---------- intervention: Interventions to apply. A dictionary mapping node => value. """ self._fix_lowercase_variables(intervention) for variable in intervention: for p in self.get_parents(variable): # remove ingoing edges self.remove_edge(p, variable) self.add_cpds( FunctionCPD(variable, lambda: intervention[variable], domain=self.model.domain.get(variable, None)))
def impute_optimal_decision(self, decision: str) -> None: """Impute an optimal policy to the given decision node""" # self.add_cpds(random.choice(self.optimal_pure_decision_rules(d))) self.impute_random_decision(decision) domain = self.get_cpds(decision).domain utility_nodes = self.agent_utilities[self.decision_agent[decision]] descendant_utility_nodes = list( set(utility_nodes).intersection(nx.descendants(self, decision))) copy = self.copy( ) # using a copy "freezes" the policy so it doesn't adapt to future interventions @lru_cache(maxsize=1000) def opt_policy(**parent_values: Outcome) -> Outcome: eu = {} for d in domain: parent_values[decision] = d eu[d] = sum( copy.expected_value(descendant_utility_nodes, parent_values)) return max(eu, key=eu.get) # type: ignore self.add_cpds( FunctionCPD(decision, opt_policy, domain=domain, label="opt"))
def get_minimal_cid() -> CID: cid = CID([("A", "B")], decisions=["A"], utilities=["B"]) cpd_a = DecisionDomain("A", [0, 1]) cpd_b = FunctionCPD("B", lambda a: a) # type: ignore cid.add_cpds(cpd_a, cpd_b) return cid
def get_minimal_cbn() -> CausalBayesianNetwork: cbn = CausalBayesianNetwork([("A", "B")]) cpd_a = UniformRandomCPD("A", [0, 1]) cpd_b = FunctionCPD("B", lambda a: a) # type: ignore cbn.add_cpds(cpd_a, cpd_b) return cbn
def test_initialize_function_cpd(self) -> None: cid = get_minimal_cid() cpd_a = FunctionCPD("A", lambda: 2) cpd_a.initialize_tabular_cpd(cid) self.assertTrue(cpd_a.get_values(), np.array([[1]])) self.assertEqual(cpd_a.get_cardinality(["A"])["A"], 1) self.assertEqual(cpd_a.get_state_names("A", 0), 2) cpd_b = FunctionCPD("B", lambda a: a) # type: ignore cpd_b.initialize_tabular_cpd(cid) self.assertTrue(cpd_a.get_values(), np.array([[1]])) self.assertEqual(cpd_a.get_cardinality(["A"])["A"], 1) self.assertEqual(cpd_a.get_state_names("A", 0), 2)