"""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)