Module lang_main.analysis.graphs

Functions

def add_betweenness_centrality(graph: DiGraph | Graph,
edge_weight_property: str | None = None,
property_name: str = 'betweenness_centrality') ‑> None
Expand source code
def add_betweenness_centrality(
    graph: DiGraph | Graph,
    edge_weight_property: str | None = None,
    property_name: str = PROPERTY_NAME_BETWEENNESS_CENTRALITY,
) -> None:
    """adds the betweenness centrality as property to each node of the given graph
    Operation is performed inplace.

    Parameters
    ----------
    graph : DiGraph | Graph
        Graph with betweenness centrality as node property added inplace
    edge_weight_property : str | None, optional
        property of the edges which contains the weight information,
        not necessarily needed, by default 'None'
    property_name : str, optional
        target name for the property containing the betweenness centrality in nodes,
        by default PROPERTY_NAME_BETWEENNESS_CENTRALITY
    """

    node_property_mapping = cast(
        dict[str, float],
        nx.betweenness_centrality(graph, normalized=True, weight=edge_weight_property),  # type: ignore
    )
    nx.set_node_attributes(
        graph,
        node_property_mapping,
        name=property_name,
    )

adds the betweenness centrality as property to each node of the given graph Operation is performed inplace.

Parameters

graph : DiGraph | Graph
Graph with betweenness centrality as node property added inplace
edge_weight_property : str | None, optional
property of the edges which contains the weight information, not necessarily needed, by default 'None'
property_name : str, optional
target name for the property containing the betweenness centrality in nodes, by default PROPERTY_NAME_BETWEENNESS_CENTRALITY
def add_importance_metric(graph: DiGraph | Graph,
property_name: str = 'importance',
property_name_weighted_degree: str = 'degree_weighted',
property_name_betweenness: str = 'betweenness_centrality') ‑> None
Expand source code
def add_importance_metric(
    graph: DiGraph | Graph,
    property_name: str = PROPERTY_NAME_IMPORTANCE,
    property_name_weighted_degree: str = PROPERTY_NAME_DEGREE_WEIGHTED,
    property_name_betweenness: str = PROPERTY_NAME_BETWEENNESS_CENTRALITY,
) -> None:
    """Adds a custom importance metric as property to each node of the given graph.
    Can be used to decide which nodes are of high importance and also to build node size
    mappings.
    Operation is performed inplace.

    Parameters
    ----------
    graph : DiGraph | Graph
        Graph with weighted degree as node property added inplace
    property_name : str, optional
        target name for the property containing the weighted degree in nodes,
        by default PROPERTY_NAME_DEGREE_WEIGHTED
    property_name_betweenness : str, optional
        target name for the property containing the betweenness centrality in nodes,
        by default PROPERTY_NAME_BETWEENNESS_CENTRALITY
    """
    # build mapping for importance metric
    node_property_mapping: dict[str, float] = {}
    for node in cast(Iterable[str], graph.nodes):
        node_data = cast(dict[str, float], graph.nodes[node])

        if property_name_weighted_degree not in node_data:
            raise NodePropertyNotContainedError(
                (
                    f'Node data does not contain weighted degree '
                    f'with name {property_name_weighted_degree}.'
                )
            )
        elif property_name_betweenness not in node_data:
            raise NodePropertyNotContainedError(
                (
                    f'Node data does not contain betweenness centrality '
                    f'with name {property_name_betweenness}.'
                )
            )

        prio = node_data[property_name_weighted_degree] * node_data[property_name_betweenness]
        node_property_mapping[node] = prio

    nx.set_node_attributes(
        graph,
        node_property_mapping,
        name=property_name,
    )

Adds a custom importance metric as property to each node of the given graph. Can be used to decide which nodes are of high importance and also to build node size mappings. Operation is performed inplace.

Parameters

graph : DiGraph | Graph
Graph with weighted degree as node property added inplace
property_name : str, optional
target name for the property containing the weighted degree in nodes, by default PROPERTY_NAME_DEGREE_WEIGHTED
property_name_betweenness : str, optional
target name for the property containing the betweenness centrality in nodes, by default PROPERTY_NAME_BETWEENNESS_CENTRALITY
def add_weighted_degree(graph: DiGraph | Graph,
edge_weight_property: str = 'weight',
property_name: str = 'degree_weighted') ‑> None
Expand source code
def add_weighted_degree(
    graph: DiGraph | Graph,
    edge_weight_property: str = 'weight',
    property_name: str = PROPERTY_NAME_DEGREE_WEIGHTED,
) -> None:
    """adds the weighted degree as property to each node of the given graph
    Operation is performed inplace.

    Parameters
    ----------
    graph : DiGraph | Graph
        Graph with weighted degree as node property added inplace
    edge_weight_property : str, optional
        property of the edges which contains the weight information, by default 'weight'
    property_name : str, optional
        target name for the property containing the weighted degree in nodes,
        by default PROPERTY_NAME_DEGREE_WEIGHTED
    """
    node_property_mapping = cast(
        dict[str, float],
        dict(graph.degree(weight=edge_weight_property)),  # type: ignore
    )
    nx.set_node_attributes(
        graph,
        node_property_mapping,
        name=property_name,
    )

adds the weighted degree as property to each node of the given graph Operation is performed inplace.

Parameters

