Skip to content

Commit d6ba4bb

Browse files
v0.1.2
Adds lift functionality for `PeriodicComponent`: * Added `lift_patch(...)` to extract finite patches of the infinite lift (BFS radius and/or abs/rel cell-index boxes) with snapshot edge attributes and deterministic deduplication for undirected containers. * For directed periodic containers, `lift_patch(...)` now produces directed patches by default, and `LiftPatch.to_networkx(as_undirected=True, undirected_mode=...)` provides undirected views via either per-edge multigraph export (`_pbc_tail/_pbc_head`) or a collapsed simple graph with `orig_edges=[...]` snapshots. * Added `canonical_lift(...)` to select one instance per quotient node for a chosen strand, with placements `tree`, `best_anchor`, and `greedy_cut`, and deterministic spanning-tree parent edges for optional `tree_edges`. * Added `CanonicalLiftError` and `LiftPatchError` for well-scoped failure modes.
2 parents d574783 + 8189d7a commit d6ba4bb

21 files changed

Lines changed: 2017 additions & 11 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@
33
This project follows a lightweight "keep a log" style.
44

55

6+
## 0.1.2 - Finite lifts and canonical lifts
7+
8+
- **Finite lift patches**
9+
- Added `lift_patch(...)`: extract a finite patch of the infinite lift around a seed instance, using either a BFS radius and/or absolute/relative cell-index bounding boxes.
10+
- Patch edges store **snapshot** attribute dicts.
11+
- For undirected containers, paired directed realizations are deduplicated deterministically.
12+
13+
- **Patch export and directed semantics**
14+
- For directed periodic containers, `lift_patch(...)` now produces a **directed** patch by default (exported as `nx.DiGraph` / `nx.MultiDiGraph`).
15+
- `LiftPatch.to_networkx(as_undirected=True, undirected_mode=...)` provides undirected views of directed patches:
16+
- `undirected_mode='multigraph'`: one undirected multiedge per directed edge (direction preserved in `_pbc_tail`/`_pbc_head`).
17+
- `undirected_mode='orig_edges'`: collapsed simple graph with `orig_edges=[...]` snapshots for each adjacency.
18+
19+
- **Canonical lifts (strand representatives)**
20+
- Added `canonical_lift(...)` to select one instance per quotient node for a chosen strand (coset in `Z^d/L`).
21+
- Implemented placements: `tree`, `best_anchor`, and `greedy_cut`.
22+
- Stored deterministic spanning-tree parent edges on `PeriodicComponent` to optionally return `tree_edges`.
23+
24+
- **Errors**
25+
- Added `CanonicalLiftError` and `LiftPatchError` for well-scoped failure modes.
26+
27+
628
## 0.1.1 - Refactoring
729

830
- **Deterministic iteration**

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ What you get in v0.1:
1717
- `PeriodicGraph` / `PeriodicDiGraph`: unique edge per `(u, v, tvec)`.
1818
- `PeriodicMultiGraph` / `PeriodicMultiDiGraph`: parallel edges allowed for the same `(u, v, tvec)`.
1919
- `PeriodicComponent`: lattice invariants (rank, SNF torsion) and exact instance connectivity via `same_fragment(...)`.
20+
- `lift_patch(...)`: extract a finite (non-periodic) patch of the infinite lift around a seed instance.
21+
- `canonical_lift(...)`: select one lifted instance per quotient node for a chosen strand (coset in `Z^d/L`).
2022

2123
## Status
2224

@@ -25,15 +27,13 @@ The API may still evolve, but the library is already useful for research code an
2527

2628
## Install
2729

28-
Requires Python 3.10+.
29-
30-
Once the project is published on PyPI:
30+
Requires Python 3.10+. Latest stable version is usually published on PyPI:
3131

3232
```bash
3333
python -m pip install pbcgraph
3434
```
3535

36-
Until then (or for the latest `dev` branch), install from GitHub:
36+
To install the latest version (or for the latest `dev` branch), install from GitHub:
3737

3838
```bash
3939
python -m pip install git+https://github.com/IvanChernyshov/pbcgraph.git
@@ -70,6 +70,20 @@ neighbors = list(G.neighbors_inst(('A', (0, 0))))
7070
comp = G.components()[0]
7171
assert comp.same_fragment(('A', (0, 0)), ('A', (1, 0)))
7272
assert not comp.same_fragment(('A', (0, 0)), ('A', (0, 1)))
73+
74+
# Extract a finite patch of the infinite lift around a seed instance.
75+
patch = G.lift_patch(('A', (0, 0)), radius=2)
76+
nx_patch = patch.to_networkx() # nx.Graph / nx.MultiGraph for undirected sources
77+
78+
# For directed sources, patches are directed by default:
79+
# nx_patch = patch.to_networkx() # nx.DiGraph / nx.MultiDiGraph
80+
# and you can obtain undirected views via:
81+
# nx_u = patch.to_networkx(as_undirected=True, undirected_mode='multigraph')
82+
# nx_c = patch.to_networkx(as_undirected=True, undirected_mode='orig_edges')
83+
84+
# Canonical lift: pick one instance per quotient node for a strand.
85+
lift = comp.canonical_lift(placement='tree')
86+
assert len(lift.instances) == len(comp.nodes)
7387
```
7488

