Incidence matrix in Python
V×E matrix marking which vertices each edge touches.
- File
incidence_matrix.py, 78 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/incidence-matrix/. The authors are listed in prov/7DQ56G.jsonl.
class IncidenceMatrix:
"""The n-by-m vertex-edge incidence matrix B of a graph, stored densely.
Undirected: B[v][e] is the number of times v is an end of e (1, or 2 for a
self-loop). Directed (the convention of CLRS): B[v][e] = -1 if e leaves v,
+1 if e enters v, and 0 otherwise; self-loops are not allowed. Parallel
edges are allowed: they are different columns.
"""
def __init__(self, n: int, directed: bool = False) -> None:
self.directed = directed
self.B: list[list[int]] = [[] for _ in range(n)]
def n(self) -> int:
return len(self.B)
def m(self) -> int:
return len(self.B[0]) if self.B else 0
def add_edge(self, u: int, v: int) -> int:
if self.directed and u == v:
raise ValueError("a self-loop has no column in a signed incidence matrix")
for row in self.B: # @line L1
row.append(0)
if self.directed: # @line L2
self.B[u][-1] = -1
self.B[v][-1] = 1
else:
self.B[u][-1] += 1 # @line L3
self.B[v][-1] += 1
return self.m() - 1 # @line L4
def ends(self, e: int) -> tuple[int, int]:
"""Return (tail, head) of a directed edge, or the two ends of an undirected one."""
found: list[int] = []
tail = head = -1
for v in range(self.n()): # @line L5
x = self.B[v][e]
if x == -1:
tail = v
elif x == 1 and self.directed:
head = v
elif x != 0:
found += [v] * x # @line L6
if self.directed:
return tail, head
return found[0], found[1] # @line L7
def incident(self, v: int) -> list[int]:
"""The edges at v (for a directed graph: leaving or entering v), in column order."""
return [e for e, x in enumerate(self.B[v]) if x != 0] # @line L8
def adjacent(self, u: int, v: int) -> bool:
"""Is there an edge from u to v (between u and v if undirected)?"""
for e in range(self.m()): # @line L9
if self.directed:
if self.B[u][e] == -1 and self.B[v][e] == 1: # @line L10
return True
elif u == v:
if self.B[u][e] == 2:
return True
elif self.B[u][e] != 0 and self.B[v][e] != 0:
return True
return False # @line L11
def b_bt(self) -> list[list[int]]:
"""The n-by-n product B times its transpose."""
n, m = self.n(), self.m()
return [[sum(self.B[i][k] * self.B[j][k] for k in range(m)) for j in range(n)] for i in range(n)]
def bt_b(self) -> list[list[int]]:
"""The m-by-m product of the transpose of B times B."""
n, m = self.n(), self.m()
return [[sum(self.B[k][i] * self.B[k][j] for k in range(n)) for j in range(m)] for i in range(m)]
test_incidence_matrix.py 69 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/incidence-matrix/. The authors are listed in prov/7DQ56G.jsonl.
import random
from incidence_matrix import IncidenceMatrix
EDGES = [(0, 1), (0, 2), (1, 2), (2, 3)]
def test_undirected_example() -> None:
g = IncidenceMatrix(4)
for u, v in EDGES:
g.add_edge(u, v)
assert g.B == [[1, 1, 0, 0], [1, 0, 1, 0], [0, 1, 1, 1], [0, 0, 0, 1]]
assert g.ends(2) == (1, 2)
assert g.incident(2) == [1, 2, 3]
assert g.adjacent(3, 2) and not g.adjacent(0, 3)
# B B^T = D + A for a simple undirected graph
assert g.b_bt() == [[2, 1, 1, 0], [1, 2, 1, 0], [1, 1, 3, 1], [0, 0, 1, 1]]
# B^T B = 2I + A(L(G))
assert g.bt_b() == [[2, 1, 1, 0], [1, 2, 1, 1], [1, 1, 2, 1], [0, 1, 1, 2]]
def test_directed_example() -> None:
g = IncidenceMatrix(4, directed=True)
for u, v in EDGES:
g.add_edge(u, v)
assert g.B == [[-1, -1, 0, 0], [1, 0, -1, 0], [0, 1, 1, -1], [0, 0, 0, 1]]
assert g.ends(3) == (2, 3)
assert g.adjacent(0, 1) and not g.adjacent(1, 0)
# B B^T = D - A of the underlying undirected graph: the Laplacian
assert g.b_bt() == [[2, -1, -1, 0], [-1, 2, -1, 0], [-1, -1, 3, -1], [0, 0, -1, 1]]
def test_loop_and_parallel() -> None:
g = IncidenceMatrix(2)
g.add_edge(0, 0)
g.add_edge(0, 1)
g.add_edge(1, 0)
assert g.B == [[2, 1, 1], [0, 1, 1]]
assert g.ends(0) == (0, 0)
assert g.adjacent(0, 0) and not g.adjacent(1, 1)
assert [sum(r) for r in g.B] == [4, 2]
def test_random() -> None:
rng = random.Random(5)
for directed in (False, True):
for _ in range(50):
n = rng.randrange(1, 7)
g = IncidenceMatrix(n, directed)
es = []
for _ in range(rng.randrange(12)):
u, v = rng.randrange(n), rng.randrange(n)
if directed and u == v:
continue
es.append((u, v))
g.add_edge(u, v)
for e, (u, v) in enumerate(es):
a, b = g.ends(e)
assert (a, b) == (u, v) or (not directed and (b, a) == (u, v))
for u in range(n):
for v in range(n):
want = any((a, b) == (u, v) or (not directed and (b, a) == (u, v)) for a, b in es)
assert g.adjacent(u, v) == want
for col in range(len(es)):
s = sum(g.B[v][col] for v in range(n))
assert s == (0 if directed else 2)
What the next level needs
- the tests did not run in this build