Example #1
0
def _add_sufficient_recall(mb: MACIDBase, d1: str, d2: str,
                           utility_node: str) -> None:
    """Add edges to a (MA)CID until an agent at `d2` has sufficient recall of `d1` to optimise utility_node.

    d1, d2 and utility node all belong to the same agent.

    `d2' has sufficient recall of `d1' if d2 does not strategically rely on d1. This means
    that d1 is not s-reachable from d2.

    edges are added from non-collider nodes along an active path from the mechanism of `d1' to
    somue utilty node descended from d2 until recall is sufficient.
    """

    if d2 in mb._get_ancestors_of(d1):
        raise ValueError("{} is an ancestor of {}".format(d2, d1))

    mg = MechanismGraph(mb)
    while mg.is_active_trail(d1 + "mec",
                             utility_node,
                             observed=mg.get_parents(d2) + [d2]):
        path = find_active_path(mg, d1 + "mec", utility_node,
                                {d2, *mg.get_parents(d2)})
        if path is None:
            raise RuntimeError(
                "couldn't find path even though there should be an active trail"
            )
        while True:
            idx = random.randrange(1, len(path) - 1)
            if get_motif(mg, path, idx) != "collider":
                if d2 not in mg._get_ancestors_of(
                        path[idx]):  # to prevent cycles
                    mb.add_edge(path[idx], d2)
                    mg.add_edge(path[idx], d2)
                    break
Example #2
0
def _add_random_cpds(mb: MACIDBase) -> None:
    """
    add cpds to the random (MA)CID.
    """
    for node in mb.nodes:
        if node in mb.decisions:
            mb.add_cpds(DecisionDomain(node, [0, 1]))
        else:
            mb.add_cpds(RandomCPD(node))
Example #3
0
def add_random_cpds(mb: MACIDBase) -> None:
    """
    add cpds to the random (MA)CID.
    """
    node_cpds: Dict[str, Relationship] = {}
    for node in mb.nodes:
        if node in mb.decisions:
            node_cpds[node] = [0, 1]
        else:
            node_cpds[node] = RandomCPD()
    mb.add_cpds(**node_cpds)
Example #4
0
 def __init__(self, cid: MACIDBase, decisions: Iterable[str] = None):
     super().__init__()
     if decisions is None:
         decisions = cid.decisions
     self.add_nodes_from(decisions)
     dec_pair_perms = list(itertools.permutations(decisions, 2))
     for dec_pair in dec_pair_perms:
         if cid.is_s_reachable(dec_pair[0], dec_pair[1]):
             self.add_edge(dec_pair[0], dec_pair[1])
Example #5
0
def _check_max_in_degree(mb: MACIDBase, max_in_degree: int) -> bool:
    """
    check that the degree of each vertex in the DAG is less than the set maximum.
    """
    for node in mb.nodes:
        if mb.in_degree(node) > max_in_degree:
            return False
    else:
        return True
Example #6
0
def requisite_graph(cid: MACIDBase) -> MACIDBase:
    """The requiste graph of the original CID.

    The requisite graph is also called a minimal reduction, d reduction, or the trimmed graph.

    The requisite graph G∗ of a multi-decision CID G is the result of repeatedely
    removing from G all nonrequisite observation links.
    ("Representing and Solving Decision Problems with Limited Information", Lauritzen and Nielsen, 2001)
    """
    requisite_graph = cid.copy()
    decisions = cid.get_valid_order()

    for decision in reversed(decisions):
        non_requisite_nodes = set(cid.get_parents(decision)) - set(
            requisite_list(requisite_graph, decision))
        for nr in non_requisite_nodes:
            requisite_graph.remove_edge(nr, decision)
    return requisite_graph
Example #7
0
def requisite(cid: MACIDBase, decision: str, node: str) -> bool:
    r"""Check if a CID node is a requisite observation for a decision.

    A node is a requisite observation if it is possibly material.
    A node can be material if:
        i) it is a parent of D.
        ii) X is d-connected to (U ∩ Desc(D)) given Fa_D \ {X}
    "A note about redundancy in influence diagrams" Fagiuoli and Zaffalon, 1998.

    Returns True if the node is requisite.
    """
    if node not in cid.get_parents(decision):
        raise KeyError(f"{node} is not a parent of {decision}")

    agent_utilities = cid.agent_utilities[cid.decision_agent[decision]]
    descended_agent_utilities = set(agent_utilities).intersection(
        nx.descendants(cid, decision))
    family_d = [decision] + cid.get_parents(decision)
    conditioning_nodes = [i for i in family_d if i != node]
    return any(
        cid.is_active_trail(node, u_node, conditioning_nodes)
        for u_node in descended_agent_utilities)
