Source code for cegpy.graphs._ceg_reducer

"""Reduced Chain Event Graph"""

from copy import deepcopy
from typing import Iterable, List, Set, Tuple
import networkx as nx

from cegpy.graphs._ceg import ChainEventGraph


[docs]class ChainEventGraphReducer: """ Reduces Chain Event Graphs given certain and/or uncertain evidence. :param ceg: Chain event graph object to reduce. :type ceg: ChainEventGraph """ # pylint: disable=too-many-instance-attributes,too-many-public-methods _path_list: List[List[Tuple[str]]] def __init__(self, ceg: ChainEventGraph): self._ceg = ceg self._path_list = ceg.path_list self._certain_edges = [] self._uncertain_edges = [] self._certain_nodes = set() self._uncertain_nodes = [] self._edges = set() self._vertices = set() def __repr__(self) -> str: return ( f"Evidence(certain_edges={str(self.certain_edges)}, " f"certain_nodes={str(self.certain_nodes)}, " f"uncertain_edges={str(self.uncertain_edges)}, " f"uncertain_nodes={str(self.uncertain_nodes)})" ) def __str__(self) -> str: """Returns human readable version of the evidence you've provided.""" def evidence_str(base_str, edges, nodes): if edges == []: base_str += " Edges = []\n" else: base_str += " Edges = [\n" for edge in edges: base_str += f" {str(edge)},\n" base_str += " ]\n" if nodes == set(): base_str += " Nodes = {}\n" else: base_str += " Nodes = {\n" for node in nodes: base_str += f" {str(node)},\n" base_str += " }\n\n" return base_str base_str = "The evidence you have given is as follows:\n" base_str += " Evidence you are certain of:\n" base_str = evidence_str(base_str, self.certain_edges, self.certain_nodes) base_str += " Evidence you are uncertain of:\n" base_str = evidence_str(base_str, self.uncertain_edges, self.uncertain_nodes) return base_str @property def certain_edges(self) -> List[Tuple[str]]: """ :return: A list of all edges of the ChainEventGraph that have been observed. `certain_edges` is a list of edge tuples of the form: `[edge_1, edge_2, ... edge_n]` Each edge tuple takes the form: `("source_node_name", "destination_node_name", "edge_label")` """ return self._certain_edges @property def uncertain_edges(self) -> List[Tuple[str]]: """ :return: A list of sets of edges of the ChainEventGraph which might have occurred. `uncertain_edges` is a list of sets of edge tuples of the form: `[{(a, b, label), (a, c, label)}, {(x, y, label), (x, z, label)}]` Each edge tuple takes the form: `("source_node_name", "destination_node_name", "edge_label")` """ return self._uncertain_edges @property def certain_nodes(self) -> Set[str]: """ :return: A set of all nodes of the ChainEventGraph that have been observed. `certain_nodes` is a set of nodes of the form: `{"node_1", "node_2", "node_3", ... "node_n"}` """ return self._certain_nodes @property def uncertain_nodes(self) -> List[Set[str]]: """ :return: A list of sets of nodes of the ChainEventGraph where there is uncertainty which of the nodes in each set happened. `uncertain_nodes` is a list of sets of nodes of the form: `[{"node_1", "node_2"}, {"node_3", "node_4"}, ...]` """ return self._uncertain_nodes @property def paths(self) -> List[List[Tuple[str]]]: """ :return: A list of all paths through the reduced ChainEventGraph. """ return self._path_list @property def graph(self) -> ChainEventGraph: """ :return: The reduced graph once all evidence has been taken into account. :rtype: ChainEventGraph """ self._update_path_list() self._update_edges_and_vertices() subgraph_view = nx.subgraph_view( G=self._ceg, filter_node=self._filter_node, filter_edge=self._filter_edge ) reduced_graph = ChainEventGraph(subgraph_view, generate=False).copy() self._propagate_reduced_graph_probabilities(reduced_graph) return reduced_graph
[docs] def clear_all_evidence(self) -> None: """Resets the evidence provided.""" self._certain_edges = [] self._uncertain_edges = [] self._certain_nodes = set() self._uncertain_nodes = [] self._edges = set() self._vertices = set()
[docs] def add_certain_edge(self, src: str, dst: str, label: str) -> None: """ Specify an edge that has been observed. :param src: Edge source node label :type src: str :param dst: Edge destination node label :type dst: str :param label: Label of certain edge :type label: str """ edge = (src, dst, label) if edge not in self._ceg.edges: raise ValueError( f"This edge {edge}, does not exist" f" in the Chain Event Graph." ) self._certain_edges.append(edge)
[docs] def remove_certain_edge(self, src: str, dst: str, label: str) -> None: """ Specify an edge to remove from the certain edges. :param src: Edge source node label :type src: str :param dst: Edge destination node label :type dst: str :param label: Label of certain edge :type label: str """ try: edge = (src, dst, label) self._certain_edges.remove(edge) except ValueError as err: raise ValueError( f"Edge {(src, dst, label)} not found in the certain " f"edge list." ) from err
[docs] def add_certain_edge_list(self, edges: List[Tuple[str]]) -> None: """ Specify a list of edges that have all been observed. :param edges: List of edge tuples of the form ("src", "dst", "label") :type edges: List[Tuple[str]] """ for edge in edges: self.add_certain_edge(*edge)
[docs] def remove_certain_edge_list(self, edges: List[Tuple[str]]) -> None: """ Specify a list of edges that in the certain edge list to remove. :param edges: List of edge tuples of the form ("src", "dst", "label") :type edges: List[Tuple[str]] """ for edge in edges: self.remove_certain_edge(*edge)
[docs] def add_uncertain_edge_set(self, edge_set: Set[Tuple[str]]) -> None: """ Specify a set of edges where one of the edges has occurred, but you are uncertain of which one it is. :param edge_set: Set of edge tuples of the form ("src", "dst", "label") :type edge_set: Set[Tuple[str]] """ for edge in edge_set: if edge not in self._ceg.edges: raise ValueError( f"The edge {edge}, does not exist" f" in the Chain Event Graph." ) self._uncertain_edges.append(edge_set)
[docs] def remove_uncertain_edge_set(self, edge_set: Set[Tuple[str]]) -> None: """ Specify a set of edges to remove from the uncertain edges. :param edge_set: Set of edge tuples of the form ("src", "dst", "label") :type edge_set: Set[Tuple[str]] """ try: self._uncertain_edges.remove(edge_set) except ValueError as err: raise ValueError( f"{edge_set} not found in the uncertain edge list." ) from err
[docs] def add_uncertain_edge_set_list(self, edge_sets: List[Set[Tuple[str]]]) -> None: """ Specify a list of sets of edges where one of the edges has occurred, but you are uncertain of which one it is. :param edge_set: List of sets of edge tuples of the form ("src", "dst", "label") :type edge_set: List[Set[Tuple[str]]] """ for edge_set in edge_sets: self.add_uncertain_edge_set(edge_set)
[docs] def remove_uncertain_edge_set_list(self, edge_sets: List[Set[Tuple[str]]]) -> None: """ Specify a list of sets of edges to remove from the evidence list. :param edge_set: List of sets of edge tuples of the form ("src", "dst", "label") :type edge_set: List[Set[Tuple[str]]] """ for edge_set in edge_sets: self.remove_uncertain_edge_set(edge_set)
[docs] def add_certain_node(self, node: str) -> None: """Specify a node in the graph that has been observed. :param node: A node label e.g. "w4" :type node: str """ if node not in self._ceg.nodes: raise ValueError( f"The node {node}, does not exist in the Chain Event Graph." ) self._certain_nodes.add(node)
[docs] def remove_certain_node(self, node: str) -> None: """ Specify a node to be removed from the certain nodes list. :param node: A node label e.g. "w4" :type node: str """ try: self._certain_nodes.remove(node) except KeyError as err: raise ValueError( f"Node {node} not found in the set of certain nodes." ) from err
[docs] def add_certain_node_set(self, nodes: Set[str]) -> None: """ Specify a set of nodes that have been observed. :param nodes: A set of node labels e.g. {"w4", "w8"} :type nodes: Set[str] """ for node in nodes: self.add_certain_node(node)
[docs] def remove_certain_node_set(self, nodes: Set[str]) -> None: """ Specify a list of nodes to remove from the list of nodes that have been observed. :param nodes: A set of node labels e.g. {"w4", "w8"} :type nodes: Set[str] """ for node in nodes: self.remove_certain_node(node)
[docs] def add_uncertain_node_set(self, node_set: Set[str]) -> None: """ Specify a set of nodes where one of the nodes has occurred, but you are uncertain of which one it is. :param nodes: A set of node labels e.g. {"w4", "w8"} :type nodes: Set[str] """ for node in node_set: if node not in self._ceg.nodes: raise ValueError( f"The node {node}, does not exist" f" in the Chain Event Graph." ) self._uncertain_nodes.append(node_set)
[docs] def remove_uncertain_node_set(self, node_set: Set[str]) -> None: """ Specify a set of nodes to be removed from the uncertain nodes set list. :param nodes: A set of node labels e.g. {"w4", "w8"} :type nodes: Set[str] """ try: self._uncertain_nodes.remove(node_set) except ValueError as err: raise ValueError( f"{node_set} not found in the uncertain node list." ) from err
[docs] def add_uncertain_node_set_list(self, node_sets: List[Set[str]]) -> None: """ Specify a list of sets of nodes where in each set, one of the nodes has occurred, but you are uncertain of which one it is. :param nodes: A collection of sets of uncertain nodes. :type nodes: List[Set[str]] """ for node_set in node_sets: self.add_uncertain_node_set(node_set)
[docs] def remove_uncertain_node_set_list(self, node_sets: List[Set[str]]) -> None: """ Specify a list of sets nodes to remove from the list of uncertain sets of nodes. :param nodes: A collection of sets of uncertain nodes. :type nodes: List[Set[str]] """ for node_set in node_sets: self.remove_uncertain_node_set(node_set)
@staticmethod def _remove_paths(paths: List, to_remove: Iterable): for path in to_remove: paths.remove(path) def _apply_certain_edges(self, paths): to_remove = [] for edge in self.certain_edges: for path in paths: if (edge not in path) and (path not in to_remove): to_remove.append(path) self._remove_paths(paths, to_remove) def _apply_certain_nodes(self, paths): to_remove = [] for node in self.certain_nodes: for path in paths: for (src, dst, _) in path: if node in (src, dst): break else: if path not in to_remove: to_remove.append(path) self._remove_paths(paths, to_remove) def _apply_uncertain_edges(self, paths): to_remove = [] def find_edge_and_add_to_remove(): edge_found = False for edge in path: if edge in edge_set: if edge_found: if path not in to_remove: to_remove.append(path) break edge_found = True else: if not edge_found and path not in to_remove: to_remove.append(path) for edge_set in self.uncertain_edges: for path in paths: find_edge_and_add_to_remove() self._remove_paths(paths, to_remove) def _apply_uncertain_nodes(self, paths): to_remove = [] def find_node_and_add_to_remove(): node_found = False for (src, _, _) in path: if src in node_set: if node_found: if path not in to_remove: to_remove.append(path) break node_found = True else: if not node_found and path not in to_remove: to_remove.append(path) for node_set in self.uncertain_nodes: for path in paths: find_node_and_add_to_remove() self._remove_paths(paths, to_remove) def _update_path_list(self): path_list = deepcopy(self._ceg.path_list) if self.certain_edges: self._apply_certain_edges(path_list) if self.certain_nodes: self._apply_certain_nodes(path_list) if self.uncertain_edges: self._apply_uncertain_edges(path_list) if self.uncertain_nodes: self._apply_uncertain_nodes(path_list) self._path_list = path_list def _update_edges_and_vertices(self): edges = set() vertices = set() for path in self.paths: for (src, dst, label) in path: edges.add((src, dst, label)) vertices.add(src) vertices.add(dst) self._edges = edges self._vertices = vertices def _filter_edge(self, src, dst, label) -> bool: return bool((src, dst, label) in self._edges) def _filter_node(self, node) -> bool: return bool(node in self._vertices) @staticmethod def _propagate_reduced_graph_probabilities(graph: ChainEventGraph) -> None: # pylint: disable=too-many-locals sink = graph.sink root = graph.root graph.nodes[sink]["emphasis"] = 1 node_set = set([sink]) while node_set != {root}: try: dst = node_set.pop() except KeyError as err: raise KeyError( "Graph has more than one root... Propagation cannot continue." ) from err for src, edges in graph.pred[dst].items(): for edge_label, data in edges.items(): edge = (src, dst, edge_label) emph = graph.nodes[dst]["emphasis"] prob = data["probability"] graph.edges[edge]["potential"] = prob * emph successors = graph.succ[src] try: emphasis = 0 for _, succ_edges in successors.items(): for edge_label, data in succ_edges.items(): emphasis += data["potential"] graph.nodes[src]["emphasis"] = emphasis node_set.add(src) except KeyError: pass for (src, dst, label) in list(graph.edges): edge_potential = graph.edges[(src, dst, label)]["potential"] pred_node_emphasis = graph.nodes[src]["emphasis"] probability = edge_potential / pred_node_emphasis graph.edges[(src, dst, label)]["probability"] = probability