Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@
This project follows a lightweight "keep a log" style.


## 0.1.2 - Finite lifts and canonical lifts

- **Finite lift patches**
- 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.
- Patch edges store **snapshot** attribute dicts.
- For undirected containers, paired directed realizations are deduplicated deterministically.

- **Patch export and directed semantics**
- For directed periodic containers, `lift_patch(...)` now produces a **directed** patch by default (exported as `nx.DiGraph` / `nx.MultiDiGraph`).
- `LiftPatch.to_networkx(as_undirected=True, undirected_mode=...)` provides undirected views of directed patches:
- `undirected_mode='multigraph'`: one undirected multiedge per directed edge (direction preserved in `_pbc_tail`/`_pbc_head`).
- `undirected_mode='orig_edges'`: collapsed simple graph with `orig_edges=[...]` snapshots for each adjacency.

- **Canonical lifts (strand representatives)**
- Added `canonical_lift(...)` to select one instance per quotient node for a chosen strand (coset in `Z^d/L`).
- Implemented placements: `tree`, `best_anchor`, and `greedy_cut`.
- Stored deterministic spanning-tree parent edges on `PeriodicComponent` to optionally return `tree_edges`.

- **Errors**
- Added `CanonicalLiftError` and `LiftPatchError` for well-scoped failure modes.


## 0.1.1 - Refactoring

- **Deterministic iteration**
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ What you get in v0.1:
- `PeriodicGraph` / `PeriodicDiGraph`: unique edge per `(u, v, tvec)`.
- `PeriodicMultiGraph` / `PeriodicMultiDiGraph`: parallel edges allowed for the same `(u, v, tvec)`.
- `PeriodicComponent`: lattice invariants (rank, SNF torsion) and exact instance connectivity via `same_fragment(...)`.
- `lift_patch(...)`: extract a finite (non-periodic) patch of the infinite lift around a seed instance.
- `canonical_lift(...)`: select one lifted instance per quotient node for a chosen strand (coset in `Z^d/L`).

## Status

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

## Install

Requires Python 3.10+.

Once the project is published on PyPI:
Requires Python 3.10+. Latest stable version is usually published on PyPI:

```bash
python -m pip install pbcgraph
```

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

```bash
python -m pip install git+https://github.com/IvanChernyshov/pbcgraph.git
Expand Down Expand Up @@ -70,6 +70,20 @@ neighbors = list(G.neighbors_inst(('A', (0, 0))))
comp = G.components()[0]
assert comp.same_fragment(('A', (0, 0)), ('A', (1, 0)))
assert not comp.same_fragment(('A', (0, 0)), ('A', (0, 1)))

# Extract a finite patch of the infinite lift around a seed instance.
patch = G.lift_patch(('A', (0, 0)), radius=2)
nx_patch = patch.to_networkx() # nx.Graph / nx.MultiGraph for undirected sources

# For directed sources, patches are directed by default:
# nx_patch = patch.to_networkx() # nx.DiGraph / nx.MultiDiGraph
# and you can obtain undirected views via:
# nx_u = patch.to_networkx(as_undirected=True, undirected_mode='multigraph')
# nx_c = patch.to_networkx(as_undirected=True, undirected_mode='orig_edges')

# Canonical lift: pick one instance per quotient node for a strand.
lift = comp.canonical_lift(placement='tree')
assert len(lift.instances) == len(comp.nodes)
```

## Documentation
Expand Down
18 changes: 18 additions & 0 deletions docs/api/algorithms.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@
options:
show_source: false

## Lifts

::: pbcgraph.alg.lift.lift_patch
options:
show_source: false

::: pbcgraph.alg.lift.LiftPatch
options:
show_source: false

::: pbcgraph.alg.lift.canonical_lift
options:
show_source: false

::: pbcgraph.alg.lift.CanonicalLift
options:
show_source: false

## Lattice utilities