Example #8
0
def add_sufficient_recalls(mb: MACIDBase) -> None:
    """add edges to a macid until all agents have sufficient recall of all of their previous decisions"""
    agents = mb.agents
    for agent in agents:
        decisions = mb.agent_decisions[agent]
        for utility_node in mb.agent_utilities[agent]:
            for i, dec1 in enumerate(decisions):
                for dec2 in decisions[i + 1 :]:
                    if dec1 in mb._get_ancestors_of(dec2):
                        if utility_node in nx.descendants(mb, dec2):
                            _add_sufficient_recall(mb, dec1, dec2, utility_node)
                    else:
                        if utility_node in nx.descendants(mb, dec1):
                            _add_sufficient_recall(mb, dec2, dec1, utility_node)
Example #9
0
def random_macidbase(
    number_of_nodes: int = 10,
    agent_decisions_num: Tuple[int, ...] = (1, 2),
    agent_utilities_num: Tuple[int, ...] = (2, 1),
    add_cpds: bool = False,
    sufficient_recall: bool = False,
    edge_density: float = 0.4,
    max_in_degree: int = 4,
    max_resampling_attempts: int = 5000,
) -> MACIDBase:
    """
    Generate a random MACIDBase

    Returns
    -------
    A MACIDBase that satisfies the given constraints or a ValueError if it was unable to meet
     the constraints in the specified number of attempts.

    """
    if len(agent_decisions_num) != len(agent_utilities_num):
        raise ValueError(
            f"The number of agents specified for agent_decisions_num {len(agent_decisions_num)} does not match \
        the number of agents specified for agent_utilities_num {len(agent_utilities_num)}"
        )

    for _ in range(max_resampling_attempts):

        dag = random_dag(number_of_nodes=number_of_nodes,
                         edge_density=edge_density,
                         max_in_degree=max_in_degree)

        # assign utility nodes to each agent based on the barren nodes in the random dag
        barren_nodes = [
            node for node in dag.nodes if not list(dag.successors(node))
        ]
        if sum(agent_utilities_num) > len(barren_nodes):
            # there are not enough barren_nodes: resample a new random DAG.
            continue
        np.random.shuffle(barren_nodes)  # randomise
        util_node_candidates = iter(barren_nodes)
        agent_utilities_old_name = {
            agent: [next(util_node_candidates) for _ in range(num)]
            for agent, num in enumerate(agent_utilities_num)
        }

        used_nodes = set()  # type: Set[str]
        agent_decisions: Mapping[AgentLabel, Iterable[str]] = {}
        agent_utilities: Mapping[AgentLabel, Iterable[str]] = {}
        node_name_change_map: Dict[str, str] = {}

        for agent in agent_utilities_old_name.keys():
            # assign decision nodes to agent
            num_decs = agent_decisions_num[agent]
            agent_utils = agent_utilities_old_name[agent]
            possible_dec_nodes: Set[str] = (
                set().union(
                    *
                    [set(dag._get_ancestors_of(node))
                     for node in agent_utils]) - set(agent_utils) -
                used_nodes  # type: ignore
            )
            if num_decs > len(possible_dec_nodes):
                break
            agent_decs = random.sample(possible_dec_nodes, num_decs)
            used_nodes.update(agent_decs)

            # rename decision and utility nodes
            agent_util_name_change = {
                old_util_name: "U^" + str(agent) + "_" + str(i)
                for i, old_util_name in enumerate(agent_utils)
            }
            agent_dec_name_change = {
                old_dec_name: "D^" + str(agent) + "_" + str(i)
                for i, old_dec_name in enumerate(agent_decs)
            }
            agent_utilities[agent] = list(
                agent_util_name_change.values())  # type: ignore
            agent_decisions[agent] = list(
                agent_dec_name_change.values())  # type: ignore
            node_name_change_map.update(**agent_util_name_change,
                                        **agent_dec_name_change)

        else:
            # rename chance nodes
            chance_nodes = [
                node for node in dag.nodes
                if node not in node_name_change_map.keys()
            ]
            chance_name_change = {
                old_chance_name: "X_" + str(i)
                for i, old_chance_name in enumerate(chance_nodes)
            }
            node_name_change_map.update(chance_name_change)

            dag = nx.relabel_nodes(dag, node_name_change_map)
            mb = MACIDBase(dag.edges,
                           agent_decisions=agent_decisions,
                           agent_utilities=agent_utilities)

            if sufficient_recall:
                _add_sufficient_recalls(mb)
                if not _check_max_in_degree(mb, max_in_degree):
                    # adding edges for sufficient recall requirement violates max_in_degree: resample a new random DAG
                    continue

            if add_cpds:
                add_random_cpds(mb)

            return mb
        continue
    else:
        raise ValueError(
            f"Could not create a MACID satisfying all constraints in {max_resampling_attempts} sampling attempts"
        )
