poly2graph


Namepoly2graph JSON
Version 0.2.0 PyPI version JSON
download
home_pageNone
SummaryAutomated Non-Hermitian Spectral Graph Construction
upload_time2025-08-31 06:14:06
maintainerNone
docs_urlNone
authorNone
requires_python>=3.11
licenseMIT License Copyright (c) 2024 Xianquan (Sarinstein) Yan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
keywords hamiltonian spectral graph algebraic geometry computer vision graph representation learning morphological image processing non-bloch band non-hermitian spectral graph
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Poly2Graph

[![arXiv](https://img.shields.io/badge/arXiv-2412.00568---?logo=arXiv&labelColor=b31b1b&color=grey)](https://arxiv.org/abs/2506.08618)
[![PyPI](https://img.shields.io/pypi/v/poly2graph)](https://pypi.org/project/poly2graph/)
<a target="_blank" href="https://colab.research.google.com/github/sarinstein-yan/poly2graph/blob/main/poly2graph_demo.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a>

`Poly2Graph` is a Python package for automatic *Hamiltonian spectral graph* construction. It takes in the characteristic polynomial and returns the spectral graph.

Topological physics is one of the most dynamic and rapidly advancing fields in modern physics. Conventionally, topological classification focuses on eigenstate windings, a concept central to Hermitian topological lattices (e.g., topological insulators). 
Beyond such notion of topology, we unravel a distinct and diverse graph topology emerging in 1D crystal's energy spectra (under open boundary condition). 
Particularly, for non-Hermitian crystals, their *spectral graphs* features a kaleidoscope of exotic shapes like stars, kites, insects, and braids.

<!-- <figure align="center">
  <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/SGs_demo.png" width="600">
  <figcaption style="text-align:left;">
    <strong>Poly2Graph pipeline.</strong>
    <b>(a)</b> Starting from a 1-D crystal Hamiltonian&nbsp;H(z) in momentum space — or, equivalently, its <em>characteristic polynomial</em> P(z,E) = det[ <b>H</b>(z) − E<b>I</b> ]. The crystal’s open-boundary spectrum solely depends on&nbsp;P(z,E).
    <b>(b)</b> The <em>spectral potential</em> Φ(E) (Ronkin function) is computed from the roots of P(z,E) = 0, following recent advances in non-Bloch band theory.
    <b>(c)</b> The density of states&nbsp;ρ(E) is obtained as the Laplacian of Φ(E).
    <b>(d)</b> The spectral graph is extracted from ρ(E) via a morphological computer-vision pipeline. Varying the coefficients of P(z,E) produces diverse graph morphologies in the real domain&nbsp;(d1)–(d3) and imaginary domain&nbsp;(di)–(diii).
  </figcaption>
</figure> -->

<p align="center">
  <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/SGs_demo.png"
       width="800"
       alt="Poly2Graph pipeline">
</p>

<strong>Figure: Poly2Graph Pipeline —</strong>
**(a)** Starting from a 1-D crystal Hamiltonian $H(z)$ in momentum space — or, equivalently, its *characteristic polynomial* $P(z,E)=\det[\mathbf{H}(z)-E\mathbf{I}]$. The crystal’s open-boundary spectrum solely depends on $P(z,E)$.
**(b)** The *spectral potential* $\Phi(E)$ (Ronkin function) is computed from the roots of $P(z,E)=0$, following recent advances in non-Bloch band theory.
**(c)** The density of states $\rho(E)$ is obtained as the Laplacian of $\Phi(E)$.
**(d)** The spectral graph is extracted from $\rho(E)$ via a morphological computer-vision pipeline. Varying the coefficients of $P(z,E)$ produces diverse graph morphologies in the real domain (d1)–(d3) and imaginary domain (di)–(diii).


## Features
- **Poly2Graph**
  1. High-performance
     - Fast construction of spectral graph from any one-dimensional models
     - Adaptive resolution to reduce floating operation cost and memory usage
     - Automatic backend for computation bottleneck. If `tensorflow` / `torch` is available, any device (e.g. '/GPU:0', '/TPU:0', 'cuda:0', etc.) that they support can be used for acceleration.
  2. Cover generic topological lattices
     - Support generic one-band and multi-band models
     - Flexible multiple input choices, be they characteristic polynomials or Bloch Hamiltonians; formats include strings, `sympy.Poly`, and `sympy.Matrix`
  3. Automatic and Robust
     - By default, no hyper-parameters are needed. Just input the characteristic of your model and `poly2graph` handles the rest
     - Automatic spectral boundary inference
     - Relatively robust on multiband models that are prone to "component fragmentation"
  4. Helper functionalities generally useful
     - `skeleton2graph` module: Convert a skeleton image to its graph representation
     - `hamiltonian` module: Conversion among different Hamiltonian representations and efficient computation of a range of properties
  <!-- 6. Dataset generation
  1. Visualization of spectral potential, density of states, and spectral graph -->

## Installation

You can install the package via pip:

```bash
$ pip install poly2graph
```

or clone the repository and install it manually:

```bash
$ git clone https://github.com/sarinstein-yan/poly2graph.git
$ cd poly2graph
$ pip install .
```

Optionally, if [`TensorFlow`](https://www.tensorflow.org/install) or [`PyTorch`](https://pytorch.org/get-started/locally/) is available, `poly2graph` will make use of them automatically to accelerate the computation bottleneck. Priority: `tensorflow` > `torch` > `numpy`.

This module is tested on `Python >= 3.11`.
Check the installation:

```python
import poly2graph as p2g
print(p2g.__version__)
```

## Usage

See the [Poly2Graph Tutorial JupyterNotebook](https://github.com/sarinstein-yan/poly2graph/blob/main/poly2graph_demo.ipynb).

`p2g.SpectralGraph` and `p2g.CharPolyClass` are the two main classes in the package.

`p2g.SpectralGraph` investigates the spectral graph topology of **a specific** given characteristic polynomial or Bloch Hamiltonian. `p2g.CharPolyClass` investigates **a class** of **parametrized** characteristic polynomials or Bloch Hamiltonians, and is optimized for generating spectral properties in parallel.

```python
import numpy as np
import networkx as nx
import sympy as sp
import matplotlib.pyplot as plt

# always start by initializing the symbols for k, z, and E
k = sp.symbols('k', real=True)
z, E = sp.symbols('z E', complex=True)
```

### A generic **one-band** example (`p2g.SpectralGraph`):

characteristic polynomial:

$$P(E,z) := h(z) - E = z^4 -z -z^{-2} -E$$

Its Bloch Hamiltonian (Fourier transformed Hamiltonian in momentum space) is a scalar function:

$$h(z) = z^4 - z - z^{-2}$$

where the phase factor is defined as $z:=e^{ik}$.

Expressed in terms of crystal momentum $k$:

$$h(k) = e^{4ik} - e^{ik} - e^{-2ik}$$

---
The valid input formats to initialize a `p2g.SpectralGraph` object are:
1. Characteristic polynomial in terms of `z` and `E`:
   - as a string of the Poly in terms of `z` and `E`
   - as a `sympy.Poly` with {`z`, `1/z`, `E`} as generators
2. Bloch Hamiltonian in terms of `k` or `z`
   - as a `sympy.Matrix` in terms of `k`
   - as a `sympy.Matrix` in terms of `z`

All the following `characteristic`s are valid and will initialize to the same characteristic polynomial and therefore produce the same spectral graph:
```python
char_poly_str = '-z**-2 - E - z + z**4'

char_poly_Poly = sp.Poly(
    -z**-2 - E - z + z**4,
    z, 1/z, E # generators are z, 1/z, E
)

phase_k = sp.exp(sp.I*k)
char_hamil_k = sp.Matrix([-phase_k**2 - phase_k + phase_k**4])

char_hamil_z = sp.Matrix([-z**-2 - E - z + z**4])
```

Let us just use the string to initialize and see a set of properties that are computed automatically:

```python
sg = p2g.SpectralGraph(char_poly_str, k=k, z=z, E=E)
```

---
**Characteristic polynomial**:

```python
sg.ChP
```

<span style="color:#d73a49;font-weight:bold">>>></span> $\text{Poly}{\left( z^{4} - z -\frac{1}{z^{2}} - E, ~ z, \frac{1}{z}, E, ~ domain=\mathbb{Z} \right)}$

---
**Bloch Hamiltonian**:
- For one-band model, it is a unique, rank-0 matrix (scalar)

```python
sg.h_k
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}e^{4 i k} - e^{i k} - e^{- 2 i k}\end{bmatrix}$$

```python
sg.h_z
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}- \frac{- z^{6} + z^{3} + 1}{z^{2}}\end{bmatrix}$$

---
**The Frobenius companion matrix of `P(E)(z)`**:
- treating `E` as parameter and `z` as variable
- Its eigenvalues are the roots of the characteristic polynomial at a fixed complex energy `E`. Thus it is useful to calculate the GBZ (generalized Brillouin zone), the spectral potential (Ronkin function), etc.

```python
sg.companion_E
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}0 & 0 & 0 & 0 & 0 & 1 \\
1 & 0 & 0 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 & 0 & E \\
0 & 0 & 1 & 0 & 0 & 1 \\
0 & 0 & 0 & 1 & 0 & 0 \\
0 & 0 & 0 & 0 & 1 & 0\end{bmatrix}$$

---
**Number of bands & hopping range**:
```python
print('Number of bands:', sg.num_bands)
print('Max hopping length to the right:', sg.poly_p)
print('Max hopping length to the left:', sg.poly_q)
```

<span style="color:#d73a49;font-weight:bold">>>></span>

```text
Number of bands: 1
Max hopping length to the right: 2
Max hopping length to the left: 4
```

---
**A real-space Hamiltonian of a finite chain and its energy spectrum**:

```python
H = sg.real_space_H(
    N=40,        # number of unit cells
    pbc=False,   # open boundary conditions
    max_dim=500  # maximum dimension of the Hamiltonian matrix (for numerical accuracy)
)

energy = np.linalg.eigvals(H)

fig, ax = plt.subplots(figsize=(3, 3))
ax.plot(energy.real, energy.imag, 'k.', markersize=5)
ax.set(xlabel='Re(E)', ylabel='Im(E)', \
xlim=sg.spectral_square[:2], ylim=sg.spectral_square[2:])
plt.tight_layout(); plt.show()
```

<p align="center">
    <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/finite_spectrum_one_band.png" width="300" />
</p>

---
#### **The Set of Spectral Functions**
(whose values plotted on the complex energy square, returned as a 2D array)

- **Density of States (DOS)**

  Defined as the number of states per unit energy area in the complex energy plane.

  $$\rho(E) = \lim_{N\to\infty}\sum_n \frac{1}{N} \delta(E-\epsilon_n)$$

  where $\epsilon_n$ are the eigenvalues of the Hamiltonian $H$.

  Imagine to assign electric charge $1/N$ to each eigenvalue $\epsilon_n$, then the density of states $\rho(E)$ is treated as a *charge density*, therefore can be interpreted as the laplacian of a *spectral potential* $\Phi(E)$:

  $$\rho(E) = -\frac{1}{2\pi} \Delta \Phi(E)$$

  $\Delta = \partial_{\text{Re} E}^2 + \partial_{\text{Im} E}^2$ is the Laplacian operator on the complex energy plane. Laplacian operator extracts curvature; thus, geometrically speaking, the loci of spectral graph $\mathcal{G}$ resides on the *ridges* of the Coulomb potential landscape.

- **Spectral Potential (Ronkin function)**

  It can be proven that the spectral potential $\Phi(E)$ can be efficiently computed from the roots $|z_i(E)|$ of the characteristic polynomial $P(E)(z)$ and the leading coefficient $a_q(E)$ at a complex energy $E$:

  $$\Phi(E) = - \lim_{N\to\infty} \sum_{\epsilon_n} \log|E-\epsilon_n| \\
  = - \int \rho(E')\log|E-E'| d^2E' \\
  = - \log|a_q(E)| - \sum_{i=p+1}^{p+q} \log|z_i(E)|$$

- Graph Skeleton (Binarized DOS)

```python
phi, dos, binaried_dos = sg.spectral_images()

fig, axes = plt.subplots(1, 3, figsize=(8, 3), sharex=True, sharey=True)
axes[0].imshow(phi, extent=sg.spectral_square, cmap='terrain')
axes[0].set(xlabel='Re(E)', ylabel='Im(E)', title='Spectral Potential')
axes[1].imshow(dos, extent=sg.spectral_square, cmap='viridis')
axes[1].set(xlabel='Re(E)', title='Density of States')
axes[2].imshow(binaried_dos, extent=sg.spectral_square, cmap='gray')
axes[2].set(xlabel='Re(E)', title='Graph Skeleton')
plt.tight_layout()
plt.show()
```

<p align="center">
    <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/spectral_images_one_band.png" width="900" />
</p>

---
#### The spectral graph $\mathcal{G}$

```python
graph = sg.spectral_graph()

fig, ax = plt.subplots(figsize=(3, 3))
pos = nx.get_node_attributes(graph, 'pos')
nx.draw_networkx_nodes(graph, pos, alpha=0.8, ax=ax,
            node_size=50, node_color='#A60628')
nx.draw_networkx_edges(graph, pos, alpha=0.8, ax=ax,
            width=5, edge_color='#348ABD')
plt.tight_layout(); plt.show()
```

<p align="center">
    <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/spectral_graph_one_band.png" width="300" />
</p>


> [!TIP]
> If `tensorflow` or `torch` is available, `poly2graph` will automatically use them and run on **CPU** by default. If other device, e.g. GPU / TPU is available, one can pass `device = {device string}` to the method `spectral_images` and `spectral_graph`:
> ```python
> SpectralGraph.spectral_images(device='/cpu:0')
> SpectralGraph.spectral_graph(device='/gpu:1')
> SpectralGraph.spectral_images(device='cpu')
> SpectralGraph.spectral_graph(device='cuda:0')
> ...
> ```
> However, some functions may not have gpu kernel in `tf`/`torch`, in which case the computation will fallback to CPU.

### A generic **multi-band** example (`p2g.SpectralGraph`):

characteristic polynomial (four bands):

$$P(E,z) := \det(\textbf{h}(z) - E\;\textbf{I}) = z^2 + 1/z^2 + E z - E^4$$

One of its possible Bloch Hamiltonians in terms of $z$:

$$\textbf{h}(z)=\begin{bmatrix}
0 & 0 & 0 & z^2 + 1/z^2 \\
1 & 0 & 0 & z \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
\end{bmatrix}$$

---

```python
sg_multi = p2g.SpectralGraph("z**2 + 1/z**2 + E*z - E**4", k, z, E)
```

---
**Characteristic polynomial**:

```python
sg_multi.ChP
```

<span style="color:#d73a49;font-weight:bold">>>></span> $\text{Poly}{\left( z^{2} + zE + \frac{1}{z^{2}} - E^{4}, ~ z, \frac{1}{z}, E, ~ domain=\mathbb{Z} \right)}$

---
**Bloch Hamiltonian**:
- For multi-band model, if the `p2g.SpectralGraph` is not initialized with a `sympy` `Matrix`, then `poly2graph` will use the companion matrix of the characteristic polynomial `P(z)(E)` (treating `z` as parameter and `E` as variable) as the Bloch Hamiltonian -- this is one of the set of possible band Hamiltonians that possesses the same energy spectrum and thus the same spectral graph.

```python
sg_multi.h_k
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}0 & 0 & 0 & 2 \cos{\left(2 k \right)} \\
1 & 0 & 0 & e^{i k} \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0\end{bmatrix}$$

```python
sg_multi.h_z
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}0 & 0 & 0 & z^{2} + \frac{1}{z^{2}} \\
1 & 0 & 0 & z \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0\end{bmatrix}$$

---
**The Frobenius companion matrix of `P(E)(z)`**:

```python
sg_multi.companion_E
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}0 & 0 & 0 & -1 \\
1 & 0 & 0 & 0 \\
0 & 1 & 0 & E^{4} \\
0 & 0 & 1 & - E\end{bmatrix}$$

---
**Number of bands & hopping range**:
```python
print('Number of bands:', sg_multi.num_bands)
print('Max hopping length to the right:', sg_multi.poly_p)
print('Max hopping length to the left:', sg_multi.poly_q)
```

<span style="color:#d73a49;font-weight:bold">>>></span>

```text
Number of bands: 4
Max hopping length to the right: 2
Max hopping length to the left: 2
```

---
**A real-space Hamiltonian of a finite chain and its energy spectrum**:

```python
H_multi = sg_multi.real_space_H(
    N=40,        # number of unit cells
    pbc=False,   # open boundary conditions
    max_dim=500  # maximum dimension of the Hamiltonian matrix (for numerical accuracy)
)

energy_multi = np.linalg.eigvals(H_multi)

fig, ax = plt.subplots(figsize=(3, 3))
ax.plot(energy_multi.real, energy_multi.imag, 'k.', markersize=5)
ax.set(xlabel='Re(E)', ylabel='Im(E)', \
xlim=sg_multi.spectral_square[:2], ylim=sg_multi.spectral_square[2:])
plt.tight_layout(); plt.show()
```

<p align="center">
    <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/finite_spectrum_multi_band.png" width="300" />
</p>

---
#### **The Set of Spectral Functions**

```python
phi_multi, dos_multi, binaried_dos_multi = sg_multi.spectral_images(device='/cpu:0')

fig, axes = plt.subplots(1, 3, figsize=(8, 3), sharex=True, sharey=True)
axes[0].imshow(phi_multi, extent=sg_multi.spectral_square, cmap='terrain')
axes[0].set(xlabel='Re(E)', ylabel='Im(E)', title='Spectral Potential')
axes[1].imshow(dos_multi, extent=sg_multi.spectral_square, cmap='viridis')
axes[1].set(xlabel='Re(E)', title='Density of States')
axes[2].imshow(binaried_dos_multi, extent=sg_multi.spectral_square, cmap='gray')
axes[2].set(xlabel='Re(E)', title='Graph Skeleton')
plt.tight_layout(); plt.show()
```

<p align="center">
    <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/spectral_images_multi_band.png" width="900" />
</p>

---
#### The spectral graph $\mathcal{G}$

```python
graph_multi = sg_multi.spectral_graph(
    short_edge_threshold=20, 
    # ^ node pairs or edges with distance < threshold pixels are merged
)

fig, ax = plt.subplots(figsize=(3, 3))
pos_multi = nx.get_node_attributes(graph_multi, 'pos')
nx.draw(graph_multi, pos_multi, ax=ax, 
        node_size=10, node_color='#A60628', 
        edge_color='#348ABD', width=2, alpha=0.8)
plt.tight_layout(); plt.show()
```

<p align="center">
    <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/spectral_graph_multi_band.png" width="300" />
</p>


## Node and Edge Attributes of the Spectral Graph Object

The spectral graph is a `networkx.MultiGraph` object.

- Node Attributes
  1. `pos` : (2,)-numpy array
     - the position of the node $(\text{Re}(E), \text{Im}(E))$
  2. `dos` : float
     - the density of states at the node
  3. `potential` : float
     - the spectral potential at the node
- Edge Attributes
  1. `weight` : float
     - the weight of the edge, which is the **length** of the edge in the complex energy plane
  2. `pts` : (w, 2)-numpy array
     - the positions of the points constituting the edge, where `w` is the number of points along the edge, i.e., the length of the edge, equals `weight`
  3. `avg_dos` : float
     - the average density of states along the edge
  4. `avg_potential` : float
     - the average spectral potential along the edge

```python
node_attr = dict(graph.nodes(data=True))
edge_attr = list(graph.edges(data=True))
print('The attributes of the first node\n', node_attr[0], '\n')
print('The attributes of the first edge\n', edge_attr[0][-1], '\n')
```

<span style="color:#d73a49;font-weight:bold">>>></span>

```text
The attributes of the first node
 {'pos': array([-0.20403848, -2.11668106]), 
  'dos': 0.0011466597206890583, 
  'potential': -0.655870258808136} 

The attributes of the first edge
 {'weight': 1.4176547247784077, 
  'pts': array([[-2.04038482e-01, -2.11668106e+00],
       [-1.99792382e-01, -2.11243496e+00],
       ...
       [ 5.94228396e-01, -1.02967935e+00]]), 
  'avg_dos': 0.10761458, 
  'avg_potential': -0.5068641}
```


---
### A generic **multi-band** class (`p2g.CharPolyClass`):

Let us add two parameters `{a,b}` to the aforementioned multi-band example and construct a `p2g.CharPolyClass` object:

```python
a, b = sp.symbols('a b', real=True)

cp = p2g.CharPolyClass(
    "z**2 + a/z**2 + b*E*z - E**4", 
    k=k, z=z, E=E,
    params={a, b}, # pass parameters as a set
)
```

<span style="color:#d73a49;font-weight:bold">>>></span> 

```text
Derived Bloch Hamiltonian `h_z` with 4 bands.
```

---
View a few auto-computed properties

**Characteristic polynomial**:

```python
cp.ChP
```

<span style="color:#d73a49;font-weight:bold">>>></span> $\text{Poly}{\left( z^{2} + a \frac{1}{z^{2}} + b zE - E^{4}, z, \frac{1}{z}, E, domain=\mathbb{Z}\left[a, b\right] \right)}$

---
**Bloch Hamiltonian**:

```python
cp.h_k
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}
0 & 0 & 0 & (a + e^{4 i k})e^{- 2 i k} \\
1 & 0 & 0 & b e^{i k} \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0
\end{bmatrix}$$

```python
cp.h_z
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}
0 & 0 & 0 & \frac{a}{z^{2}} + z^{2} \\
1 & 0 & 0 & b z \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0
\end{bmatrix}$$

---
**The Frobenius companion matrix of `P(E)(z)`**:

```python
cp.companion_E
```

<span style="color:#d73a49;font-weight:bold">>>></span>

$$\begin{bmatrix}
0 & 0 & 0 & -a \\
1 & 0 & 0 & 0 \\
0 & 1 & 0 & E^{4} \\
0 & 0 & 1 & - E b
\end{bmatrix}$$

---
#### **An Array of Spectral Functions**

To get an array of spectral images or spectral graphs, we first prepare the values of the parameters `{a,b}`

```python
a_array = np.linspace(-2, 1, 6)
b_array = np.linspace(-1, 1, 6)
a_grid, b_grid = np.meshgrid(a_array, b_array)
param_dict = {a: a_grid, b: b_grid}
print('a_grid shape:', a_grid.shape,
    '\nb_grid shape:', b_grid.shape)
```

<span style="color:#d73a49;font-weight:bold">>>></span> 

```text
a_grid shape: (6, 6)
b_grid shape: (6, 6)
```

Note that **the value array of the parameters should have the same shape**, which is also **the shape of the output array of spectral images**

```python
phi_arr, dos_arr, binaried_dos_arr, spectral_square = \
    cp.spectral_images(param_dict=param_dict)
print('phi_arr shape:', phi_arr.shape,
    '\ndos_arr shape:', dos_arr.shape,
    '\nbinaried_dos_arr shape:', binaried_dos_arr.shape)
```

<span style="color:#d73a49;font-weight:bold">>>></span> 

```text
phi_arr shape: (6, 6, 1024, 1024) 
dos_arr shape: (6, 6, 1024, 1024) 
binaried_dos_arr shape: (6, 6, 1024, 1024)
```

```python
from mpl_toolkits.axes_grid1 import ImageGrid

fig = plt.figure(figsize=(13, 13))
grid = ImageGrid(fig, 111, nrows_ncols=(6, 6), axes_pad=0, 
                 label_mode='L', share_all=True)

for ax, (i, j) in zip(grid, [(i, j) for i in range(6) for j in range(6)]):
    ax.imshow(phi_arr[i, j], extent=spectral_square[i, j], cmap='terrain')
    ax.set(xlabel='Re(E)', ylabel='Im(E)')
    ax.text(
        0.03, 0.97, f'a = {a_array[i]:.2f}, b = {b_array[j]:.2f}',
        ha='left', va='top', transform=ax.transAxes,
        fontsize=10, color='tab:red',
        bbox=dict(alpha=0.8, facecolor='white')
    )

plt.tight_layout()
plt.savefig('./assets/ChP_spectral_potential_grid.png', dpi=72)
plt.show()
```

<p align="center">
    <img src="https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/ChP_spectral_potential_grid.png" width="1000" />
</p>

---
#### An Array of Spectral Graphs

```python
graph_flat, param_dict_flat = cp.spectral_graph(param_dict=param_dict)
print(graph_flat, '\n')
print(param_dict_flat)
```

```text
[<networkx.classes.multigraph.MultiGraph object at 0x000001966DFCD190>, 
<networkx.classes.multigraph.MultiGraph object at 0x000001966DFCECF0>, 
...
<networkx.classes.multigraph.MultiGraph object at 0x000001966DFCE750>]

{a: 
array([-2. , -1.4, -0.8, -0.2,  0.4,  1. , -2. , -1.4, -0.8, -0.2,  0.4,
        1. , -2. , -1.4, -0.8, -0.2,  0.4,  1. , -2. , -1.4, -0.8, -0.2,
        0.4,  1. , -2. , -1.4, -0.8, -0.2,  0.4,  1. , -2. , -1.4, -0.8,
       -0.2,  0.4,  1. ]), 
b: 
array([-1. , -1. , -1. , -1. , -1. , -1. , -0.6, -0.6, -0.6, -0.6, -0.6,
       -0.6, -0.2, -0.2, -0.2, -0.2, -0.2, -0.2,  0.2,  0.2,  0.2,  0.2,
        0.2,  0.2,  0.6,  0.6,  0.6,  0.6,  0.6,  0.6,  1. ,  1. ,  1. ,
        1. ,  1. ,  1. ])}
```

> [!NOTE]
> The spectral graph is a `networkx.MultiGraph` object, which cannot be directly returned as a multi-dimensional numpy array of `MultiGraph`, except for the case of 1D array.
> Instead, we return a flattened list of `networkx.MultiGraph` objects, and the accompanying `param_dict_flat` is the dictionary that contains the corresponding flattened parameter values.

> [!TIP]
> It's recommended to pass the values of the parameters as `vectors` (1D arrays) instead of higher dimensional `ND arrays` to avoid the overhead of reshaping the output and the difficulty to retrieve / postprocess the spectral graphs.


## Citation
If you find this work useful, please cite our paper:

```bibtex
@misc{yan2025hsg12mlargescalespatialmultigraph,
      title={HSG-12M: A Large-Scale Spatial Multigraph Dataset}, 
      author={Xianquan Yan and Hakan Akgün and Kenji Kawaguchi and N. Duane Loh and Ching Hua Lee},
      year={2025},
      eprint={2506.08618},
      archivePrefix={arXiv},
      primaryClass={cs.LG},
      url={https://arxiv.org/abs/2506.08618}, 
}
```
            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "poly2graph",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.11",
    "maintainer_email": "\"Xianquan (Sarinstein) Yan\" <xianquanyan@gmail.com>",
    "keywords": "Hamiltonian spectral graph, algebraic geometry, computer vision, graph representation learning, morphological image processing, non-bloch band, non-hermitian spectral graph",
    "author": null,
    "author_email": "\"Xianquan (Sarinstein) Yan\" <xianquanyan@gmail.com>, Hakan Akg\u00fcn <hakanakgun317@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/87/97/f1ae3dfdf5a4aceddd3e4a8a36dcb9cd3a2335ea22bec85d265b7485d9f2/poly2graph-0.2.0.tar.gz",
    "platform": null,
    "description": "# Poly2Graph\n\n[![arXiv](https://img.shields.io/badge/arXiv-2412.00568---?logo=arXiv&labelColor=b31b1b&color=grey)](https://arxiv.org/abs/2506.08618)\n[![PyPI](https://img.shields.io/pypi/v/poly2graph)](https://pypi.org/project/poly2graph/)\n<a target=\"_blank\" href=\"https://colab.research.google.com/github/sarinstein-yan/poly2graph/blob/main/poly2graph_demo.ipynb\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n\n`Poly2Graph` is a Python package for automatic *Hamiltonian spectral graph* construction. It takes in the characteristic polynomial and returns the spectral graph.\n\nTopological physics is one of the most dynamic and rapidly advancing fields in modern physics. Conventionally, topological classification focuses on eigenstate windings, a concept central to Hermitian topological lattices (e.g., topological insulators). \nBeyond such notion of topology, we unravel a distinct and diverse graph topology emerging in 1D crystal's energy spectra (under open boundary condition). \nParticularly, for non-Hermitian crystals, their *spectral graphs* features a kaleidoscope of exotic shapes like stars, kites, insects, and braids.\n\n<!-- <figure align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/SGs_demo.png\" width=\"600\">\n  <figcaption style=\"text-align:left;\">\n    <strong>Poly2Graph pipeline.</strong>\n    <b>(a)</b> Starting from a 1-D crystal Hamiltonian&nbsp;H(z) in momentum space \u2014 or, equivalently, its <em>characteristic polynomial</em> P(z,E) = det[ <b>H</b>(z) \u2212 E<b>I</b> ]. The crystal\u2019s open-boundary spectrum solely depends on&nbsp;P(z,E).\n    <b>(b)</b> The <em>spectral potential</em> \u03a6(E) (Ronkin function) is computed from the roots of P(z,E) = 0, following recent advances in non-Bloch band theory.\n    <b>(c)</b> The density of states&nbsp;\u03c1(E) is obtained as the Laplacian of \u03a6(E).\n    <b>(d)</b> The spectral graph is extracted from \u03c1(E) via a morphological computer-vision pipeline. Varying the coefficients of P(z,E) produces diverse graph morphologies in the real domain&nbsp;(d1)\u2013(d3) and imaginary domain&nbsp;(di)\u2013(diii).\n  </figcaption>\n</figure> -->\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/SGs_demo.png\"\n       width=\"800\"\n       alt=\"Poly2Graph pipeline\">\n</p>\n\n<strong>Figure: Poly2Graph Pipeline \u2014</strong>\n**(a)** Starting from a 1-D crystal Hamiltonian $H(z)$ in momentum space \u2014 or, equivalently, its *characteristic polynomial* $P(z,E)=\\det[\\mathbf{H}(z)-E\\mathbf{I}]$. The crystal\u2019s open-boundary spectrum solely depends on $P(z,E)$.\n**(b)** The *spectral potential* $\\Phi(E)$ (Ronkin function) is computed from the roots of $P(z,E)=0$, following recent advances in non-Bloch band theory.\n**(c)** The density of states $\\rho(E)$ is obtained as the Laplacian of $\\Phi(E)$.\n**(d)** The spectral graph is extracted from $\\rho(E)$ via a morphological computer-vision pipeline. Varying the coefficients of $P(z,E)$ produces diverse graph morphologies in the real domain (d1)\u2013(d3) and imaginary domain (di)\u2013(diii).\n\n\n## Features\n- **Poly2Graph**\n  1. High-performance\n     - Fast construction of spectral graph from any one-dimensional models\n     - Adaptive resolution to reduce floating operation cost and memory usage\n     - Automatic backend for computation bottleneck. If `tensorflow` / `torch` is available, any device (e.g. '/GPU:0', '/TPU:0', 'cuda:0', etc.) that they support can be used for acceleration.\n  2. Cover generic topological lattices\n     - Support generic one-band and multi-band models\n     - Flexible multiple input choices, be they characteristic polynomials or Bloch Hamiltonians; formats include strings, `sympy.Poly`, and `sympy.Matrix`\n  3. Automatic and Robust\n     - By default, no hyper-parameters are needed. Just input the characteristic of your model and `poly2graph` handles the rest\n     - Automatic spectral boundary inference\n     - Relatively robust on multiband models that are prone to \"component fragmentation\"\n  4. Helper functionalities generally useful\n     - `skeleton2graph` module: Convert a skeleton image to its graph representation\n     - `hamiltonian` module: Conversion among different Hamiltonian representations and efficient computation of a range of properties\n  <!-- 6. Dataset generation\n  1. Visualization of spectral potential, density of states, and spectral graph -->\n\n## Installation\n\nYou can install the package via pip:\n\n```bash\n$ pip install poly2graph\n```\n\nor clone the repository and install it manually:\n\n```bash\n$ git clone https://github.com/sarinstein-yan/poly2graph.git\n$ cd poly2graph\n$ pip install .\n```\n\nOptionally, if [`TensorFlow`](https://www.tensorflow.org/install) or [`PyTorch`](https://pytorch.org/get-started/locally/) is available, `poly2graph` will make use of them automatically to accelerate the computation bottleneck. Priority: `tensorflow` > `torch` > `numpy`.\n\nThis module is tested on `Python >= 3.11`.\nCheck the installation:\n\n```python\nimport poly2graph as p2g\nprint(p2g.__version__)\n```\n\n## Usage\n\nSee the [Poly2Graph Tutorial JupyterNotebook](https://github.com/sarinstein-yan/poly2graph/blob/main/poly2graph_demo.ipynb).\n\n`p2g.SpectralGraph` and `p2g.CharPolyClass` are the two main classes in the package.\n\n`p2g.SpectralGraph` investigates the spectral graph topology of **a specific** given characteristic polynomial or Bloch Hamiltonian. `p2g.CharPolyClass` investigates **a class** of **parametrized** characteristic polynomials or Bloch Hamiltonians, and is optimized for generating spectral properties in parallel.\n\n```python\nimport numpy as np\nimport networkx as nx\nimport sympy as sp\nimport matplotlib.pyplot as plt\n\n# always start by initializing the symbols for k, z, and E\nk = sp.symbols('k', real=True)\nz, E = sp.symbols('z E', complex=True)\n```\n\n### A generic **one-band** example (`p2g.SpectralGraph`):\n\ncharacteristic polynomial:\n\n$$P(E,z) := h(z) - E = z^4 -z -z^{-2} -E$$\n\nIts Bloch Hamiltonian (Fourier transformed Hamiltonian in momentum space) is a scalar function:\n\n$$h(z) = z^4 - z - z^{-2}$$\n\nwhere the phase factor is defined as $z:=e^{ik}$.\n\nExpressed in terms of crystal momentum $k$:\n\n$$h(k) = e^{4ik} - e^{ik} - e^{-2ik}$$\n\n---\nThe valid input formats to initialize a `p2g.SpectralGraph` object are:\n1. Characteristic polynomial in terms of `z` and `E`:\n   - as a string of the Poly in terms of `z` and `E`\n   - as a `sympy.Poly` with {`z`, `1/z`, `E`} as generators\n2. Bloch Hamiltonian in terms of `k` or `z`\n   - as a `sympy.Matrix` in terms of `k`\n   - as a `sympy.Matrix` in terms of `z`\n\nAll the following `characteristic`s are valid and will initialize to the same characteristic polynomial and therefore produce the same spectral graph:\n```python\nchar_poly_str = '-z**-2 - E - z + z**4'\n\nchar_poly_Poly = sp.Poly(\n    -z**-2 - E - z + z**4,\n    z, 1/z, E # generators are z, 1/z, E\n)\n\nphase_k = sp.exp(sp.I*k)\nchar_hamil_k = sp.Matrix([-phase_k**2 - phase_k + phase_k**4])\n\nchar_hamil_z = sp.Matrix([-z**-2 - E - z + z**4])\n```\n\nLet us just use the string to initialize and see a set of properties that are computed automatically:\n\n```python\nsg = p2g.SpectralGraph(char_poly_str, k=k, z=z, E=E)\n```\n\n---\n**Characteristic polynomial**:\n\n```python\nsg.ChP\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span> $\\text{Poly}{\\left( z^{4} - z -\\frac{1}{z^{2}} - E, ~ z, \\frac{1}{z}, E, ~ domain=\\mathbb{Z} \\right)}$\n\n---\n**Bloch Hamiltonian**:\n- For one-band model, it is a unique, rank-0 matrix (scalar)\n\n```python\nsg.h_k\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}e^{4 i k} - e^{i k} - e^{- 2 i k}\\end{bmatrix}$$\n\n```python\nsg.h_z\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}- \\frac{- z^{6} + z^{3} + 1}{z^{2}}\\end{bmatrix}$$\n\n---\n**The Frobenius companion matrix of `P(E)(z)`**:\n- treating `E` as parameter and `z` as variable\n- Its eigenvalues are the roots of the characteristic polynomial at a fixed complex energy `E`. Thus it is useful to calculate the GBZ (generalized Brillouin zone), the spectral potential (Ronkin function), etc.\n\n```python\nsg.companion_E\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}0 & 0 & 0 & 0 & 0 & 1 \\\\\n1 & 0 & 0 & 0 & 0 & 0 \\\\\n0 & 1 & 0 & 0 & 0 & E \\\\\n0 & 0 & 1 & 0 & 0 & 1 \\\\\n0 & 0 & 0 & 1 & 0 & 0 \\\\\n0 & 0 & 0 & 0 & 1 & 0\\end{bmatrix}$$\n\n---\n**Number of bands & hopping range**:\n```python\nprint('Number of bands:', sg.num_bands)\nprint('Max hopping length to the right:', sg.poly_p)\nprint('Max hopping length to the left:', sg.poly_q)\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n```text\nNumber of bands: 1\nMax hopping length to the right: 2\nMax hopping length to the left: 4\n```\n\n---\n**A real-space Hamiltonian of a finite chain and its energy spectrum**:\n\n```python\nH = sg.real_space_H(\n    N=40,        # number of unit cells\n    pbc=False,   # open boundary conditions\n    max_dim=500  # maximum dimension of the Hamiltonian matrix (for numerical accuracy)\n)\n\nenergy = np.linalg.eigvals(H)\n\nfig, ax = plt.subplots(figsize=(3, 3))\nax.plot(energy.real, energy.imag, 'k.', markersize=5)\nax.set(xlabel='Re(E)', ylabel='Im(E)', \\\nxlim=sg.spectral_square[:2], ylim=sg.spectral_square[2:])\nplt.tight_layout(); plt.show()\n```\n\n<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/finite_spectrum_one_band.png\" width=\"300\" />\n</p>\n\n---\n#### **The Set of Spectral Functions**\n(whose values plotted on the complex energy square, returned as a 2D array)\n\n- **Density of States (DOS)**\n\n  Defined as the number of states per unit energy area in the complex energy plane.\n\n  $$\\rho(E) = \\lim_{N\\to\\infty}\\sum_n \\frac{1}{N} \\delta(E-\\epsilon_n)$$\n\n  where $\\epsilon_n$ are the eigenvalues of the Hamiltonian $H$.\n\n  Imagine to assign electric charge $1/N$ to each eigenvalue $\\epsilon_n$, then the density of states $\\rho(E)$ is treated as a *charge density*, therefore can be interpreted as the laplacian of a *spectral potential* $\\Phi(E)$:\n\n  $$\\rho(E) = -\\frac{1}{2\\pi} \\Delta \\Phi(E)$$\n\n  $\\Delta = \\partial_{\\text{Re} E}^2 + \\partial_{\\text{Im} E}^2$ is the Laplacian operator on the complex energy plane. Laplacian operator extracts curvature; thus, geometrically speaking, the loci of spectral graph $\\mathcal{G}$ resides on the *ridges* of the Coulomb potential landscape.\n\n- **Spectral Potential (Ronkin function)**\n\n  It can be proven that the spectral potential $\\Phi(E)$ can be efficiently computed from the roots $|z_i(E)|$ of the characteristic polynomial $P(E)(z)$ and the leading coefficient $a_q(E)$ at a complex energy $E$:\n\n  $$\\Phi(E) = - \\lim_{N\\to\\infty} \\sum_{\\epsilon_n} \\log|E-\\epsilon_n| \\\\\n  = - \\int \\rho(E')\\log|E-E'| d^2E' \\\\\n  = - \\log|a_q(E)| - \\sum_{i=p+1}^{p+q} \\log|z_i(E)|$$\n\n- Graph Skeleton (Binarized DOS)\n\n```python\nphi, dos, binaried_dos = sg.spectral_images()\n\nfig, axes = plt.subplots(1, 3, figsize=(8, 3), sharex=True, sharey=True)\naxes[0].imshow(phi, extent=sg.spectral_square, cmap='terrain')\naxes[0].set(xlabel='Re(E)', ylabel='Im(E)', title='Spectral Potential')\naxes[1].imshow(dos, extent=sg.spectral_square, cmap='viridis')\naxes[1].set(xlabel='Re(E)', title='Density of States')\naxes[2].imshow(binaried_dos, extent=sg.spectral_square, cmap='gray')\naxes[2].set(xlabel='Re(E)', title='Graph Skeleton')\nplt.tight_layout()\nplt.show()\n```\n\n<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/spectral_images_one_band.png\" width=\"900\" />\n</p>\n\n---\n#### The spectral graph $\\mathcal{G}$\n\n```python\ngraph = sg.spectral_graph()\n\nfig, ax = plt.subplots(figsize=(3, 3))\npos = nx.get_node_attributes(graph, 'pos')\nnx.draw_networkx_nodes(graph, pos, alpha=0.8, ax=ax,\n            node_size=50, node_color='#A60628')\nnx.draw_networkx_edges(graph, pos, alpha=0.8, ax=ax,\n            width=5, edge_color='#348ABD')\nplt.tight_layout(); plt.show()\n```\n\n<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/spectral_graph_one_band.png\" width=\"300\" />\n</p>\n\n\n> [!TIP]\n> If `tensorflow` or `torch` is available, `poly2graph` will automatically use them and run on **CPU** by default. If other device, e.g. GPU / TPU is available, one can pass `device = {device string}` to the method `spectral_images` and `spectral_graph`:\n> ```python\n> SpectralGraph.spectral_images(device='/cpu:0')\n> SpectralGraph.spectral_graph(device='/gpu:1')\n> SpectralGraph.spectral_images(device='cpu')\n> SpectralGraph.spectral_graph(device='cuda:0')\n> ...\n> ```\n> However, some functions may not have gpu kernel in `tf`/`torch`, in which case the computation will fallback to CPU.\n\n### A generic **multi-band** example (`p2g.SpectralGraph`):\n\ncharacteristic polynomial (four bands):\n\n$$P(E,z) := \\det(\\textbf{h}(z) - E\\;\\textbf{I}) = z^2 + 1/z^2 + E z - E^4$$\n\nOne of its possible Bloch Hamiltonians in terms of $z$:\n\n$$\\textbf{h}(z)=\\begin{bmatrix}\n0 & 0 & 0 & z^2 + 1/z^2 \\\\\n1 & 0 & 0 & z \\\\\n0 & 1 & 0 & 0 \\\\\n0 & 0 & 1 & 0 \\\\\n\\end{bmatrix}$$\n\n---\n\n```python\nsg_multi = p2g.SpectralGraph(\"z**2 + 1/z**2 + E*z - E**4\", k, z, E)\n```\n\n---\n**Characteristic polynomial**:\n\n```python\nsg_multi.ChP\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span> $\\text{Poly}{\\left( z^{2} + zE + \\frac{1}{z^{2}} - E^{4}, ~ z, \\frac{1}{z}, E, ~ domain=\\mathbb{Z} \\right)}$\n\n---\n**Bloch Hamiltonian**:\n- For multi-band model, if the `p2g.SpectralGraph` is not initialized with a `sympy` `Matrix`, then `poly2graph` will use the companion matrix of the characteristic polynomial `P(z)(E)` (treating `z` as parameter and `E` as variable) as the Bloch Hamiltonian -- this is one of the set of possible band Hamiltonians that possesses the same energy spectrum and thus the same spectral graph.\n\n```python\nsg_multi.h_k\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}0 & 0 & 0 & 2 \\cos{\\left(2 k \\right)} \\\\\n1 & 0 & 0 & e^{i k} \\\\\n0 & 1 & 0 & 0 \\\\\n0 & 0 & 1 & 0\\end{bmatrix}$$\n\n```python\nsg_multi.h_z\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}0 & 0 & 0 & z^{2} + \\frac{1}{z^{2}} \\\\\n1 & 0 & 0 & z \\\\\n0 & 1 & 0 & 0 \\\\\n0 & 0 & 1 & 0\\end{bmatrix}$$\n\n---\n**The Frobenius companion matrix of `P(E)(z)`**:\n\n```python\nsg_multi.companion_E\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}0 & 0 & 0 & -1 \\\\\n1 & 0 & 0 & 0 \\\\\n0 & 1 & 0 & E^{4} \\\\\n0 & 0 & 1 & - E\\end{bmatrix}$$\n\n---\n**Number of bands & hopping range**:\n```python\nprint('Number of bands:', sg_multi.num_bands)\nprint('Max hopping length to the right:', sg_multi.poly_p)\nprint('Max hopping length to the left:', sg_multi.poly_q)\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n```text\nNumber of bands: 4\nMax hopping length to the right: 2\nMax hopping length to the left: 2\n```\n\n---\n**A real-space Hamiltonian of a finite chain and its energy spectrum**:\n\n```python\nH_multi = sg_multi.real_space_H(\n    N=40,        # number of unit cells\n    pbc=False,   # open boundary conditions\n    max_dim=500  # maximum dimension of the Hamiltonian matrix (for numerical accuracy)\n)\n\nenergy_multi = np.linalg.eigvals(H_multi)\n\nfig, ax = plt.subplots(figsize=(3, 3))\nax.plot(energy_multi.real, energy_multi.imag, 'k.', markersize=5)\nax.set(xlabel='Re(E)', ylabel='Im(E)', \\\nxlim=sg_multi.spectral_square[:2], ylim=sg_multi.spectral_square[2:])\nplt.tight_layout(); plt.show()\n```\n\n<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/finite_spectrum_multi_band.png\" width=\"300\" />\n</p>\n\n---\n#### **The Set of Spectral Functions**\n\n```python\nphi_multi, dos_multi, binaried_dos_multi = sg_multi.spectral_images(device='/cpu:0')\n\nfig, axes = plt.subplots(1, 3, figsize=(8, 3), sharex=True, sharey=True)\naxes[0].imshow(phi_multi, extent=sg_multi.spectral_square, cmap='terrain')\naxes[0].set(xlabel='Re(E)', ylabel='Im(E)', title='Spectral Potential')\naxes[1].imshow(dos_multi, extent=sg_multi.spectral_square, cmap='viridis')\naxes[1].set(xlabel='Re(E)', title='Density of States')\naxes[2].imshow(binaried_dos_multi, extent=sg_multi.spectral_square, cmap='gray')\naxes[2].set(xlabel='Re(E)', title='Graph Skeleton')\nplt.tight_layout(); plt.show()\n```\n\n<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/spectral_images_multi_band.png\" width=\"900\" />\n</p>\n\n---\n#### The spectral graph $\\mathcal{G}$\n\n```python\ngraph_multi = sg_multi.spectral_graph(\n    short_edge_threshold=20, \n    # ^ node pairs or edges with distance < threshold pixels are merged\n)\n\nfig, ax = plt.subplots(figsize=(3, 3))\npos_multi = nx.get_node_attributes(graph_multi, 'pos')\nnx.draw(graph_multi, pos_multi, ax=ax, \n        node_size=10, node_color='#A60628', \n        edge_color='#348ABD', width=2, alpha=0.8)\nplt.tight_layout(); plt.show()\n```\n\n<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/spectral_graph_multi_band.png\" width=\"300\" />\n</p>\n\n\n## Node and Edge Attributes of the Spectral Graph Object\n\nThe spectral graph is a `networkx.MultiGraph` object.\n\n- Node Attributes\n  1. `pos` : (2,)-numpy array\n     - the position of the node $(\\text{Re}(E), \\text{Im}(E))$\n  2. `dos` : float\n     - the density of states at the node\n  3. `potential` : float\n     - the spectral potential at the node\n- Edge Attributes\n  1. `weight` : float\n     - the weight of the edge, which is the **length** of the edge in the complex energy plane\n  2. `pts` : (w, 2)-numpy array\n     - the positions of the points constituting the edge, where `w` is the number of points along the edge, i.e., the length of the edge, equals `weight`\n  3. `avg_dos` : float\n     - the average density of states along the edge\n  4. `avg_potential` : float\n     - the average spectral potential along the edge\n\n```python\nnode_attr = dict(graph.nodes(data=True))\nedge_attr = list(graph.edges(data=True))\nprint('The attributes of the first node\\n', node_attr[0], '\\n')\nprint('The attributes of the first edge\\n', edge_attr[0][-1], '\\n')\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n```text\nThe attributes of the first node\n {'pos': array([-0.20403848, -2.11668106]), \n  'dos': 0.0011466597206890583, \n  'potential': -0.655870258808136} \n\nThe attributes of the first edge\n {'weight': 1.4176547247784077, \n  'pts': array([[-2.04038482e-01, -2.11668106e+00],\n       [-1.99792382e-01, -2.11243496e+00],\n       ...\n       [ 5.94228396e-01, -1.02967935e+00]]), \n  'avg_dos': 0.10761458, \n  'avg_potential': -0.5068641}\n```\n\n\n---\n### A generic **multi-band** class (`p2g.CharPolyClass`):\n\nLet us add two parameters `{a,b}` to the aforementioned multi-band example and construct a `p2g.CharPolyClass` object:\n\n```python\na, b = sp.symbols('a b', real=True)\n\ncp = p2g.CharPolyClass(\n    \"z**2 + a/z**2 + b*E*z - E**4\", \n    k=k, z=z, E=E,\n    params={a, b}, # pass parameters as a set\n)\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span> \n\n```text\nDerived Bloch Hamiltonian `h_z` with 4 bands.\n```\n\n---\nView a few auto-computed properties\n\n**Characteristic polynomial**:\n\n```python\ncp.ChP\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span> $\\text{Poly}{\\left( z^{2} + a \\frac{1}{z^{2}} + b zE - E^{4}, z, \\frac{1}{z}, E, domain=\\mathbb{Z}\\left[a, b\\right] \\right)}$\n\n---\n**Bloch Hamiltonian**:\n\n```python\ncp.h_k\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}\n0 & 0 & 0 & (a + e^{4 i k})e^{- 2 i k} \\\\\n1 & 0 & 0 & b e^{i k} \\\\\n0 & 1 & 0 & 0 \\\\\n0 & 0 & 1 & 0\n\\end{bmatrix}$$\n\n```python\ncp.h_z\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}\n0 & 0 & 0 & \\frac{a}{z^{2}} + z^{2} \\\\\n1 & 0 & 0 & b z \\\\\n0 & 1 & 0 & 0 \\\\\n0 & 0 & 1 & 0\n\\end{bmatrix}$$\n\n---\n**The Frobenius companion matrix of `P(E)(z)`**:\n\n```python\ncp.companion_E\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span>\n\n$$\\begin{bmatrix}\n0 & 0 & 0 & -a \\\\\n1 & 0 & 0 & 0 \\\\\n0 & 1 & 0 & E^{4} \\\\\n0 & 0 & 1 & - E b\n\\end{bmatrix}$$\n\n---\n#### **An Array of Spectral Functions**\n\nTo get an array of spectral images or spectral graphs, we first prepare the values of the parameters `{a,b}`\n\n```python\na_array = np.linspace(-2, 1, 6)\nb_array = np.linspace(-1, 1, 6)\na_grid, b_grid = np.meshgrid(a_array, b_array)\nparam_dict = {a: a_grid, b: b_grid}\nprint('a_grid shape:', a_grid.shape,\n    '\\nb_grid shape:', b_grid.shape)\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span> \n\n```text\na_grid shape: (6, 6)\nb_grid shape: (6, 6)\n```\n\nNote that **the value array of the parameters should have the same shape**, which is also **the shape of the output array of spectral images**\n\n```python\nphi_arr, dos_arr, binaried_dos_arr, spectral_square = \\\n    cp.spectral_images(param_dict=param_dict)\nprint('phi_arr shape:', phi_arr.shape,\n    '\\ndos_arr shape:', dos_arr.shape,\n    '\\nbinaried_dos_arr shape:', binaried_dos_arr.shape)\n```\n\n<span style=\"color:#d73a49;font-weight:bold\">>>></span> \n\n```text\nphi_arr shape: (6, 6, 1024, 1024) \ndos_arr shape: (6, 6, 1024, 1024) \nbinaried_dos_arr shape: (6, 6, 1024, 1024)\n```\n\n```python\nfrom mpl_toolkits.axes_grid1 import ImageGrid\n\nfig = plt.figure(figsize=(13, 13))\ngrid = ImageGrid(fig, 111, nrows_ncols=(6, 6), axes_pad=0, \n                 label_mode='L', share_all=True)\n\nfor ax, (i, j) in zip(grid, [(i, j) for i in range(6) for j in range(6)]):\n    ax.imshow(phi_arr[i, j], extent=spectral_square[i, j], cmap='terrain')\n    ax.set(xlabel='Re(E)', ylabel='Im(E)')\n    ax.text(\n        0.03, 0.97, f'a = {a_array[i]:.2f}, b = {b_array[j]:.2f}',\n        ha='left', va='top', transform=ax.transAxes,\n        fontsize=10, color='tab:red',\n        bbox=dict(alpha=0.8, facecolor='white')\n    )\n\nplt.tight_layout()\nplt.savefig('./assets/ChP_spectral_potential_grid.png', dpi=72)\nplt.show()\n```\n\n<p align=\"center\">\n    <img src=\"https://raw.githubusercontent.com/sarinstein-yan/poly2graph/main/assets/ChP_spectral_potential_grid.png\" width=\"1000\" />\n</p>\n\n---\n#### An Array of Spectral Graphs\n\n```python\ngraph_flat, param_dict_flat = cp.spectral_graph(param_dict=param_dict)\nprint(graph_flat, '\\n')\nprint(param_dict_flat)\n```\n\n```text\n[<networkx.classes.multigraph.MultiGraph object at 0x000001966DFCD190>, \n<networkx.classes.multigraph.MultiGraph object at 0x000001966DFCECF0>, \n...\n<networkx.classes.multigraph.MultiGraph object at 0x000001966DFCE750>]\n\n{a: \narray([-2. , -1.4, -0.8, -0.2,  0.4,  1. , -2. , -1.4, -0.8, -0.2,  0.4,\n        1. , -2. , -1.4, -0.8, -0.2,  0.4,  1. , -2. , -1.4, -0.8, -0.2,\n        0.4,  1. , -2. , -1.4, -0.8, -0.2,  0.4,  1. , -2. , -1.4, -0.8,\n       -0.2,  0.4,  1. ]), \nb: \narray([-1. , -1. , -1. , -1. , -1. , -1. , -0.6, -0.6, -0.6, -0.6, -0.6,\n       -0.6, -0.2, -0.2, -0.2, -0.2, -0.2, -0.2,  0.2,  0.2,  0.2,  0.2,\n        0.2,  0.2,  0.6,  0.6,  0.6,  0.6,  0.6,  0.6,  1. ,  1. ,  1. ,\n        1. ,  1. ,  1. ])}\n```\n\n> [!NOTE]\n> The spectral graph is a `networkx.MultiGraph` object, which cannot be directly returned as a multi-dimensional numpy array of `MultiGraph`, except for the case of 1D array.\n> Instead, we return a flattened list of `networkx.MultiGraph` objects, and the accompanying `param_dict_flat` is the dictionary that contains the corresponding flattened parameter values.\n\n> [!TIP]\n> It's recommended to pass the values of the parameters as `vectors` (1D arrays) instead of higher dimensional `ND arrays` to avoid the overhead of reshaping the output and the difficulty to retrieve / postprocess the spectral graphs.\n\n\n## Citation\nIf you find this work useful, please cite our paper:\n\n```bibtex\n@misc{yan2025hsg12mlargescalespatialmultigraph,\n      title={HSG-12M: A Large-Scale Spatial Multigraph Dataset}, \n      author={Xianquan Yan and Hakan Akg\u00fcn and Kenji Kawaguchi and N. Duane Loh and Ching Hua Lee},\n      year={2025},\n      eprint={2506.08618},\n      archivePrefix={arXiv},\n      primaryClass={cs.LG},\n      url={https://arxiv.org/abs/2506.08618}, \n}\n```",
    "bugtrack_url": null,
    "license": "MIT License\n        \n        Copyright (c) 2024 Xianquan (Sarinstein) Yan\n        \n        Permission is hereby granted, free of charge, to any person obtaining a copy\n        of this software and associated documentation files (the \"Software\"), to deal\n        in the Software without restriction, including without limitation the rights\n        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n        copies of the Software, and to permit persons to whom the Software is\n        furnished to do so, subject to the following conditions:\n        \n        The above copyright notice and this permission notice shall be included in all\n        copies or substantial portions of the Software.\n        \n        THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n        SOFTWARE.",
    "summary": "Automated Non-Hermitian Spectral Graph Construction",
    "version": "0.2.0",
    "project_urls": {
        "Homepage": "https://github.com/sarinstein-yan/poly2graph",
        "Repository": "https://github.com/sarinstein-yan/poly2graph"
    },
    "split_keywords": [
        "hamiltonian spectral graph",
        " algebraic geometry",
        " computer vision",
        " graph representation learning",
        " morphological image processing",
        " non-bloch band",
        " non-hermitian spectral graph"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "8a1e7f61f2521a06bea0b9356a666c642554840a2b0a7efc50393e312a5a2dbd",
                "md5": "378f7e1e1bd5dc8c2c38745c71db9b32",
                "sha256": "97d67a3fc0b3d1c28d53eb569e521d6205927de20f1f79f6d4eaa1d8bc4559e5"
            },
            "downloads": -1,
            "filename": "poly2graph-0.2.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "378f7e1e1bd5dc8c2c38745c71db9b32",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.11",
            "size": 42169,
            "upload_time": "2025-08-31T06:14:08",
            "upload_time_iso_8601": "2025-08-31T06:14:08.099205Z",
            "url": "https://files.pythonhosted.org/packages/8a/1e/7f61f2521a06bea0b9356a666c642554840a2b0a7efc50393e312a5a2dbd/poly2graph-0.2.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "8797f1ae3dfdf5a4aceddd3e4a8a36dcb9cd3a2335ea22bec85d265b7485d9f2",
                "md5": "3cd47e62b725f7c03447c1f529e68db5",
                "sha256": "f29279df4dbdfb3bdba40a6c5c5f8a5a68046f630c16bf908489106a0e956f90"
            },
            "downloads": -1,
            "filename": "poly2graph-0.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "3cd47e62b725f7c03447c1f529e68db5",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.11",
            "size": 37738,
            "upload_time": "2025-08-31T06:14:06",
            "upload_time_iso_8601": "2025-08-31T06:14:06.238769Z",
            "url": "https://files.pythonhosted.org/packages/87/97/f1ae3dfdf5a4aceddd3e4a8a36dcb9cd3a2335ea22bec85d265b7485d9f2/poly2graph-0.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-31 06:14:06",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "sarinstein-yan",
    "github_project": "poly2graph",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": false,
    "lcname": "poly2graph"
}
        
Elapsed time: 1.45796s