7589
## Documentation

docs/api/algorithms.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@
1212
options:
1313
show_source: false
1414

15+
## Lifts
16+
17+
::: pbcgraph.alg.lift.lift_patch
18+
options:
19+
show_source: false
20+
21+
::: pbcgraph.alg.lift.LiftPatch
22+
options:
23+
show_source: false
24+
25+
::: pbcgraph.alg.lift.canonical_lift
26+
options:
27+
show_source: false
28+
29+
::: pbcgraph.alg.lift.CanonicalLift
30+
options:
31+
show_source: false
32+
1533
## Lattice utilities
1634

1735
::: pbcgraph.alg.lattice.snf_decomposition

docs/examples/canonical_lift.ipynb

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "ef2e2a4a",
6+
"metadata": {},
7+
"source": [
8+
"# Canonical lift\n",
9+
"\n",
10+
"`canonical_lift(...)` selects **exactly one lifted instance** for every quotient\n",
11+
"node in a `PeriodicComponent`, producing a deterministic finite representation\n",
12+
"of a single *strand* (a connected component of the infinite lift).\n",
13+
"\n",
14+
"In v0.1.2 you can choose between three placement modes:\n",
15+
"\n",
16+
"- `placement='tree'`: place the deterministic spanning tree with a chosen anchor\n",
17+
"- `placement='best_anchor'`: try all valid anchors and pick the best score\n",
18+
"- `placement='greedy_cut'`: locally improve the score while preserving connectivity\n"
19+
]
20+
},
21+
{
22+
"cell_type": "code",
23+
"execution_count": null,
24+
"id": "e379cb70",
25+
"metadata": {},
26+
"outputs": [],
27+
"source": [
28+
"from pprint import pprint\n",
29+
"\n",
30+
"from pbcgraph import PeriodicDiGraph\n"
31+
]
32+
},
33+
{
34+
"cell_type": "markdown",
35+
"id": "f2d4646c",
36+
"metadata": {},
37+
"source": [
38+
"## Helper: inspect a canonical lift\n"
39+
]
40+
},
41+
{
42+
"cell_type": "code",
43+
"execution_count": null,
44+
"id": "eae34cdf",
45+
"metadata": {},
46+
"outputs": [],
47+
"source": [
48+
"def summarize_canon(component, out):\n",
49+
" print('placement:', out.placement)\n",
50+
" print('score:', out.score)\n",
51+
" print('strand_key:', out.strand_key)\n",
52+
" print('anchor_site:', out.anchor_site)\n",
53+
" print('anchor_shift:', out.anchor_shift)\n",
54+
" print('\\nnodes (u, shift):')\n",
55+
" pprint(list(out.nodes))\n",
56+
" print('\\nall nodes are in the target strand:', all(\n",
57+
" component.inst_key((u, s)) == out.strand_key for u, s in out.nodes\n",
58+
" ))\n",
59+
" if out.tree_edges is not None:\n",
60+
" print('\\ntree edges (parent, child, tvec, key):')\n",
61+
" pprint(list(out.tree_edges))\n"
62+
]
63+
},
64+
{
65+
"cell_type": "markdown",
66+
"id": "a025daf5",
67+
"metadata": {},
68+
"source": [
69+
"## 1) Tree placement and `tree_edges`\n",
70+
"\n",
71+
"This is a small 1D quotient with a periodic cycle.\n",
72+
"We request `return_tree=True` to see the spanning-tree edges used to compute\n",
73+
"potentials.\n"
74+
]
75+
},
76+
{
77+
"cell_type": "code",
78+
"execution_count": null,
79+
"id": "6e943f16",
80+
"metadata": {},
81+
"outputs": [],
82+
"source": [
83+
"G = PeriodicDiGraph(dim=1)\n",
84+
"G.add_edge('A', 'B', (0,))\n",
85+
"G.add_edge('B', 'C', (0,))\n",
86+
"G.add_edge('C', 'A', (1,))\n",
87+
"\n",
88+
"c = G.components()[0]\n",
89+
"out_tree = c.canonical_lift(seed=('B', (0,)), anchor_shift=(0,), return_tree=True)\n",
90+
"summarize_canon(c, out_tree)\n"
91+
]
92+
},
93+
{
94+
"cell_type": "markdown",
95+
"id": "980ee941",
96+
"metadata": {},
97+
"source": [
98+
"## 2) `best_anchor`: same strand, better score\n",
99+
"\n",
100+
"Here we intentionally make deterministic potentials very unbalanced.\n",
101+
"`best_anchor` tries all anchors that exist in the requested strand inside the\n",
102+
"anchor cell and chooses the one that minimizes the score.\n"
103+
]
104+
},
105+
{
106+
"cell_type": "code",
107+
"execution_count": null,
108+
"id": "e2a33ce2",
109+
"metadata": {},
110+
"outputs": [],
111+
"source": [
112+
"H = PeriodicDiGraph(dim=1)\n",
113+
"H.add_edge('A', 'B', (2,))\n",
114+
"H.add_edge('B', 'C', (98,))\n",
115+
"H.add_edge('C', 'A', (-99,)) # cycle generator = 1 -> L = Z\n",
116+
"\n",
117+
"c2 = H.components()[0]\n",
118+
"out_tree2 = c2.canonical_lift(anchor_shift=(0,), placement='tree', score='l1')\n",
119+
"out_best2 = c2.canonical_lift(anchor_shift=(0,), placement='best_anchor', score='l1')\n",
120+
"\n",
121+
"summarize_canon(c2, out_tree2)\n",
122+
"print('\\n---')\n",
123+
"summarize_canon(c2, out_best2)\n",
124+
"print('\\nbest_anchor improves score:', out_best2.score < out_tree2.score)\n"
125+
]
126+
},
127+
{
128+
"cell_type": "markdown",
129+
"id": "e4257447",
130+
"metadata": {},
131+
"source": [
132+
"## 3) `greedy_cut`: local improvement beyond `best_anchor`\n",
133+
"\n",
134+
"This example has two distinct quotient edges between `C` and `A`.\n",
135+
"The deterministic spanning tree picks one of them, but `greedy_cut` can locally\n",
136+
"switch to the alternative periodic relation and reduce the score while keeping\n",
137+
"the induced internal graph connected.\n"
138+
]
139+
},
140+
{
141+
"cell_type": "code",
142+
"execution_count": null,
143+
"id": "ab80728a",
144+
"metadata": {},
145+
"outputs": [],
146+
"source": [
147+
"K = PeriodicDiGraph(dim=1)\n",
148+
"K.add_edge('A', 'B', (2,))\n",
149+
"K.add_edge('B', 'C', (98,))\n",
150+
"K.add_edge('C', 'A', (-100,))\n",
151+
"K.add_edge('C', 'A', (-99,))\n",
152+
"\n",
153+
"c3 = K.components()[0]\n",
154+
"out_best3 = c3.canonical_lift(anchor_shift=(0,), placement='best_anchor', score='l1')\n",
155+
"out_greedy3 = c3.canonical_lift(anchor_shift=(0,), placement='greedy_cut', score='l1')\n",
156+
"\n",
157+
"summarize_canon(c3, out_best3)\n",
158+
"print('\\n---')\n",
159+
"summarize_canon(c3, out_greedy3)\n",
160+
"print('\\ngreedy_cut improves score:', out_greedy3.score < out_best3.score)\n"
161+
]
162+
},
163+
{
164+
"cell_type": "markdown",
165+
"id": "05db5fe5",
166+
"metadata": {},
167+
"source": [
168+
"## 4) Strand keys and the \"strand absent in the anchor cell\" error\n",
169+
"\n",
170+
"If the translation subgroup is a proper sublattice of `Z^d`, the infinite lift\n",
171+
"splits into multiple disconnected strands (torsion / interpenetration).\n",
172+
"\n",
173+
"In this case, a requested `strand_key` might have **no representatives in the\n",
174+
"anchor cell**. Then `canonical_lift` raises `CanonicalLiftError`.\n"
175+
]
176+
},
177+
{
178+
"cell_type": "code",
179+
"execution_count": null,
180+
"id": "5dde9333",
181+
"metadata": {},
182+
"outputs": [],
183+
"source": [
184+
"from pbcgraph.core.exceptions import CanonicalLiftError\n",
185+
"\n",
186+
"T = PeriodicDiGraph(dim=1)\n",
187+
"T.add_edge('A', 'A', (2,)) # L = 2Z -> torsion 2 (even/odd strands)\n",
188+
"\n",
189+
"c4 = T.components()[0]\n",
190+
"print('torsion invariants:', c4.torsion_invariants)\n",
191+
"\n",
192+
"k0 = c4.inst_key(('A', (0,)))\n",
193+
"k1 = c4.inst_key(('A', (1,)))\n",
194+
"print('strand key at A@(0):', k0)\n",
195+
"print('strand key at A@(1):', k1)\n",
196+
"\n",
197+
"try:\n",
198+
" c4.canonical_lift(strand_key=k1, anchor_shift=(0,))\n",
199+
"except CanonicalLiftError as e:\n",
200+
" print('expected error:', e)\n",
201+
"\n",
202+
"# Fix: choose an anchor cell that actually contains the strand.\n",
203+
"out_fix = c4.canonical_lift(strand_key=k1, anchor_shift=(1,))\n",
204+
"summarize_canon(c4, out_fix)\n"
205+
]
206+
}
207+
],
208+
"metadata": {
209+
"kernelspec": {
210+
"display_name": "Python 3",
211+
"name": "python3"
212+
}
213+
},
214+
"nbformat": 4,
215+
"nbformat_minor": 5
216+
}

0 commit comments

Comments
 (0)