graph : DiGraph | Graph
Graph with weighted degree as node property added inplace
edge_weight_property : str, optional
property of the edges which contains the weight information, by default 'weight'
property_name : str, optional
target name for the property containing the weighted degree in nodes, by default PROPERTY_NAME_DEGREE_WEIGHTED
def convert_graph_to_cytoscape(graph: Graph | DiGraph) ‑> tuple[list[lang_main.types.CytoscapeData], lang_main.types.WeightData]
Expand source code
def convert_graph_to_cytoscape(
    graph: Graph | DiGraph,
) -> tuple[list[CytoscapeData], WeightData]:
    cyto_data: list[CytoscapeData] = []
    # iterate over nodes
    nodes = cast(Iterable[NodeTitle], graph.nodes)
    for node in nodes:
        node_data: CytoscapeData = {
            'data': {
                'id': node,
                'label': node,
            }
        }
        cyto_data.append(node_data)
    # iterate over edges
    weights: set[int] = set()

    edges = cast(
        Iterable[
            tuple[
                NodeTitle,
                NodeTitle,
                EdgeWeight,
            ]
        ],
        graph.edges.data('weight', default=1),  # type: ignore
    )
    for source, target, weight in edges:
        weights.add(weight)
        edge_data: CytoscapeData = {
            'data': {
                'source': source,
                'target': target,
                'weight': weight,
            }
        }
        cyto_data.append(edge_data)

    # TODO: add internal behaviour (if edge added check for new min/max)
    min_weight: int = 0
    max_weight: int = 0
    if weights:
        min_weight = min(weights)
        max_weight = max(weights)
    weight_metadata: WeightData = {'min': min_weight, 'max': max_weight}

    return cyto_data, weight_metadata
def convert_graph_to_undirected(graph: DiGraph, logging: bool = False, cast_int: bool = False) ‑> networkx.classes.graph.Graph
Expand source code
def convert_graph_to_undirected(
    graph: DiGraph,
    logging: bool = LOGGING_DEFAULT_GRAPHS,
    cast_int: bool = False,
) -> Graph:
    dtype = np.float32
    if cast_int:
        dtype = np.uint32
    # get adjacency matrix
    adj_mat = typing.cast(DataFrame, nx.to_pandas_adjacency(G=graph, dtype=dtype))
    arr = typing.cast(npt.NDArray[np.float32 | np.uint32], adj_mat.to_numpy())
    if not cast_int:
        arr = arr * (10**EDGE_WEIGHT_DECIMALS)
        arr = np.round(arr, decimals=0)
        arr = arr.astype(np.uint32)
    # build undirected array: adding edges of lower triangular matrix to upper one
    arr_upper = np.triu(arr)
    arr_lower = np.tril(arr)
    arr_lower = np.rot90(np.fliplr(arr_lower))
    arr_new = arr_upper + arr_lower
    if not cast_int:
        arr_new = (arr_new / 10**EDGE_WEIGHT_DECIMALS).astype(np.float32)
        arr_new = np.round(arr_new, decimals=EDGE_WEIGHT_DECIMALS)
    # assign new data and create graph
    adj_mat.loc[:] = arr_new  # type: ignore
    graph_undir = typing.cast(Graph, nx.from_pandas_adjacency(df=adj_mat))

    # info about graph
    if logging:
        logger.info('Successfully converted graph to one with undirected edges.')
    _ = get_graph_metadata(graph=graph_undir, logging=logging)

    return graph_undir
def filter_graph_by_edge_weight(graph: TokenGraph,
bound_lower: int | None,
bound_upper: int | None,
property: str = 'weight') ‑> TokenGraph
Expand source code
def filter_graph_by_edge_weight(
    graph: TokenGraph,
    bound_lower: int | None,
    bound_upper: int | None,
    property: str = 'weight',
) -> TokenGraph:
    """filters all edges which are within the provided bounds
    inclusive limits: bound_lower <= edge_weight <= bound_upper are retained

    Parameters
    ----------
    bound_lower : int | None
        lower bound for edge weights, edges with weight equal to this value are retained
    bound_upper : int | None
        upper bound for edge weights, edges with weight equal to this value are retained

    Returns
    -------
    TokenGraph
        a copy of the graph with filtered edges
    """
    original_graph_edges = copy.deepcopy(graph.edges)
    filtered_graph = graph.copy()

    if not any((bound_lower, bound_upper)):
        logger.warning('No bounds provided, returning original graph.')
        return filtered_graph

    for edge in original_graph_edges:
        weight = typing.cast(int, filtered_graph[edge[0]][edge[1]][property])
        if bound_lower is not None and weight < bound_lower:
            filtered_graph.remove_edge(edge[0], edge[1])
        if bound_upper is not None and weight > bound_upper:
            filtered_graph.remove_edge(edge[0], edge[1])

    filtered_graph.to_undirected(inplace=True, logging=False)
    filtered_graph.update_metadata(logging=False)

    return filtered_graph

filters all edges which are within the provided bounds inclusive limits: bound_lower <= edge_weight <= bound_upper are retained

Parameters

bound_lower : int | None
lower bound for edge weights, edges with weight equal to this value are retained
bound_upper : int | None
upper bound for edge weights, edges with weight equal to this value are retained

Returns

TokenGraph
a copy of the graph with filtered edges
def filter_graph_by_node_degree(graph: TokenGraph,
bound_lower: int | None,
bound_upper: int | None) ‑> TokenGraph
Expand source code
def filter_graph_by_node_degree(
    graph: TokenGraph,
    bound_lower: int | None,
    bound_upper: int | None,
) -> TokenGraph:
    """filters all nodes which are within the provided bounds by their degree,
    inclusive limits: bound_lower <= node_degree <= bound_upper are retained

    Parameters
    ----------
    bound_lower : int | None
        lower bound for node degree, nodes with degree equal to this value are retained
    bound_upper : int | None
        upper bound for node degree, nodes with degree equal to this value are retained

    Returns
    -------
    TokenGraph
        a copy of the graph with filtered nodes
    """
    # filter nodes by degree
    original_graph_nodes = copy.deepcopy(graph.nodes)
    filtered_graph = graph.copy()
    filtered_graph_degree = copy.deepcopy(filtered_graph.degree)

    if not any([bound_lower, bound_upper]):
        logger.warning('No bounds provided, returning original graph.')
        return filtered_graph

    for node in original_graph_nodes:
        degree = cast(int, filtered_graph_degree[node])  # type: ignore
        if bound_lower is not None and degree < bound_lower:
            filtered_graph.remove_node(node)
        if bound_upper is not None and degree > bound_upper:
            filtered_graph.remove_node(node)

    filtered_graph.to_undirected(inplace=True, logging=False)
    filtered_graph.update_metadata(logging=False)

    return filtered_graph