::: pbcgraph.alg.lattice.snf_decomposition
Expand Down
216 changes: 216 additions & 0 deletions docs/examples/canonical_lift.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "ef2e2a4a",
"metadata": {},
"source": [
"# Canonical lift\n",
"\n",
"`canonical_lift(...)` selects **exactly one lifted instance** for every quotient\n",
"node in a `PeriodicComponent`, producing a deterministic finite representation\n",
"of a single *strand* (a connected component of the infinite lift).\n",
"\n",
"In v0.1.2 you can choose between three placement modes:\n",
"\n",
"- `placement='tree'`: place the deterministic spanning tree with a chosen anchor\n",
"- `placement='best_anchor'`: try all valid anchors and pick the best score\n",
"- `placement='greedy_cut'`: locally improve the score while preserving connectivity\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e379cb70",
"metadata": {},
"outputs": [],
"source": [
"from pprint import pprint\n",
"\n",
"from pbcgraph import PeriodicDiGraph\n"
]
},
{
"cell_type": "markdown",
"id": "f2d4646c",
"metadata": {},
"source": [
"## Helper: inspect a canonical lift\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eae34cdf",
"metadata": {},
"outputs": [],
"source": [
"def summarize_canon(component, out):\n",
" print('placement:', out.placement)\n",
" print('score:', out.score)\n",
" print('strand_key:', out.strand_key)\n",
" print('anchor_site:', out.anchor_site)\n",
" print('anchor_shift:', out.anchor_shift)\n",
" print('\\nnodes (u, shift):')\n",
" pprint(list(out.nodes))\n",
" print('\\nall nodes are in the target strand:', all(\n",
" component.inst_key((u, s)) == out.strand_key for u, s in out.nodes\n",
" ))\n",
" if out.tree_edges is not None:\n",
" print('\\ntree edges (parent, child, tvec, key):')\n",
" pprint(list(out.tree_edges))\n"
]
},
{
"cell_type": "markdown",
"id": "a025daf5",
"metadata": {},
"source": [
"## 1) Tree placement and `tree_edges`\n",
"\n",
"This is a small 1D quotient with a periodic cycle.\n",
"We request `return_tree=True` to see the spanning-tree edges used to compute\n",
"potentials.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6e943f16",
"metadata": {},
"outputs": [],
"source": [
"G = PeriodicDiGraph(dim=1)\n",
"G.add_edge('A', 'B', (0,))\n",
"G.add_edge('B', 'C', (0,))\n",
"G.add_edge('C', 'A', (1,))\n",
"\n",
"c = G.components()[0]\n",
"out_tree = c.canonical_lift(seed=('B', (0,)), anchor_shift=(0,), return_tree=True)\n",
"summarize_canon(c, out_tree)\n"
]
},
{
"cell_type": "markdown",
"id": "980ee941",
"metadata": {},
"source": [
"## 2) `best_anchor`: same strand, better score\n",
"\n",
"Here we intentionally make deterministic potentials very unbalanced.\n",
"`best_anchor` tries all anchors that exist in the requested strand inside the\n",
"anchor cell and chooses the one that minimizes the score.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e2a33ce2",
"metadata": {},
"outputs": [],
"source": [
"H = PeriodicDiGraph(dim=1)\n",
"H.add_edge('A', 'B', (2,))\n",
"H.add_edge('B', 'C', (98,))\n",
"H.add_edge('C', 'A', (-99,)) # cycle generator = 1 -> L = Z\n",
"\n",
"c2 = H.components()[0]\n",
"out_tree2 = c2.canonical_lift(anchor_shift=(0,), placement='tree', score='l1')\n",
"out_best2 = c2.canonical_lift(anchor_shift=(0,), placement='best_anchor', score='l1')\n",
"\n",
"summarize_canon(c2, out_tree2)\n",
"print('\\n---')\n",
"summarize_canon(c2, out_best2)\n",
"print('\\nbest_anchor improves score:', out_best2.score < out_tree2.score)\n"
]
},
{
"cell_type": "markdown",
"id": "e4257447",
"metadata": {},
"source": [
"## 3) `greedy_cut`: local improvement beyond `best_anchor`\n",
"\n",
"This example has two distinct quotient edges between `C` and `A`.\n",
"The deterministic spanning tree picks one of them, but `greedy_cut` can locally\n",
"switch to the alternative periodic relation and reduce the score while keeping\n",
"the induced internal graph connected.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ab80728a",
"metadata": {},
"outputs": [],
"source": [
"K = PeriodicDiGraph(dim=1)\n",
"K.add_edge('A', 'B', (2,))\n",
"K.add_edge('B', 'C', (98,))\n",
"K.add_edge('C', 'A', (-100,))\n",
"K.add_edge('C', 'A', (-99,))\n",
"\n",
"c3 = K.components()[0]\n",
"out_best3 = c3.canonical_lift(anchor_shift=(0,), placement='best_anchor', score='l1')\n",
"out_greedy3 = c3.canonical_lift(anchor_shift=(0,), placement='greedy_cut', score='l1')\n",
"\n",
"summarize_canon(c3, out_best3)\n",
"print('\\n---')\n",
"summarize_canon(c3, out_greedy3)\n",
"print('\\ngreedy_cut improves score:', out_greedy3.score < out_best3.score)\n"
]
},
{
"cell_type": "markdown",
"id": "05db5fe5",
"metadata": {},
"source": [
"## 4) Strand keys and the \"strand absent in the anchor cell\" error\n",
"\n",
"If the translation subgroup is a proper sublattice of `Z^d`, the infinite lift\n",
"splits into multiple disconnected strands (torsion / interpenetration).\n",
"\n",
"In this case, a requested `strand_key` might have **no representatives in the\n",
"anchor cell**. Then `canonical_lift` raises `CanonicalLiftError`.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5dde9333",
"metadata": {},
"outputs": [],
"source": [
"from pbcgraph.core.exceptions import CanonicalLiftError\n",
"\n",
"T = PeriodicDiGraph(dim=1)\n",
"T.add_edge('A', 'A', (2,)) # L = 2Z -> torsion 2 (even/odd strands)\n",
"\n",
"c4 = T.components()[0]\n",
"print('torsion invariants:', c4.torsion_invariants)\n",
"\n",
"k0 = c4.inst_key(('A', (0,)))\n",
"k1 = c4.inst_key(('A', (1,)))\n",
"print('strand key at A@(0):', k0)\n",
"print('strand key at A@(1):', k1)\n",
"\n",
"try:\n",
" c4.canonical_lift(strand_key=k1, anchor_shift=(0,))\n",
"except CanonicalLiftError as e:\n",
" print('expected error:', e)\n",
"\n",
"# Fix: choose an anchor cell that actually contains the strand.\n",
"out_fix = c4.canonical_lift(strand_key=k1, anchor_shift=(1,))\n",
"summarize_canon(c4, out_fix)\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading