Algorithm Search⌘K

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
incidence_matrix.pyRawGitHub
# 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

Read the full page about Incidence matrix