bilinear-quantum 1.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of bilinear-quantum might be problematic. Click here for more details.
- bilinear_quantum/__init__.py +44 -0
- bilinear_quantum/analysis/__init__.py +1 -0
- bilinear_quantum/analysis/audit.py +351 -0
- bilinear_quantum/backends/__init__.py +5 -0
- bilinear_quantum/backends/cirq_backend.py +38 -0
- bilinear_quantum/backends/tfq_backend.py +44 -0
- bilinear_quantum/circuits/__init__.py +5 -0
- bilinear_quantum/circuits/bql_exact.py +56 -0
- bilinear_quantum/circuits/canonical.py +86 -0
- bilinear_quantum/configs/experiment_registry.yaml +67 -0
- bilinear_quantum/core/__init__.py +15 -0
- bilinear_quantum/core/bql.py +175 -0
- bilinear_quantum/core/factorization.py +129 -0
- bilinear_quantum/core/hirota.py +89 -0
- bilinear_quantum/data/__init__.py +5 -0
- bilinear_quantum/data/fourier.py +147 -0
- bilinear_quantum/data/har.py +202 -0
- bilinear_quantum/data/kdv.py +148 -0
- bilinear_quantum/data/synthetic.py +73 -0
- bilinear_quantum/data/wireless.py +52 -0
- bilinear_quantum/estimators/__init__.py +15 -0
- bilinear_quantum/estimators/parameter_shift.py +122 -0
- bilinear_quantum/experiments/__init__.py +1 -0
- bilinear_quantum/experiments/cli.py +68 -0
- bilinear_quantum/experiments/common.py +153 -0
- bilinear_quantum/experiments/e10_dequantization.py +229 -0
- bilinear_quantum/experiments/e1_controlled_rank.py +187 -0
- bilinear_quantum/experiments/e2_true_hirota.py +191 -0
- bilinear_quantum/experiments/e3_kdv_operator.py +221 -0
- bilinear_quantum/experiments/e4_har.py +285 -0
- bilinear_quantum/experiments/e4_har_quantum.py +238 -0
- bilinear_quantum/experiments/e5_order.py +110 -0
- bilinear_quantum/experiments/e6_resources.py +104 -0
- bilinear_quantum/experiments/e7_shots.py +157 -0
- bilinear_quantum/experiments/e8_noise.py +134 -0
- bilinear_quantum/experiments/e9_trainability.py +187 -0
- bilinear_quantum/experiments/w0_wireless.py +98 -0
- bilinear_quantum/gates/__init__.py +1 -0
- bilinear_quantum/gates/environment.py +84 -0
- bilinear_quantum/gates/runner.py +121 -0
- bilinear_quantum/layers/__init__.py +6 -0
- bilinear_quantum/layers/bql.py +178 -0
- bilinear_quantum/layers/hqbl_core.py +123 -0
- bilinear_quantum/models/__init__.py +5 -0
- bilinear_quantum/models/bilinear.py +85 -0
- bilinear_quantum/observables/__init__.py +5 -0
- bilinear_quantum/observables/pauli.py +237 -0
- bilinear_quantum/resources/__init__.py +5 -0
- bilinear_quantum/resources/circuits.py +44 -0
- bilinear_quantum/statistics/__init__.py +5 -0
- bilinear_quantum/statistics/inference.py +68 -0
- bilinear_quantum/utils/__init__.py +1 -0
- bilinear_quantum/utils/artifacts.py +86 -0
- bilinear_quantum-1.0.0.dist-info/METADATA +130 -0
- bilinear_quantum-1.0.0.dist-info/RECORD +59 -0
- bilinear_quantum-1.0.0.dist-info/WHEEL +5 -0
- bilinear_quantum-1.0.0.dist-info/entry_points.txt +5 -0
- bilinear_quantum-1.0.0.dist-info/licenses/LICENSE +201 -0
- bilinear_quantum-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Bilinear Quantum Learning and Hirota Quantum Bilinear Learning.
|
|
2
|
+
|
|
3
|
+
The package separates exact mathematical reductions from empirical claims.
|
|
4
|
+
Importing the top-level package does not import TensorFlow Quantum; backend
|
|
5
|
+
modules load that relatively expensive dependency only when it is requested.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core.bql import (
|
|
9
|
+
Calibration,
|
|
10
|
+
CalibrationParameters,
|
|
11
|
+
bql_exact_numpy,
|
|
12
|
+
calibration_parameters,
|
|
13
|
+
classical_bilinear,
|
|
14
|
+
)
|
|
15
|
+
from .core.factorization import (
|
|
16
|
+
BilinearFactorization,
|
|
17
|
+
factorize_bilinear_matrix,
|
|
18
|
+
projection_radii,
|
|
19
|
+
truncated_svd_error_bound,
|
|
20
|
+
)
|
|
21
|
+
from .core.hirota import (
|
|
22
|
+
exchange_parity_residual,
|
|
23
|
+
hirota_bilinear,
|
|
24
|
+
hirota_rank_channels,
|
|
25
|
+
required_jet_indices,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"BilinearFactorization",
|
|
30
|
+
"Calibration",
|
|
31
|
+
"CalibrationParameters",
|
|
32
|
+
"bql_exact_numpy",
|
|
33
|
+
"calibration_parameters",
|
|
34
|
+
"classical_bilinear",
|
|
35
|
+
"exchange_parity_residual",
|
|
36
|
+
"factorize_bilinear_matrix",
|
|
37
|
+
"hirota_bilinear",
|
|
38
|
+
"hirota_rank_channels",
|
|
39
|
+
"projection_radii",
|
|
40
|
+
"required_jet_indices",
|
|
41
|
+
"truncated_svd_error_bound",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Post-run statistical and claim-evidence audits."""
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Verify immutable evidence, run paired inference, and adjudicate claims."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from bilinear_quantum.statistics import holm_adjust, paired_comparison
|
|
14
|
+
from bilinear_quantum.utils.artifacts import build_manifest, sha256_file, write_json_atomic
|
|
15
|
+
|
|
16
|
+
EXPERIMENT_IDS = ("E1", "E2", "E3", "E4", "E4Q", "E5", "E6", "E7", "E8", "E9", "E10", "W0")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _read_json(path: Path):
|
|
20
|
+
with path.open(encoding="utf-8") as handle:
|
|
21
|
+
return json.load(handle)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _verify_manifest(directory: Path) -> dict[str, object]:
|
|
25
|
+
manifest_path = directory / "manifest.json"
|
|
26
|
+
if not manifest_path.is_file():
|
|
27
|
+
raise FileNotFoundError(f"missing manifest: {manifest_path}")
|
|
28
|
+
manifest = _read_json(manifest_path)
|
|
29
|
+
checked = []
|
|
30
|
+
for record in manifest.get("files", []):
|
|
31
|
+
relative = Path(record["path"])
|
|
32
|
+
if relative.is_absolute() or ".." in relative.parts:
|
|
33
|
+
raise ValueError(f"unsafe manifest path in {manifest_path}: {relative}")
|
|
34
|
+
path = directory / relative
|
|
35
|
+
if not path.is_file():
|
|
36
|
+
raise FileNotFoundError(f"manifested file is missing: {path}")
|
|
37
|
+
actual_size, actual_hash = path.stat().st_size, sha256_file(path)
|
|
38
|
+
if actual_size != record["bytes"] or actual_hash != record["sha256"]:
|
|
39
|
+
raise ValueError(f"artifact integrity mismatch: {path}")
|
|
40
|
+
checked.append(relative.as_posix())
|
|
41
|
+
if not checked:
|
|
42
|
+
raise ValueError(f"empty evidence manifest: {manifest_path}")
|
|
43
|
+
return {"directory": directory.name, "files_verified": len(checked), "status": "pass"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_experiment(root: Path, experiment: str) -> dict[str, object]:
|
|
47
|
+
directory = root / experiment
|
|
48
|
+
integrity = _verify_manifest(directory)
|
|
49
|
+
paths = {name: directory / name for name in ("results.csv", "run.json", "summary.json")}
|
|
50
|
+
missing = [str(path) for path in paths.values() if not path.is_file()]
|
|
51
|
+
if missing:
|
|
52
|
+
raise FileNotFoundError("missing mandatory result artifacts: " + ", ".join(missing))
|
|
53
|
+
return {
|
|
54
|
+
"frame": pd.read_csv(paths["results.csv"]),
|
|
55
|
+
"run": _read_json(paths["run.json"]),
|
|
56
|
+
"summary": _read_json(paths["summary.json"]),
|
|
57
|
+
"integrity": integrity,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _paired_from_frame(
|
|
62
|
+
frame: pd.DataFrame,
|
|
63
|
+
model_a: str,
|
|
64
|
+
model_b: str,
|
|
65
|
+
metric: str,
|
|
66
|
+
*,
|
|
67
|
+
higher_is_better: bool,
|
|
68
|
+
filters: dict[str, object] | None = None,
|
|
69
|
+
) -> dict[str, object]:
|
|
70
|
+
selected = frame
|
|
71
|
+
for column, value in (filters or {}).items():
|
|
72
|
+
selected = selected[selected[column] == value]
|
|
73
|
+
pivot = selected.pivot_table(index="seed", columns="model", values=metric, aggfunc="mean")
|
|
74
|
+
if model_a not in pivot or model_b not in pivot:
|
|
75
|
+
raise ValueError(f"missing paired models {model_a}/{model_b} for {metric}")
|
|
76
|
+
pivot = pivot[[model_a, model_b]].dropna()
|
|
77
|
+
a, b = pivot[model_a].to_numpy(), pivot[model_b].to_numpy()
|
|
78
|
+
# Positive difference always means model A is better.
|
|
79
|
+
comparison = paired_comparison(a, b) if higher_is_better else paired_comparison(b, a)
|
|
80
|
+
return {
|
|
81
|
+
"model_a": model_a,
|
|
82
|
+
"model_b": model_b,
|
|
83
|
+
"metric": metric,
|
|
84
|
+
"higher_is_better": higher_is_better,
|
|
85
|
+
"filters": filters or {},
|
|
86
|
+
**comparison,
|
|
87
|
+
"model_a_mean": float(np.mean(a)),
|
|
88
|
+
"model_b_mean": float(np.mean(b)),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _superiority_gate(comparison: dict[str, object]) -> bool:
|
|
93
|
+
return bool(
|
|
94
|
+
comparison["mean_difference"] > 0
|
|
95
|
+
and comparison["ci95_low"] > 0
|
|
96
|
+
and comparison["holm_adjusted_p"] < 0.05
|
|
97
|
+
and comparison["fraction_positive"] >= 0.8
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _online_quantum_value_decision(
|
|
102
|
+
*,
|
|
103
|
+
teacher_test_r2: float,
|
|
104
|
+
minimum_teacher_test_r2: float,
|
|
105
|
+
best_modest_fidelity: float,
|
|
106
|
+
fidelity_tolerance: float,
|
|
107
|
+
) -> str:
|
|
108
|
+
"""Adjudicate dequantization only after the teacher is predictively viable."""
|
|
109
|
+
if teacher_test_r2 < minimum_teacher_test_r2:
|
|
110
|
+
return "not_evaluable_teacher_below_predictive_skill_gate"
|
|
111
|
+
if best_modest_fidelity <= fidelity_tolerance:
|
|
112
|
+
return "weak_due_to_dequantization"
|
|
113
|
+
return "survives_tested_surrogates"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run(experiment_root: Path, gate_root: Path, output_directory: Path) -> None:
|
|
117
|
+
experiment_root, gate_root, output_directory = map(
|
|
118
|
+
Path, (experiment_root, gate_root, output_directory)
|
|
119
|
+
)
|
|
120
|
+
gate_integrity = _verify_manifest(gate_root)
|
|
121
|
+
gate_report = _read_json(gate_root / "gate_report.json")
|
|
122
|
+
if gate_report.get("status") != "pass":
|
|
123
|
+
raise RuntimeError("the mandatory mathematical/backend gate did not pass")
|
|
124
|
+
|
|
125
|
+
evidence = {
|
|
126
|
+
identifier: _load_experiment(experiment_root, identifier)
|
|
127
|
+
for identifier in EXPERIMENT_IDS
|
|
128
|
+
}
|
|
129
|
+
commits = {item["run"]["git_commit"] for item in evidence.values()}
|
|
130
|
+
profiles = {item["run"].get("profile") for item in evidence.values()}
|
|
131
|
+
registry_hashes = {item["run"]["registry_sha256"] for item in evidence.values()}
|
|
132
|
+
if profiles != {"full"}:
|
|
133
|
+
raise RuntimeError(f"claim audit accepts full-profile evidence only, found {profiles}")
|
|
134
|
+
if len(commits) != 1 or commits != {gate_report["git_commit"]}:
|
|
135
|
+
raise RuntimeError(
|
|
136
|
+
f"source provenance mismatch: experiments={sorted(commits)}, "
|
|
137
|
+
f"gate={gate_report['git_commit']}"
|
|
138
|
+
)
|
|
139
|
+
if len(registry_hashes) != 1:
|
|
140
|
+
raise RuntimeError(f"experiment registry hashes disagree: {sorted(registry_hashes)}")
|
|
141
|
+
|
|
142
|
+
frames = {identifier: item["frame"] for identifier, item in evidence.items()}
|
|
143
|
+
e2, e3, e4, e4q, e7, e8, e9, e10 = (
|
|
144
|
+
frames[identifier]
|
|
145
|
+
for identifier in ("E2", "E3", "E4", "E4Q", "E7", "E8", "E9", "E10")
|
|
146
|
+
)
|
|
147
|
+
comparisons = [
|
|
148
|
+
_paired_from_frame(
|
|
149
|
+
e2,
|
|
150
|
+
"quantum_hirota_core",
|
|
151
|
+
"ordinary_bilinear",
|
|
152
|
+
"rmse",
|
|
153
|
+
higher_is_better=False,
|
|
154
|
+
filters={"program": "anti_shortcut", "task": "TH", "domain": "iid"},
|
|
155
|
+
),
|
|
156
|
+
_paired_from_frame(
|
|
157
|
+
e2,
|
|
158
|
+
"quantum_hirota_core",
|
|
159
|
+
"free_derivative",
|
|
160
|
+
"rmse",
|
|
161
|
+
higher_is_better=False,
|
|
162
|
+
filters={"program": "anti_shortcut", "task": "TH", "domain": "iid"},
|
|
163
|
+
),
|
|
164
|
+
_paired_from_frame(
|
|
165
|
+
e3,
|
|
166
|
+
"operator_hirota",
|
|
167
|
+
"free_derivative",
|
|
168
|
+
"rmse",
|
|
169
|
+
higher_is_better=False,
|
|
170
|
+
filters={"task": "residual_regression", "domain": "ood_frequency"},
|
|
171
|
+
),
|
|
172
|
+
_paired_from_frame(
|
|
173
|
+
e4,
|
|
174
|
+
"quantum_hirota_core_ps",
|
|
175
|
+
"quantum_bilinear_core",
|
|
176
|
+
"macro_f1",
|
|
177
|
+
higher_is_better=True,
|
|
178
|
+
),
|
|
179
|
+
_paired_from_frame(
|
|
180
|
+
e4,
|
|
181
|
+
"quantum_hirota_core_ps",
|
|
182
|
+
"parameter_matched_mlp",
|
|
183
|
+
"macro_f1",
|
|
184
|
+
higher_is_better=True,
|
|
185
|
+
),
|
|
186
|
+
_paired_from_frame(
|
|
187
|
+
e4q,
|
|
188
|
+
"structured_interact",
|
|
189
|
+
"no_entanglement",
|
|
190
|
+
"macro_f1",
|
|
191
|
+
higher_is_better=True,
|
|
192
|
+
),
|
|
193
|
+
_paired_from_frame(
|
|
194
|
+
e4q,
|
|
195
|
+
"structured_interact",
|
|
196
|
+
"generic_pqc",
|
|
197
|
+
"macro_f1",
|
|
198
|
+
higher_is_better=True,
|
|
199
|
+
),
|
|
200
|
+
]
|
|
201
|
+
adjusted = holm_adjust([comparison["p_value"] for comparison in comparisons])
|
|
202
|
+
for comparison, adjusted_p in zip(comparisons, adjusted, strict=True):
|
|
203
|
+
comparison["holm_adjusted_p"] = adjusted_p
|
|
204
|
+
|
|
205
|
+
e2_th = e2[(e2.program == "anti_shortcut") & (e2.task == "TH") & (e2.domain == "iid")]
|
|
206
|
+
means_e2 = e2_th.groupby("model").rmse.mean()
|
|
207
|
+
e4_means = e4.groupby("model").macro_f1.mean()
|
|
208
|
+
e4q_means = e4q.groupby("model").macro_f1.mean()
|
|
209
|
+
modest = (
|
|
210
|
+
e10[e10.query_budget <= 256]
|
|
211
|
+
.groupby(["surrogate", "query_budget"], as_index=False)
|
|
212
|
+
.surrogate_fidelity_rmse.mean()
|
|
213
|
+
)
|
|
214
|
+
best_modest_row = modest.loc[modest.surrogate_fidelity_rmse.idxmin()]
|
|
215
|
+
best_modest_fidelity = float(best_modest_row.surrogate_fidelity_rmse)
|
|
216
|
+
teacher_scale = float(e10.groupby("seed").teacher_output_sd.first().mean())
|
|
217
|
+
teacher_test_r2 = float(e10.groupby("seed").teacher_test_r2.first().mean())
|
|
218
|
+
e10_summary = evidence["E10"]["summary"]
|
|
219
|
+
minimum_teacher_test_r2 = float(e10_summary["minimum_teacher_test_r2"])
|
|
220
|
+
fidelity_tolerance = max(
|
|
221
|
+
float(e10_summary["maximum_modest_query_fidelity_absolute"]),
|
|
222
|
+
float(e10_summary["maximum_modest_query_fidelity_fraction"]) * teacher_scale,
|
|
223
|
+
)
|
|
224
|
+
teacher_viable = bool(teacher_test_r2 >= minimum_teacher_test_r2)
|
|
225
|
+
symmetric = e8[e8.regime == "symmetric"].mean_parity_defect.mean()
|
|
226
|
+
asymmetric = e8[e8.regime == "asymmetric"].mean_parity_defect.mean()
|
|
227
|
+
c3_ordinary, _, _, c5_bilinear, c5_mlp, c4_local, c4_generic = comparisons
|
|
228
|
+
c3_competitive_free = bool(
|
|
229
|
+
means_e2["quantum_hirota_core"] <= 1.05 * means_e2["free_derivative"]
|
|
230
|
+
)
|
|
231
|
+
c4_supported = _superiority_gate(c4_local) and _superiority_gate(c4_generic)
|
|
232
|
+
c5_supported = _superiority_gate(c5_bilinear)
|
|
233
|
+
summaries = {identifier: item["summary"] for identifier, item in evidence.items()}
|
|
234
|
+
structural_pass = bool(
|
|
235
|
+
summaries["E1"]["oracle_bound_pass"]
|
|
236
|
+
and summaries["E3"]["symbolic_identity_pass"]
|
|
237
|
+
and summaries["E3"]["numeric_identity_pass"]
|
|
238
|
+
and summaries["E4"]["leakage_gate_pass"]
|
|
239
|
+
and summaries["E8"]["clean_parity_gate_pass"]
|
|
240
|
+
)
|
|
241
|
+
claim_decisions = {
|
|
242
|
+
"C1_exact_bql": {
|
|
243
|
+
"decision": "supported",
|
|
244
|
+
"basis": "canonical cross-backend gate passed with zero skips",
|
|
245
|
+
},
|
|
246
|
+
"C2_hirota_structure": {
|
|
247
|
+
"decision": "supported_under_stated_assumptions" if structural_pass else "rejected",
|
|
248
|
+
"basis": "analytic reduction, rank bound, identities, leakage, and clean parity gates",
|
|
249
|
+
},
|
|
250
|
+
"C3_hirota_specificity": {
|
|
251
|
+
"decision": (
|
|
252
|
+
"supported_on_controlled_TH"
|
|
253
|
+
if _superiority_gate(c3_ordinary) and c3_competitive_free
|
|
254
|
+
else "rejected_or_inconclusive"
|
|
255
|
+
),
|
|
256
|
+
"beats_ordinary_gate": _superiority_gate(c3_ordinary),
|
|
257
|
+
"within_five_percent_of_free_derivative": c3_competitive_free,
|
|
258
|
+
"fixed_hirota_rmse": float(means_e2["quantum_hirota_core"]),
|
|
259
|
+
"ordinary_rmse": float(means_e2["ordinary_bilinear"]),
|
|
260
|
+
"free_derivative_rmse": float(means_e2["free_derivative"]),
|
|
261
|
+
},
|
|
262
|
+
"C4_quantum_interaction": {
|
|
263
|
+
"decision": "supported_on_har" if c4_supported else "not_supported_on_har",
|
|
264
|
+
"both_strict_superiority_gates_pass": c4_supported,
|
|
265
|
+
"macro_f1": e4q_means.to_dict(),
|
|
266
|
+
},
|
|
267
|
+
"C5_real_transfer": {
|
|
268
|
+
"decision": "supported_on_har" if c5_supported else "rejected_on_har",
|
|
269
|
+
"strict_superiority_over_ordinary_gate_pass": c5_supported,
|
|
270
|
+
"comparison_to_mlp_is_context_only": _superiority_gate(c5_mlp),
|
|
271
|
+
"macro_f1": {
|
|
272
|
+
"hirota_core": float(e4_means["quantum_hirota_core_ps"]),
|
|
273
|
+
"bilinear_core": float(e4_means["quantum_bilinear_core"]),
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
"C6_online_quantum_value": {
|
|
277
|
+
"decision": _online_quantum_value_decision(
|
|
278
|
+
teacher_test_r2=teacher_test_r2,
|
|
279
|
+
minimum_teacher_test_r2=minimum_teacher_test_r2,
|
|
280
|
+
best_modest_fidelity=best_modest_fidelity,
|
|
281
|
+
fidelity_tolerance=fidelity_tolerance,
|
|
282
|
+
),
|
|
283
|
+
"teacher_viability_gate_pass": teacher_viable,
|
|
284
|
+
"mean_teacher_test_r2": teacher_test_r2,
|
|
285
|
+
"minimum_teacher_test_r2": minimum_teacher_test_r2,
|
|
286
|
+
"best_modest_query_fidelity_rmse": best_modest_fidelity,
|
|
287
|
+
"best_modest_surrogate": str(best_modest_row.surrogate),
|
|
288
|
+
"best_modest_query_budget": int(best_modest_row.query_budget),
|
|
289
|
+
"teacher_output_sd_scale": teacher_scale,
|
|
290
|
+
"fidelity_tolerance": fidelity_tolerance,
|
|
291
|
+
},
|
|
292
|
+
"C7_quantum_advantage": {
|
|
293
|
+
"decision": "not_authorized",
|
|
294
|
+
"basis": "no computational/access-model separation is proved",
|
|
295
|
+
},
|
|
296
|
+
"noise_parity_diagnostic": {
|
|
297
|
+
"symmetric_mean_defect": float(symmetric),
|
|
298
|
+
"asymmetric_mean_defect": float(asymmetric),
|
|
299
|
+
},
|
|
300
|
+
"trainability_scope": {
|
|
301
|
+
"cells": len(e9),
|
|
302
|
+
"decision": "finite_size_characterization_only",
|
|
303
|
+
},
|
|
304
|
+
"shot_scope": {
|
|
305
|
+
"cells": len(e7),
|
|
306
|
+
"decision": "simulated_pauli_sampling_only",
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
provenance = {
|
|
310
|
+
"schema_version": 1,
|
|
311
|
+
"status": "pass",
|
|
312
|
+
"git_commit": next(iter(commits)),
|
|
313
|
+
"registry_sha256": next(iter(registry_hashes)),
|
|
314
|
+
"gate": gate_integrity,
|
|
315
|
+
"experiments": [item["integrity"] for item in evidence.values()],
|
|
316
|
+
"full_profile_only": True,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if output_directory.exists():
|
|
320
|
+
raise FileExistsError(f"immutable audit output already exists: {output_directory}")
|
|
321
|
+
output_directory.parent.mkdir(parents=True, exist_ok=True)
|
|
322
|
+
staging = output_directory.parent / f".{output_directory.name}.staging-{os.getpid()}"
|
|
323
|
+
if staging.exists():
|
|
324
|
+
raise FileExistsError(f"stale audit staging directory requires review: {staging}")
|
|
325
|
+
staging.mkdir()
|
|
326
|
+
csv_path = staging / "paired_comparisons.csv"
|
|
327
|
+
pd.DataFrame(comparisons).to_csv(csv_path, index=False, lineterminator="\n")
|
|
328
|
+
comparison_path = staging / "paired_comparisons.json"
|
|
329
|
+
claim_path = staging / "claim_decisions.json"
|
|
330
|
+
provenance_path = staging / "provenance.json"
|
|
331
|
+
write_json_atomic(comparison_path, comparisons)
|
|
332
|
+
write_json_atomic(claim_path, claim_decisions)
|
|
333
|
+
write_json_atomic(provenance_path, provenance)
|
|
334
|
+
write_json_atomic(
|
|
335
|
+
staging / "manifest.json",
|
|
336
|
+
build_manifest([csv_path, comparison_path, claim_path, provenance_path], staging),
|
|
337
|
+
)
|
|
338
|
+
staging.replace(output_directory)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def main() -> None:
|
|
342
|
+
parser = argparse.ArgumentParser()
|
|
343
|
+
parser.add_argument("--experiments", type=Path, default=Path("artifacts/experiments"))
|
|
344
|
+
parser.add_argument("--gates", type=Path, default=Path("artifacts/gates"))
|
|
345
|
+
parser.add_argument("--output", type=Path, default=Path("artifacts/audit"))
|
|
346
|
+
arguments = parser.parse_args()
|
|
347
|
+
run(arguments.experiments, arguments.gates, arguments.output)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == "__main__":
|
|
351
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Noiseless Cirq backend for exact BQL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
import cirq
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from bilinear_quantum.circuits.bql_exact import build_parallel_bql_circuit
|
|
11
|
+
from bilinear_quantum.core.bql import Calibration, calibration_parameters, encoded_coordinates
|
|
12
|
+
from bilinear_quantum.core.factorization import BilinearFactorization
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cirq_channel_expectations(
|
|
16
|
+
a: np.ndarray,
|
|
17
|
+
b: np.ndarray,
|
|
18
|
+
theta: float | Iterable[float],
|
|
19
|
+
) -> np.ndarray:
|
|
20
|
+
circuit, observables, _, _ = build_parallel_bql_circuit(a, b, theta)
|
|
21
|
+
simulator = cirq.Simulator(dtype=np.complex128)
|
|
22
|
+
values = simulator.simulate_expectation_values(circuit, observables=observables)
|
|
23
|
+
return np.asarray([complex(value).real for value in values], dtype=np.float64)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def bql_exact_cirq(
|
|
27
|
+
f: np.ndarray,
|
|
28
|
+
g: np.ndarray,
|
|
29
|
+
factorization: BilinearFactorization,
|
|
30
|
+
r_f: float,
|
|
31
|
+
r_g: float,
|
|
32
|
+
*,
|
|
33
|
+
calibration: Calibration | str = Calibration.READOUT,
|
|
34
|
+
margin: float = 1.0,
|
|
35
|
+
) -> float:
|
|
36
|
+
a, b = encoded_coordinates(f, g, factorization, r_f, r_g)
|
|
37
|
+
params = calibration_parameters(factorization, r_f, r_g, calibration, margin=margin)
|
|
38
|
+
return float(params.readout @ cirq_channel_expectations(a, b, params.theta))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""TensorFlow Quantum expectation backend for exact BQL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
|
|
8
|
+
# TFQ 0.7.6 is distributed against the legacy Keras API. This must be set
|
|
9
|
+
# before importing TensorFlow in a fresh interpreter.
|
|
10
|
+
os.environ.setdefault("TF_USE_LEGACY_KERAS", "1")
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import tensorflow as tf
|
|
14
|
+
import tensorflow_quantum as tfq
|
|
15
|
+
|
|
16
|
+
from bilinear_quantum.circuits.bql_exact import build_parallel_bql_circuit
|
|
17
|
+
from bilinear_quantum.core.bql import Calibration, calibration_parameters, encoded_coordinates
|
|
18
|
+
from bilinear_quantum.core.factorization import BilinearFactorization
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def tfq_channel_expectations(
|
|
22
|
+
a: np.ndarray,
|
|
23
|
+
b: np.ndarray,
|
|
24
|
+
theta: float | Iterable[float],
|
|
25
|
+
) -> np.ndarray:
|
|
26
|
+
circuit, observables, _, _ = build_parallel_bql_circuit(a, b, theta)
|
|
27
|
+
circuit_tensor = tfq.convert_to_tensor([circuit])
|
|
28
|
+
output = tfq.layers.Expectation()(circuit_tensor, operators=observables)
|
|
29
|
+
return np.asarray(tf.cast(output[0], tf.float64).numpy(), dtype=np.float64)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def bql_exact_tfq(
|
|
33
|
+
f: np.ndarray,
|
|
34
|
+
g: np.ndarray,
|
|
35
|
+
factorization: BilinearFactorization,
|
|
36
|
+
r_f: float,
|
|
37
|
+
r_g: float,
|
|
38
|
+
*,
|
|
39
|
+
calibration: Calibration | str = Calibration.READOUT,
|
|
40
|
+
margin: float = 1.0,
|
|
41
|
+
) -> float:
|
|
42
|
+
a, b = encoded_coordinates(f, g, factorization, r_f, r_g)
|
|
43
|
+
params = calibration_parameters(factorization, r_f, r_g, calibration, margin=margin)
|
|
44
|
+
return float(params.readout @ tfq_channel_expectations(a, b, params.theta))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Cirq construction of the exact rank-parallel BQL channel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
import cirq
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def xz_rotation_operations(
|
|
12
|
+
q_f: cirq.Qid, q_g: cirq.Qid, theta: float
|
|
13
|
+
) -> tuple[cirq.Operation, ...]:
|
|
14
|
+
"""Decompose ``exp(-i theta X_F Z_G / 2)`` into elementary gates."""
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
cirq.H(q_f),
|
|
18
|
+
cirq.CNOT(q_f, q_g),
|
|
19
|
+
cirq.rz(float(theta)).on(q_g),
|
|
20
|
+
cirq.CNOT(q_f, q_g),
|
|
21
|
+
cirq.H(q_f),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_parallel_bql_circuit(
|
|
26
|
+
a: np.ndarray,
|
|
27
|
+
b: np.ndarray,
|
|
28
|
+
theta: float | Iterable[float],
|
|
29
|
+
) -> tuple[cirq.Circuit, list[cirq.PauliString], list[cirq.Qid], list[cirq.Qid]]:
|
|
30
|
+
"""Build one independent two-qubit channel per rank component."""
|
|
31
|
+
|
|
32
|
+
a = np.asarray(a, dtype=np.float64)
|
|
33
|
+
b = np.asarray(b, dtype=np.float64)
|
|
34
|
+
if a.ndim != 1 or b.shape != a.shape:
|
|
35
|
+
raise ValueError("a and b must be equal-length vectors")
|
|
36
|
+
if not np.all(np.isfinite(a)) or not np.all(np.isfinite(b)):
|
|
37
|
+
raise ValueError("coordinates must be finite")
|
|
38
|
+
if np.any(np.abs(a) > 1 + 1e-11) or np.any(np.abs(b) > 1 + 1e-11):
|
|
39
|
+
raise ValueError("all encoded coordinates must lie in [-1, 1]")
|
|
40
|
+
a, b = np.clip(a, -1, 1), np.clip(b, -1, 1)
|
|
41
|
+
theta_array = np.asarray(theta, dtype=np.float64)
|
|
42
|
+
if theta_array.ndim == 0:
|
|
43
|
+
theta_array = np.full(a.size, float(theta_array), dtype=np.float64)
|
|
44
|
+
if theta_array.shape != a.shape or not np.all(np.isfinite(theta_array)):
|
|
45
|
+
raise ValueError("theta must be finite and scalar or one value per channel")
|
|
46
|
+
|
|
47
|
+
f_qubits = [cirq.GridQubit(0, 2 * index) for index in range(a.size)]
|
|
48
|
+
g_qubits = [cirq.GridQubit(0, 2 * index + 1) for index in range(a.size)]
|
|
49
|
+
circuit = cirq.Circuit()
|
|
50
|
+
for q_f, q_g, a_value, b_value in zip(f_qubits, g_qubits, a, b, strict=True):
|
|
51
|
+
circuit.append(cirq.ry(float(np.arccos(a_value))).on(q_f))
|
|
52
|
+
circuit.append(cirq.ry(float(np.arccos(b_value))).on(q_g))
|
|
53
|
+
for q_f, q_g, angle in zip(f_qubits, g_qubits, theta_array, strict=True):
|
|
54
|
+
circuit.append(xz_rotation_operations(q_f, q_g, float(angle)))
|
|
55
|
+
observables = [cirq.PauliString(cirq.Y(q_f)) for q_f in f_qubits]
|
|
56
|
+
return circuit, observables, f_qubits, g_qubits
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Canonical two-register, tied-template, swap-equivariant HQBL circuit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import cirq
|
|
8
|
+
import sympy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class CanonicalCircuit:
|
|
13
|
+
circuit: cirq.Circuit
|
|
14
|
+
f_qubits: tuple[cirq.Qid, ...]
|
|
15
|
+
g_qubits: tuple[cirq.Qid, ...]
|
|
16
|
+
data_symbols: tuple[sympy.Symbol, ...]
|
|
17
|
+
trainable_symbols: tuple[sympy.Symbol, ...]
|
|
18
|
+
mixer_symbols: tuple[sympy.Symbol, ...]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _zz_rotation(q0: cirq.Qid, q1: cirq.Qid, angle) -> cirq.Operation:
|
|
22
|
+
return cirq.ZZPowGate(exponent=angle / sympy.pi).on(q0, q1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_canonical_hqbl_circuit(
|
|
26
|
+
register_size: int,
|
|
27
|
+
*,
|
|
28
|
+
reuploads: int = 2,
|
|
29
|
+
include_mixer: bool = True,
|
|
30
|
+
) -> CanonicalCircuit:
|
|
31
|
+
"""Build the architecture contract with tied local trainable parameters.
|
|
32
|
+
|
|
33
|
+
Branch data symbols are independent so that swapping inputs swaps symbols;
|
|
34
|
+
all trainable local symbols are shared between corresponding branch gates.
|
|
35
|
+
The cross-register ``ZZ`` mixer is invariant under register exchange.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if register_size <= 0 or reuploads <= 0:
|
|
39
|
+
raise ValueError("register_size and reuploads must be positive")
|
|
40
|
+
f_qubits = tuple(cirq.GridQubit(0, index) for index in range(register_size))
|
|
41
|
+
g_qubits = tuple(cirq.GridQubit(1, index) for index in range(register_size))
|
|
42
|
+
circuit = cirq.Circuit()
|
|
43
|
+
data_symbols: list[sympy.Symbol] = []
|
|
44
|
+
trainable_symbols: list[sympy.Symbol] = []
|
|
45
|
+
mixer_symbols: list[sympy.Symbol] = []
|
|
46
|
+
for layer in range(reuploads):
|
|
47
|
+
for index, (q_f, q_g) in enumerate(zip(f_qubits, g_qubits, strict=True)):
|
|
48
|
+
x_f = sympy.Symbol(f"x_f_{layer}_{index}")
|
|
49
|
+
x_g = sympy.Symbol(f"x_g_{layer}_{index}")
|
|
50
|
+
phi_y = sympy.Symbol(f"phi_y_{layer}_{index}")
|
|
51
|
+
phi_z = sympy.Symbol(f"phi_z_{layer}_{index}")
|
|
52
|
+
data_symbols.extend((x_f, x_g))
|
|
53
|
+
trainable_symbols.extend((phi_y, phi_z))
|
|
54
|
+
circuit.append((cirq.rx(x_f)(q_f), cirq.rx(x_g)(q_g)))
|
|
55
|
+
circuit.append((cirq.ry(phi_y)(q_f), cirq.ry(phi_y)(q_g)))
|
|
56
|
+
circuit.append((cirq.rz(phi_z)(q_f), cirq.rz(phi_z)(q_g)))
|
|
57
|
+
if register_size > 1:
|
|
58
|
+
for index in range(register_size - 1):
|
|
59
|
+
tied = sympy.Symbol(f"local_zz_{layer}_{index}")
|
|
60
|
+
trainable_symbols.append(tied)
|
|
61
|
+
circuit.append(
|
|
62
|
+
(
|
|
63
|
+
_zz_rotation(f_qubits[index], f_qubits[index + 1], tied),
|
|
64
|
+
_zz_rotation(g_qubits[index], g_qubits[index + 1], tied),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
if include_mixer:
|
|
68
|
+
for index, (q_f, q_g) in enumerate(zip(f_qubits, g_qubits, strict=True)):
|
|
69
|
+
mixer = sympy.Symbol(f"mixer_zz_{layer}_{index}")
|
|
70
|
+
mixer_symbols.append(mixer)
|
|
71
|
+
circuit.append(_zz_rotation(q_f, q_g, mixer))
|
|
72
|
+
return CanonicalCircuit(
|
|
73
|
+
circuit=circuit,
|
|
74
|
+
f_qubits=f_qubits,
|
|
75
|
+
g_qubits=g_qubits,
|
|
76
|
+
data_symbols=tuple(data_symbols),
|
|
77
|
+
trainable_symbols=tuple(trainable_symbols),
|
|
78
|
+
mixer_symbols=tuple(mixer_symbols),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def swap_qubit_map(canonical: CanonicalCircuit) -> dict[cirq.Qid, cirq.Qid]:
|
|
83
|
+
return {
|
|
84
|
+
**dict(zip(canonical.f_qubits, canonical.g_qubits, strict=True)),
|
|
85
|
+
**dict(zip(canonical.g_qubits, canonical.f_qubits, strict=True)),
|
|
86
|
+
}
|