Quickstart¶
This notebook builds a few tiny periodic graphs to show the core ideas:
- you work with a finite quotient graph on templates (nodes are site/molecule labels),
- every directed edge carries a translation vector
tvec in Z^d, - a lifted node instance is
(node_id, shift)withshift in Z^d, PeriodicComponent.same_fragment(...)answers the practical question: "are these two lifted instances in the same connected fragment of the infinite lift?"
Mental model (read this first)¶
- Think of the quotient as "what is inside the reference cell".
tvecsays how the cell index changes when you traverse an edge.- The infinite lift contains infinitely many copies of each template node.
A specific copy is written as
(u, shift). - For undirected graphs,
pbcgraphstores two directed realizations per undirected edge:u -> vwithtvec, andv -> uwith-tvec. You still use it as an undirected graph.
from pbcgraph import PeriodicGraph, PeriodicMultiGraph, PeriodicDiGraph
1) Undirected periodic graph (PeriodicGraph)¶
We build a quotient graph in Z^2 with a rank-1 periodic direction along x.
G = PeriodicGraph(dim=2)
G.add_edge('A', 'B', tvec=(0, 0))
G.add_edge('B', 'C', tvec=(0, 0))
G.add_edge('C', 'A', tvec=(1, 0)) # closes a periodic cycle; generator along x
print('nodes:', list(G.nodes()))
print('directed edge count (internal):', G.number_of_edges())
nodes: ['A', 'B', 'C'] directed edge count (internal): 6
# Show the directed realizations and their translation vectors.
for u in G.nodes():
for v, tvec, key, attrs in G.neighbors(u, keys=True, data=True):
print(f'{u} -> {v} tvec={tvec} key={key} attrs={attrs}')
A -> B tvec=(0, 0) key=0 attrs={}
A -> C tvec=(-1, 0) key=0 attrs={}
B -> A tvec=(0, 0) key=0 attrs={}
B -> C tvec=(0, 0) key=0 attrs={}
C -> A tvec=(1, 0) key=0 attrs={}
C -> B tvec=(0, 0) key=0 attrs={}
Lifted neighbors¶
neighbors_inst((u, shift)) follows quotient edges and adds the edge tvec to the current shift.
list(G.neighbors_inst(('A', (0, 0)), keys=True))
[('B', (0, 0), 0), ('C', (-1, 0), 0)]
Components and exact instance connectivity¶
components() returns PeriodicComponent objects with lattice invariants.
The most important practical method is same_fragment(...).
comp = G.components()[0]
print('rank:', comp.rank)
print('torsion invariants:', comp.torsion_invariants)
# Same template, different cell shifts:
print(comp.same_fragment(('A', (0, 0)), ('A', (1, 0)))) # True: x is periodic here
print(comp.same_fragment(('A', (0, 0)), ('A', (0, 1)))) # False: y is not generated
rank: 1 torsion invariants: () True False
2) Multi-edge undirected graph (PeriodicMultiGraph)¶
Use a multi-edge container when you want multiple distinct edges for the same (u, v, tvec).
This is typical for molecular contact graphs where a molecule pair can have multiple interactions.
H = PeriodicMultiGraph(dim=2)
k0 = H.add_edge('M1', 'M2', tvec=(0, 0), kind='Hbond', dist=2.01)
k1 = H.add_edge('M1', 'M2', tvec=(0, 0), kind='pi', dist=3.42)
k2 = H.add_edge('M1', 'M2', tvec=(1, 0), kind='contact', dist=3.90)
print('edge keys:', k0, k1, k2)
print('directed edge count (internal):', H.number_of_edges())
for v, tvec, key, attrs in H.neighbors('M1', keys=True, data=True):
print(f'M1 -> {v} tvec={tvec} key={key} attrs={attrs}')
edge keys: 0 1 2
directed edge count (internal): 6
M1 -> M2 tvec=(0, 0) key=0 attrs={'kind': 'Hbond', 'dist': 2.01}
M1 -> M2 tvec=(0, 0) key=1 attrs={'kind': 'pi', 'dist': 3.42}
M1 -> M2 tvec=(1, 0) key=2 attrs={'kind': 'contact', 'dist': 3.9}
3) Directed graph (PeriodicDiGraph)¶
Directed containers are useful when orientation is part of the semantics (transport/flow, directed state transitions, chosen backbone direction, etc.).
In v0.1, component construction and same_fragment(...) use weak connectivity even for directed graphs
(directions are ignored for connectivity questions).
D = PeriodicDiGraph(dim=1)
# A tiny directed 2-node quotient that generates translations by 1 along the lift.
D.add_edge('A', 'B', tvec=(0,))
D.add_edge('B', 'A', tvec=(1,))
c = D.components()[0]
print('rank:', c.rank)
print('same_fragment:', c.same_fragment(('A', (0,)), ('A', (3,))))
rank: 1 same_fragment: True