Source code for toponetx.classes.combinatorial_complex

"""Creation and manipulation of a combinatorial complex."""

from collections.abc import Collection, Hashable, Iterable
from typing import Any, Literal

import networkx as nx
from typing_extensions import Self

from toponetx.classes.colored_hypergraph import ColoredHyperGraph
from toponetx.classes.complex import Complex
from toponetx.classes.hyperedge import HyperEdge
from toponetx.classes.reportviews import HyperEdgeView, NodeView
from toponetx.utils.structure import compute_set_incidence

__all__ = ["CombinatorialComplex"]


[docs] class CombinatorialComplex(ColoredHyperGraph): r"""Class for Combinatorial Complex. A Combinatorial Complex (CCC) is a triple $CCC = (S, X, rk)$ where: - $S$ is an abstract set of entities, - $X$ a subset of the power set of $S$, and - $rk$ is the a rank function that associates for every set x in X a rank, a positive integer. The rank function $rk$ must satisfy $x \subseteq y$ then $rk(x) \leq rk(y)$. We call this condition the CCC condition. A CCC is a generalization of graphs, hypergraphs, cellular and simplicial complexes. Mathematical Example: Let $S = \{1, 2, 3, 4\}$ be a set of abstract entities. Let $X = \{\{1, 2\}, \{1, 2, 3\}, \{1, 3\}, \{1, 4\}\}$ be a subset of the power set of $S$. Let rk be the ranking function that assigns the length of a set as its rank, i.e. $rk(\{1, 2\}) = 2$, $rk(\{1, 2, 3\}) = 3$, etc. Then, $(S, X, rk)$ is a combinatorial complex. Parameters ---------- cells : Collection, optional A collection of cells to add to the combinatorial complex. ranks : Collection, optional When cells is an iterable or dictionary, ranks cannot be None and it must be iterable/dict of the same size as cells. graph_based : bool, default=False When true rank 1 edges must have cardinality equals to 1. **kwargs : keyword arguments, optional Attributes to add to the complex as key=value pairs. Raises ------ TypeError If cells is not given as an Iterable. ValueError If input cells is not an instance of HyperEdge when rank is None. If input HyperEdge has None rank when rank is specified. If cells and ranks do not have an equal number of elements. Attributes ---------- complex : dict A dictionary that can be used to store additional information about the complex. Examples -------- Define an empty combinatorial complex: >>> CCC = tnx.CombinatorialComplex() Add cells to the combinatorial complex: >>> CCC = tnx.CombinatorialComplex() >>> CCC.add_cell([1, 2], rank=1) >>> CCC.add_cell([3, 4], rank=1) >>> CCC.add_cell([1, 2, 3, 4], rank=2) >>> CCC.add_cell([1, 2, 4], rank=2) >>> CCC.add_cell([1, 2, 3, 4, 5, 6, 7], rank=3) """
[docs] def __init__( self, cells: Collection | None = None, ranks: Collection | None = None, graph_based: bool = False, **kwargs, ) -> None: Complex.__init__(self, **kwargs) self.graph_based = graph_based # rank 1 edges have cardinality equals to 1 self._node_membership = {} self._complex_set = HyperEdgeView() if cells is not None: if not isinstance(cells, Iterable): raise TypeError( f"Input cells must be given as Iterable, got {type(cells)}." ) if not isinstance(cells, nx.Graph): if ranks is None: for cell in cells: if not isinstance(cell, HyperEdge): raise ValueError( f"input must be an HyperEdge {cell} object when rank is None" ) if cell.rank is None: raise ValueError(f"input HyperEdge {cell} has None rank") self.add_cell(cell, rank=cell.rank) else: if isinstance(cells, Iterable) and isinstance(ranks, Iterable): for cell, rank in zip(cells, ranks, strict=True): self.add_cell(cell, rank) if isinstance(cells, Iterable) and isinstance(ranks, int): for cell in cells: self.add_cell(cell, ranks) else: for node in cells.nodes: # cells is a networkx graph self.add_node(node, **cells.nodes[node]) for edge in cells.edges: u, v = edge self.add_cell([u, v], 1, **cells.get_edge_data(u, v))
def __str__(self) -> str: """Return detailed string representation. Returns ------- str Description of Combinatorial Complex. """ return f"Combinatorial Complex with {len(self.nodes)} nodes and cells with ranks {self.ranks} and sizes {self.shape} " def __repr__(self) -> str: """Return string representation. Returns ------- str Description of Combinatorial Complex. """ return "CombinatorialComplex()" def __setitem__(self, cell, attr): """Set the attributes of a hyperedge or node in the CCC. Parameters ---------- cell : hashable The cell (hyperedge or node) for which to set the attributes. attr : dict The attributes to set for the specified cell. Raises ------ KeyError If the input cell is not found in the complex. Notes ----- This method updates the attributes of a hyperedge or node in the Combinatorial Complex. If the cell is a node, it updates the attributes of the corresponding node in the complex. If the cell is a hyperedge, it updates the attributes of the hyperedge with the specified rank. Examples -------- >>> complex_instance["node_A"] = {"color": "red"} >>> complex_instance["hyperedge_B"] = {"weight": 5} Returns ------- None Returns None. """ if cell in self.nodes: self.nodes[cell].update(attr) return # we now check if the input is a cell in the CCC if cell in self.cells: hyperedge_ = HyperEdgeView._to_frozen_set(cell) rank = self.cells.get_rank(hyperedge_) if hyperedge_ in self._complex_set.hyperedge_dict[rank]: self._complex_set.hyperedge_dict[rank][hyperedge_] = attr else: raise KeyError(f"input {cell} is not in the complex") def __contains__(self, atom) -> bool: """Check whether this combinatorial complex contains the given atom. Parameters ---------- atom : Any The atom to be checked. Returns ------- bool Returns `True` if this combinatorial complex contains the atom, else `False`. """ return atom in self._complex_set @property def __shortstr__(self) -> str: """Return the short string generic representation. Returns ------- str CCC. """ return "CCC"
[docs] def number_of_nodes(self, node_set=None) -> int: """Compute the number of nodes in node_set belonging to the CCC. Parameters ---------- node_set : iterable of Entities, optional If None, then return the number of nodes in the CCC. Returns ------- int The number of nodes in node_set belonging to this combinatorial complex. """ return super().number_of_nodes(node_set)
@property def nodes(self): """Object associated with self.elements. Returns ------- NodeView Returns all the nodes of the combinatorial complex. """ return NodeView( self._complex_set.hyperedge_dict, cell_type=HyperEdge, colored_nodes=False ) @property def cells(self): """Object associated with self._cells. Returns ------- HyperEdgeView Returns all the present cells in the combinatorial complex along with their rank. """ return self._complex_set
[docs] def number_of_cells(self, cell_set=None) -> int: """Compute the number of cells in cell_set belonging to the CCC. Parameters ---------- cell_set : iterable of HyperEdge, optional If None, then return the number of cells. Returns ------- int The number of cells in cell_set belonging to this combinatorial complex. """ return super().number_of_cells(cell_set)
@property def shape(self): """Return shape. This is: (number of cells[i], for i in range(0,dim(CCC)) ) Returns ------- tuple of ints Shape of the CC object. """ return self._complex_set.shape
[docs] def skeleton( self, rank: int, level: Literal[ "equal", "upper", "up", "lower", "down", "uppereq", "upeq", "lowereq", "downeq", ] = "equal", ): """Skeleton of the CCC. Parameters ---------- rank : int The rank of the skeleton. level : str, default="equal" Level of the skeleton. Returns ------- list of HyperEdge The skeleton of the CCC. """ return self._complex_set.skeleton(rank, level=level)
[docs] def order(self) -> int: """Compute the number of nodes in the CCC. Returns ------- int The number of nodes in this combinatorial complex. """ return super().order()
def _remove_node_helper(self, node) -> None: """Remove a node from the hyperedges in the combinatorial complex. This function assumes that the node is present in the combinatorial complex. Parameters ---------- node : hashable The node to be removed from the hyperedges. Returns ------- None This method does not return any value. It removes the specified node from the hyperedges in the complex. Notes ----- This function iterates over the hyperedges in the complex and removes the specified node from each hyperedge. If a hyperedge becomes empty after removing the node, it is removed from the hyperedge_dict. """ # Removing node in hyperedgeview for key in list(self.cells.hyperedge_dict.keys()): for key_rank in list(self.cells.hyperedge_dict[key].keys()): replace_key = key_rank.difference(node) if len(replace_key) > 0: if key_rank != replace_key: del self.cells.hyperedge_dict[key][key_rank] else: # Remove original hyperedge from the ranks del self.cells.hyperedge_dict[key][key_rank] if self.cells.hyperedge_dict[key] == {}: del self.cells.hyperedge_dict[key]
[docs] def remove_nodes(self, node_set) -> None: """Remove nodes from cells. This also deletes references in combinatorial complex nodes. Parameters ---------- node_set : an iterable of hashables The nodes to remove from this combinatorial complex. """ super().remove_nodes(node_set)
[docs] def remove_node(self, node) -> None: """Remove node from cells. This also deletes any reference in the nodes of the CCC. This also deletes cell references in higher ranks for the particular node. Parameters ---------- node : hashable or HyperEdge The node to remove from this combinatorial complex. """ super()._remove_node(node)
[docs] def set_cell_attributes(self, values, name: str | None = None) -> None: """Set cell attributes. Parameters ---------- values : dict Dictionary of cell attributes to set keyed by cell name. name : str, optional Attribute name. Examples -------- After computing some property of the cell of a combinatorial complex, you may want to assign a cell attribute to store the value of that property for each cell: >>> CCC = tnx.CombinatorialComplex() >>> CCC.add_cell([1, 2, 3, 4], rank=2) >>> CCC.add_cell([1, 2, 4], rank=2) >>> CCC.add_cell([3, 4], rank=2) >>> d = {(1, 2, 3, 4): "red", (1, 2, 3): "blue", (3, 4): "green"} >>> CCC.set_cell_attributes(d, name="color") >>> CCC.cells[(3, 4)]["color"] 'green' If you provide a dictionary of dictionaries as the second argument, the entire dictionary will be used to update edge attributes: >>> G = nx.path_graph(3) >>> CCC = tnx.CombinatorialComplex(G) >>> d = { ... (1, 2): {"color": "red", "attr2": 1}, ... (0, 1): {"color": "blue", "attr2": 3}, ... } >>> CCC.set_cell_attributes(d) >>> CCC.cells[(0, 1)]["color"] 'blue' 3 Note that if the dict contains cells that are not in `self.cells`, they are silently ignored. """ super().set_cell_attributes(values, name)
[docs] def get_node_attributes(self, name: str) -> dict[Hashable, Any]: """Get node attributes. Parameters ---------- name : str Attribute name. Returns ------- dict[Hashable, Any] Dictionary mapping each node to the value of the given attribute name. Examples -------- >>> G = nx.path_graph(3) >>> CCC = tnx.CombinatorialComplex(G) >>> d = {0: {"color": "red", "attr2": 1}, 1: {"color": "blue", "attr2": 3}} >>> CCC.set_node_attributes(d) >>> CCC.get_node_attributes("color") {0: 'red', 1: 'blue'} >>> G = nx.Graph() >>> G.add_nodes_from([1, 2, 3], color="blue") >>> CCC = tnx.CombinatorialComplex(G) >>> nodes_color = CCC.get_node_attributes("color") >>> nodes_color[1] 'blue' """ return super().get_node_attributes(name)
[docs] def get_cell_attributes(self, name: str, rank: int | None = None): """Get node attributes from graph. Parameters ---------- name : str Attribute name. rank : int Restrict the returned attribute values to cells of a specific rank. Returns ------- dict Dictionary of attributes keyed by cell or k-cells if k is not None. Examples -------- >>> G = nx.path_graph(3) >>> CCC = tnx.CombinatorialComplex(G) >>> d = { ... (1, 2): {"color": "red", "attr2": 1}, ... (0, 1): {"color": "blue", "attr2": 3}, ... } >>> CCC.set_cell_attributes(d) >>> cell_color = CCC.get_cell_attributes("color") >>> cell_color[frozenset({0, 1})] 'blue' """ return super().get_cell_attributes(name, rank)
def _add_node(self, node, **attr) -> None: """Add one node as a hyperedge of rank 0. Parameters ---------- node : hashable The node to add as a hyperedge of rank 0. **attr : dict Additional attributes associated with the node. Returns ------- None This method does not return any value. It adds the node as a hyperedge in-place. """ if node in self: self._complex_set.hyperedge_dict[0][frozenset({node})].update(**attr) else: self._add_hyperedge(hyperedge=node, rank=0, **attr)
[docs] def add_node(self, node, **attr) -> None: """Add a node to a CCC. Parameters ---------- node : Hashable The node to add to this combinatorial complex. **attr : keyword arguments, optional Attributes to add to the node as key=value pairs. """ self._add_node(node, **attr)
def _add_nodes_of_hyperedge(self, hyperedge_): """Adding nodes of a hyperedge. Parameters ---------- hyperedge_ : frozenset of elements The hyperedge for which to add nodes. Returns ------- None This method does not return any value. """ for i in hyperedge_: if 0 not in self._complex_set.hyperedge_dict: self._complex_set.hyperedge_dict[0] = {} if i not in self._complex_set.hyperedge_dict[0]: self._complex_set.hyperedge_dict[0][frozenset({i})] = {"weight": 1} def _add_hyperedge_helper(self, hyperedge_, rank, **attr): """Add hyperedge. Parameters ---------- hyperedge_ : frozenset of elements A cell in a combinatorial complex. rank : int The rank of a hyperedge. **attr : arbitrary attrs Additional attributes associated with the hyperedge. Returns ------- None This method does not return any value. """ if rank not in self._complex_set.hyperedge_dict: self._complex_set.hyperedge_dict[rank] = {} if hyperedge_ not in self._complex_set.hyperedge_dict[rank]: self._complex_set.hyperedge_dict[rank][hyperedge_] = {} self._complex_set.hyperedge_dict[rank][hyperedge_] = {"weight": 1} self._complex_set.hyperedge_dict[rank][hyperedge_].update(**attr) else: self._complex_set.hyperedge_dict[rank][hyperedge_].update(**attr) self._add_nodes_of_hyperedge(hyperedge_) for i in hyperedge_: if i not in self._node_membership: self._node_membership[i] = set() self._node_membership[i].add(hyperedge_) def _CCC_condition(self, hyperedge_: set, rank: int) -> None: """Check if hyperedge_ satisfies the CCC condition. Parameters ---------- hyperedge_ : frozenset of elements The hyperedge to check for CCC condition. rank : int The rank of the hyperedge. Raises ------ ValueError If a violation of the combinatorial complex condition is detected. Notes ----- The CCC condition ensures that hyperedges in the complex follow the combinatorial complex condition. This method checks if adding the given hyperedge would violate this condition. Returns ------- None If there is no issue with the CCC. """ for node in hyperedge_: if node in self._node_membership: for existing_hyperedge in self._node_membership[node]: if existing_hyperedge == hyperedge_: continue e_rank = self._complex_set.get_rank(existing_hyperedge) if rank > e_rank and existing_hyperedge.issuperset(hyperedge_): raise ValueError( "a violation of the combinatorial complex condition:" f"the hyperedge {existing_hyperedge} in the complex has rank {e_rank} is larger than {rank}, the rank of the input hyperedge {hyperedge_} " ) if rank < e_rank and hyperedge_.issuperset(existing_hyperedge): raise ValueError( "violation of the combinatorial complex condition : " f"the hyperedge {existing_hyperedge} in the complex has rank {e_rank} is smaller than {rank}, the rank of the input hyperedge {hyperedge_} " ) def _add_hyperedge(self, hyperedge, rank, **attr): """Add hyperedge. Parameters ---------- hyperedge : HyperEdge, Hashable, or Iterable A cell in a combinatorial complex. rank : int The rank of a hyperedge; must be zero when the hyperedge is Hashable. **attr : attribute associated with hyperedge Additional attributes associated with the hyperedge. Returns ------- None This method does not return any value. It simply adds the hyperedge in-place. Notes ----- The `add_hyperedge` method is used for adding hyperedges to the `HyperEdgeView` instance. It takes three arguments: `hyperedge`, `rank`, and `**attr`. `hyperedge` is a tuple or `HyperEdge` instance representing the hyperedge to be added, and `rank` is an integer representing the rank of the hyperedge. The `**attr` argument allows the inclusion of additional attributes associated with the hyperedge. The `add_hyperedge` method then adds the hyperedge to the `hyperedge_dict` attribute of the `HyperEdgeView` instance, using the hyperedge's rank as the key and the hyperedge itself as the value. This allows the hyperedge to be accessed later using its rank. """ if not isinstance(rank, int) or rank < 0: raise ValueError(f"rank must be a non-negative integer, got {rank}") if isinstance(hyperedge, str): if rank != 0: raise ValueError(f"rank must be zero for string input, got rank {rank}") hyperedge_set = frozenset({hyperedge}) elif isinstance(hyperedge, Hashable) and not isinstance(hyperedge, Iterable): if rank != 0: raise ValueError(f"rank must be zero for hashables, got rank {rank}") hyperedge_set = frozenset({hyperedge}) elif isinstance(hyperedge, Iterable | HyperEdge): if len(hyperedge) == 1 and rank != 0: raise ValueError( f"rank must be zero cells with single element, got rank {rank} with input hyperedge {hyperedge} " ) if isinstance(hyperedge, HyperEdge): hyperedge_ = hyperedge.elements else: if not all(isinstance(i, Hashable) for i in hyperedge): raise ValueError( f"Input hyperedge {hyperedge} contain non-hashable elements." ) hyperedge_ = frozenset(hyperedge) if rank == 0 and len(hyperedge_) > 1: raise ValueError( "rank must be positive for higher order hyperedges, got rank = 0" ) hyperedge_set = hyperedge_ else: raise ValueError("Invalid hyperedge type") if rank == 0: if 0 not in self._complex_set.hyperedge_dict: self._complex_set.hyperedge_dict[0] = {} self._complex_set.hyperedge_dict[0][hyperedge_set] = {} self._complex_set.hyperedge_dict[0][hyperedge_set].update(attr) if "weight" not in self._complex_set.hyperedge_dict[0][hyperedge_set]: self._complex_set.hyperedge_dict[0][hyperedge_set]["weight"] = 1 else: if hyperedge_set in self.cells: e_rank = self._complex_set.get_rank(hyperedge_set) if e_rank > rank: self.remove_cell(hyperedge_set) self._add_hyperedge_helper(hyperedge_set, rank, **attr) if ( "weight" not in self._complex_set.hyperedge_dict[rank][hyperedge_set] ): self._complex_set.hyperedge_dict[rank][hyperedge_set][ "weight" ] = 1 return if e_rank < rank: self._CCC_condition(hyperedge_, rank) self.remove_cell(hyperedge_set) self._add_hyperedge_helper(hyperedge_set, rank, **attr) if ( "weight" not in self._complex_set.hyperedge_dict[rank][hyperedge_set] ): self._complex_set.hyperedge_dict[rank][hyperedge_set][ "weight" ] = 1 return self._add_hyperedge_helper(hyperedge_set, rank, **attr) if ( "weight" not in self._complex_set.hyperedge_dict[rank][hyperedge_set] ): self._complex_set.hyperedge_dict[rank][hyperedge_set]["weight"] = 1 return self._CCC_condition(hyperedge_, rank) self._add_hyperedge_helper(hyperedge_set, rank, **attr) if "weight" not in self._complex_set.hyperedge_dict[rank][hyperedge_set]: self._complex_set.hyperedge_dict[rank][hyperedge_set]["weight"] = 1 if isinstance(hyperedge, HyperEdge): self._complex_set.hyperedge_dict[rank][hyperedge_set].update( hyperedge._attributes ) def _incidence_matrix( self, rank: int, to_rank: int | None = None, incidence_type: Literal["up", "down"] = "up", weight: Any | None = None, sparse: bool = True, index: bool = False, ): """Compute incidence matrix. An incidence matrix indexed by r-ranked hyperedges k-ranked hyperedges r != k, when k is None incidence_type will be considered instead Parameters ---------- rank : int For which rank of cells to compute the incidence matrix. to_rank : int, optional The rank for computing the incidence matrix. incidence_type : {'up', 'down'}, default='up' Whether to compute the up or down incidence matrix. weight : None Functionality to be added. Currently set to None. sparse : bool, default=True Whether to return a sparse or dense incidence matrix. index : bool, default=False Whether to return the indices of the rows and columns of the incidence matrix. Returns ------- incidence_matrix : scipy.sparse.csr.csr_matrix or numpy.ndarray. The incidence matrix. row dictionary : dict Dictionary identifying rows with items in the entityset's children. column dictionary : dict Dictionary identifying columns with items in the entityset's uidset. Notes ----- The `incidence_matrix` method is for generating the incidence matrix of a ColoredHyperGraph. An incidence matrix describes the relationships between the hyperedges of a complex. The rows correspond to the hyperedges, and the columns correspond to the faces. The entries in the matrix are either 0 or 1, depending on whether a hyperedge contains a given face or not. To generate the incidence matrix, the method first creates a dictionary where the keys are the faces of the complex, and the values are the hyperedges that contain that face. The method then iterates over the hyperedges in the `HyperEdgeView` instance, and for each hyperedge, it checks which faces it contains. For each face that the hyperedge contains, the method increments the corresponding entry in the matrix. Finally, the method returns the completed incidence matrix. """ if rank == to_rank: raise ValueError( "incidence matrix can be computed for k!=r, got equal r and k." ) if to_rank is None: if incidence_type == "up": children = self.skeleton(rank) uidset = self.skeleton(rank, level="up") elif incidence_type == "down": uidset = self.skeleton(rank) children = self.skeleton(rank, level="down") else: raise ValueError( "Invalid value for incidence_type. Must be 'up' or 'down'" ) else: if ( rank < to_rank ): # up incidence is defined between two skeletons of different ranks children = self.skeleton(rank) uidset = self.skeleton(to_rank) elif rank > to_rank: raise ValueError("incidence matrix can be computed for r<k, got r>k.") return compute_set_incidence(children, uidset, sparse, index)
[docs] def incidence_matrix( self, rank: int, to_rank=None, incidence_type: Literal["up", "down"] = "up", weight: str | None = None, sparse: bool = True, index: bool = False, ): """Compute incidence matrix for the CCC between rank and to_rank skeleti. Parameters ---------- rank, to_rank : int For which rank of cells to compute the incidence matrix. incidence_type : {"up", "down"}, default="up" Whether to compute the up or down incidence matrix. weight : bool, default=False The name of the cell attribute to use as weights for the incidence matrix. If `None`, all cell weights are considered to be one. sparse : bool, default=True Whether to return a sparse or dense incidence matrix. index : bool, default=False Whether to return the indices of the rows and columns of the incidence matrix. Returns ------- row_indices, col_indices : dict Dictionary assigning each row and column of the incidence matrix to a cell. incidence_matrix : scipy.sparse.csr.csr_matrix The incidence matrix. """ return self._incidence_matrix( rank, to_rank, incidence_type=incidence_type, sparse=sparse, index=index )
[docs] def adjacency_matrix(self, rank, via_rank, s: int = 1, index: bool = False): """Sparse weighted `s-adjacency matrix`. Parameters ---------- rank, via_rank : int Two ranks for skeletons in the input combinatorial complex. s : int, default=1 Minimum number of edges shared by neighbors with node. index : bool, default=False Whether to return the indices of the rows and columns of the adjacency matrix. Returns ------- indices : list List identifying the rows and columns of the adjacency matrix with the cells of the combinatorial complex. Only returned if `index` is True. adjacency_matrix : scipy.sparse.csr.csr_matrix The adjacency matrix of this combinatorial complex. Examples -------- >>> CCC = tnx.CombinatorialComplex() >>> CCC.add_cell([1, 2], rank=1) >>> CCC.add_cell([3, 4], rank=1) >>> CCC.add_cell([1, 2, 3, 4], rank=2) >>> CCC.add_cell([1, 2, 4], rank=2) >>> CCC.add_cell([1, 2, 3, 4, 5, 6, 7], rank=3) >>> CCC.adjacency_matrix(0, 1) """ if via_rank is not None and rank > via_rank: raise ValueError("rank must be lesser than via_rank, must be r<k, got r>k") return super().adjacency_matrix(rank, via_rank, s, index)
[docs] def coadjacency_matrix(self, rank, via_rank, s: int = 1, index: bool = False): """Compute the coadjacency matrix of self. Parameters ---------- rank, via_rank : int Two ranks for skeletons in the input combinatorial complex , such that r>k. s : int, default=1 Minimum number of edges shared by neighbors with node. index : bool, default=False Whether to return the indices of the rows and columns of the coadjacency matrix. Returns ------- indices : list List identifying the rows and columns of the coadjacency matrix with the cells of the combinatorial complex. Only returned if `index` is True. coadjacency_matrix : scipy.sparse.csr.csr_matrix The coadjacency matrix of this combinatorial complex. """ if via_rank is not None and rank < via_rank: raise ValueError("rank must be greater than via_rank") return super().coadjacency_matrix(rank, via_rank, s, index)
[docs] def dirac_operator_matrix(self, weight: str | None = None, index: bool = False): """Compute dirac operator matrix of self. Parameters ---------- weight : str, optional The name of the cell attribute to use as weights for the dirac operator matrix. If `None`, the matrix is binary. index : bool, default=False If True, return will include a dictionary of all cells in the complex uid. Returns ------- scipy.sparse.csr.csc_matrix | tuple[dict, dict, scipy.sparse.csc_matrix] The dirac operator matrix, if `index` is False; otherwise, row_indices, col_indices : dict List identifying rows and columns of the dirac operator matrix. Only returned if `index` is True. dirac_matrix : scipy.sparse.csr.csc_matrix The dirac operator matrix of this combinatorial complex. Examples -------- >>> CCC = tnx.CombinatorialComplex() >>> CCC.add_cell([1, 2, 3, 4], rank=2) >>> CCC.add_cell([1, 2], rank=1) >>> CCC.add_cell([2, 3], rank=1) >>> CCC.add_cell([1, 4], rank=1) >>> CCC.add_cell([3, 4, 8], rank=2) >>> CCC.dirac_operator_matrix() """ from scipy.sparse import bmat index_set = [] incidence = {} for i in range(self.dim + 1): for j in range(i + 1, self.dim + 1): indexj, indexi, Bij = self.incidence_matrix( i, j, weight=weight, index=True ) incidence[(i, j)] = Bij index_set.append(indexj) index_set.append(indexi) dirac = [] for i in range(self.dim + 1): row = [] for j in range(self.dim + 1): if (i, j) in incidence: row.append(incidence[(i, j)]) elif (j, i) in incidence: row.append(incidence[(j, i)].T) else: row.append(None) dirac.append(row) dirac_mat = bmat(dirac) if index: d = {} shift = 0 for i in index_set: i = {k: v + shift for k, v in i.items()} d.update(i) shift = len(d) return d, dirac_mat return dirac_mat
[docs] def add_cells_from(self, cells, ranks: Iterable[int] | int | None = None) -> None: """Add cells to combinatorial complex. Parameters ---------- cells : iterable of hashables For hashables the cells returned will be empty. ranks : iterable or int, optional When iterable, len(ranks) == len(cells). """ if ranks is None: for cell in cells: if not isinstance(cell, HyperEdge): raise ValueError( f"input must be an HyperEdge {cell} object when rank is None" ) if cell.rank is None: raise ValueError(f"input HyperEdge {cell} has None rank") self.add_cell(cell, cell.rank) else: if isinstance(cells, Iterable) and isinstance(ranks, Iterable): for cell, rank in zip(cells, ranks, strict=True): self.add_cell(cell, rank) if isinstance(cells, Iterable) and isinstance(ranks, int): for cell in cells: self.add_cell(cell, ranks)
[docs] def add_cell(self, cell, rank=None, **attr) -> None: """Add a single cells to combinatorial complex. Parameters ---------- cell : hashable, iterable or HyperEdge If hashable the cell returned will be empty. rank : int Rank of the cell. **attr : keyword arguments, optional Attributes to add to the cell as key=value pairs. """ if self.graph_based and rank == 1: if not isinstance(cell, Iterable): raise TypeError( "Rank 1 cells in graph-based CombinatorialComplex must be Iterable." ) if len(cell) != 2: raise ValueError( f"Rank 1 cells in graph-based CombinatorialComplex must have size equal to 1 got {cell}." ) self._add_hyperedge(cell, rank, **attr)
[docs] def remove_cell(self, cell) -> None: """Remove a single cell from CCC. Parameters ---------- cell : hashable or RankedEntity The cell to remove from this combinatorial complex. Notes ----- Deletes reference to cell from all of its nodes. If any of its nodes do not belong to any other cells the node is dropped from self. """ super().remove_cell(cell)
[docs] def remove_cells(self, cell_set) -> None: """Remove cells from CCC. Parameters ---------- cell_set : iterable of hashables The cells to remove from this combinatorial complex. """ super().remove_cells(cell_set)
[docs] def clone(self) -> Self: """Return a copy of the simplex. The clone method by default returns an independent shallow copy of the simplex and attributes. That is, if an attribute is a container, that container is shared by the original and the copy. Use Python's `copy.deepcopy` for new containers. Returns ------- CombinatorialComplex A copy of this combinatorial complex. """ CCC = self.__class__(graph_based=self.graph_based) for cell in self.cells: CCC.add_cell(cell, self.cells.get_rank(cell)) return CCC
[docs] def singletons(self): """Return a list of singleton cell. A singleton cell is a node of degree 0. Returns ------- list A list of cells uids. Examples -------- >>> CCC = tnx.CombinatorialComplex() >>> CCC.add_cell([1, 2], rank=1) >>> CCC.add_cell([3, 4], rank=1) >>> CCC.add_cell([9], rank=0) >>> CCC.singletons() [frozenset({9})] """ return [k for k in self.skeleton(0) if self.degree(next(iter(k)), None) == 0]
[docs] def remove_singletons(self): """Construct new CCC with singleton cells removed. Returns ------- CombinatorialComplex A copy of this combinatorial complex with singleton cells removed. """ cells = [cell for cell in self.cells if cell not in self.singletons()] return self.restrict_to_cells(cells)