filters all nodes which are within the provided bounds by their degree, inclusive limits: bound_lower <= node_degree <= bound_upper are retained

Parameters

bound_lower : int | None
lower bound for node degree, nodes with degree equal to this value are retained
bound_upper : int | None
upper bound for node degree, nodes with degree equal to this value are retained

Returns

TokenGraph
a copy of the graph with filtered nodes
def filter_graph_by_number_edges(graph: TokenGraph,
limit: int | None,
property: str = 'weight',
descending: bool = True) ‑> TokenGraph
Expand source code
def filter_graph_by_number_edges(
    graph: TokenGraph,
    limit: int | None,
    property: str = 'weight',
    descending: bool = True,
) -> TokenGraph:
    graph = graph.copy()
    # edges
    original = set(graph.edges(data=property))  # type: ignore
    original_sorted = sorted(original, key=lambda tup: tup[2], reverse=descending)
    if limit is not None:
        chosen = set(original_sorted[:limit])
    else:
        chosen = set(original_sorted)
    edges_to_drop = original.difference(chosen)
    graph.remove_edges_from(edges_to_drop)

    return graph
def get_graph_metadata(graph: Graph | DiGraph, logging: bool = False) ‑> dict[str, float]
Expand source code
def get_graph_metadata(
    graph: Graph | DiGraph,
    logging: bool = LOGGING_DEFAULT_GRAPHS,
) -> dict[str, float]:
    # info about graph
    graph_info: dict[str, float] = {}
    # nodes and edges
    num_nodes = len(graph.nodes)
    num_edges = len(graph.edges)
    # edge weights
    min_edge_weight: int = 1_000_000
    max_edge_weight: int = 0
    for edge in graph.edges:
        weight = typing.cast(int, graph[edge[0]][edge[1]]['weight'])
        if weight < min_edge_weight:
            min_edge_weight = weight
        if weight > max_edge_weight:
            max_edge_weight = weight

    # memory
    edge_mem = sum([sys.getsizeof(e) for e in graph.edges])
    node_mem = sum([sys.getsizeof(n) for n in graph.nodes])
    total_mem = edge_mem + node_mem

    graph_info.update(
        num_nodes=num_nodes,
        num_edges=num_edges,
        min_edge_weight=min_edge_weight,
        max_edge_weight=max_edge_weight,
        node_memory=node_mem,
        edge_memory=edge_mem,
        total_memory=total_mem,
    )

    if logging:
        logger.info('Graph properties: %d Nodes, %d Edges', num_nodes, num_edges)
        logger.info('Node memory: %.2f KB', (node_mem / 1024))
        logger.info('Edge memory: %.2f KB', (edge_mem / 1024))
        logger.info('Total memory: %.2f KB', (total_mem / 1024))

    return graph_info
def normalise_array_linear(array: npt.NDArray[np.float32]) ‑> numpy.ndarray[typing.Any, numpy.dtype[numpy.float32]]
Expand source code
def normalise_array_linear(
    array: npt.NDArray[np.float32],
) -> npt.NDArray[np.float32]:
    """apply standard linear normalisation

    Parameters
    ----------
    array : npt.NDArray[np.float_]
        array which shall be normalised

    Returns
    -------
    npt.NDArray[np.float32]
        min/max normalised array
    """
    div = array.max() - array.min()
    if div != 0:
        arr_norm = (array - array.min()) / div
        return arr_norm.astype(np.float32)
    else:
        return np.zeros(shape=array.shape, dtype=np.float32)

apply standard linear normalisation

Parameters

array : npt.NDArray[np.float_]
array which shall be normalised

Returns

npt.NDArray[np.float32]
min/max normalised array
def pipe_add_graph_metrics(*graphs: DiGraph | Graph) ‑> tuple[networkx.classes.digraph.DiGraph | networkx.classes.graph.Graph, ...]
Expand source code
def pipe_add_graph_metrics(
    *graphs: DiGraph | Graph,
) -> tuple[DiGraph | Graph, ...]:
    collection: list[DiGraph | Graph] = []
    for graph in graphs:
        graph_copy = copy.deepcopy(graph)
        add_weighted_degree(graph_copy)
        add_betweenness_centrality(graph_copy)
        add_importance_metric(graph_copy)
        collection.append(graph_copy)

    return tuple(collection)
def pipe_rescale_graph_edge_weights(graph: TokenGraph) ‑> tuple[TokenGraph, networkx.classes.graph.Graph]
Expand source code
def pipe_rescale_graph_edge_weights(
    graph: TokenGraph,
) -> tuple[TokenGraph, Graph]:
    """helper function to allow calls in pipelines

    Parameters
    ----------
    graph : TokenGraph
        token graph pushed through pipeline

    Returns
    -------
    tuple[TokenGraph, Graph]
        token graph (directed) and undirected version with rescaled edge weights
    """
    graph = graph.copy()

    return graph.rescale_edge_weights()

helper function to allow calls in pipelines

Parameters

graph : TokenGraph
token graph pushed through pipeline

Returns

tuple[TokenGraph, Graph]
token graph (directed) and undirected version with rescaled edge weights
def rescale_edge_weights(graph: Graph | DiGraph | TokenGraph,
weight_property: str = 'weight') ‑> networkx.classes.graph.Graph | networkx.classes.digraph.DiGraph | TokenGraph
Expand source code
def rescale_edge_weights(
    graph: Graph | DiGraph | TokenGraph,
    weight_property: str = 'weight',
) -> Graph | DiGraph | TokenGraph:
    graph = graph.copy()
    # check non-emptiness
    verify_non_empty_graph(graph, including_edges=True)
    # check if all edges contain weight property
    verify_property(graph, property=weight_property)

    weights = cast(list[int], [data['weight'] for data in graph.edges.values()])
    w_log = cast(npt.NDArray[np.float32], np.log(weights, dtype=np.float32))
    weights_norm = normalise_array_linear(w_log)
    weights_adjusted = weight_scaling(weights_norm)
    # assign new weight values
    for idx, (node_1, node_2) in enumerate(graph.edges):
        graph[node_1][node_2]['weight'] = weights_adjusted[idx]

    return graph
