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