Skip to content

Concepts and API guide

This page explains how to think about pbcgraph objects and what the main operations mean in practice. For exact function/class signatures, see the API Reference section.

Quotient graph vs infinite lift

A periodic graph in pbcgraph is stored as a finite quotient graph (internally a NetworkX MultiDiGraph):

  • nodes are "templates" (e.g., sites, molecules, net vertices),
  • directed edges represent connections between templates,
  • each directed edge carries a translation vector \(t\in\mathbb{Z}^d\) telling you how the unit-cell index changes.

A lifted instance node is written as:

\[ (u, s),\quad u\in V_{quot},\; s\in \mathbb{Z}^d \]

and an edge \(u\to v\) with translation \(t\) maps:

\[ (u, s) \to (v, s+t) \]

This gives you a precise infinite graph semantics without materializing the infinite graph.

Directed vs undirected

  • PeriodicDiGraph stores directed quotient edges exactly as you add them.
  • PeriodicGraph is undirected but is stored internally as two directed realizations per undirected edge:
    • u -> v with translation tvec
    • v -> u with translation -tvec

Both realizations share the same underlying user-attributes dict.

Note that get_edge_data() returns a read-only live view of that mapping (so updating attributes must be done via set_edge_attrs()).

Important invariant (PeriodicGraph)

Removing an edge by (u, v, key) removes both directed realizations. A single directed realization does not represent a valid undirected periodic edge by itself.

Multi-edges and edge identity

pbcgraph distinguishes two container families:

  • PeriodicDiGraph / PeriodicGraph: at most one edge per (u, v, tvec). Here, the translation vector is treated as part of the edge identity.
  • PeriodicMultiDiGraph / PeriodicMultiGraph: allow multiple edges per (u, v, tvec).

In all cases, edges are addressed by (u, v, key). Edge keys must be Python integers (bool is rejected).

  • If you do not provide a key, pbcgraph generates a deterministic fresh integer key.
  • A base key is shared between the two realizations of an undirected edge in PeriodicGraph / PeriodicMultiGraph.

Internal representation for self-loops

A crystallographically common pattern is a quotient self-loop with non-zero translation (`u == v`, `tvec != 0`), representing a bond to a periodic image.

NetworkX identifies edges by `(u, v, key)`, so the two directed realizations of such an undirected edge would collide if they shared the same key.

To support this, pbcgraph stores undirected realizations using private internal keys derived from the base key (a pair `_UKey(base, +1)` / `_UKey(base, -1)`). The public API always exposes only the base key (an int).

Attributes and version counters

pbcgraph tracks two counters:

  • structural_version: increments on node/edge add/remove.
  • data_version: increments on attribute updates performed via pbcgraph APIs.

get_node_data() / get_edge_data() return a read-only live view of the underlying mapping. Direct mutation is not allowed; use set_node_attrs() / set_edge_attrs().

Components, rank, and torsion

A connected component in the quotient graph corresponds to a periodic component in the infinite lift. For directed containers in v0.1, pbcgraph uses weak connectivity (directions are ignored) when building components. Inside such a component, translations generated by cycles form a subgroup:

\[ L \subset \mathbb{Z}^d \]

pbcgraph computes:

  • rank: the rank of \(L\), i.e. the periodic dimension of the component (0D/1D/2D/3D inside a 3D lattice).
  • torsion_invariants: torsion invariants from Smith Normal Form (SNF). Non-empty invariants indicate multiple interpenetrating fragments in the infinite lift.

Instance keys (inst_key)

PeriodicComponent.inst_key((u, shift)) returns a tuple of integers encoding the coset of the lifted instance in \(\mathbb{Z}^d/L\). The numerical representation depends on a unimodular change of coordinates (see Theory), but the induced equivalence relation is exact:

  • same_fragment(a, b) is True iff the lifted nodes lie in the same infinite-lift connected fragment.

Quotient shortest paths

shortest_path_quotient(G, source, target, connectivity=...) runs BFS on the quotient graph:

  • connectivity='directed': successors only (default for PeriodicDiGraph)
  • connectivity='weak': successors ∪ predecessors (default behavior for undirected use-cases)

This is a quotient path; it does not compute an instance-aware shortest path in the infinite lift.

Finite patches of the lift

Sometimes you want a finite non-periodic graph that represents a local fragment of the infinite lift (for visualization, local feature computation, or feeding to non-periodic algorithms).

lift_patch(...) builds such a patch around a seed instance (u, shift) using either a BFS radius and/or a cell-index bounding box.

Important details:

  • Traversal is weakly connected in the lift: from an instance it considers both outgoing and incoming periodic edges (successors ∪ predecessors). This makes patch extraction useful even for directed quotient graphs.

  • Patch direction follows the container:

    • from undirected containers (PeriodicGraph, PeriodicMultiGraph), the patch is undirected;
    • from directed containers (PeriodicDiGraph, PeriodicMultiDiGraph), the patch is directed.
  • Undirected views of directed patches are available via LiftPatch.to_networkx(as_undirected=True, undirected_mode=...):

    • undirected_mode='multigraph': one undirected multiedge per directed edge; direction metadata is stored under the __pbcgraph__ edge attribute.
    • undirected_mode='orig_edges': collapsed simple graph; each undirected adjacency stores orig_edges=[...] snapshots under the __pbcgraph__ edge attribute.

NetworkX export metadata key. Direction/origin information is stored under a single reserved edge attribute named __pbcgraph__. In code, prefer using the constant PBC_META_KEY (exported from pbcgraph) instead of hardcoding the string. The library reserves this key for its own metadata; attempting to set it as a user edge attribute will raise an error.

These export options avoid silent loss of information when you want an undirected representation for an inherently directed relation.