def save_to_GraphML(graph: DiGraph | Graph, saving_path: Path, filename: str | None = None) ‑> None
Expand source code
def save_to_GraphML(
    graph: DiGraph | Graph,
    saving_path: Path,
    filename: str | None = None,
) -> None:
    if filename is not None:
        saving_path = saving_path.joinpath(filename)
    saving_path = saving_path.with_suffix('.graphml')
    nx.write_graphml(G=graph, path=saving_path)
    logger.info('Successfully saved graph as GraphML file under %s.', saving_path)
def static_graph_analysis(graph: TokenGraph) ‑> tuple[TokenGraph]
Expand source code
def static_graph_analysis(
    graph: TokenGraph,
) -> tuple[TokenGraph]:
    """helper function to allow the calculation of static metrics in pipelines

    Parameters
    ----------
    tk_graph_directed : TokenGraph
        token graph (directed)

    Returns
    -------
    tuple[TokenGraph]
        token graph (directed) with included undirected version and calculated KPIs
    """
    graph = graph.copy()
    graph.perform_static_analysis()

    return (graph,)

helper function to allow the calculation of static metrics in pipelines

Parameters

tk_graph_directed : TokenGraph
token graph (directed)

Returns

tuple[TokenGraph]
token graph (directed) with included undirected version and calculated KPIs
def update_graph(graph: Graph | DiGraph,
*,
batch: Iterable[tuple[Hashable, Hashable]] | None = None,
parent: Hashable | None = None,
child: Hashable | None = None,
weight_connection: int | None = None) ‑> None
Expand source code
def update_graph(
    graph: Graph | DiGraph,
    *,
    batch: Iterable[tuple[Hashable, Hashable]] | None = None,
    parent: Hashable | None = None,
    child: Hashable | None = None,
    weight_connection: int | None = None,
) -> None:
    if weight_connection is None:
        weight_connection = 1
    # check if edge not in Graph
    if batch is not None:
        graph.add_edges_from(batch, weight=weight_connection)
    elif not graph.has_edge(parent, child):
        # create new edge, nodes will be created if not already present
        graph.add_edge(parent, child, weight=weight_connection)
    else:
        # update edge
        graph[parent][child]['weight'] += weight_connection
def verify_non_empty_graph(graph: DiGraph | Graph, including_edges: bool = True) ‑> None
Expand source code
def verify_non_empty_graph(
    graph: DiGraph | Graph,
    including_edges: bool = True,
) -> None:
    """check if the given graph is empty, presence of nodes is checked first,
    then of edges

    Parameters
    ----------
    graph : DiGraph | Graph
        graph to check for emptiness
    including_edges : bool, optional
        whether to check for non-existence of edges, by default True

    Raises
    ------
    EmptyGraphError
        if graph does not contain any nodes and therefore edges
    EmptyEdgesError
        if graph does not contain any edges
    """
    if not tuple(graph.nodes):
        raise EmptyGraphError(f'Graph object >>{graph}<< does not contain any nodes.')
    elif including_edges and not tuple(graph.edges):
        raise EmptyEdgesError(f'Graph object >>{graph}<< does not contain any edges.')

check if the given graph is empty, presence of nodes is checked first, then of edges

Parameters

graph : DiGraph | Graph
graph to check for emptiness
including_edges : bool, optional
whether to check for non-existence of edges, by default True

Raises

EmptyGraphError
if graph does not contain any nodes and therefore edges
EmptyEdgesError
if graph does not contain any edges
def verify_property(graph: Graph | DiGraph, property: str) ‑> None
Expand source code
def verify_property(
    graph: Graph | DiGraph,
    property: str,
) -> None:
    for node_1, node_2 in graph.edges:
        if property not in graph[node_1][node_2]:
            raise EdgePropertyNotContainedError(
                (
                    f'Edge property >>{property}<< not '
                    f'available for edge >>({node_1}, {node_2})<<'
                )
            )
def weight_scaling(weights: npt.NDArray[np.float32], a: float = 1.1, b: float = 0.05) ‑> numpy.ndarray[typing.Any, numpy.dtype[numpy.float32]]
Expand source code
def weight_scaling(
    weights: npt.NDArray[np.float32],
    a: float = 1.1,
    b: float = 0.05,
) -> npt.NDArray[np.float32]:
    """non-linear scaling of already normalised edge weights [0;1]: bigger weights
    have smaller weight delta than smaller weights. Bigger values für parameter
    `b` reinforce this effect.
    Based on:
    https://math.stackexchange.com/questions/4297805/exponential-function-that-passes-through-0-0-and-1-1-with-variable-slope

    With default values the range of edge weights lies approximately between [0.1; 1]

    Parameters
    ----------
    weights : npt.NDArray[np.float32]
        pre-normalised edge weights as 1D array
    a : float, optional
        factor to determine the value for edge weights with value 0
        with default approx. 0.1, by default 1.1
    b : float, optional
        adjust the curvature, smaller values increase it, by default 0.05

    Returns
    -------
    npt.NDArray[np.float32]
        non-linear adjusted edge weights as 1D array
    """
    adjusted_weights = (b**weights - a) / (b - a)

    return np.round(adjusted_weights, decimals=EDGE_WEIGHT_DECIMALS)

non-linear scaling of already normalised edge weights [0;1]: bigger weights have smaller weight delta than smaller weights. Bigger values für parameter b reinforce this effect. Based on: https://math.stackexchange.com/questions/4297805/exponential-function-that-passes-through-0-0-and-1-1-with-variable-slope

