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.

Files changed (59) hide show
  1. bilinear_quantum/__init__.py +44 -0
  2. bilinear_quantum/analysis/__init__.py +1 -0
  3. bilinear_quantum/analysis/audit.py +351 -0
  4. bilinear_quantum/backends/__init__.py +5 -0
  5. bilinear_quantum/backends/cirq_backend.py +38 -0
  6. bilinear_quantum/backends/tfq_backend.py +44 -0
  7. bilinear_quantum/circuits/__init__.py +5 -0
  8. bilinear_quantum/circuits/bql_exact.py +56 -0
  9. bilinear_quantum/circuits/canonical.py +86 -0
  10. bilinear_quantum/configs/experiment_registry.yaml +67 -0
  11. bilinear_quantum/core/__init__.py +15 -0
  12. bilinear_quantum/core/bql.py +175 -0
  13. bilinear_quantum/core/factorization.py +129 -0
  14. bilinear_quantum/core/hirota.py +89 -0
  15. bilinear_quantum/data/__init__.py +5 -0
  16. bilinear_quantum/data/fourier.py +147 -0
  17. bilinear_quantum/data/har.py +202 -0
  18. bilinear_quantum/data/kdv.py +148 -0
  19. bilinear_quantum/data/synthetic.py +73 -0
  20. bilinear_quantum/data/wireless.py +52 -0
  21. bilinear_quantum/estimators/__init__.py +15 -0
  22. bilinear_quantum/estimators/parameter_shift.py +122 -0
  23. bilinear_quantum/experiments/__init__.py +1 -0
  24. bilinear_quantum/experiments/cli.py +68 -0
  25. bilinear_quantum/experiments/common.py +153 -0
  26. bilinear_quantum/experiments/e10_dequantization.py +229 -0
  27. bilinear_quantum/experiments/e1_controlled_rank.py +187 -0
  28. bilinear_quantum/experiments/e2_true_hirota.py +191 -0
  29. bilinear_quantum/experiments/e3_kdv_operator.py +221 -0
  30. bilinear_quantum/experiments/e4_har.py +285 -0
  31. bilinear_quantum/experiments/e4_har_quantum.py +238 -0
  32. bilinear_quantum/experiments/e5_order.py +110 -0
  33. bilinear_quantum/experiments/e6_resources.py +104 -0
  34. bilinear_quantum/experiments/e7_shots.py +157 -0
  35. bilinear_quantum/experiments/e8_noise.py +134 -0
  36. bilinear_quantum/experiments/e9_trainability.py +187 -0
  37. bilinear_quantum/experiments/w0_wireless.py +98 -0
  38. bilinear_quantum/gates/__init__.py +1 -0
  39. bilinear_quantum/gates/environment.py +84 -0
  40. bilinear_quantum/gates/runner.py +121 -0
  41. bilinear_quantum/layers/__init__.py +6 -0
  42. bilinear_quantum/layers/bql.py +178 -0
  43. bilinear_quantum/layers/hqbl_core.py +123 -0
  44. bilinear_quantum/models/__init__.py +5 -0
  45. bilinear_quantum/models/bilinear.py +85 -0
  46. bilinear_quantum/observables/__init__.py +5 -0
  47. bilinear_quantum/observables/pauli.py +237 -0
  48. bilinear_quantum/resources/__init__.py +5 -0
  49. bilinear_quantum/resources/circuits.py +44 -0
  50. bilinear_quantum/statistics/__init__.py +5 -0
  51. bilinear_quantum/statistics/inference.py +68 -0
  52. bilinear_quantum/utils/__init__.py +1 -0
  53. bilinear_quantum/utils/artifacts.py +86 -0
  54. bilinear_quantum-1.0.0.dist-info/METADATA +130 -0
  55. bilinear_quantum-1.0.0.dist-info/RECORD +59 -0
  56. bilinear_quantum-1.0.0.dist-info/WHEEL +5 -0
  57. bilinear_quantum-1.0.0.dist-info/entry_points.txt +5 -0
  58. bilinear_quantum-1.0.0.dist-info/licenses/LICENSE +201 -0
  59. 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,5 @@
1
+ """Execution backends for canonical circuits."""
2
+
3
+ from .cirq_backend import bql_exact_cirq, cirq_channel_expectations
4
+
5
+ __all__ = ["bql_exact_cirq", "cirq_channel_expectations"]
@@ -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,5 @@
1
+ """Cirq circuit builders used by exact and trainable quantum backends."""
2
+
3
+ from .bql_exact import build_parallel_bql_circuit, xz_rotation_operations
4
+
5
+ __all__ = ["build_parallel_bql_circuit", "xz_rotation_operations"]
@@ -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
+ }