qcoder 0.1.0a0__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.
- qcoder/__init__.py +3 -0
- qcoder/__main__.py +6 -0
- qcoder/cli.py +116 -0
- qcoder/core/__init__.py +1 -0
- qcoder/core/context.py +16 -0
- qcoder/core/qasm2/__init__.py +1 -0
- qcoder/core/qasm2/adjoint_eligibility.py +128 -0
- qcoder/core/qasm2/mirror_build.py +234 -0
- qcoder/core/run_config.py +84 -0
- qcoder/core/schema.py +26 -0
- qcoder/engines/feature_extraction/adapters/__init__.py +1 -0
- qcoder/engines/feature_extraction/adapters/qiskit_intake.py +46 -0
- qcoder/engines/feature_extraction/extractor.py +43 -0
- qcoder/engines/feature_extraction/features/compute_v0.py +157 -0
- qcoder/engines/feature_extraction/features/schema_v0.py +84 -0
- qcoder/engines/feature_extraction/ir.py +41 -0
- qcoder/engines/feature_extraction/labeling.py +68 -0
- qcoder/engines/feature_extraction/parsers/__init__.py +21 -0
- qcoder/engines/feature_extraction/qasm2_regex_parser.py +184 -0
- qcoder/engines/feature_extraction/reps/cut_profile.py +106 -0
- qcoder/engines/feature_extraction/reps/depth.py +47 -0
- qcoder/engines/feature_extraction/reps/entangling_layers.py +57 -0
- qcoder/engines/feature_extraction/reps/gate_set_stats.py +82 -0
- qcoder/engines/feature_extraction/reps/interaction_graph.py +30 -0
- qcoder/engines/feature_extraction/reps/interaction_graph_metrics.py +113 -0
- qcoder/engines/feature_extraction/reps/spans.py +89 -0
- qcoder/engines/prediction_model/__init__.py +16 -0
- qcoder/engines/prediction_model/artifact.py +85 -0
- qcoder/engines/prediction_model/engine.py +209 -0
- qcoder/engines/prediction_model/models.py +62 -0
- qcoder/engines/prediction_model/policy.py +45 -0
- qcoder/engines/prediction_model/schema_alignment.py +41 -0
- qcoder/engines/quantumness/__init__.py +8 -0
- qcoder/engines/quantumness/scorer.py +254 -0
- qcoder/pipelines/analyze.py +131 -0
- qcoder/pipelines/batch.py +56 -0
- qcoder/tools/analyze.py +88 -0
- qcoder/tools/analyze_shot_scaling.py +239 -0
- qcoder/tools/batch.py +39 -0
- qcoder/tools/generate_corpus.py +491 -0
- qcoder/tools/harness.py +15 -0
- qcoder/tools/inspect_corpus_features.py +273 -0
- qcoder/tools/join_runs_features.py +252 -0
- qcoder/tools/mirror.py +15 -0
- qcoder/tools/predict_baseline.py +347 -0
- qcoder/tools/qr_dll_bootstrap.py +31 -0
- qcoder/tools/runner.py +15 -0
- qcoder/tools/runners/__init__.py +1 -0
- qcoder/tools/runners/quantum_rings/__init__.py +1 -0
- qcoder/tools/runners/quantum_rings/v12/__init__.py +1 -0
- qcoder/tools/runners/quantum_rings/v12/harness.py +1350 -0
- qcoder/tools/runners/quantum_rings/v12/mirror.py +459 -0
- qcoder/tools/runners/quantum_rings/v12/runner.py +549 -0
- qcoder/tools/train_baseline_models.py +619 -0
- qcoder/tools/validate_baseline.py +307 -0
- qcoder-0.1.0a0.dist-info/METADATA +86 -0
- qcoder-0.1.0a0.dist-info/RECORD +62 -0
- qcoder-0.1.0a0.dist-info/WHEEL +5 -0
- qcoder-0.1.0a0.dist-info/entry_points.txt +2 -0
- qcoder-0.1.0a0.dist-info/licenses/LICENSE +201 -0
- qcoder-0.1.0a0.dist-info/licenses/NOTICE +11 -0
- qcoder-0.1.0a0.dist-info/top_level.txt +1 -0
qcoder/__init__.py
ADDED
qcoder/__main__.py
ADDED
qcoder/cli.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from qcoder.pipelines.analyze import analyze_qasm
|
|
8
|
+
from qcoder.tools.batch import analyze_qasm_dir_to_jsonl
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _cmd_analyze(argv: list[str]) -> int:
|
|
12
|
+
p = argparse.ArgumentParser(prog="qcoder analyze", add_help=True)
|
|
13
|
+
p.add_argument("qasm", help="Path to a .qasm file")
|
|
14
|
+
|
|
15
|
+
# Circuit identity / metadata
|
|
16
|
+
p.add_argument("--id", dest="circuit_id", default=None, help="Optional circuit id")
|
|
17
|
+
p.add_argument("--name", dest="circuit_name", default=None, help="Optional circuit name")
|
|
18
|
+
|
|
19
|
+
# Run config (conditioning vars for predictors; not used by feature extraction)
|
|
20
|
+
p.add_argument(
|
|
21
|
+
"--processor",
|
|
22
|
+
"--backend",
|
|
23
|
+
dest="processor",
|
|
24
|
+
default="CPU",
|
|
25
|
+
help='Processor/backend label (aliases: Scarlet/Amber, CPU/GPU)',
|
|
26
|
+
)
|
|
27
|
+
p.add_argument("--precision", default="single", help="Precision: single|double (aliases: fp32/fp64)")
|
|
28
|
+
p.add_argument("--threshold", type=float, default=None, help="Optional threshold/bond-dim conditioning value")
|
|
29
|
+
p.add_argument("--mirror-artifacts-dir", default=None, metavar="DIR", help="If set, write mirror QASM to DIR and add adjoint_supported/adjoint_reason/mirror_qasm_ref to output")
|
|
30
|
+
|
|
31
|
+
p.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
|
|
32
|
+
args = p.parse_args(argv)
|
|
33
|
+
|
|
34
|
+
report = analyze_qasm(
|
|
35
|
+
args.qasm,
|
|
36
|
+
circuit_id=args.circuit_id,
|
|
37
|
+
circuit_name=args.circuit_name,
|
|
38
|
+
processor=args.processor,
|
|
39
|
+
precision=args.precision,
|
|
40
|
+
threshold=args.threshold,
|
|
41
|
+
mirror_artifacts_dir=args.mirror_artifacts_dir or None,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if args.json:
|
|
45
|
+
print(json.dumps(report.to_json_dict(), indent=2, sort_keys=True))
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
ex = report.example
|
|
49
|
+
rc = report.run_config
|
|
50
|
+
print(f"file: {ex.qasm_path}")
|
|
51
|
+
print(f"format: {ex.ir.source_format}")
|
|
52
|
+
if ex.name:
|
|
53
|
+
print(f"name: {ex.name}")
|
|
54
|
+
print(f"function_hint: {ex.function_hint} ({ex.function_source})")
|
|
55
|
+
print(f"processor: {rc.processor} backend: {rc.backend} precision: {rc.precision} threshold: {rc.threshold}")
|
|
56
|
+
print(f"n_qubits: {ex.ir.n_qubits}")
|
|
57
|
+
print(f"n_ops: {ex.ir.n_ops}")
|
|
58
|
+
fv = ex.global_features
|
|
59
|
+
print(f"schema: {fv.schema_version}")
|
|
60
|
+
print(f"n_features: {len(fv.features)}")
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cmd_batch(argv: list[str]) -> int:
|
|
65
|
+
p = argparse.ArgumentParser(prog="qcoder batch", add_help=True)
|
|
66
|
+
p.add_argument("circuits_dir", help="Directory containing QASM files")
|
|
67
|
+
p.add_argument("--out", required=True, help="Output JSONL path")
|
|
68
|
+
p.set_defaults(recursive=True)
|
|
69
|
+
p.add_argument("--recursive", dest="recursive", action="store_true", help="Discover files recursively (default)")
|
|
70
|
+
p.add_argument("--non-recursive", dest="recursive", action="store_false", help="Only discover top-level files")
|
|
71
|
+
p.add_argument("--pattern", default="*.qasm", help="Glob pattern for files (default: *.qasm)")
|
|
72
|
+
p.add_argument("--skip-errors", action="store_true", help="Continue on error, emit error records (default: fail-fast)")
|
|
73
|
+
p.add_argument("--processor", default=None, help="Processor/backend label for run_config")
|
|
74
|
+
p.add_argument("--backend", default=None, help="Backend label (CPU/GPU, etc.)")
|
|
75
|
+
p.add_argument("--precision", default=None, help="Precision: single|double|fp32|fp64")
|
|
76
|
+
p.add_argument("--threshold", type=float, default=None, help="Optional threshold for run_config")
|
|
77
|
+
p.add_argument("--mirror-artifacts-dir", default=None, metavar="DIR", help="If set, write mirror QASM to DIR and add adjoint_supported/adjoint_reason/mirror_qasm_ref to each record")
|
|
78
|
+
args = p.parse_args(argv)
|
|
79
|
+
|
|
80
|
+
n = analyze_qasm_dir_to_jsonl(
|
|
81
|
+
args.circuits_dir,
|
|
82
|
+
args.out,
|
|
83
|
+
processor=args.processor,
|
|
84
|
+
backend=args.backend,
|
|
85
|
+
precision=args.precision,
|
|
86
|
+
threshold=args.threshold,
|
|
87
|
+
recursive=args.recursive,
|
|
88
|
+
pattern=args.pattern,
|
|
89
|
+
fail_fast=not args.skip_errors,
|
|
90
|
+
mirror_artifacts_dir=args.mirror_artifacts_dir or None,
|
|
91
|
+
)
|
|
92
|
+
print(f"Wrote {n} records to {args.out}", file=sys.stderr)
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(argv: list[str] | None = None) -> int:
|
|
97
|
+
if argv is None:
|
|
98
|
+
argv = sys.argv[1:]
|
|
99
|
+
|
|
100
|
+
parser = argparse.ArgumentParser(prog="qcoder")
|
|
101
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
102
|
+
sub.add_parser("analyze", help="Analyze a QASM file (feature extraction + metadata + run config)")
|
|
103
|
+
sub.add_parser("batch", help="Batch extract directory to JSONL")
|
|
104
|
+
ns, rest = parser.parse_known_args(argv)
|
|
105
|
+
|
|
106
|
+
if ns.cmd == "analyze":
|
|
107
|
+
return _cmd_analyze(rest)
|
|
108
|
+
if ns.cmd == "batch":
|
|
109
|
+
return _cmd_batch(rest)
|
|
110
|
+
|
|
111
|
+
parser.error(f"Unknown command: {ns.cmd}")
|
|
112
|
+
return 2
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
raise SystemExit(main())
|
qcoder/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Core shared types and utilities
|
qcoder/core/context.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# Backwards-compatible shim.
|
|
4
|
+
# Prefer: from qcoder.core.run_config import RunConfig
|
|
5
|
+
from qcoder.core.run_config import (
|
|
6
|
+
CPU_ALIASES,
|
|
7
|
+
GPU_ALIASES,
|
|
8
|
+
SINGLE_ALIASES,
|
|
9
|
+
DOUBLE_ALIASES,
|
|
10
|
+
RunConfig,
|
|
11
|
+
normalize_backend,
|
|
12
|
+
normalize_precision,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Older code referred to Context; keep it as an alias.
|
|
16
|
+
Context = RunConfig
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# QASM2 lightweight utilities (e.g. mirror build)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight adjoint/mirror eligibility for OpenQASM 2.
|
|
3
|
+
|
|
4
|
+
Detects unitary eligibility (no measure/reset; conservative) and attempts
|
|
5
|
+
to generate mirror QASM via existing inversion utilities. Does not modify
|
|
6
|
+
the 48-feature vector; for metadata only.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from .mirror_build import UnsupportedQasm, build_mirror_qasm
|
|
16
|
+
|
|
17
|
+
_RE_MEASURE = re.compile(r"^\s*measure\s+", re.I)
|
|
18
|
+
_RE_RESET = re.compile(r"^\s*reset\s+", re.I)
|
|
19
|
+
_RE_IF = re.compile(r"^\s*if\s*\(", re.I)
|
|
20
|
+
_RE_OPENQASM = re.compile(r"^\s*openqasm\s+", re.I)
|
|
21
|
+
_RE_INCLUDE = re.compile(r'^\s*include\s+"[^"]+"\s*;\s*$', re.I)
|
|
22
|
+
_RE_QREG = re.compile(r"^\s*qreg\s+[A-Za-z_]\w*\s*\[\s*\d+\s*\]\s*;\s*$", re.I)
|
|
23
|
+
_RE_CREG = re.compile(r"^\s*creg\s+[A-Za-z_]\w*\s*\[\s*\d+\s*\]\s*;\s*$", re.I)
|
|
24
|
+
_RE_BARRIER = re.compile(r"^\s*barrier\s+", re.I)
|
|
25
|
+
_RE_OP = re.compile(r"^\s*[A-Za-z_]\w*\s*(\([^)]*\))?\s+.+;\s*$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class AdjointEligibility:
|
|
30
|
+
adjoint_supported: bool
|
|
31
|
+
adjoint_reason: str
|
|
32
|
+
mirror_qasm: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
def to_metadata_dict(self, *, include_mirror_qasm: bool = False) -> dict:
|
|
35
|
+
out = {
|
|
36
|
+
"adjoint_supported": self.adjoint_supported,
|
|
37
|
+
"adjoint_reason": self.adjoint_reason,
|
|
38
|
+
}
|
|
39
|
+
if include_mirror_qasm and self.mirror_qasm is not None:
|
|
40
|
+
out["mirror_qasm"] = self.mirror_qasm
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def classify_mirror_eligibility(qasm_text: str) -> tuple[str, str]:
|
|
45
|
+
"""
|
|
46
|
+
Return (classification, reason) where classification is one of:
|
|
47
|
+
- ok
|
|
48
|
+
- non_unitary
|
|
49
|
+
- parse_error
|
|
50
|
+
Policy:
|
|
51
|
+
- allow terminal measurement block
|
|
52
|
+
- non_unitary on reset
|
|
53
|
+
- non_unitary on measurements before terminal block
|
|
54
|
+
- non_unitary on classical conditional execution (if (...))
|
|
55
|
+
"""
|
|
56
|
+
saw_terminal_measure = False
|
|
57
|
+
|
|
58
|
+
for line in qasm_text.splitlines():
|
|
59
|
+
s = line.strip()
|
|
60
|
+
if not s or s.startswith("//"):
|
|
61
|
+
continue
|
|
62
|
+
# Remove trailing inline comments for simple statement checks.
|
|
63
|
+
if "//" in s:
|
|
64
|
+
s = s.split("//", 1)[0].strip()
|
|
65
|
+
if not s:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
if _RE_RESET.match(s):
|
|
69
|
+
return "non_unitary", "circuit contains reset (not unitary)"
|
|
70
|
+
if _RE_IF.match(s):
|
|
71
|
+
return "non_unitary", "circuit contains classical conditional execution"
|
|
72
|
+
if _RE_MEASURE.match(s):
|
|
73
|
+
saw_terminal_measure = True
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Non-measurement statement after measure => mid-circuit measurement.
|
|
77
|
+
if saw_terminal_measure:
|
|
78
|
+
return "non_unitary", "circuit has measurement before terminal measurement block"
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
_RE_OPENQASM.match(s)
|
|
82
|
+
or _RE_INCLUDE.match(s)
|
|
83
|
+
or _RE_QREG.match(s)
|
|
84
|
+
or _RE_CREG.match(s)
|
|
85
|
+
or _RE_BARRIER.match(s)
|
|
86
|
+
or _RE_OP.match(s)
|
|
87
|
+
):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
return "parse_error", f"unrecognized statement: {s[:80]}"
|
|
91
|
+
|
|
92
|
+
return "ok", ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def check_adjoint_eligibility(
|
|
96
|
+
qasm_text: str,
|
|
97
|
+
*,
|
|
98
|
+
drop_barriers: bool = True,
|
|
99
|
+
include_mirror_qasm: bool = True,
|
|
100
|
+
) -> AdjointEligibility:
|
|
101
|
+
"""
|
|
102
|
+
Check whether the circuit is eligible for mirror/adjoint (unitary) and
|
|
103
|
+
attempt to generate mirror QASM.
|
|
104
|
+
|
|
105
|
+
Returns AdjointEligibility with adjoint_supported, adjoint_reason, and
|
|
106
|
+
optionally mirror_qasm text. Unitary eligibility is conservative:
|
|
107
|
+
presence of measure or reset lines makes the circuit non-unitary.
|
|
108
|
+
"""
|
|
109
|
+
cls, reason = classify_mirror_eligibility(qasm_text)
|
|
110
|
+
if cls != "ok":
|
|
111
|
+
return AdjointEligibility(
|
|
112
|
+
adjoint_supported=False,
|
|
113
|
+
adjoint_reason=reason or cls,
|
|
114
|
+
mirror_qasm=None,
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
mirror_qasm, _ = build_mirror_qasm(qasm_text, drop_barriers=drop_barriers)
|
|
118
|
+
return AdjointEligibility(
|
|
119
|
+
adjoint_supported=True,
|
|
120
|
+
adjoint_reason="",
|
|
121
|
+
mirror_qasm=mirror_qasm if include_mirror_qasm else None,
|
|
122
|
+
)
|
|
123
|
+
except UnsupportedQasm as e:
|
|
124
|
+
return AdjointEligibility(
|
|
125
|
+
adjoint_supported=False,
|
|
126
|
+
adjoint_reason=str(e),
|
|
127
|
+
mirror_qasm=None,
|
|
128
|
+
)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build mirror QASM (U then U†) from OpenQASM 2 source for counts-based mirror runs.
|
|
3
|
+
|
|
4
|
+
Tolerates standard OpenQASM 2 include "qelib1.inc"; and preserves it in output.
|
|
5
|
+
Raises UnsupportedQasm for other include statements or gates that cannot be
|
|
6
|
+
inverted in this minimal implementation (no gate definitions, no opaque).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import List, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UnsupportedQasm(Exception):
|
|
16
|
+
"""Raised when the QASM cannot be mirrored (e.g. include, unsupported gate)."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Gate name -> (adjoint_param_negate_mask, self_adjoint)
|
|
21
|
+
# u1(t)† = u1(-t); u2(p,l)† = u2(-l,-p); u3(t,p,l)† = u3(-t,-l,-p)
|
|
22
|
+
# cx, cz, swap, id, x, y, z, h are self-adjoint; s<->sdg, t<->tdg
|
|
23
|
+
_ADJOINT_U1 = (True,) # one param negate
|
|
24
|
+
_ADJOINT_U2 = (True, True) # swap and negate both
|
|
25
|
+
_ADJOINT_U3 = (True, True, True) # negate all three
|
|
26
|
+
_SELF_ADJOINT = ()
|
|
27
|
+
_GATE_ADJOINT: dict[str, tuple] = {
|
|
28
|
+
"u1": _ADJOINT_U1,
|
|
29
|
+
"u2": _ADJOINT_U2,
|
|
30
|
+
"u3": _ADJOINT_U3,
|
|
31
|
+
"u": (True, True, True), # U(theta,phi,lambda) same as u3
|
|
32
|
+
"cu3": _ADJOINT_U3,
|
|
33
|
+
"p": _ADJOINT_U1,
|
|
34
|
+
"cp": _ADJOINT_U1,
|
|
35
|
+
"cu1": _ADJOINT_U1,
|
|
36
|
+
"rx": _ADJOINT_U1,
|
|
37
|
+
"ry": _ADJOINT_U1,
|
|
38
|
+
"rz": _ADJOINT_U1,
|
|
39
|
+
"rxx": _ADJOINT_U1,
|
|
40
|
+
"rzz": _ADJOINT_U1,
|
|
41
|
+
"crx": _ADJOINT_U1,
|
|
42
|
+
"cry": _ADJOINT_U1,
|
|
43
|
+
"crz": _ADJOINT_U1,
|
|
44
|
+
"cx": _SELF_ADJOINT,
|
|
45
|
+
"ch": _SELF_ADJOINT,
|
|
46
|
+
"cy": _SELF_ADJOINT,
|
|
47
|
+
"cz": _SELF_ADJOINT,
|
|
48
|
+
"swap": _SELF_ADJOINT,
|
|
49
|
+
"cswap": _SELF_ADJOINT,
|
|
50
|
+
"ccx": _SELF_ADJOINT,
|
|
51
|
+
"rccx": _SELF_ADJOINT,
|
|
52
|
+
"id": _SELF_ADJOINT,
|
|
53
|
+
"x": _SELF_ADJOINT,
|
|
54
|
+
"y": _SELF_ADJOINT,
|
|
55
|
+
"z": _SELF_ADJOINT,
|
|
56
|
+
"h": _SELF_ADJOINT,
|
|
57
|
+
"s": ("sdg",), # S† = Sdg (name swap, no params)
|
|
58
|
+
"sdg": ("s",),
|
|
59
|
+
"t": ("tdg",),
|
|
60
|
+
"tdg": ("t",),
|
|
61
|
+
"sx": ("sxdg",),
|
|
62
|
+
"sxdg": ("sx",),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_RE_INCLUDE = re.compile(r'^\s*include\s+"([^"]+)"\s*;\s*$', re.I)
|
|
66
|
+
_RE_OPENQASM = re.compile(r'^\s*OPENQASM\s+', re.I)
|
|
67
|
+
_RE_QREG = re.compile(r'^\s*qreg\s+([A-Za-z_]\w*)\s*\[\s*(\d+)\s*\]\s*;\s*$')
|
|
68
|
+
_RE_CREG = re.compile(r'^\s*creg\s+', re.I)
|
|
69
|
+
_RE_BARRIER = re.compile(r'^\s*barrier\s+', re.I)
|
|
70
|
+
_RE_MEASURE = re.compile(r'^\s*measure\s+', re.I)
|
|
71
|
+
# gate line: name ( params )? qubit_operands ;
|
|
72
|
+
_RE_OP = re.compile(r'^\s*([A-Za-z_]\w*)\s*(\([^)]*\))?\s+(.+?)\s*;\s*$')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _parse_float(s: str) -> float:
|
|
76
|
+
s = s.strip()
|
|
77
|
+
try:
|
|
78
|
+
return float(s)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return 0.0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _negate_param(p: str) -> str:
|
|
84
|
+
s = p.strip()
|
|
85
|
+
|
|
86
|
+
def _strip_outer_parens(x: str) -> str:
|
|
87
|
+
x = x.strip()
|
|
88
|
+
while x.startswith("(") and x.endswith(")"):
|
|
89
|
+
depth = 0
|
|
90
|
+
ok = True
|
|
91
|
+
for i, ch in enumerate(x):
|
|
92
|
+
if ch == "(":
|
|
93
|
+
depth += 1
|
|
94
|
+
elif ch == ")":
|
|
95
|
+
depth -= 1
|
|
96
|
+
if depth == 0 and i != len(x) - 1:
|
|
97
|
+
ok = False
|
|
98
|
+
break
|
|
99
|
+
if not ok or depth != 0:
|
|
100
|
+
break
|
|
101
|
+
x = x[1:-1].strip()
|
|
102
|
+
return x
|
|
103
|
+
|
|
104
|
+
def _is_zero_literal(x: str) -> bool:
|
|
105
|
+
try:
|
|
106
|
+
return float(x.strip()) == 0.0
|
|
107
|
+
except Exception:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
v = -float(s)
|
|
112
|
+
return "0" if v == 0.0 else str(v)
|
|
113
|
+
except ValueError:
|
|
114
|
+
core = _strip_outer_parens(s)
|
|
115
|
+
# Simplify double negation: -(-x) -> x
|
|
116
|
+
if core.startswith("-"):
|
|
117
|
+
pos = _strip_outer_parens(core[1:])
|
|
118
|
+
return "0" if _is_zero_literal(pos) else pos
|
|
119
|
+
# Keep symbolic form explicit for non-numeric params.
|
|
120
|
+
return f"-({core})"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _emit_adjoint_gate(name: str, params: List[str], operands: str) -> str:
|
|
124
|
+
key = name.lower()
|
|
125
|
+
adj = _GATE_ADJOINT.get(key)
|
|
126
|
+
if adj is None:
|
|
127
|
+
raise UnsupportedQasm(f"Unsupported gate for mirror: {name}")
|
|
128
|
+
if adj == _SELF_ADJOINT:
|
|
129
|
+
param_str = f"({', '.join(params)})" if params else ""
|
|
130
|
+
return f"{name}{param_str} {operands};"
|
|
131
|
+
if isinstance(adj, tuple) and len(adj) == 1 and isinstance(adj[0], str):
|
|
132
|
+
# name swap only: s->sdg, sdg->s, t->tdg, tdg->t
|
|
133
|
+
adj_name = adj[0]
|
|
134
|
+
param_str = f"({', '.join(params)})" if params else ""
|
|
135
|
+
return f"{adj_name}{param_str} {operands};"
|
|
136
|
+
# negate params: adj is (bool,) for each param to negate
|
|
137
|
+
out_params: List[str] = []
|
|
138
|
+
for i, p in enumerate(params):
|
|
139
|
+
if i < len(adj) and adj[i]:
|
|
140
|
+
out_params.append(_negate_param(p))
|
|
141
|
+
else:
|
|
142
|
+
out_params.append(p)
|
|
143
|
+
if key == "u2":
|
|
144
|
+
# u2(phi, lambda)† = u2(-lambda, -phi): swap and negate
|
|
145
|
+
if len(out_params) >= 2:
|
|
146
|
+
out_params[0], out_params[1] = out_params[1], out_params[0]
|
|
147
|
+
if key == "cu3":
|
|
148
|
+
# cu3(theta, phi, lambda)† = cu3(-theta, -lambda, -phi)
|
|
149
|
+
if len(out_params) >= 3:
|
|
150
|
+
out_params[1], out_params[2] = out_params[2], out_params[1]
|
|
151
|
+
param_str = f"({', '.join(out_params)})" if out_params else ""
|
|
152
|
+
return f"{name}{param_str} {operands};"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def build_mirror_qasm(orig_text: str, drop_barriers: bool = True) -> Tuple[str, Optional[int]]:
|
|
156
|
+
"""
|
|
157
|
+
Build mirror circuit QASM (U then U†) from OpenQASM 2 source.
|
|
158
|
+
|
|
159
|
+
Returns (mirror_qasm_string, n_qubits). Drops measure and optionally barrier.
|
|
160
|
+
Raises UnsupportedQasm if non-standard includes or unsupported gates are present.
|
|
161
|
+
"""
|
|
162
|
+
lines = orig_text.splitlines()
|
|
163
|
+
header_lines: List[str] = []
|
|
164
|
+
qregs: List[Tuple[str, int]] = []
|
|
165
|
+
gate_lines: List[Tuple[str, List[str], str]] = [] # (name, params, operands)
|
|
166
|
+
|
|
167
|
+
for raw in lines:
|
|
168
|
+
s = raw.strip()
|
|
169
|
+
if not s or s.startswith("//"):
|
|
170
|
+
continue
|
|
171
|
+
m = _RE_INCLUDE.match(s)
|
|
172
|
+
if m:
|
|
173
|
+
inc = m.group(1).strip().lower()
|
|
174
|
+
if inc != "qelib1.inc":
|
|
175
|
+
raise UnsupportedQasm(f'Unsupported include for mirror: "{m.group(1).strip()}"')
|
|
176
|
+
header_lines.append(s)
|
|
177
|
+
continue
|
|
178
|
+
if _RE_OPENQASM.match(s):
|
|
179
|
+
header_lines.append(s)
|
|
180
|
+
continue
|
|
181
|
+
if _RE_CREG.match(s):
|
|
182
|
+
continue
|
|
183
|
+
if _RE_MEASURE.match(s):
|
|
184
|
+
continue
|
|
185
|
+
if _RE_BARRIER.match(s):
|
|
186
|
+
if not drop_barriers:
|
|
187
|
+
raise UnsupportedQasm("barrier in circuit (use drop_barriers=True)")
|
|
188
|
+
continue
|
|
189
|
+
m = _RE_QREG.match(s)
|
|
190
|
+
if m:
|
|
191
|
+
header_lines.append(s)
|
|
192
|
+
qregs.append((m.group(1), int(m.group(2))))
|
|
193
|
+
continue
|
|
194
|
+
m = _RE_OP.match(s)
|
|
195
|
+
if m:
|
|
196
|
+
name = m.group(1)
|
|
197
|
+
params_raw = (m.group(2) or "").strip()
|
|
198
|
+
operands = (m.group(3) or "").strip()
|
|
199
|
+
params: List[str] = []
|
|
200
|
+
if params_raw.startswith("(") and params_raw.endswith(")"):
|
|
201
|
+
inside = params_raw[1:-1].strip()
|
|
202
|
+
if inside:
|
|
203
|
+
params = [x.strip() for x in inside.split(",")]
|
|
204
|
+
gate_lines.append((name, params, operands))
|
|
205
|
+
continue
|
|
206
|
+
# unknown line (gate def, etc.)
|
|
207
|
+
raise UnsupportedQasm(f"Unsupported line for mirror: {s[:60]}")
|
|
208
|
+
|
|
209
|
+
if not qregs:
|
|
210
|
+
raise UnsupportedQasm("No qreg declaration found")
|
|
211
|
+
total_width = sum(sz for _, sz in qregs)
|
|
212
|
+
|
|
213
|
+
# Ensure OPENQASM 2.0 header
|
|
214
|
+
if not any(_RE_OPENQASM.match(h) for h in header_lines):
|
|
215
|
+
header_lines.insert(0, "OPENQASM 2.0;")
|
|
216
|
+
|
|
217
|
+
out: List[str] = []
|
|
218
|
+
out.extend(header_lines)
|
|
219
|
+
# U
|
|
220
|
+
for name, params, operands in gate_lines:
|
|
221
|
+
param_str = f"({', '.join(params)})" if params else ""
|
|
222
|
+
out.append(f"{name}{param_str} {operands};")
|
|
223
|
+
# U† (reversed, adjoint)
|
|
224
|
+
for name, params, operands in reversed(gate_lines):
|
|
225
|
+
out.append(_emit_adjoint_gate(name, params, operands))
|
|
226
|
+
# Final measurements for counts-based mirror mode.
|
|
227
|
+
out.append(f"creg c[{total_width}];")
|
|
228
|
+
cidx = 0
|
|
229
|
+
for qreg_name, qreg_size in qregs:
|
|
230
|
+
for i in range(int(qreg_size)):
|
|
231
|
+
out.append(f"measure {qreg_name}[{i}] -> c[{cidx}];")
|
|
232
|
+
cidx += 1
|
|
233
|
+
|
|
234
|
+
return "\n".join(out) + "\n", total_width
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
CPU_ALIASES = {"cpu", "scarlet"}
|
|
6
|
+
GPU_ALIASES = {"gpu", "amber"}
|
|
7
|
+
|
|
8
|
+
SINGLE_ALIASES = {"single", "fp32", "float32"}
|
|
9
|
+
DOUBLE_ALIASES = {"double", "fp64", "float64"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_backend(x: str | None) -> str:
|
|
13
|
+
s = (x or "").strip().lower()
|
|
14
|
+
if s in CPU_ALIASES:
|
|
15
|
+
return "CPU"
|
|
16
|
+
if s in GPU_ALIASES:
|
|
17
|
+
return "GPU"
|
|
18
|
+
return "CPU"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_precision(x: str | None) -> str:
|
|
22
|
+
s = (x or "").strip().lower()
|
|
23
|
+
if s in SINGLE_ALIASES:
|
|
24
|
+
return "single"
|
|
25
|
+
if s in DOUBLE_ALIASES:
|
|
26
|
+
return "double"
|
|
27
|
+
return "single"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class RunConfig:
|
|
32
|
+
"""
|
|
33
|
+
Conditioning variables for runtime/fidelity predictors.
|
|
34
|
+
|
|
35
|
+
These are NOT circuit-derived features.
|
|
36
|
+
"""
|
|
37
|
+
processor: str | None # raw label like "Amber", "Scarlet", "CPU", "GPU"
|
|
38
|
+
backend: str # normalized: "CPU" | "GPU"
|
|
39
|
+
precision: str # normalized: "single" | "double"
|
|
40
|
+
threshold: float | None = None # e.g. bond-dimension/threshold conditioning
|
|
41
|
+
prediction_artifact_path: str | None = None
|
|
42
|
+
prediction_allow_schema_mismatch: bool = False
|
|
43
|
+
prediction_fidelity_target: float | None = None
|
|
44
|
+
prediction_fidelity_metric: str | None = None
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def from_raw(
|
|
48
|
+
*,
|
|
49
|
+
processor: str | None = None,
|
|
50
|
+
backend: str | None = None,
|
|
51
|
+
precision: str | None = None,
|
|
52
|
+
threshold: float | None = None,
|
|
53
|
+
prediction_artifact_path: str | None = None,
|
|
54
|
+
prediction_allow_schema_mismatch: bool | None = None,
|
|
55
|
+
prediction_fidelity_target: float | None = None,
|
|
56
|
+
prediction_fidelity_metric: str | None = None,
|
|
57
|
+
) -> "RunConfig":
|
|
58
|
+
# accept processor OR backend as the same input channel
|
|
59
|
+
raw = processor if processor is not None else backend
|
|
60
|
+
proc = (raw or "").strip()
|
|
61
|
+
proc = proc if proc else None
|
|
62
|
+
|
|
63
|
+
return RunConfig(
|
|
64
|
+
processor=proc,
|
|
65
|
+
backend=normalize_backend(raw or backend),
|
|
66
|
+
precision=normalize_precision(precision),
|
|
67
|
+
threshold=float(threshold) if threshold is not None else None,
|
|
68
|
+
prediction_artifact_path=prediction_artifact_path,
|
|
69
|
+
prediction_allow_schema_mismatch=prediction_allow_schema_mismatch or False,
|
|
70
|
+
prediction_fidelity_target=prediction_fidelity_target,
|
|
71
|
+
prediction_fidelity_metric=prediction_fidelity_metric,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> dict:
|
|
75
|
+
return {
|
|
76
|
+
"processor": self.processor,
|
|
77
|
+
"backend": self.backend,
|
|
78
|
+
"precision": self.precision,
|
|
79
|
+
"threshold": self.threshold,
|
|
80
|
+
"prediction_artifact_path": self.prediction_artifact_path,
|
|
81
|
+
"prediction_allow_schema_mismatch": self.prediction_allow_schema_mismatch,
|
|
82
|
+
"prediction_fidelity_target": self.prediction_fidelity_target,
|
|
83
|
+
"prediction_fidelity_metric": self.prediction_fidelity_metric,
|
|
84
|
+
}
|
qcoder/core/schema.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class FeatureSchema:
|
|
9
|
+
version: str
|
|
10
|
+
feature_names: tuple[str, ...]
|
|
11
|
+
transforms: dict[str, str]
|
|
12
|
+
|
|
13
|
+
def index(self) -> dict[str, int]:
|
|
14
|
+
return {n: i for i, n in enumerate(self.feature_names)}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Feature schema version (append-only updates).
|
|
18
|
+
SCHEMA_VERSION = "0.4.0"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def make_schema(names: Iterable[str], *, transforms: dict[str, str] | None = None) -> FeatureSchema:
|
|
22
|
+
return FeatureSchema(
|
|
23
|
+
version=SCHEMA_VERSION,
|
|
24
|
+
feature_names=tuple(names),
|
|
25
|
+
transforms=dict(transforms or {}),
|
|
26
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional format adapters (e.g. Qiskit). Imports here must remain lazy where noted."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional Qiskit QuantumCircuit intake via OpenQASM 2 export.
|
|
3
|
+
|
|
4
|
+
Qiskit is imported only when functions in this module are called.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..features.compute_v0 import FeatureVector, compute_features_v0
|
|
12
|
+
from ..ir import CircuitIR
|
|
13
|
+
from ..qasm2_regex_parser import parse_qasm2_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_qiskit() -> tuple[type, Any]:
|
|
17
|
+
try:
|
|
18
|
+
from qiskit import QuantumCircuit
|
|
19
|
+
from qiskit.qasm2 import dumps
|
|
20
|
+
except ImportError as e:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"qcoder: Qiskit is not installed. "
|
|
23
|
+
"Install with: pip install qiskit or pip install 'qcoder[qiskit]' "
|
|
24
|
+
"(optional extra)."
|
|
25
|
+
) from e
|
|
26
|
+
return QuantumCircuit, dumps
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def circuit_ir_from_qiskit(qc: Any) -> CircuitIR:
|
|
30
|
+
"""
|
|
31
|
+
Export ``qc`` with ``qiskit.qasm2.dumps`` and parse as OpenQASM 2 text.
|
|
32
|
+
|
|
33
|
+
Raises ImportError when Qiskit is not installed (only at call time).
|
|
34
|
+
Raises TypeError if ``qc`` is not a ``qiskit.QuantumCircuit``.
|
|
35
|
+
"""
|
|
36
|
+
QuantumCircuit, dumps = _load_qiskit()
|
|
37
|
+
if not isinstance(qc, QuantumCircuit):
|
|
38
|
+
raise TypeError(f"expected qiskit.QuantumCircuit, got {type(qc).__name__}")
|
|
39
|
+
qasm_text = dumps(qc)
|
|
40
|
+
return parse_qasm2_text(qasm_text, source_label="qiskit.qasm2.dumps")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_features_from_qiskit_circuit(qc: Any) -> FeatureVector:
|
|
44
|
+
"""``circuit_ir_from_qiskit`` + ``compute_features_v0`` (same schema as file intake)."""
|
|
45
|
+
ir = circuit_ir_from_qiskit(qc)
|
|
46
|
+
return compute_features_v0(ir)
|