With default values the range of edge weights lies approximately between [0.1; 1]

Parameters

weights : npt.NDArray[np.float32]
pre-normalised edge weights as 1D array
a : float, optional
factor to determine the value for edge weights with value 0 with default approx. 0.1, by default 1.1
b : float, optional
adjust the curvature, smaller values increase it, by default 0.05

Returns

npt.NDArray[np.float32]
non-linear adjusted edge weights as 1D array

Classes

class TokenGraph (name: str = 'TokenGraph',
enable_logging: bool = True,
incoming_graph_data: Any | None = None,
**attr)
Expand source code
class TokenGraph(DiGraph):
    def __init__(
        self,
        name: str = 'TokenGraph',
        enable_logging: bool = True,
        incoming_graph_data: Any | None = None,
        **attr,
    ) -> None:
        super().__init__(incoming_graph_data, **attr)
        # logging of different actions
        self.logging = enable_logging
        # properties
        self._name = name
        # directed and undirected graph data
        self._directed = self
        self._metadata_directed: dict[str, float] = {}
        self._undirected: Graph | None = None
        self._metadata_undirected: dict[str, float] = {}
        # indicate rescaled weights
        self.rescaled_weights: bool = False

    def __repr__(self) -> str:
        return self.__str__()

    def __str__(self) -> str:
        return (
            f'TokenGraph(name: {self.name}, number of nodes: '
            f'{len(self.nodes)}, number of edges: '
            f'{len(self.edges)})'
        )

    def disable_logging(self) -> None:
        self.logging = False

    # !! only used to verify that saving was done correctly
    """
    def __key(self) -> tuple[Hashable, ...]:
        return (self.name, tuple(self.nodes), tuple(self.edges))
    
    def __hash__(self) -> int:
        return hash(self.__key())
    """

    def copy(self) -> Self:
        """returns a (deep) copy of the graph

        Returns
        -------
        Self
            deep copy of the graph
        """
        return copy.deepcopy(self)

    @property
    def name(self) -> str:
        return self._name

    @property
    def directed(self) -> Self:
        return self._directed

    @property
    def undirected(self) -> Graph:
        if self._undirected is None:
            self._undirected = self.to_undirected(inplace=False, logging=False)

        return self._undirected

    @property
    def metadata_directed(self) -> dict[str, float]:
        return self._metadata_directed

    @property
    def metadata_undirected(self) -> dict[str, float]:
        return self._metadata_undirected

    @overload
    def to_undirected(
        self,
        inplace: Literal[True] = ...,
        logging: bool | None = ...,
    ) -> None: ...

    @overload
    def to_undirected(
        self,
        inplace: Literal[False],
        logging: bool | None = ...,
    ) -> Graph: ...

    def to_undirected(
        self,
        inplace: bool = True,
        logging: bool | None = None,
    ) -> Graph | None:
        if logging is None:
            logging = self.logging
        # cast to integer edge weights only if edges were not rescaled previously
        cast_int: bool = True
        if self.rescaled_weights:
            cast_int = False

        self._undirected = convert_graph_to_undirected(
            graph=self,
            logging=logging,
            cast_int=cast_int,
        )
        self._metadata_undirected = get_graph_metadata(graph=self._undirected, logging=False)
        if not inplace:
            return self._undirected

    def update_metadata(
        self,
        logging: bool | None = None,
    ) -> None:
        if logging is None:
            logging = self.logging

        self._metadata_directed = get_graph_metadata(graph=self, logging=logging)
        if self._undirected is not None:
            self._metadata_undirected = get_graph_metadata(
                graph=self._undirected, logging=logging
            )

    def rescale_edge_weights(
        self,
    ) -> tuple[TokenGraph, Graph]:
        """generate new instances of the directed and undirected TokenGraph with
        rescaled edge weights
        Only this method ensures that undirected graphs are scaled properly. If
        the underlying `to_undirected` method of the directed and rescaled
        TokenGraph instance is called the weights are not rescaled again. Thus,
        the maximum edge weight can exceed the theoretical maximum value of 1. To
        ensure consistent behaviour across different applications of the conversion to
        undirected graphs new instances are returned, especially for the undirected
        graph.
        In contrast, the new directed TokenGraph contains an undirected version without
        rescaling of the weights. Therefore, this undirected version differs from the version
        returned by this method.

        Returns
        -------
        tuple[TokenGraph, Graph]
            directed and undirected instances
        """
        self.to_undirected(inplace=True, logging=False)
        token_graph = rescale_edge_weights(self.directed)
        token_graph.rescaled_weights = True
        token_graph.update_metadata(logging=False)
        undirected = rescale_edge_weights(self.undirected)

        return token_graph, undirected

    def perform_static_analysis(self) -> None:
        """calculate different metrics directly on the data of the underlying graphs
        (directed and undirected)

        Current operations:
            - adding weighted degree
        """
        add_weighted_degree(self)
        add_weighted_degree(self.undirected)

    def _save_prepare(
        self,
        path: Path,
        filename: str | None = None,
    ) -> Path:
        if filename is not None:
            saving_path = path.joinpath(f'{filename}')
        else:
            saving_path = path.joinpath(f'{self.name}')

        return saving_path

    def to_GraphML(
        self,
        path: Path,
        filename: str | None = None,
        directed: bool = False,
    ) -> None:
        """save one of the stored graphs to GraphML format on disk,

        Parameters
        ----------
        path : Path
            target path for saving the file
        filename : str | None, optional
            filename to be given, by default None
        directed : bool, optional
            indicator whether directed or undirected graph
            should be exported, by default False (undirected)

        Raises
        ------
        ValueError
            undirected graph should be exported but is not available
        """
        saving_path = self._save_prepare(path=path, filename=filename)

        if directed:
            target_graph = self.directed
        else:
            target_graph = self.undirected

        save_to_GraphML(graph=target_graph, saving_path=saving_path)

    def to_pickle(
        self,
        path: Path,
        filename: str | None = None,
    ) -> None:
        """save whole TokenGraph object as pickle file

        Parameters
        ----------
        path : Path
            target path for saving the file
        filename : str | None, optional
            filename to be given, by default None
        """
        saving_path = self._save_prepare(path=path, filename=filename)
        saving_path = saving_path.with_suffix('.pkl')
        save_pickle(obj=self, path=saving_path)

    @classmethod
    def from_file(
        cls,
        path: Path,
        node_type_graphml: type = str,
    ) -> Self:
        # !! no validity checks for pickle files
        # !! GraphML files not correct because not all properties
        # !! are parsed correctly
        # TODO REWORK
        match path.suffix:
            case '.graphml':
                graph = typing.cast(Self, nx.read_graphml(path, node_type=node_type_graphml))
                logger.info('Successfully loaded graph from GraphML file %s.', path)
            case '.pkl' | '.pickle':
                graph = typing.cast(Self, load_pickle(path))
                logger.info('Successfully loaded graph from pickle file %s.', path)
            case _:
                raise ValueError('File format not supported.')

        return graph