Example #10
0
def random_macidbase(
    number_of_nodes: int = 8,
    number_of_agents: int = 2,
    max_decisions_for_agent: int = 1,
    max_utilities_for_agent: int = 1,
    add_cpds: bool = False,
    sufficient_recall: bool = False,
    edge_density: float = 0.4,
    max_in_degree: int = 4,
    max_resampling_attempts: int = 1000,
) -> MACIDBase:
    """
    Generate a random MACIDBase

    Returns
    -------
    A MACIDBase that satisfies the given constraints or a ValueError if it was unable to meet
     the constraints in the specified number of attempts.

    """
    for _ in range(max_resampling_attempts):

        dag = random_dag(number_of_nodes=number_of_nodes, edge_density=edge_density, max_in_degree=max_in_degree)

        barren_nodes = [node for node in dag.nodes if not list(dag.successors(node))]
        if max_utilities_for_agent * number_of_agents > len(barren_nodes):
            # there are not enough barren_nodes: resample a new random DAG.
            continue

        agent_utilities, util_nodes_name_change = _create_random_utility_nodes(
            number_of_agents, max_utilities_for_agent, barren_nodes
        )
        dag = nx.relabel_nodes(dag, util_nodes_name_change)

        agent_decisions: Mapping[AgentLabel, Iterable[str]] = {}
        all_dec_name_change: Dict[str, str] = {}
        used_nodes = set()  # type: Set[str]
        for agent in range(number_of_agents):
            agent_utils = agent_utilities[agent]

            ancestors = set()  # type: Set[str]
            possible_dec_nodes = (
                ancestors.union(*[set(dag._get_ancestors_of(node)) for node in agent_utils])
                - set(agent_utils)
                - used_nodes
            )
            if not possible_dec_nodes:
                # this agent has no possible decision nodes: resample a new random DAG.
                break

            # in the single-agent CID setting, we want the number of decisions to be equal to what we we specified:
            if number_of_agents == 1:
                number_of_decisions = max_decisions_for_agent
                if number_of_decisions > len(possible_dec_nodes):
                    # there are not enough possible decision nodes: resample a new random DAG
                    break
                sample_dec_nodes = random.sample(possible_dec_nodes, number_of_decisions)

            # in the multi-agent CID setting, the number of decisions for each agent can vary, but should never be
            # more than the max_decisions_for_agent we specified.
            else:
                sample_dec_nodes = random.sample(
                    possible_dec_nodes, min(len(possible_dec_nodes), max_decisions_for_agent)
                )
                used_nodes.update(sample_dec_nodes)

            dec_name_change = {
                old_dec_name: "D^" + str(agent) + "_" + str(i) for i, old_dec_name in enumerate(sample_dec_nodes)
            }
            agent_decisions[agent] = list(dec_name_change.values())  # type: ignore
            all_dec_name_change.update(dec_name_change)

        else:
            dag = nx.relabel_nodes(dag, all_dec_name_change)

            mb = MACIDBase(dag.edges, agent_decisions=agent_decisions, agent_utilities=agent_utilities)

            if sufficient_recall:
                add_sufficient_recalls(mb)
                if not _check_max_in_degree(mb, max_in_degree):
                    # adding edges for sufficient recall requirement violates max_in_degree: resample a new random DAG
                    continue

            if add_cpds:
                _add_random_cpds(mb)

            return mb

        continue
    else:
        raise ValueError(
            f"Could not create a MACID satisfying all constraints in {max_resampling_attempts} sampling attempts"
        )
Example #11
0
def requisite_list(cid: MACIDBase, decision: str) -> List[str]:
    """Returns list of requisite nodes for decision"""
    return [
        node for node in cid.get_parents(decision)
        if requisite(cid, decision, node)
    ]