smooth_disorder.barcode

barcode.py — H1 Persistent Homology Barcodes and Bond-Network Entropy

This module implements the core algorithm for computing the H1 persistent homology barcode of a local atomic environment and the Bond-Network Entropy (BNE) derived from it.

Background

Amorphous solids do not have long-range order and instead we can look at the local atomic environments to understand their structure. In here we will treat a local atomic environment as a selection of atoms around a given atom and construct a subgraph where atoms are nodes and bonds between them are edges.

A “barcode” in persistent homology counts topological loops (rings) in that subgraph at different distance scales. Each atom has a corresponding barcode; the distribution of barcodes across all atoms is summarized with a single number — the information entropy of barcode probability distribution — called the Bond-Network Entropy (BNE).

References

B. Schweinhart, D. Rodney, and J.K. Mason, Phys. Rev. E 101, 052312 (2020) — definition of H1 barcode used here.

K. Iwanowski, G. Csányi, and M. Simoncelli, Phys. Rev. X 15, 041041 (2025) — BNE applied to disordered energy materials.

smooth_disorder.barcode.clear_mu_cache()[source]

Clear the Möbius inversion memoization cache.

The mu dictionary accumulates values across calls to recursive_find_mu. Call this function in test teardown to prevent state leaking between tests. In normal usage (single script) it never needs to be called.

smooth_disorder.barcode.obtain_H1_barcode(adjacency_matrix: ndarray, layers: List[List[int]], mu: dict)[source]

Compute the H1 persistent homology barcode for a local atomic environment.

What is an H1 barcode?

H1 (first homology) counts closed loops (rings) in a graph. A “barcode” in persistent homology is a record of which loops exist at different distance scales. Here we count loops present in each pair of shells (i, j), where shell i contains all atoms reachable in exactly i bond hops from the centre.

Algorithm overview

  1. Compute F[i,j] = number of algebraically independent rings in the union of shells i through j, using the Euler characteristic formula rank = (components) - (atoms) + (edges). The rank of the first homology equals the number of independent loops.

  2. Apply the Möbius inversion to go from cumulative F to local G: G[c,d] = Σ_{(a,b) (c, d)} F[a,b] * μ((a,b),(c,d)). G[c,d] counts rings that start in shell c and end in shell d (i.e. rings that first close when shell d is reached).

Parameters:
  • adjacency_matrix (np.ndarray, shape (n, n)) – Local adjacency matrix of the environment (from obtain_local_number_environment_big_structures).

  • layers (list of lists) – Layer assignment from the BFS (output of same function).

  • mu (dict) – Möbius function cache (pass the module-level mu dict).

Returns:

  • G (np.ndarray, shape (n_layers, n_layers)) – H1 barcode matrix. G[c,d] = number of independent rings that first appear between shells c and d.

  • F (np.ndarray, shape (n_layers, n_layers)) – Cumulative ring count matrix. F[i,j] = total independent rings in shells i through j.

smooth_disorder.barcode.obtain_local_number_environment_big_structures(adjacency_matrix: ndarray, atom_index: int, distances: ndarray, idx_distances: ndarray, n_environment_atoms: int)[source]

Extract the local atomic environment (LAE) centred on a given atom.

The LAE is the subgraph formed by the n_environment_atoms atoms that are geometrically closest to the central atom (including central atom). We also record which “layer” (bond-distance shell) each atom belongs to, which is needed for the barcode calculation.

How the layer discovery works

Layers are found using a breadth-first search (BFS) implemented via matrix multiplication:

current_layer = A @ previous_layer_indicator

where A is the local adjacency matrix and the indicator vector has 1 at atoms already found and 0 elsewhere. Multiplying spreads the “signal” to all bonded neighbours in one step. By taking the XOR of the new indicator with the old one we find only the newly discovered atoms, i.e. the next shell.

Parameters:
  • adjacency_matrix (np.ndarray, shape (N, K)) – Global adjacency matrix for the full structure. Entry [i, j] = 1 means atoms i and idx_distances[i, j] are bonded, 0 otherwise.

  • atom_index (int) – Index of the central atom in the global structure.

  • distances (np.ndarray, shape (N, K)) – Pre-computed distances from each atom to itself and its K-1 nearest neighbours (K in total; output of obtain_distances_ase or obtain_distances_big_structures).

  • idx_distances (np.ndarray, shape (N, K)) – Global atom indices corresponding to the distances array.

  • n_environment_atoms (int) – Number of atoms to include in the local environment (n in “LAE of n”).

Returns:

  • local_adjacency_matrix (np.ndarray, shape (n_environment_atoms, n_environment_atoms)) – Adjacency matrix restricted to the local environment.

  • layers (list of lists) – layers[k] contains the local indices of atoms first reached in hop k. layers[0] = [local_atom_index] (the central atom itself).

  • local_atom_index (int) – Index of the central atom within the local environment (0 … n-1).

  • global_index (np.ndarray, shape (n_environment_atoms,)) – Global atom indices of the atoms in the local environment, sorted.

smooth_disorder.barcode.recursive_find_mu(mu: dict, a: int, b: int, c: int, d: int)[source]

Recursively compute the Möbius inversion function μ((a,b),(c,d)).

Background

The Möbius inversion is a mathematical tool from combinatorics that “inverts” a cumulative sum over a partially ordered set. Here the partial order is defined on pairs of non-negative integers (i, j) with i ≤ j, ordered by (a,b) ≤ (c,d) iff c ≤ a and d ≥ b.

In the barcode calculation, F[a,b] counts rings in a union of shells a through b. The Möbius inversion is used to extract G[c,d] — rings that first appear in the interval [c,d] specifically — from the cumulative F values.

Reference: B. Schweinhart et al., Phys. Rev. E 101, 052312 (2020).

Memoization

Results are stored in the mu dictionary so each (a,b,c,d) combination is computed only once, then reused on later calls.

Parameters:
  • mu (dict) – Lookup table passed by reference; modified in-place.

  • a (int) – Integer indices satisfying c ≤ a, a ≤ b, b ≤ d.

  • b (int) – Integer indices satisfying c ≤ a, a ≤ b, b ≤ d.

  • c (int) – Integer indices satisfying c ≤ a, a ≤ b, b ≤ d.

  • d (int) – Integer indices satisfying c ≤ a, a ≤ b, b ≤ d.

Returns:

int – The value of the Möbius function μ((a,b),(c,d)).

smooth_disorder.barcode.reduce_barcode(G_matrix)[source]

Remove trailing all-zero rows and columns from a barcode matrix.

Why this is needed

Two barcodes should be considered identical if they encode the same topological information. A zero-padded matrix and its trimmed version describe the same rings, but would compare as different arrays. Reducing to a canonical (minimal) form allows us to count how many atoms share the same barcode type.

The function works recursively: if the last column sums to zero, it (and the matching last row) carry no ring information and are removed. We keep trimming until either a 1x1 matrix remains or the last column is non-zero.

Parameters:

G_matrix (np.ndarray, shape (m, m)) – Barcode matrix to reduce.

Returns:

np.ndarray – Reduced barcode matrix with trailing zero rows/columns removed.