Base class for directed graphs.

A DiGraph stores nodes and edges with optional data, or attributes.

DiGraphs hold directed edges. Self loops are allowed but multiple (parallel) edges are not.

Nodes can be arbitrary (hashable) Python objects with optional key/value attributes. By convention None is not used as a node.

Edges are represented as links between nodes with optional key/value attributes.

Parameters

incoming_graph_data : input graph (optional, default: None)
Data to initialize graph. If None (default) an empty graph is created. The data can be any format that is supported by the to_networkx_graph() function, currently including edge list, dict of dicts, dict of lists, NetworkX graph, 2D NumPy array, SciPy sparse matrix, or PyGraphviz graph.
attr : keyword arguments, optional (default= no attributes)
Attributes to add to graph as key=value pairs.

See Also

Graph MultiGraph MultiDiGraph

Examples

Create an empty graph structure (a "null graph") with no nodes and no edges.

>>> G = nx.DiGraph()

G can be grown in several ways.

Nodes:

Add one node at a time:

>>> G.add_node(1)

Add the nodes from any container (a list, dict, set or even the lines from a file or the nodes from another graph).

>>> G.add_nodes_from([2, 3])
>>> G.add_nodes_from(range(100, 110))
>>> H = nx.path_graph(10)
>>> G.add_nodes_from(H)

In addition to strings and integers any hashable Python object (except None) can represent a node, e.g. a customized node object, or even another Graph.

>>> G.add_node(H)

Edges:

G can also be grown by adding edges.

Add one edge,

>>> G.add_edge(1, 2)

a list of edges,

>>> G.add_edges_from([(1, 2), (1, 3)])

or a collection of edges,

>>> G.add_edges_from(H.edges)

If some edges connect nodes not yet in the graph, the nodes are added automatically. There are no errors when adding nodes or edges that already exist.

Attributes:

Each graph, node, and edge can hold key/value attribute pairs in an associated attribute dictionary (the keys must be hashable). By default these are empty, but can be added or changed using add_edge, add_node or direct manipulation of the attribute dictionaries named graph, node and edge respectively.

>>> G = nx.DiGraph(day="Friday")
>>> G.graph
{'day': 'Friday'}

Add node attributes using add_node(), add_nodes_from() or G.nodes

>>> G.add_node(1, time="5pm")
>>> G.add_nodes_from([3], time="2pm")
>>> G.nodes[1]
{'time': '5pm'}
>>> G.nodes[1]["room"] = 714
>>> del G.nodes[1]["room"]  # remove attribute
>>> list(G.nodes(data=True))
[(1, {'time': '5pm'}), (3, {'time': '2pm'})]

Add edge attributes using add_edge(), add_edges_from(), subscript notation, or G.edges.

>>> G.add_edge(1, 2, weight=4.7)
>>> G.add_edges_from([(3, 4), (4, 5)], color="red")
>>> G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})])
>>> G[1][2]["weight"] = 4.7
>>> G.edges[1, 2]["weight"] = 4

Warning: we protect the graph data structure by making G.edges[1, 2] a read-only dict-like structure. However, you can assign to attributes in e.g. G.edges[1, 2]. Thus, use 2 sets of brackets to add/change data attributes: G.edges[1, 2]['weight'] = 4 (For multigraphs: MG.edges[u, v, key][name] = value).

Shortcuts:

Many common graph features allow python syntax to speed reporting.

>>> 1 in G  # check if node in graph
True
>>> [n for n in G if n < 3]  # iterate through nodes
[1, 2]
>>> len(G)  # number of nodes in graph
5

Often the best way to traverse all edges of a graph is via the neighbors. The neighbors are reported as an adjacency-dict G.adj or G.adjacency()

>>> for n, nbrsdict in G.adjacency():
...     for nbr, eattr in nbrsdict.items():
...         if "weight" in eattr:
...             # Do something useful with the edges
...             pass

But the edges reporting object is often more convenient:

>>> for u, v, weight in G.edges(data="weight"):
...     if weight is not None:
...         # Do something useful with the edges
...         pass

Reporting:

Simple graph information is obtained using object-attributes and methods. Reporting usually provides views instead of containers to reduce memory usage. The views update as the graph is updated similarly to dict-views. The objects nodes, edges and adj provide access to data attributes via lookup (e.g. nodes[n], edges[u, v], adj[u][v]) and iteration (e.g. nodes.items(), nodes.data('color'), nodes.data('color', default='blue') and similarly for edges) Views exist for nodes, edges, neighbors()/adj and degree.

For details on these and other miscellaneous methods, see below.

Subclasses (Advanced):

