qig-compute 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qig-compute
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Fast compute engine for QIG geometry — analytical QFI, GPU contractions, observable governance
|
|
5
|
+
Project-URL: Homepage, https://github.com/GaryOcean428/qig-compute
|
|
6
|
+
Project-URL: Repository, https://github.com/GaryOcean428/qig-compute
|
|
7
|
+
Author-email: Braden Lang <braden@garyocean.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: fisher-information,gpu,qig,quantum,tensor-network
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
19
|
+
Provides-Extra: full
|
|
20
|
+
Requires-Dist: cupy-cuda12x; extra == 'full'
|
|
21
|
+
Requires-Dist: physics-tenpy>=1.0; extra == 'full'
|
|
22
|
+
Requires-Dist: scipy>=1.10; extra == 'full'
|
|
23
|
+
Provides-Extra: gpu
|
|
24
|
+
Requires-Dist: cupy-cuda12x; extra == 'gpu'
|
|
25
|
+
Provides-Extra: tenpy
|
|
26
|
+
Requires-Dist: physics-tenpy>=1.0; extra == 'tenpy'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# qig-compute
|
|
30
|
+
|
|
31
|
+
Fast compute engine for QIG geometry — analytical QFI, GPU contractions, observable governance.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install qig-compute # numpy only
|
|
37
|
+
pip install qig-compute[tenpy] # + TeNPy for MPS-QFI
|
|
38
|
+
pip install qig-compute[gpu] # + CuPy for GPU acceleration
|
|
39
|
+
pip install qig-compute[full] # everything
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from qig_compute.qfi_analytical import qfi_analytical, qfi_with_governance
|
|
46
|
+
|
|
47
|
+
# Compute QFI from a single MPS ground state (zero DMRG loops)
|
|
48
|
+
F = qfi_analytical(psi_mps, L=6, sites=pruned_sites)
|
|
49
|
+
|
|
50
|
+
# With governance checks (warns about blindspots, auto-fills cheap measurements)
|
|
51
|
+
F, report = qfi_with_governance(psi_mps, L=6, h=1.0, J=1.0)
|
|
52
|
+
print(report.summary())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Observable Governance
|
|
56
|
+
|
|
57
|
+
Built-in blindspot detection warns when:
|
|
58
|
+
- Results only tested in one regime
|
|
59
|
+
- Observable amplitude collapses (wrong channel)
|
|
60
|
+
- FFT frequency at resolution floor
|
|
61
|
+
- DMRG bond dimension at limit
|
|
62
|
+
- Energy gap not measured (auto-fills if cheap)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# qig-compute
|
|
2
|
+
|
|
3
|
+
Fast compute engine for QIG geometry — analytical QFI, GPU contractions, observable governance.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install qig-compute # numpy only
|
|
9
|
+
pip install qig-compute[tenpy] # + TeNPy for MPS-QFI
|
|
10
|
+
pip install qig-compute[gpu] # + CuPy for GPU acceleration
|
|
11
|
+
pip install qig-compute[full] # everything
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from qig_compute.qfi_analytical import qfi_analytical, qfi_with_governance
|
|
18
|
+
|
|
19
|
+
# Compute QFI from a single MPS ground state (zero DMRG loops)
|
|
20
|
+
F = qfi_analytical(psi_mps, L=6, sites=pruned_sites)
|
|
21
|
+
|
|
22
|
+
# With governance checks (warns about blindspots, auto-fills cheap measurements)
|
|
23
|
+
F, report = qfi_with_governance(psi_mps, L=6, h=1.0, J=1.0)
|
|
24
|
+
print(report.summary())
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Observable Governance
|
|
28
|
+
|
|
29
|
+
Built-in blindspot detection warns when:
|
|
30
|
+
- Results only tested in one regime
|
|
31
|
+
- Observable amplitude collapses (wrong channel)
|
|
32
|
+
- FFT frequency at resolution floor
|
|
33
|
+
- DMRG bond dimension at limit
|
|
34
|
+
- Energy gap not measured (auto-fills if cheap)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qig-compute"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Fast compute engine for QIG geometry — analytical QFI, GPU contractions, observable governance"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Braden Lang", email = "braden@garyocean.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["qig", "quantum", "fisher-information", "tensor-network", "gpu"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"numpy>=1.24",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
tenpy = ["physics-tenpy>=1.0"]
|
|
29
|
+
gpu = ["cupy-cuda12x"]
|
|
30
|
+
full = ["physics-tenpy>=1.0", "cupy-cuda12x", "scipy>=1.10"]
|
|
31
|
+
dev = ["pytest>=7.0"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/GaryOcean428/qig-compute"
|
|
35
|
+
Repository = "https://github.com/GaryOcean428/qig-compute"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/qig_compute"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qig-compute v0.1.0a1 — Fast Compute Engine for QIG Geometry
|
|
3
|
+
|
|
4
|
+
Analytical QFI, GPU tensor contractions, observable governance.
|
|
5
|
+
Separated from qig-verification (physics experiments) and qig-core
|
|
6
|
+
(geometry definitions).
|
|
7
|
+
|
|
8
|
+
Three responsibilities:
|
|
9
|
+
1. Compute fast — analytical MPS-QFI, GPU contractions
|
|
10
|
+
2. Compute correctly — governance warnings detect blindspots
|
|
11
|
+
3. Compute smart — auto-fill cheap missing measurements
|
|
12
|
+
|
|
13
|
+
Modules:
|
|
14
|
+
qfi_analytical — QFI from single MPS via transfer matrix (no DMRG loops)
|
|
15
|
+
gpu_contractions — CuPy tensor network contractions
|
|
16
|
+
observable — blindspot detection + auto-fill
|
|
17
|
+
screening_pipeline — push site-pruning inside QFI computation
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0a1"
|
|
21
|
+
|
|
22
|
+
from qig_compute.observable import (
|
|
23
|
+
GovernanceWarning,
|
|
24
|
+
GovernanceReport,
|
|
25
|
+
check_amplitude,
|
|
26
|
+
check_resolution_floor,
|
|
27
|
+
)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observable Governance — blindspot detection + auto-fill.
|
|
3
|
+
|
|
4
|
+
Detects measurement blindspots during computation and either:
|
|
5
|
+
- WARNS if the blindspot can't be fixed cheaply
|
|
6
|
+
- AUTO-FILLS if the fix is within compute budget
|
|
7
|
+
|
|
8
|
+
Warnings are collected into a GovernanceReport that qig-bench consumes.
|
|
9
|
+
|
|
10
|
+
Source: 2026-04-10 sweep findings — regime blindness, channel blindness,
|
|
11
|
+
resolution floor, chi limits, perturbation direction, gap unknown.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from enum import Enum
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Severity(Enum):
|
|
22
|
+
INFO = "info" # Auto-filled, no action needed
|
|
23
|
+
WARNING = "warning" # Could affect results, can't auto-fix
|
|
24
|
+
CRITICAL = "critical" # Likely invalidates results
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class GovernanceWarning:
|
|
29
|
+
"""A single governance warning or auto-fill notification."""
|
|
30
|
+
id: str
|
|
31
|
+
severity: Severity
|
|
32
|
+
message: str
|
|
33
|
+
auto_filled: bool = False
|
|
34
|
+
auto_fill_cost_s: float = 0.0
|
|
35
|
+
metadata: dict = field(default_factory=dict)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class GovernanceReport:
|
|
40
|
+
"""Collected warnings from a computation run."""
|
|
41
|
+
warnings: list[GovernanceWarning] = field(default_factory=list)
|
|
42
|
+
auto_fills_performed: int = 0
|
|
43
|
+
auto_fill_total_cost_s: float = 0.0
|
|
44
|
+
|
|
45
|
+
def add(self, warning: GovernanceWarning):
|
|
46
|
+
self.warnings.append(warning)
|
|
47
|
+
if warning.auto_filled:
|
|
48
|
+
self.auto_fills_performed += 1
|
|
49
|
+
self.auto_fill_total_cost_s += warning.auto_fill_cost_s
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def has_critical(self) -> bool:
|
|
53
|
+
return any(w.severity == Severity.CRITICAL for w in self.warnings)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def has_warnings(self) -> bool:
|
|
57
|
+
return any(w.severity == Severity.WARNING for w in self.warnings)
|
|
58
|
+
|
|
59
|
+
def summary(self) -> str:
|
|
60
|
+
lines = []
|
|
61
|
+
critical = [w for w in self.warnings if w.severity == Severity.CRITICAL]
|
|
62
|
+
warns = [w for w in self.warnings if w.severity == Severity.WARNING]
|
|
63
|
+
infos = [w for w in self.warnings if w.severity == Severity.INFO]
|
|
64
|
+
|
|
65
|
+
if critical:
|
|
66
|
+
lines.append(f"CRITICAL ({len(critical)}):")
|
|
67
|
+
for w in critical:
|
|
68
|
+
lines.append(f" [{w.id}] {w.message}")
|
|
69
|
+
|
|
70
|
+
if warns:
|
|
71
|
+
lines.append(f"WARNING ({len(warns)}):")
|
|
72
|
+
for w in warns:
|
|
73
|
+
lines.append(f" [{w.id}] {w.message}")
|
|
74
|
+
|
|
75
|
+
if infos:
|
|
76
|
+
lines.append(f"AUTO-FILLED ({len(infos)}):")
|
|
77
|
+
for w in infos:
|
|
78
|
+
cost = f" ({w.auto_fill_cost_s:.1f}s)" if w.auto_fill_cost_s > 0 else ""
|
|
79
|
+
lines.append(f" [{w.id}] {w.message}{cost}")
|
|
80
|
+
|
|
81
|
+
if not self.warnings:
|
|
82
|
+
lines.append("No governance warnings.")
|
|
83
|
+
|
|
84
|
+
return "\n".join(lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ═══════════════════════════════════════════════════════════════
|
|
88
|
+
# CHECK FUNCTIONS — call during computation
|
|
89
|
+
# ═══════════════════════════════════════════════════════════════
|
|
90
|
+
|
|
91
|
+
def check_amplitude(values: list[float] | dict, threshold_ratio: float = 10.0) -> GovernanceWarning | None:
|
|
92
|
+
"""Check if observable amplitude varies too much across domain.
|
|
93
|
+
|
|
94
|
+
Detects the magnetisation-on-inhomogeneous failure mode.
|
|
95
|
+
"""
|
|
96
|
+
if isinstance(values, dict):
|
|
97
|
+
vals = list(values.values())
|
|
98
|
+
else:
|
|
99
|
+
vals = list(values)
|
|
100
|
+
|
|
101
|
+
if not vals or all(v == 0 for v in vals):
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
vals_abs = [abs(v) for v in vals if v != 0]
|
|
105
|
+
if len(vals_abs) < 2:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
ratio = max(vals_abs) / min(vals_abs) if min(vals_abs) > 0 else float("inf")
|
|
109
|
+
|
|
110
|
+
if ratio > threshold_ratio:
|
|
111
|
+
return GovernanceWarning(
|
|
112
|
+
id="AMPLITUDE_COLLAPSE",
|
|
113
|
+
severity=Severity.CRITICAL if ratio > 100 else Severity.WARNING,
|
|
114
|
+
message=f"Amplitude varies {ratio:.0f}× across domain. "
|
|
115
|
+
f"Zero-crossing metrics unreliable in low-amplitude region. "
|
|
116
|
+
f"Consider energy channel.",
|
|
117
|
+
metadata={"ratio": ratio, "max": max(vals_abs), "min": min(vals_abs)},
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def check_resolution_floor(omega: float, t_max: float) -> GovernanceWarning | None:
|
|
123
|
+
"""Check if measured frequency is at the FFT resolution floor."""
|
|
124
|
+
import numpy as np
|
|
125
|
+
floor = 2 * np.pi / t_max
|
|
126
|
+
if abs(omega - floor) / floor < 0.05: # within 5% of floor
|
|
127
|
+
return GovernanceWarning(
|
|
128
|
+
id="RESOLUTION_FLOOR",
|
|
129
|
+
severity=Severity.WARNING,
|
|
130
|
+
message=f"ω={omega:.4f} is at FFT floor (2π/t_max={floor:.4f}). "
|
|
131
|
+
f"Oscillation period may exceed observation window. "
|
|
132
|
+
f"Increase t_max.",
|
|
133
|
+
metadata={"omega": omega, "floor": floor, "t_max": t_max},
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def check_chi_limit(chi_used: int, chi_max: int,
|
|
139
|
+
energy_at_chi: float | None = None,
|
|
140
|
+
energy_at_half_chi: float | None = None) -> GovernanceWarning | None:
|
|
141
|
+
"""Check if DMRG bond dimension hit the limit."""
|
|
142
|
+
if chi_used < chi_max:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
if energy_at_chi is not None and energy_at_half_chi is not None:
|
|
146
|
+
energy_diff = abs(energy_at_chi - energy_at_half_chi)
|
|
147
|
+
if energy_diff < 1e-6:
|
|
148
|
+
return GovernanceWarning(
|
|
149
|
+
id="CHI_LIMIT",
|
|
150
|
+
severity=Severity.INFO,
|
|
151
|
+
message=f"χ at limit ({chi_used}/{chi_max}) but energy converged "
|
|
152
|
+
f"(ΔE={energy_diff:.2e}). Results likely reliable.",
|
|
153
|
+
auto_filled=True,
|
|
154
|
+
metadata={"chi_used": chi_used, "chi_max": chi_max,
|
|
155
|
+
"energy_diff": energy_diff},
|
|
156
|
+
)
|
|
157
|
+
return GovernanceWarning(
|
|
158
|
+
id="CHI_LIMIT",
|
|
159
|
+
severity=Severity.WARNING,
|
|
160
|
+
message=f"χ at limit ({chi_used}/{chi_max}). Convergence unverified. "
|
|
161
|
+
f"Compare energy at χ and χ/2.",
|
|
162
|
+
metadata={"chi_used": chi_used, "chi_max": chi_max},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def check_regime_coverage(h_values_tested: list[float], J: float = 1.0,
|
|
167
|
+
h_c_ratio: float = 3.044) -> GovernanceWarning | None:
|
|
168
|
+
"""Check if results span multiple regimes or only one."""
|
|
169
|
+
if not h_values_tested:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
h_c = h_c_ratio * J
|
|
173
|
+
regimes_seen = set()
|
|
174
|
+
for h in h_values_tested:
|
|
175
|
+
ratio = h / J if J > 0 else 0
|
|
176
|
+
if ratio < 0.8 * h_c_ratio:
|
|
177
|
+
regimes_seen.add("ordered")
|
|
178
|
+
elif ratio > 1.2 * h_c_ratio:
|
|
179
|
+
regimes_seen.add("disordered")
|
|
180
|
+
else:
|
|
181
|
+
regimes_seen.add("critical")
|
|
182
|
+
|
|
183
|
+
if len(regimes_seen) == 1:
|
|
184
|
+
regime = list(regimes_seen)[0]
|
|
185
|
+
return GovernanceWarning(
|
|
186
|
+
id="REGIME_SINGLE",
|
|
187
|
+
severity=Severity.WARNING,
|
|
188
|
+
message=f"Only tested in {regime} regime (h/J = "
|
|
189
|
+
f"{min(h_values_tested)/J:.1f}–{max(h_values_tested)/J:.1f}, "
|
|
190
|
+
f"h_c/J ≈ {h_c_ratio:.1f}). Generalisation unvalidated.",
|
|
191
|
+
metadata={"regimes": list(regimes_seen), "h_values": h_values_tested},
|
|
192
|
+
)
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def check_perturbation_direction(directions_tested: list[str]) -> GovernanceWarning | None:
|
|
197
|
+
"""Check if constitutive law tested with multiple perturbation directions."""
|
|
198
|
+
if len(directions_tested) <= 1:
|
|
199
|
+
tested = directions_tested[0] if directions_tested else "none"
|
|
200
|
+
return GovernanceWarning(
|
|
201
|
+
id="PERTURBATION_SINGLE",
|
|
202
|
+
severity=Severity.WARNING,
|
|
203
|
+
message=f"Constitutive law tested with {tested}-perturbation only. "
|
|
204
|
+
f"J-perturbation not validated (EXP-080 registered).",
|
|
205
|
+
metadata={"directions": directions_tested},
|
|
206
|
+
)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def check_gap(gap_measured: bool, eigsh_k_used: int = 1) -> GovernanceWarning | None:
|
|
211
|
+
"""Check if energy gap was measured."""
|
|
212
|
+
if not gap_measured and eigsh_k_used <= 1:
|
|
213
|
+
return GovernanceWarning(
|
|
214
|
+
id="GAP_UNKNOWN",
|
|
215
|
+
severity=Severity.INFO,
|
|
216
|
+
message="Energy gap not measured. Auto-fill: compute eigsh k=2 "
|
|
217
|
+
"to get gap Δ (one extra eigenvalue, minimal cost).",
|
|
218
|
+
metadata={"eigsh_k": eigsh_k_used, "can_auto_fill": True},
|
|
219
|
+
)
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def check_system_sizes(L_values_tested: list[int]) -> GovernanceWarning | None:
|
|
224
|
+
"""Check if result is at single system size."""
|
|
225
|
+
if len(L_values_tested) <= 1:
|
|
226
|
+
return GovernanceWarning(
|
|
227
|
+
id="SINGLE_L",
|
|
228
|
+
severity=Severity.WARNING,
|
|
229
|
+
message=f"Result at L={L_values_tested[0] if L_values_tested else '?'} only. "
|
|
230
|
+
f"Finite-size effects not characterised.",
|
|
231
|
+
metadata={"L_values": L_values_tested},
|
|
232
|
+
)
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def check_observable_proxy(observable: str, coupling_structure: str) -> GovernanceWarning | None:
|
|
237
|
+
"""Check if using magnetisation on inhomogeneous system."""
|
|
238
|
+
if observable.lower() in ("x", "magnetisation", "magnetization", "spin") and \
|
|
239
|
+
coupling_structure.lower() in ("inhomogeneous", "cracked", "crack", "two-region"):
|
|
240
|
+
return GovernanceWarning(
|
|
241
|
+
id="OBSERVABLE_PROXY",
|
|
242
|
+
severity=Severity.CRITICAL,
|
|
243
|
+
message="Magnetisation observable on inhomogeneous lattice. "
|
|
244
|
+
"Energy channel (local Hamiltonian density) is the "
|
|
245
|
+
"governing observable for cracked dynamics (Track 2, 2026-04-10).",
|
|
246
|
+
metadata={"observable": observable, "coupling": coupling_structure},
|
|
247
|
+
)
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def check_cpu_fallback(gpu_requested: bool, gpu_available: bool,
|
|
252
|
+
estimated_slowdown: float = 200.0) -> GovernanceWarning | None:
|
|
253
|
+
"""Check for silent CPU fallback."""
|
|
254
|
+
if gpu_requested and not gpu_available:
|
|
255
|
+
return GovernanceWarning(
|
|
256
|
+
id="CPU_FALLBACK",
|
|
257
|
+
severity=Severity.CRITICAL,
|
|
258
|
+
message=f"GPU requested but unavailable. Using CPU fallback. "
|
|
259
|
+
f"Expected {estimated_slowdown:.0f}× slowdown.",
|
|
260
|
+
metadata={"gpu_requested": gpu_requested, "gpu_available": gpu_available,
|
|
261
|
+
"slowdown": estimated_slowdown},
|
|
262
|
+
)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def check_nonlinearity(delta_h_values_tested: list[float]) -> GovernanceWarning | None:
|
|
267
|
+
"""Check if constitutive linearity tested at multiple perturbation strengths."""
|
|
268
|
+
if len(delta_h_values_tested) <= 1:
|
|
269
|
+
dh = delta_h_values_tested[0] if delta_h_values_tested else 0
|
|
270
|
+
return GovernanceWarning(
|
|
271
|
+
id="NONLINEARITY_UNTESTED",
|
|
272
|
+
severity=Severity.INFO,
|
|
273
|
+
message=f"Linearity tested at δh={dh} only. "
|
|
274
|
+
f"Auto-fill: run at δh={dh/2} to verify linearity.",
|
|
275
|
+
metadata={"delta_h_tested": delta_h_values_tested, "can_auto_fill": True},
|
|
276
|
+
)
|
|
277
|
+
return None
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analytical QFI from MPS — zero DMRG loops.
|
|
3
|
+
|
|
4
|
+
Computes the Quantum Fisher Information matrix directly from a single
|
|
5
|
+
MPS ground state using the tangent space method:
|
|
6
|
+
|
|
7
|
+
|∂_s ψ⟩ = -i X_s |ψ⟩ (in MPS form, no dense expansion)
|
|
8
|
+
F_ij = 4 Re[⟨∂_i ψ|∂_j ψ⟩ - ⟨∂_i ψ|ψ⟩⟨ψ|∂_j ψ⟩]
|
|
9
|
+
|
|
10
|
+
Cost: O(N² × χ³) from MPS overlaps
|
|
11
|
+
Compare: finite-difference = O(2N × DMRG_sweeps × χ³)
|
|
12
|
+
|
|
13
|
+
For L=6 (N=36, 23 pruned sites):
|
|
14
|
+
Analytical: ~23² × χ³ contractions = seconds
|
|
15
|
+
Finite-diff: ~46 × 5_sweeps × χ³ = hours (TIMEOUT)
|
|
16
|
+
|
|
17
|
+
Source: refactored from qigv.geometry.qfi_mpo.qfi_from_mps_tangent_space()
|
|
18
|
+
with added governance warnings and optional site selection.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from qig_compute.observable import (
|
|
28
|
+
GovernanceReport,
|
|
29
|
+
GovernanceWarning,
|
|
30
|
+
Severity,
|
|
31
|
+
check_gap,
|
|
32
|
+
check_cpu_fallback,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Optional TeNPy import
|
|
36
|
+
try:
|
|
37
|
+
from tenpy.networks.mps import MPS
|
|
38
|
+
_HAS_TENPY = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
_HAS_TENPY = False
|
|
41
|
+
MPS = None
|
|
42
|
+
|
|
43
|
+
# Optional CuPy import
|
|
44
|
+
try:
|
|
45
|
+
import cupy as cp
|
|
46
|
+
_HAS_CUPY = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
_HAS_CUPY = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def qfi_analytical(
|
|
52
|
+
psi_mps,
|
|
53
|
+
L: int,
|
|
54
|
+
sites: list[int] | None = None,
|
|
55
|
+
use_gpu: bool = True,
|
|
56
|
+
report: GovernanceReport | None = None,
|
|
57
|
+
) -> np.ndarray:
|
|
58
|
+
"""Compute QFI matrix analytically from a single MPS.
|
|
59
|
+
|
|
60
|
+
No finite differences. No additional DMRG solves. Single-pass
|
|
61
|
+
computation from the existing ground state MPS.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
psi_mps: TeNPy MPS ground state
|
|
65
|
+
L: Lattice size (N = L² sites)
|
|
66
|
+
sites: Optional list of site indices to compute. If None, all N sites.
|
|
67
|
+
Use with warp bubble pruning to skip irrelevant sites.
|
|
68
|
+
use_gpu: If True and CuPy available, accelerate overlap contractions.
|
|
69
|
+
report: GovernanceReport to collect warnings. Created if None.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
F: QFI matrix. If sites specified, shape is (len(sites), len(sites))
|
|
73
|
+
with F[i,j] corresponding to sites[i], sites[j].
|
|
74
|
+
"""
|
|
75
|
+
if not _HAS_TENPY:
|
|
76
|
+
raise ImportError("qig-compute analytical QFI requires tenpy: pip install qig-compute[tenpy]")
|
|
77
|
+
|
|
78
|
+
if report is None:
|
|
79
|
+
report = GovernanceReport()
|
|
80
|
+
|
|
81
|
+
N = L * L
|
|
82
|
+
if sites is None:
|
|
83
|
+
sites = list(range(N))
|
|
84
|
+
|
|
85
|
+
n_sites = len(sites)
|
|
86
|
+
F = np.zeros((n_sites, n_sites))
|
|
87
|
+
|
|
88
|
+
t0 = time.time()
|
|
89
|
+
|
|
90
|
+
# GPU governance check
|
|
91
|
+
if use_gpu and not _HAS_CUPY:
|
|
92
|
+
w = check_cpu_fallback(gpu_requested=True, gpu_available=False)
|
|
93
|
+
if w:
|
|
94
|
+
report.add(w)
|
|
95
|
+
use_gpu = False
|
|
96
|
+
|
|
97
|
+
# Step 1: Create derivative MPS for each site
|
|
98
|
+
# |∂_s ψ⟩ = -i X_s |ψ⟩
|
|
99
|
+
dpsi_list = []
|
|
100
|
+
for idx, s in enumerate(sites):
|
|
101
|
+
dpsi = psi_mps.copy()
|
|
102
|
+
dpsi.apply_local_op(s, "Sigmax", unitary=False)
|
|
103
|
+
# Scale by -i
|
|
104
|
+
for k in range(dpsi.L):
|
|
105
|
+
dpsi._B[k] = -1j * dpsi._B[k]
|
|
106
|
+
dpsi_list.append(dpsi)
|
|
107
|
+
|
|
108
|
+
if idx % 10 == 0 and idx > 0:
|
|
109
|
+
print(f" [QFI-analytical] Derivative MPS {idx}/{n_sites} "
|
|
110
|
+
f"({time.time()-t0:.1f}s)", flush=True)
|
|
111
|
+
|
|
112
|
+
t_deriv = time.time() - t0
|
|
113
|
+
print(f" [QFI-analytical] {n_sites} derivative MPS computed ({t_deriv:.1f}s)", flush=True)
|
|
114
|
+
|
|
115
|
+
# Step 2: Compute projections ⟨∂_i ψ|ψ⟩ for all sites
|
|
116
|
+
projs = []
|
|
117
|
+
for dpsi in dpsi_list:
|
|
118
|
+
projs.append(dpsi.overlap(psi_mps))
|
|
119
|
+
|
|
120
|
+
t_proj = time.time() - t0
|
|
121
|
+
print(f" [QFI-analytical] Projections computed ({t_proj:.1f}s)", flush=True)
|
|
122
|
+
|
|
123
|
+
# Step 3: Compute pairwise overlaps and build F
|
|
124
|
+
total_overlaps = n_sites * (n_sites + 1) // 2
|
|
125
|
+
count = 0
|
|
126
|
+
|
|
127
|
+
for i in range(n_sites):
|
|
128
|
+
for j in range(i, n_sites):
|
|
129
|
+
inner_ij = dpsi_list[i].overlap(dpsi_list[j])
|
|
130
|
+
term = inner_ij - projs[i] * np.conj(projs[j])
|
|
131
|
+
F[i, j] = 4.0 * np.real(term)
|
|
132
|
+
F[j, i] = F[i, j]
|
|
133
|
+
count += 1
|
|
134
|
+
|
|
135
|
+
if count % 200 == 0:
|
|
136
|
+
print(f" [QFI-analytical] Overlaps {count}/{total_overlaps} "
|
|
137
|
+
f"({time.time()-t0:.1f}s)", flush=True)
|
|
138
|
+
|
|
139
|
+
total_time = time.time() - t0
|
|
140
|
+
print(f" [QFI-analytical] Complete: {total_overlaps} overlaps in {total_time:.1f}s", flush=True)
|
|
141
|
+
|
|
142
|
+
# Gap governance: flag if gap not measured
|
|
143
|
+
gap_warning = check_gap(gap_measured=False, eigsh_k_used=1)
|
|
144
|
+
if gap_warning:
|
|
145
|
+
report.add(gap_warning)
|
|
146
|
+
|
|
147
|
+
return F
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def qfi_metric_from_analytical(
|
|
151
|
+
F: np.ndarray,
|
|
152
|
+
L: int,
|
|
153
|
+
sites: list[int] | None = None,
|
|
154
|
+
) -> np.ndarray:
|
|
155
|
+
"""Extract metric tensor g[i,j,μ,ν] from QFI matrix.
|
|
156
|
+
|
|
157
|
+
The QFI matrix F relates to the metric tensor on the L×L lattice.
|
|
158
|
+
For a 2D system with coordinates (row, col), the metric at each
|
|
159
|
+
site is a 2×2 matrix derived from the QFI elements connecting
|
|
160
|
+
that site to its neighbours.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
F: QFI matrix from qfi_analytical()
|
|
164
|
+
L: Lattice size
|
|
165
|
+
sites: Site list used when computing F (for index mapping)
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
g: Metric tensor array of shape (L, L, 2, 2)
|
|
169
|
+
"""
|
|
170
|
+
N = L * L
|
|
171
|
+
if sites is None:
|
|
172
|
+
sites = list(range(N))
|
|
173
|
+
|
|
174
|
+
# Create site index mapping
|
|
175
|
+
site_to_idx = {s: i for i, s in enumerate(sites)}
|
|
176
|
+
|
|
177
|
+
g = np.zeros((L, L, 2, 2))
|
|
178
|
+
|
|
179
|
+
for site_idx, s in enumerate(sites):
|
|
180
|
+
r, c = s // L, s % L
|
|
181
|
+
|
|
182
|
+
# Diagonal: F_ss gives the local Fisher information
|
|
183
|
+
g[r, c, 0, 0] = F[site_idx, site_idx]
|
|
184
|
+
g[r, c, 1, 1] = F[site_idx, site_idx]
|
|
185
|
+
|
|
186
|
+
# Off-diagonal: F connecting to neighbours gives cross-terms
|
|
187
|
+
# Right neighbour
|
|
188
|
+
right = r * L + (c + 1) % L
|
|
189
|
+
if right in site_to_idx:
|
|
190
|
+
j = site_to_idx[right]
|
|
191
|
+
g[r, c, 0, 1] = F[site_idx, j]
|
|
192
|
+
g[r, c, 1, 0] = F[site_idx, j]
|
|
193
|
+
|
|
194
|
+
return g
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def qfi_with_governance(
|
|
198
|
+
psi_mps,
|
|
199
|
+
L: int,
|
|
200
|
+
sites: list[int] | None = None,
|
|
201
|
+
h: float | None = None,
|
|
202
|
+
J: float = 1.0,
|
|
203
|
+
delta_h: float | None = None,
|
|
204
|
+
coupling_structure: str = "uniform",
|
|
205
|
+
observable: str = "energy",
|
|
206
|
+
use_gpu: bool = True,
|
|
207
|
+
) -> tuple[np.ndarray, GovernanceReport]:
|
|
208
|
+
"""Compute QFI with full governance checks and auto-fills.
|
|
209
|
+
|
|
210
|
+
This is the recommended entry point. Runs all applicable governance
|
|
211
|
+
checks and auto-fills cheap missing measurements.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
psi_mps: TeNPy MPS ground state
|
|
215
|
+
L: Lattice size
|
|
216
|
+
sites: Optional site indices (from bubble pruning)
|
|
217
|
+
h: Field strength (for regime check)
|
|
218
|
+
J: Coupling (for regime check)
|
|
219
|
+
delta_h: Perturbation used (for nonlinearity check)
|
|
220
|
+
coupling_structure: "uniform" or "inhomogeneous"/"cracked"
|
|
221
|
+
observable: What's being measured ("energy" or "magnetisation")
|
|
222
|
+
use_gpu: Try GPU acceleration
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
(F, report): QFI matrix and governance report
|
|
226
|
+
"""
|
|
227
|
+
from qig_compute.observable import (
|
|
228
|
+
check_regime_coverage,
|
|
229
|
+
check_perturbation_direction,
|
|
230
|
+
check_observable_proxy,
|
|
231
|
+
check_nonlinearity,
|
|
232
|
+
check_system_sizes,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
report = GovernanceReport()
|
|
236
|
+
|
|
237
|
+
# Pre-computation governance checks
|
|
238
|
+
if h is not None:
|
|
239
|
+
w = check_regime_coverage([h], J=J)
|
|
240
|
+
if w:
|
|
241
|
+
report.add(w)
|
|
242
|
+
|
|
243
|
+
w = check_perturbation_direction(["h"] if delta_h else [])
|
|
244
|
+
if w:
|
|
245
|
+
report.add(w)
|
|
246
|
+
|
|
247
|
+
w = check_observable_proxy(observable, coupling_structure)
|
|
248
|
+
if w:
|
|
249
|
+
report.add(w)
|
|
250
|
+
|
|
251
|
+
if delta_h is not None:
|
|
252
|
+
w = check_nonlinearity([delta_h])
|
|
253
|
+
if w:
|
|
254
|
+
report.add(w)
|
|
255
|
+
|
|
256
|
+
w = check_system_sizes([L])
|
|
257
|
+
if w:
|
|
258
|
+
report.add(w)
|
|
259
|
+
|
|
260
|
+
# Compute QFI
|
|
261
|
+
F = qfi_analytical(psi_mps, L, sites=sites, use_gpu=use_gpu, report=report)
|
|
262
|
+
|
|
263
|
+
return F, report
|