Lift patch¶
This notebook demonstrates finite patches extracted from the infinite lift using
lift_patch(seed, ...).
You will see how the output behaves for different container types:
PeriodicGraph/PeriodicMultiGraph(undirected containers)PeriodicDiGraph/PeriodicMultiDiGraph(directed containers)
Key idea:
- Traversal uses weak connectivity in the lift (successors and predecessors).
- The returned patch is directed when the source container is directed.
- For directed patches, you can still obtain an undirected view via
patch.to_networkx(as_undirected=True, ...).
from pprint import pprint
import networkx as nx
from pbcgraph import (
PeriodicGraph,
PeriodicMultiGraph,
PeriodicDiGraph,
PeriodicMultiDiGraph,
PBC_META_KEY,
)
Helper: summarize a patch¶
A LiftPatch stores node instances and edge records. The most convenient way to
work with it is often to export to NetworkX via to_networkx().
def summarize_patch(patch, *, max_edges=12):
print('patch nodes:', len(patch.nodes))
print('patch edges:', len(patch.edges))
print('is_directed:', patch.is_directed)
print('is_multigraph:', patch.is_multigraph)
print('seed:', patch.seed)
print('radius:', patch.radius)
print('box:', patch.box)
print('\nfirst nodes:')
pprint(list(patch.nodes)[:10])
print('\nfirst edges:')
pprint(list(patch.edges)[:max_edges])
1) Undirected periodic graph (PeriodicGraph)¶
Here the source container is undirected (internally stored as two directed
realizations per bond). The patch exports as nx.Graph.
G = PeriodicGraph(dim=2)
G.add_edge('A', 'B', (0, 0))
G.add_edge('B', 'C', (0, 0))
G.add_edge('C', 'A', (1, 0)) # periodic cycle generator along x
patch = G.lift_patch(('A', (0, 0)), radius=2)
summarize_patch(patch)
nxG = patch.to_networkx()
print('\nexport type:', type(nxG))
print('nx nodes:', nxG.number_of_nodes())
print('nx edges:', nxG.number_of_edges())
patch nodes: 5
patch edges: 4
is_directed: False
is_multigraph: False
seed: ('A', (0, 0))
radius: 2
box: None
first nodes:
[('A', (0, 0)), ('B', (-1, 0)), ('B', (0, 0)), ('C', (-1, 0)), ('C', (0, 0))]
first edges:
[(('A', (0, 0)), ('B', (0, 0)), {}),
(('A', (0, 0)), ('C', (-1, 0)), {}),
(('B', (-1, 0)), ('C', (-1, 0)), {}),
(('B', (0, 0)), ('C', (0, 0)), {})]
export type: <class 'networkx.classes.graph.Graph'>
nx nodes: 5
nx edges: 4
2) Undirected multigraph (PeriodicMultiGraph)¶
A multigraph can store multiple periodic edges between the same quotient nodes.
The patch exports as nx.MultiGraph and preserves edge keys.
H = PeriodicMultiGraph(dim=1)
H.add_edge('A', 'B', (0,), label='bond-1')
H.add_edge('A', 'B', (1,), label='bond-2')
patch2 = H.lift_patch(('A', (0,)), radius=1)
summarize_patch(patch2)
nxH = patch2.to_networkx()
print('\nexport type:', type(nxH))
print('edges with data:')
for u, v, k, data in nxH.edges(keys=True, data=True):
print(u, ' -- ', v, ' key=', k, ' data=', data)
patch nodes: 3
patch edges: 2
is_directed: False
is_multigraph: True
seed: ('A', (0,))
radius: 1
box: None
first nodes:
[('A', (0,)), ('B', (0,)), ('B', (1,))]
first edges:
[(('A', (0,)), ('B', (0,)), 0, {'label': 'bond-1'}),
(('A', (0,)), ('B', (1,)), 1, {'label': 'bond-2'})]
export type: <class 'networkx.classes.multigraph.MultiGraph'>
edges with data:
('A', (0,)) -- ('B', (0,)) key= 0 data= {'label': 'bond-1'}
('A', (0,)) -- ('B', (1,)) key= 1 data= {'label': 'bond-2'}
3) Directed periodic graph (PeriodicDiGraph)¶
In step 5, lift_patch became direction-preserving for directed containers.
This avoids the old drawback where u -> v and v -> u could collapse in an
undirected patch.
You can still request an undirected view from the patch export:
undirected_mode='multigraph': one undirected multiedge per directed edgeundirected_mode='orig_edges': one undirected edge with__pbcgraph__={'orig_edges': [...]}
D = PeriodicDiGraph(dim=1)
D.add_edge('A', 'B', (0,), label='x')
D.add_edge('B', 'A', (0,), label='y')
patch3 = D.lift_patch(('A', (0,)), radius=1)
summarize_patch(patch3)
nxD = patch3.to_networkx()
print('\nexport type:', type(nxD))
print('directed edges:')
for u, v, data in nxD.edges(data=True):
print(u, ' -> ', v, ' data=', data)
patch nodes: 2
patch edges: 2
is_directed: True
is_multigraph: False
seed: ('A', (0,))
radius: 1
box: None
first nodes:
[('A', (0,)), ('B', (0,))]
first edges:
[(('A', (0,)), ('B', (0,)), {'label': 'x'}),
(('B', (0,)), ('A', (0,)), {'label': 'y'})]
export type: <class 'networkx.classes.digraph.DiGraph'>
directed edges:
('A', (0,)) -> ('B', (0,)) data= {'label': 'x'}
('B', (0,)) -> ('A', (0,)) data= {'label': 'y'}
# Undirected view: multigraph
nxU = patch3.to_networkx(as_undirected=True, undirected_mode='multigraph')
print(type(nxU))
print('undirected multiedges between A and B:', nxU.number_of_edges(('A', (0,)), ('B', (0,))))
for u, v, data in nxU.edges(data=True):
if {u, v} != {('A', (0,)), ('B', (0,))}:
continue
print(u, '--', v, 'label=', data.get('label'), 'tail=', data.get(PBC_META_KEY, {}).get('tail'), 'head=', data.get(PBC_META_KEY, {}).get('head'))
<class 'networkx.classes.multigraph.MultiGraph'>
undirected multiedges between A and B: 2
('A', (0,)) -- ('B', (0,)) label= x tail= ('A', (0,)) head= ('B', (0,))
('A', (0,)) -- ('B', (0,)) label= y tail= ('B', (0,)) head= ('A', (0,))
# Undirected view: collapsed Graph with orig_edges bags
nxC = patch3.to_networkx(as_undirected=True, undirected_mode='orig_edges')
print(type(nxC))
data = nxC.edges[('A', (0,)), ('B', (0,))]
print('orig_edges records:')
pprint(data[PBC_META_KEY]['orig_edges'])
<class 'networkx.classes.graph.Graph'>
orig_edges records:
[{'attrs': {'label': 'x'},
'head': ('B', (0,)),
'key': None,
'tail': ('A', (0,))},
{'attrs': {'label': 'y'},
'head': ('A', (0,)),
'key': None,
'tail': ('B', (0,))}]
4) Directed multigraph (PeriodicMultiDiGraph)¶
Parallel directed edges are preserved in the patch export as nx.MultiDiGraph.
M = PeriodicMultiDiGraph(dim=1)
M.add_edge('A', 'B', (0,), label='e1')
M.add_edge('A', 'B', (0,), label='e2')
M.add_edge('B', 'A', (0,), label='back')
patch4 = M.lift_patch(('A', (0,)), radius=1)
summarize_patch(patch4)
nxM = patch4.to_networkx()
print('\nexport type:', type(nxM))
print('directed multiedges A->B:', nxM.number_of_edges(('A', (0,)), ('B', (0,))))
for u, v, k, data in nxM.edges(keys=True, data=True):
if u == ('A', (0,)) and v == ('B', (0,)):
print('A->B key=', k, 'label=', data.get('label'))
patch nodes: 2
patch edges: 3
is_directed: True
is_multigraph: True
seed: ('A', (0,))
radius: 1
box: None
first nodes:
[('A', (0,)), ('B', (0,))]
first edges:
[(('A', (0,)), ('B', (0,)), 0, {'label': 'e1'}),
(('A', (0,)), ('B', (0,)), 1, {'label': 'e2'}),
(('B', (0,)), ('A', (0,)), 0, {'label': 'back'})]
export type: <class 'networkx.classes.multidigraph.MultiDiGraph'>
directed multiedges A->B: 2
A->B key= 0 label= e1
A->B key= 1 label= e2
5) Using a bounding box (box and box_rel)¶
Besides a BFS radius, you can restrict the patch by an absolute cell box.
The box is a tuple of (min, max) intervals for each lattice coordinate.
box_rel is convenient when you want a symmetric window around the seed shift.
P = PeriodicGraph(dim=1)
P.add_edge('A', 'A', (1,), label='step')
# radius-based patch
patch_r = P.lift_patch(('A', (0,)), radius=3)
print('radius=3 nodes:', patch_r.nodes)
# box-based patch: only shifts in [-1, 1]
patch_b = P.lift_patch(('A', (0,)), box=((-1, 1),))
print('box=[-1,1] nodes:', patch_b.nodes)
# box_rel: relative window around seed shift
patch_br = P.lift_patch(('A', (5,)), box_rel=((-1, 1),))
print('seed shift 5, box_rel=[-1,1] nodes:', patch_br.nodes)
radius=3 nodes: (('A', (-3,)), ('A', (-2,)), ('A', (-1,)), ('A', (0,)), ('A', (1,)), ('A', (2,)), ('A', (3,)))
box=[-1,1] nodes: (('A', (-1,)), ('A', (0,)))
seed shift 5, box_rel=[-1,1] nodes: (('A', (4,)), ('A', (5,)))