The Graph class uses a dict-of-dict-of-dict data structure. The outer dict (node_dict) holds adjacency information keyed by node. The next dict (adjlist_dict) represents the adjacency information and holds edge data keyed by neighbor. The inner dict (edge_attr_dict) represents the edge data and holds edge attribute values keyed by attribute names.

Each of these three dicts can be replaced in a subclass by a user defined dict-like object. In general, the dict-like features should be maintained but extra features can be added. To replace one of the dicts create a new graph class by changing the class(!) variable holding the factory for that dict-like structure. The variable names are node_dict_factory, node_attr_dict_factory, adjlist_inner_dict_factory, adjlist_outer_dict_factory, edge_attr_dict_factory and graph_attr_dict_factory.

node_dict_factory : function, (default: dict) Factory function to be used to create the dict containing node attributes, keyed by node id. It should require no arguments and return a dict-like object

node_attr_dict_factory: function, (default: dict) Factory function to be used to create the node attribute dict which holds attribute values keyed by attribute name. It should require no arguments and return a dict-like object

adjlist_outer_dict_factory : function, (default: dict) Factory function to be used to create the outer-most dict in the data structure that holds adjacency info keyed by node. It should require no arguments and return a dict-like object.

adjlist_inner_dict_factory : function, optional (default: dict) Factory function to be used to create the adjacency list dict which holds edge data keyed by neighbor. It should require no arguments and return a dict-like object

edge_attr_dict_factory : function, optional (default: dict) Factory function to be used to create the edge attribute dict which holds attribute values keyed by attribute name. It should require no arguments and return a dict-like object.

graph_attr_dict_factory : function, (default: dict) Factory function to be used to create the graph attribute dict which holds attribute values keyed by attribute name. It should require no arguments and return a dict-like object.

Typically, if your extension doesn't impact the data structure all methods will inherited without issue except: to_directed/to_undirected. By default these methods create a DiGraph/Graph class and you probably want them to create your extension of a DiGraph/Graph. To facilitate this we define two class variables that you can set in your subclass.

to_directed_class : callable, (default: DiGraph or MultiDiGraph) Class to create a new graph structure in the to_directed method. If None, a NetworkX class (DiGraph or MultiDiGraph) is used.

to_undirected_class : callable, (default: Graph or MultiGraph) Class to create a new graph structure in the to_undirected method. If None, a NetworkX class (Graph or MultiGraph) is used.

Subclassing Example

Create a low memory graph class that effectively disallows edge attributes by using a single attribute dict for all edges. This reduces the memory used, but you lose edge attributes.

>>> class ThinGraph(nx.Graph):
...     all_edge_dict = {"weight": 1}
...
...     def single_edge_dict(self):
...         return self.all_edge_dict
...
...     edge_attr_dict_factory = single_edge_dict
>>> G = ThinGraph()
>>> G.add_edge(2, 1)
>>> G[2][1]
{'weight': 1}
>>> G.add_edge(2, 2)
>>> G[2][1] is G[2][2]
True

Initialize a graph with edges, name, or graph attributes.

Parameters

incoming_graph_data : input graph (optional, default: None)
Data to initialize graph. If None (default) an empty graph is created. The data can be an edge list, or any NetworkX graph object. If the corresponding optional Python packages are installed the data can also be a 2D NumPy array, a SciPy sparse array, or a PyGraphviz graph.
attr : keyword arguments, optional (default= no attributes)
Attributes to add to graph as key=value pairs.

See Also

convert

Examples

>>> G = nx.Graph()  # or DiGraph, MultiGraph, MultiDiGraph, etc
>>> G = nx.Graph(name="my graph")
>>> e = [(1, 2), (2, 3), (3, 4)]  # list of edges
>>> G = nx.Graph(e)

Arbitrary graph attribute pairs (key=value) may be assigned

>>> G = nx.Graph(e, day="Friday")
>>> G.graph
{'day': 'Friday'}

Ancestors

  • networkx.classes.digraph.DiGraph
  • networkx.classes.graph.Graph

Static methods

def from_file(path: Path, node_type_graphml: type = builtins.str) ‑> Self

Instance variables

prop directed : Self
Expand source code
@property
def directed(self) -> Self:
    return self._directed
prop metadata_directed : dict[str, float]
Expand source code
@property
def metadata_directed(self) -> dict[str, float]:
    return self._metadata_directed
prop metadata_undirected : dict[str, float]
Expand source code
@property
def metadata_undirected(self) -> dict[str, float]:
    return self._metadata_undirected
prop name : str
Expand source code
@property
def name(self) -> str:
    return self._name

String identifier of the graph.

This graph attribute appears in the attribute dict G.graph keyed by the string "name". as well as an attribute (technically a property) G.name. This is entirely user controlled.

prop undirected : Graph
Expand source code
@property
def undirected(self) -> Graph:
    if self._undirected is None:
        self._undirected = self.to_undirected(inplace=False, logging=False)

    return self._undirected

Methods

def copy(self) ‑> Self
Expand source code
def copy(self) -> Self:
    """returns a (deep) copy of the graph

    Returns
    -------
    Self
        deep copy of the graph
    """
    return copy.deepcopy(self)

returns a (deep) copy of the graph

Returns

Self
deep copy of the graph
def disable_logging(self) ‑> None
Expand source code
def disable_logging(self) -> None:
    self.logging = False
def perform_static_analysis(self) ‑> None
Expand source code
def perform_static_analysis(self) -> None:
    """calculate different metrics directly on the data of the underlying graphs
    (directed and undirected)

    Current operations:
        - adding weighted degree
    """
    add_weighted_degree(self)
    add_weighted_degree(self.undirected)

calculate different metrics directly on the data of the underlying graphs (directed and undirected)

Current operations: - adding weighted degree

