def _add_sufficient_recall(cid: CID, dec1: str, dec2: str, utility_node: str) -> None: """Add edges to a cid until `dec2` has sufficient recall of `dec1` (to optimize utility) this is done by adding edges from non-collider nodes until recall is adequate """ if dec2 in cid._get_ancestors_of(dec1): raise ValueError('{} is an ancestor of {}'.format(dec2, dec1)) cid2 = cid.copy() cid2.add_edge('pi', dec1) while cid2.is_active_trail('pi', utility_node, observed=cid.get_parents(dec2) + [dec2]): path = find_active_path(cid2, 'pi', utility_node, cid.get_parents(dec2) + [dec2]) if path is None: raise Exception( "couldn't find path even though there should be an active trail" ) while True: i = random.randrange(1, len(path) - 1) # print('consider {}--{}--{}'.format(path[i-1], path[i], path[i+1]),end='') collider = ((path[i - 1], path[i]) in cid2.edges) and ( (path[i + 1], path[i]) in cid2.edges) if not collider: if dec2 not in cid2._get_ancestors_of(path[i]): # print('add {}->{}'.format(path[i], dec2), end=' ') cid.add_edge(path[i], dec2) cid2.add_edge(path[i], dec2) break
def admits_voi(cid: CID, decision: str, node: str) -> bool: """Return True if cid admits value of information for node. - A CID admits value of information for a node X if: i) X is not a descendant of the decision node, D. ii) X is d-connected to U given Fa_D \ {X}, where U ∈ U ∩ Desc(D) ("Agent Incentives: a Causal Perspective" by Everitt, Carey, Langlois, Ortega, and Legg, 2020) """ agent_utilities = cid.all_utility_nodes if node not in cid.nodes: raise Exception(f"{node} is not present in the cid") if decision not in cid.nodes: raise Exception(f"{decision} is not present in the cid") # condition (i) elif node == decision or node in nx.descendants(cid, decision): return False # condition (ii) descended_agent_utilities = [ util for util in agent_utilities if util in nx.descendants(cid, decision) ] d_family = [decision] + cid.get_parents(decision) con_nodes = [i for i in d_family if i != node] voi = any([ cid.is_active_trail(node, u_node, con_nodes) for u_node in descended_agent_utilities ]) return voi
def admits_indir_voc(cid: CID, decision: str, node: str) -> bool: """ Return True if a single-decision cid admits indirect positive value of control for node. - A single-decision CID G admits positive value of control for a node X ∈ V \ {D} if and only if there is a directed path X --> U in the reduced graph G∗. - The path X --> U may or may not pass through D. - The agent has a direct value of control incentive on D if the path does not pass through D. - The agent has an indirect value of control incentive on D if the path does pass through D and there is also a backdoor path X--U that begins backwards from X (...<- X) and is active when conditioning on Fa_D \ {X} """ if node not in cid.nodes: raise Exception(f"{node} is not present in the cid") if decision not in cid.nodes: raise Exception(f"{decision} is not present in the cid") agent_utilities = cid.all_utility_nodes req_graph = requisite_graph(cid) d_family = [decision] + cid.get_parents(decision) con_nodes = [i for i in d_family if i != node] if not admits_voc(cid, decision, node): return False for util in agent_utilities: if node == util or util in nx.descendants(req_graph, node): backdoor_exists = is_active_backdoor_trail(req_graph, node, util, con_nodes) x_u_paths = find_all_dir_paths(req_graph, node, util) if any(decision in paths for paths in x_u_paths) and backdoor_exists: return True return False
def random_cid(n_all: int, n_decisions: int, n_utilities: int, edge_density: float = 0.4, add_sr_edges: bool = True, add_cpds: bool = True, seed: int = None) -> CID: """Generates a random Cid with the specified number of nodes and edges""" all_names, decision_names, utility_names = get_node_names( n_all, n_decisions, n_utilities) edges = get_edges(all_names, utility_names, edge_density, seed=seed, allow_u_edges=False) cid = CID(edges, decision_names, utility_names) for uname in utility_names: for edge in edges: assert uname != edge[0] for i, d1 in enumerate(decision_names): for j, d2 in enumerate(decision_names[i + 1:]): assert d2 not in cid._get_ancestors_of(d1) if add_sr_edges: add_sufficient_recalls(cid) if add_cpds: for node in cid.nodes: if node in cid.all_decision_nodes: cid.add_cpds(DecisionDomain(node, [0, 1])) elif not cid.get_parents(node): # node is a root node cid.add_cpds(UniformRandomCPD(node, [0, 1])) else: cid.add_cpds( RandomlySampledFunctionCPD(node, cid.get_parents(node))) return cid