def rescale_edge_weights(self) ‑> tuple[TokenGraph, networkx.classes.graph.Graph]
Expand source code
def rescale_edge_weights(
    self,
) -> tuple[TokenGraph, Graph]:
    """generate new instances of the directed and undirected TokenGraph with
    rescaled edge weights
    Only this method ensures that undirected graphs are scaled properly. If
    the underlying `to_undirected` method of the directed and rescaled
    TokenGraph instance is called the weights are not rescaled again. Thus,
    the maximum edge weight can exceed the theoretical maximum value of 1. To
    ensure consistent behaviour across different applications of the conversion to
    undirected graphs new instances are returned, especially for the undirected
    graph.
    In contrast, the new directed TokenGraph contains an undirected version without
    rescaling of the weights. Therefore, this undirected version differs from the version
    returned by this method.

    Returns
    -------
    tuple[TokenGraph, Graph]
        directed and undirected instances
    """
    self.to_undirected(inplace=True, logging=False)
    token_graph = rescale_edge_weights(self.directed)
    token_graph.rescaled_weights = True
    token_graph.update_metadata(logging=False)
    undirected = rescale_edge_weights(self.undirected)

    return token_graph, undirected

generate new instances of the directed and undirected TokenGraph with rescaled edge weights Only this method ensures that undirected graphs are scaled properly. If the underlying to_undirected method of the directed and rescaled TokenGraph instance is called the weights are not rescaled again. Thus, the maximum edge weight can exceed the theoretical maximum value of 1. To ensure consistent behaviour across different applications of the conversion to undirected graphs new instances are returned, especially for the undirected graph. In contrast, the new directed TokenGraph contains an undirected version without rescaling of the weights. Therefore, this undirected version differs from the version returned by this method.

Returns

tuple[TokenGraph, Graph]
directed and undirected instances
def to_GraphML(self, path: Path, filename: str | None = None, directed: bool = False) ‑> None
Expand source code
def to_GraphML(
    self,
    path: Path,
    filename: str | None = None,
    directed: bool = False,
) -> None:
    """save one of the stored graphs to GraphML format on disk,

    Parameters
    ----------
    path : Path
        target path for saving the file
    filename : str | None, optional
        filename to be given, by default None
    directed : bool, optional
        indicator whether directed or undirected graph
        should be exported, by default False (undirected)

    Raises
    ------
    ValueError
        undirected graph should be exported but is not available
    """
    saving_path = self._save_prepare(path=path, filename=filename)

    if directed:
        target_graph = self.directed
    else:
        target_graph = self.undirected

    save_to_GraphML(graph=target_graph, saving_path=saving_path)

save one of the stored graphs to GraphML format on disk,

Parameters

path : Path
target path for saving the file
filename : str | None, optional
filename to be given, by default None
directed : bool, optional
indicator whether directed or undirected graph should be exported, by default False (undirected)

Raises

ValueError
undirected graph should be exported but is not available
def to_pickle(self, path: Path, filename: str | None = None) ‑> None
Expand source code
def to_pickle(
    self,
    path: Path,
    filename: str | None = None,
) -> None:
    """save whole TokenGraph object as pickle file

    Parameters
    ----------
    path : Path
        target path for saving the file
    filename : str | None, optional
        filename to be given, by default None
    """
    saving_path = self._save_prepare(path=path, filename=filename)
    saving_path = saving_path.with_suffix('.pkl')
    save_pickle(obj=self, path=saving_path)

save whole TokenGraph object as pickle file

Parameters

path : Path
target path for saving the file
filename : str | None, optional
filename to be given, by default None
def to_undirected(self, inplace: bool = True, logging: bool | None = None) ‑> networkx.classes.graph.Graph | None
Expand source code
def to_undirected(
    self,
    inplace: bool = True,
    logging: bool | None = None,
) -> Graph | None:
    if logging is None:
        logging = self.logging
    # cast to integer edge weights only if edges were not rescaled previously
    cast_int: bool = True
    if self.rescaled_weights:
        cast_int = False

    self._undirected = convert_graph_to_undirected(
        graph=self,
        logging=logging,
        cast_int=cast_int,
    )
    self._metadata_undirected = get_graph_metadata(graph=self._undirected, logging=False)
    if not inplace:
        return self._undirected

Returns an undirected representation of the digraph.

Parameters

reciprocal : bool (optional)
 
If True only keep edges that appear in both directions
in the original digraph.
as_view : bool (optional, default=False)
 

If True return an undirected view of the original directed graph.

Returns

G : Graph
An undirected graph with the same name and nodes and with edge (u, v, data) if either (u, v, data) or (v, u, data) is in the digraph. If both edges exist in digraph and their edge data is different, only one edge is created with an arbitrary choice of which edge data to use. You must check and correct for this manually if desired.

See Also

Graph, copy, add_edge, add_edges_from

Notes

If edges in both directions (u, v) and (v, u) exist in the graph, attributes for the new undirected edge will be a combination of the attributes of the directed edges. The edge data is updated in the (arbitrary) order that the edges are encountered. For more customized control of the edge attributes use add_edge().

This returns a "deepcopy" of the edge, node, and graph attributes which attempts to completely copy all of the data and references.

This is in contrast to the similar G=DiGraph(D) which returns a shallow copy of the data.

See the Python copy module for more information on shallow and deep copies, https://docs.python.org/3/library/copy.html.

Warning: If you have subclassed DiGraph to use dict-like objects in the data structure, those changes do not transfer to the Graph created by this method.

Examples

>>> G = nx.path_graph(2)  # or MultiGraph, etc
>>> H = G.to_directed()
>>> list(H.edges)
[(0, 1), (1, 0)]
>>> G2 = H.to_undirected()
>>> list(G2.edges)
[(0, 1)]
def update_metadata(self, logging: bool | None = None) ‑> None
Expand source code
def update_metadata(
    self,
    logging: bool | None = None,
) -> None:
    if logging is None:
        logging = self.logging

    self._metadata_directed = get_graph_metadata(graph=self, logging=logging)
    if self._undirected is not None:
        self._metadata_undirected = get_graph_metadata(
            graph=self._undirected, logging=logging
        )