zena-sdk 0.1.4__cp38-abi3-win_amd64.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.
- qsys/__init__.py +31 -0
- qsys/__pycache__/__init__.cpython-313.pyc +0 -0
- qsys/backends/base.py +39 -0
- qsys/backends/local5q.py +8 -0
- qsys/circuit/__init__.py +20 -0
- qsys/circuit/__pycache__/__init__.cpython-313.pyc +0 -0
- qsys/circuit/__pycache__/quantum_circuit.cpython-313.pyc +0 -0
- qsys/circuit/quantum_circuit.py +103 -0
- qsys/cli/__init__.py +2 -0
- qsys/cli/io_cli.py +45 -0
- qsys/errors/__init__.py +32 -0
- qsys/errors/__pycache__/__init__.cpython-313.pyc +0 -0
- qsys/io/__init__.py +21 -0
- qsys/io/json_io.py +84 -0
- qsys/io/text_io.py +58 -0
- qsys/ir/__init__.py +1 -0
- qsys/ir/__pycache__/__init__.cpython-313.pyc +0 -0
- qsys/ir/__pycache__/types.cpython-313.pyc +0 -0
- qsys/ir/from_payload.py +27 -0
- qsys/ir/types.py +9 -0
- qsys/logging.py +14 -0
- qsys/runtime/__init__.py +4 -0
- qsys/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- qsys/runtime/__pycache__/execute.cpython-313.pyc +0 -0
- qsys/runtime/execute.py +155 -0
- qsys/target.py +44 -0
- qsys/targets.py +63 -0
- qsys/transpiler/__init__.py +6 -0
- qsys/transpiler/basis.py +39 -0
- qsys/transpiler/opt1q.py +101 -0
- qsys/transpiler/passes.py +57 -0
- qsys/transpiler/routing.py +136 -0
- qsys/transpiler/validate.py +132 -0
- qsys/viz/__init__.py +1 -0
- qsys/viz/text_drawer.py +89 -0
- simulator_statevector/__init__.py +5 -0
- simulator_statevector/__pycache__/__init__.cpython-313.pyc +0 -0
- simulator_statevector/simulator_statevector.pyd +0 -0
- zena/__init__.py +7 -0
- zena/__pycache__/__init__.cpython-312.pyc +0 -0
- zena/__pycache__/__init__.cpython-313.pyc +0 -0
- zena/__pycache__/execute.cpython-312.pyc +0 -0
- zena/__pycache__/execute.cpython-313.pyc +0 -0
- zena/circuit/__init__.py +2 -0
- zena/circuit/__pycache__/__init__.cpython-312.pyc +0 -0
- zena/circuit/__pycache__/__init__.cpython-313.pyc +0 -0
- zena/circuit/__pycache__/quantum_circuit.cpython-312.pyc +0 -0
- zena/circuit/__pycache__/quantum_circuit.cpython-313.pyc +0 -0
- zena/circuit/__pycache__/register.cpython-312.pyc +0 -0
- zena/circuit/__pycache__/register.cpython-313.pyc +0 -0
- zena/circuit/quantum_circuit.py +218 -0
- zena/circuit/register.py +28 -0
- zena/compiler/__init__.py +72 -0
- zena/compiler/__pycache__/__init__.cpython-312.pyc +0 -0
- zena/compiler/__pycache__/__init__.cpython-313.pyc +0 -0
- zena/dist/zena_sdk-0.1.0-py3-none-any.whl +0 -0
- zena/dist/zena_sdk-0.1.0.tar.gz +0 -0
- zena/execute.py +35 -0
- zena/providers/__init__.py +5 -0
- zena/providers/__pycache__/__init__.cpython-312.pyc +0 -0
- zena/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- zena/providers/__pycache__/aer.cpython-312.pyc +0 -0
- zena/providers/__pycache__/aer.cpython-313.pyc +0 -0
- zena/providers/__pycache__/backend.cpython-312.pyc +0 -0
- zena/providers/__pycache__/backend.cpython-313.pyc +0 -0
- zena/providers/__pycache__/job.cpython-312.pyc +0 -0
- zena/providers/__pycache__/job.cpython-313.pyc +0 -0
- zena/providers/aer.py +71 -0
- zena/providers/backend.py +18 -0
- zena/providers/job.py +24 -0
- zena/visualization/__init__.py +28 -0
- zena/visualization/__pycache__/__init__.cpython-312.pyc +0 -0
- zena/visualization/__pycache__/__init__.cpython-313.pyc +0 -0
- zena_sdk-0.1.4.dist-info/METADATA +70 -0
- zena_sdk-0.1.4.dist-info/RECORD +76 -0
- zena_sdk-0.1.4.dist-info/WHEEL +4 -0
qsys/runtime/execute.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# qsys/runtime/execute.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from qsys.circuit.quantum_circuit import QuantumCircuit
|
|
6
|
+
from qsys.errors import ValidationError
|
|
7
|
+
try:
|
|
8
|
+
from simulator_statevector import QuantumCircuit as SimulatorCircuit
|
|
9
|
+
except ImportError:
|
|
10
|
+
# Fallback or error - for now we assume it's there as per request
|
|
11
|
+
from simulator_statevector import StateVecPy as SimulatorCircuit # This wont work, but prevents import error if structure mismatch.
|
|
12
|
+
# Actually, let's just import it. If it fails, the user needs to compile.
|
|
13
|
+
pass
|
|
14
|
+
from simulator_statevector import QuantumCircuit as SimulatorCircuit
|
|
15
|
+
|
|
16
|
+
# --- helper: try to call the transpiler / pass manager if available --- #
|
|
17
|
+
from importlib import import_module
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _maybe_run_transpiler(qc: QuantumCircuit, backend: Any) -> QuantumCircuit:
|
|
21
|
+
try:
|
|
22
|
+
transpiler = import_module("qsys.transpiler")
|
|
23
|
+
except Exception:
|
|
24
|
+
return qc
|
|
25
|
+
|
|
26
|
+
candidates = ("run_pass_manager", "pass_manager", "apply_passes", "transpile")
|
|
27
|
+
for name in candidates:
|
|
28
|
+
fn = getattr(transpiler, name, None)
|
|
29
|
+
if not callable(fn):
|
|
30
|
+
continue
|
|
31
|
+
try:
|
|
32
|
+
return fn(qc, backend=backend)
|
|
33
|
+
except TypeError:
|
|
34
|
+
pass
|
|
35
|
+
try:
|
|
36
|
+
return fn(qc, backend)
|
|
37
|
+
except TypeError:
|
|
38
|
+
pass
|
|
39
|
+
try:
|
|
40
|
+
return fn(qc)
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
return qc
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _validate_circuit_indices(circuit: QuantumCircuit) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Strict runtime validation: ensure all qubit indices are within [0, n_qubits-1]
|
|
49
|
+
and classical indices are within [0, n_clbits-1].
|
|
50
|
+
Raises ValidationError with helpful message on the first problem found.
|
|
51
|
+
"""
|
|
52
|
+
n_q = circuit.n_qubits
|
|
53
|
+
n_c = circuit.n_clbits
|
|
54
|
+
for instr in circuit.instructions:
|
|
55
|
+
# check qubit indices
|
|
56
|
+
for q in instr.qubits or ():
|
|
57
|
+
if not isinstance(q, int):
|
|
58
|
+
raise ValidationError(f"qubit index must be int, got {type(q)}")
|
|
59
|
+
if q < 0 or q >= n_q:
|
|
60
|
+
raise ValidationError(f"invalid qubit index {q}")
|
|
61
|
+
# check classical bits if present (some instr may have clbits)
|
|
62
|
+
for c in getattr(instr, "clbits", ()) or ():
|
|
63
|
+
if not isinstance(c, int):
|
|
64
|
+
raise ValidationError(f"classical bit index must be int, got {type(c)}")
|
|
65
|
+
if c < 0 or c >= n_c:
|
|
66
|
+
raise ValidationError(f"invalid classical bit index {c}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def execute(
|
|
70
|
+
circuit: QuantumCircuit,
|
|
71
|
+
backend: Any = None,
|
|
72
|
+
shots: int = 1024,
|
|
73
|
+
seed: int | None = None,
|
|
74
|
+
use_transpiler: bool = True,
|
|
75
|
+
) -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Execute `circuit` on the local statevector simulator.
|
|
78
|
+
|
|
79
|
+
- If use_transpiler is True, attempt to run the transpiler/pass manager.
|
|
80
|
+
- Validate indices strictly before running (raises ValidationError).
|
|
81
|
+
- If shots > 0, return sampled counts; if shots == 0 return the statevector.
|
|
82
|
+
"""
|
|
83
|
+
if backend is not None:
|
|
84
|
+
print("Warning: backend ignored – using simulator")
|
|
85
|
+
|
|
86
|
+
# Optionally run the transpiler first (defensive)
|
|
87
|
+
if use_transpiler:
|
|
88
|
+
circuit = _maybe_run_transpiler(circuit, backend)
|
|
89
|
+
|
|
90
|
+
# Runtime validation (strict)
|
|
91
|
+
_validate_circuit_indices(circuit)
|
|
92
|
+
|
|
93
|
+
# instantiate simulator circuit
|
|
94
|
+
sim = SimulatorCircuit(circuit.n_qubits)
|
|
95
|
+
|
|
96
|
+
# apply instructions
|
|
97
|
+
for instr in circuit.instructions:
|
|
98
|
+
op = instr.name
|
|
99
|
+
qubits = instr.qubits
|
|
100
|
+
params = instr.params or []
|
|
101
|
+
|
|
102
|
+
if op == "rz":
|
|
103
|
+
sim.rz(qubits[0], params[0])
|
|
104
|
+
elif op == "sx":
|
|
105
|
+
sim.sx(qubits[0])
|
|
106
|
+
elif op == "x":
|
|
107
|
+
sim.x(qubits[0])
|
|
108
|
+
elif op == "y":
|
|
109
|
+
sim.y(qubits[0])
|
|
110
|
+
elif op == "z":
|
|
111
|
+
sim.z(qubits[0])
|
|
112
|
+
elif op == "h":
|
|
113
|
+
sim.h(qubits[0])
|
|
114
|
+
elif op == "s":
|
|
115
|
+
sim.s(qubits[0])
|
|
116
|
+
elif op == "sdg":
|
|
117
|
+
sim.sdg(qubits[0])
|
|
118
|
+
elif op == "t":
|
|
119
|
+
sim.t(qubits[0])
|
|
120
|
+
elif op == "tdg":
|
|
121
|
+
sim.tdg(qubits[0])
|
|
122
|
+
elif op == "rx":
|
|
123
|
+
sim.rx(qubits[0], params[0])
|
|
124
|
+
elif op == "ry":
|
|
125
|
+
sim.ry(qubits[0], params[0])
|
|
126
|
+
elif op == "cx":
|
|
127
|
+
sim.cx(qubits[0], qubits[1])
|
|
128
|
+
elif op == "swap":
|
|
129
|
+
sim.swap(qubits[0], qubits[1])
|
|
130
|
+
# measures are handled by the run() method implicitly or we ignore them here
|
|
131
|
+
# because the rust `run` method just measures all at the end?
|
|
132
|
+
# WAIT. The Rust `run` method measures ALL qubits.
|
|
133
|
+
# The Python `QuantumCircuit` has explicit `measure`.
|
|
134
|
+
# The current `StateVecPy` implementation of `measure_counts` measures ALL qubits into a bitstring.
|
|
135
|
+
# So it matches the implicit behavior if we ignore specific measure instructions
|
|
136
|
+
# (assuming we want full register measurement).
|
|
137
|
+
# If the user wants partial measurement, the current Rust implementation doesn't support it well yet
|
|
138
|
+
# (it returns full n_qubits string).
|
|
139
|
+
# So we just ignore 'measure' op here and rely on `run` doing full measurement.
|
|
140
|
+
|
|
141
|
+
result: Dict[str, Any] = {"shots": shots, "seed": seed}
|
|
142
|
+
if backend is not None:
|
|
143
|
+
result["backend_id"] = getattr(backend, "id", "local_sim")
|
|
144
|
+
|
|
145
|
+
if shots is not None and shots > 0:
|
|
146
|
+
counts = sim.run(shots, seed)
|
|
147
|
+
# ensure a plain dict is returned to tests
|
|
148
|
+
result["counts"] = dict(counts)
|
|
149
|
+
else:
|
|
150
|
+
# return final statevector
|
|
151
|
+
# sim is SimulatorCircuit (QuantumCircuit). We need to call statevector().
|
|
152
|
+
sv = sim.statevector()
|
|
153
|
+
result["statevector"] = sv
|
|
154
|
+
|
|
155
|
+
return result
|
qsys/target.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# qsys/target.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List, Tuple, Dict, Optional
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Target:
|
|
11
|
+
n_qubits: int
|
|
12
|
+
basis_gates: List[str]
|
|
13
|
+
coupling_map: List[Tuple[int, int]] # directed edges control → target
|
|
14
|
+
name: str = ""
|
|
15
|
+
durations: Optional[Dict[str, float]] = None # ns, optional
|
|
16
|
+
error_rates: Optional[Dict[str, float]] = None # optional
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> dict:
|
|
19
|
+
return {
|
|
20
|
+
"name": self.name or f"{self.n_qubits}q-generic",
|
|
21
|
+
"n_qubits": self.n_qubits,
|
|
22
|
+
"basis_gates": self.basis_gates,
|
|
23
|
+
"coupling_map": self.coupling_map,
|
|
24
|
+
"durations": self.durations,
|
|
25
|
+
"error_rates": self.error_rates,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def to_json(self, path: str | Path) -> None:
|
|
29
|
+
Path(path).write_text(json.dumps(self.to_dict(), indent=2))
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_json(path: str | Path) -> "Target":
|
|
33
|
+
data = json.loads(Path(path).read_text())
|
|
34
|
+
return Target(
|
|
35
|
+
n_qubits=data["n_qubits"],
|
|
36
|
+
basis_gates=data["basis_gates"],
|
|
37
|
+
coupling_map=data["coupling_map"],
|
|
38
|
+
name=data.get("name", ""),
|
|
39
|
+
durations=data.get("durations"),
|
|
40
|
+
error_rates=data.get("error_rates"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def __str__(self) -> str:
|
|
44
|
+
return f"Target({self.name or 'unnamed'}, {self.n_qubits} qubits, {len(self.coupling_map)} edges)"
|
qsys/targets.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# qsys/backends.py - FINAL VERSION
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .target import Target
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def line_target(n_qubits: int) -> Target:
|
|
8
|
+
edges = [(i, i + 1) for i in range(n_qubits - 1)]
|
|
9
|
+
edges += [(i + 1, i) for i in range(n_qubits - 1)]
|
|
10
|
+
return Target(
|
|
11
|
+
n_qubits=n_qubits,
|
|
12
|
+
name=f"line-{n_qubits}",
|
|
13
|
+
basis_gates=["rz", "sx", "x", "cx", "measure"],
|
|
14
|
+
coupling_map=edges,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ring_target(n_qubits: int) -> Target:
|
|
19
|
+
edges = [(i, (i + 1) % n_qubits) for i in range(n_qubits)]
|
|
20
|
+
edges += [((i + 1) % n_qubits, i) for i in range(n_qubits)]
|
|
21
|
+
return Target(
|
|
22
|
+
n_qubits=n_qubits,
|
|
23
|
+
name=f"ring-{n_qubits}",
|
|
24
|
+
basis_gates=["rz", "sx", "x", "cx", "measure"],
|
|
25
|
+
coupling_map=edges,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def heavy_hex_target() -> Target:
|
|
30
|
+
edges = [
|
|
31
|
+
(0, 1),
|
|
32
|
+
(1, 0),
|
|
33
|
+
(1, 2),
|
|
34
|
+
(2, 1),
|
|
35
|
+
(2, 3),
|
|
36
|
+
(3, 2),
|
|
37
|
+
(3, 4),
|
|
38
|
+
(4, 3),
|
|
39
|
+
(4, 5),
|
|
40
|
+
(5, 4),
|
|
41
|
+
(5, 6),
|
|
42
|
+
(6, 5),
|
|
43
|
+
(0, 6),
|
|
44
|
+
(6, 0),
|
|
45
|
+
(1, 6),
|
|
46
|
+
(6, 1),
|
|
47
|
+
(3, 5),
|
|
48
|
+
(5, 3),
|
|
49
|
+
]
|
|
50
|
+
return Target(
|
|
51
|
+
n_qubits=7,
|
|
52
|
+
name="heavy-hex-7",
|
|
53
|
+
basis_gates=["rz", "sx", "x", "cx", "measure"],
|
|
54
|
+
coupling_map=edges,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
BUILTIN_TARGETS = {
|
|
59
|
+
"line_5": line_target(5),
|
|
60
|
+
"line_16": line_target(16),
|
|
61
|
+
"ring_8": ring_target(8),
|
|
62
|
+
"heavy_hex_7": heavy_hex_target(),
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# transpiler package
|
|
2
|
+
from .basis import basis_map # noqa: F401
|
|
3
|
+
from .opt1q import optimize_1q # noqa: F401
|
|
4
|
+
from .passes import PassConfig, transpile # noqa: F401
|
|
5
|
+
from .routing import route_on_coupling # noqa: F401
|
|
6
|
+
from .validate import ValidationReport, validate # noqa: F401
|
qsys/transpiler/basis.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from ..backends.base import Backend
|
|
6
|
+
from ..ir.types import Instruction
|
|
7
|
+
|
|
8
|
+
NATIVE = {"x", "sx", "rz", "cx", "measure"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def basis_map(circuit, backend: Backend):
|
|
12
|
+
"""
|
|
13
|
+
Return a *new* circuit where non-native ops are decomposed into {x,sx,rz,cx,measure}.
|
|
14
|
+
|
|
15
|
+
MVP: decompose 'h' as: sx(q); rz(q, pi/2)
|
|
16
|
+
Notes:
|
|
17
|
+
- Decomposition is correct up to a global phase, which does not affect measurement statistics.
|
|
18
|
+
- Later we can extend with rx/ry etc. using Euler-style decompositions.
|
|
19
|
+
"""
|
|
20
|
+
from ..circuit.quantum_circuit import QuantumCircuit # avoid circular import
|
|
21
|
+
|
|
22
|
+
out = QuantumCircuit(circuit.n_qubits, getattr(circuit, "n_clbits", 0))
|
|
23
|
+
|
|
24
|
+
for inst in circuit.instructions:
|
|
25
|
+
name = inst.name
|
|
26
|
+
qs = inst.qubits
|
|
27
|
+
ps = inst.params
|
|
28
|
+
cs = inst.clbits
|
|
29
|
+
|
|
30
|
+
if name in NATIVE:
|
|
31
|
+
out._instructions.append(Instruction(name, qs, ps, cs))
|
|
32
|
+
elif name == "h":
|
|
33
|
+
q = qs[0]
|
|
34
|
+
out._instructions.append(Instruction("sx", (q,), ()))
|
|
35
|
+
out._instructions.append(Instruction("rz", (q,), (math.pi / 2,)))
|
|
36
|
+
else:
|
|
37
|
+
# Unknown non-native; keep as-is for now (later phases will expand)
|
|
38
|
+
out._instructions.append(Instruction(name, qs, ps, cs))
|
|
39
|
+
return out
|
qsys/transpiler/opt1q.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from ..ir.types import Instruction
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _is_small(angle: float, eps: float) -> bool:
|
|
9
|
+
"""Normalize angle to (-pi, pi] and compare against eps."""
|
|
10
|
+
a = (angle + math.pi) % (2 * math.pi) - math.pi
|
|
11
|
+
return abs(a) < eps
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def optimize_1q(circuit, eps: float = 1e-9):
|
|
15
|
+
"""
|
|
16
|
+
Peephole optimizations on single-qubit gates (local, order-preserving w.r.t. other qubits):
|
|
17
|
+
- Merge consecutive RZ on same qubit: RZ(a); RZ(b) -> RZ(a+b); drop if ~0.
|
|
18
|
+
- Collapse SX·SX -> X on same qubit, exactly.
|
|
19
|
+
- Cancel X·X -> I (drop both) on same qubit.
|
|
20
|
+
Scope boundaries: a CX or MEASURE on a qubit ends the local 1q block
|
|
21
|
+
for that qubit (we do not commute across multi-qubit ops).
|
|
22
|
+
Returns a NEW circuit.
|
|
23
|
+
"""
|
|
24
|
+
from ..circuit.quantum_circuit import QuantumCircuit # avoid circular import
|
|
25
|
+
|
|
26
|
+
n = circuit.n_qubits
|
|
27
|
+
out = QuantumCircuit(n, getattr(circuit, "n_clbits", 0))
|
|
28
|
+
out_insts: list[Instruction] = out._instructions
|
|
29
|
+
|
|
30
|
+
# Track, per qubit, index in out_insts of the last 1q gate affecting that qubit
|
|
31
|
+
last_idx: list[int | None] = [None] * n
|
|
32
|
+
|
|
33
|
+
def bump(q: int, new_pos: int | None):
|
|
34
|
+
last_idx[q] = new_pos
|
|
35
|
+
|
|
36
|
+
for inst in circuit.instructions:
|
|
37
|
+
name, qs, ps = inst.name, inst.qubits, inst.params
|
|
38
|
+
|
|
39
|
+
# Multi-qubit gate or measure: flush context for involved qubits and copy through
|
|
40
|
+
if name == "cx":
|
|
41
|
+
out_insts.append(inst)
|
|
42
|
+
c, t = qs
|
|
43
|
+
bump(c, None)
|
|
44
|
+
bump(t, None)
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
if name == "measure":
|
|
48
|
+
q = qs[0]
|
|
49
|
+
out_insts.append(inst)
|
|
50
|
+
bump(q, None)
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Single-qubit gates we know: x, sx, rz
|
|
54
|
+
if name not in ("x", "sx", "rz"):
|
|
55
|
+
# Unknown 1q op: copy and reset its qubit context for safety
|
|
56
|
+
out_insts.append(inst)
|
|
57
|
+
bump(qs[0], None)
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
q = qs[0]
|
|
61
|
+
li = last_idx[q]
|
|
62
|
+
|
|
63
|
+
if name == "rz":
|
|
64
|
+
theta = float(ps[0])
|
|
65
|
+
if li is not None and out_insts[li].name == "rz":
|
|
66
|
+
# Merge angles at last RZ
|
|
67
|
+
prev = out_insts[li]
|
|
68
|
+
new_theta = float(prev.params[0]) + theta
|
|
69
|
+
if _is_small(new_theta, eps):
|
|
70
|
+
# Remove previous; current consumed (drop both)
|
|
71
|
+
out_insts.pop(li)
|
|
72
|
+
bump(q, None)
|
|
73
|
+
else:
|
|
74
|
+
out_insts[li] = Instruction("rz", (q,), (new_theta,), None)
|
|
75
|
+
# Do not append current
|
|
76
|
+
else:
|
|
77
|
+
out_insts.append(inst)
|
|
78
|
+
bump(q, len(out_insts) - 1)
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if name == "sx":
|
|
82
|
+
if li is not None and out_insts[li].name == "sx":
|
|
83
|
+
# SX · SX -> X (replace previous SX with X; current consumed)
|
|
84
|
+
out_insts[li] = Instruction("x", (q,), (), None)
|
|
85
|
+
# Keep context at this X (may cancel with next X)
|
|
86
|
+
else:
|
|
87
|
+
out_insts.append(inst)
|
|
88
|
+
bump(q, len(out_insts) - 1)
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
if name == "x":
|
|
92
|
+
if li is not None and out_insts[li].name == "x":
|
|
93
|
+
# X · X -> I (drop previous X and consume current)
|
|
94
|
+
out_insts.pop(li)
|
|
95
|
+
bump(q, None)
|
|
96
|
+
else:
|
|
97
|
+
out_insts.append(inst)
|
|
98
|
+
bump(q, len(out_insts) - 1)
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
return out
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from ..backends.base import Backend
|
|
6
|
+
from ..errors import ValidationError
|
|
7
|
+
from .basis import basis_map
|
|
8
|
+
from .opt1q import optimize_1q
|
|
9
|
+
from .routing import route_on_coupling
|
|
10
|
+
from .validate import validate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class PassConfig:
|
|
15
|
+
do_routing: bool = True
|
|
16
|
+
do_opt1q: bool = True
|
|
17
|
+
eps: float = 1e-9 # for small-angle pruning in opt1q
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def transpile(
|
|
21
|
+
circuit,
|
|
22
|
+
backend: Backend,
|
|
23
|
+
config: PassConfig | None = None,
|
|
24
|
+
return_report: bool = False,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Run the standard pipeline:
|
|
28
|
+
1) basis_map -> rewrite non-native gates (e.g., h) to {x,sx,rz,cx,measure}
|
|
29
|
+
2) validate -> structure + basis checks; compute 'needs_routing'
|
|
30
|
+
3) route -> (optional) insert SWAP chains so CXs follow coupling_map
|
|
31
|
+
4) optimize_1q-> (optional) peephole: merge RZ; SX·SX→X; X·X cancels
|
|
32
|
+
|
|
33
|
+
Returns either the mapped circuit, or (mapped circuit, ValidationReport) if return_report=True.
|
|
34
|
+
"""
|
|
35
|
+
cfg = config or PassConfig()
|
|
36
|
+
|
|
37
|
+
# 1) map to native
|
|
38
|
+
mapped = basis_map(circuit, backend)
|
|
39
|
+
|
|
40
|
+
# 2) validate native circuit
|
|
41
|
+
report = validate(mapped, backend)
|
|
42
|
+
|
|
43
|
+
# 3) routing if required and enabled
|
|
44
|
+
routed = mapped
|
|
45
|
+
if cfg.do_routing and report.needs_routing:
|
|
46
|
+
routed = route_on_coupling(mapped, backend)
|
|
47
|
+
# Re-validate after routing; should not need routing now
|
|
48
|
+
report = validate(routed, backend)
|
|
49
|
+
if report.needs_routing:
|
|
50
|
+
raise ValidationError("Routing pass did not satisfy coupling constraints.")
|
|
51
|
+
|
|
52
|
+
# 4) 1q optimizations
|
|
53
|
+
optimized = optimize_1q(routed, eps=cfg.eps) if cfg.do_opt1q else routed
|
|
54
|
+
|
|
55
|
+
if return_report:
|
|
56
|
+
return optimized, report
|
|
57
|
+
return optimized
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
|
|
5
|
+
from ..backends.base import Backend
|
|
6
|
+
from ..ir.types import Instruction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_graph(edges: list[tuple[int, int]]) -> dict[int, list[int]]:
|
|
10
|
+
"""Undirected adjacency from coupling_map."""
|
|
11
|
+
g: dict[int, list[int]] = {}
|
|
12
|
+
for a, b in edges or []:
|
|
13
|
+
g.setdefault(a, []).append(b)
|
|
14
|
+
g.setdefault(b, []).append(a)
|
|
15
|
+
return g
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _shortest_path(g: dict[int, list[int]], src: int, dst: int) -> list[int] | None:
|
|
19
|
+
"""BFS shortest path in an undirected graph. Returns list of vertices [src,...,dst]."""
|
|
20
|
+
if src == dst:
|
|
21
|
+
return [src]
|
|
22
|
+
q = deque([src])
|
|
23
|
+
prev: dict[int, int | None] = {src: None}
|
|
24
|
+
while q:
|
|
25
|
+
u = q.popleft()
|
|
26
|
+
for v in g.get(u, []):
|
|
27
|
+
if v in prev:
|
|
28
|
+
continue
|
|
29
|
+
prev[v] = u
|
|
30
|
+
if v == dst:
|
|
31
|
+
# reconstruct
|
|
32
|
+
path = [dst]
|
|
33
|
+
while prev[path[-1]] is not None:
|
|
34
|
+
path.append(prev[path[-1]]) # type: ignore[index]
|
|
35
|
+
path.reverse()
|
|
36
|
+
return path
|
|
37
|
+
q.append(v)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _emit_swap_as_cx_triple(out_insts: list[Instruction], a: int, b: int):
|
|
42
|
+
"""Decompose SWAP(a,b) into 3 CXs on adjacent qubits a-b."""
|
|
43
|
+
out_insts.append(Instruction("cx", (a, b), ()))
|
|
44
|
+
out_insts.append(Instruction("cx", (b, a), ()))
|
|
45
|
+
out_insts.append(Instruction("cx", (a, b), ()))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def route_on_coupling(circuit, backend: Backend):
|
|
49
|
+
"""
|
|
50
|
+
Route CXs so they respect backend.target.coupling_map by inserting SWAP chains.
|
|
51
|
+
Strategy: maintain a logical->physical mapping; 1q gates follow the mapping;
|
|
52
|
+
when a CX(log_c,log_t) isn't on an edge, move the *control's physical* toward target
|
|
53
|
+
along a shortest path with SWAPs (no swap-back). Update mapping as we swap.
|
|
54
|
+
|
|
55
|
+
Returns a NEW circuit with only native ops (cx, rz, sx, x, measure),
|
|
56
|
+
whose CXs are all between adjacent qubits per coupling_map.
|
|
57
|
+
"""
|
|
58
|
+
from ..circuit.quantum_circuit import QuantumCircuit # avoid circular import
|
|
59
|
+
|
|
60
|
+
tgt = backend.target
|
|
61
|
+
g = _build_graph(tgt.coupling_map)
|
|
62
|
+
n = circuit.n_qubits
|
|
63
|
+
|
|
64
|
+
# Start with identity layout: logical i at physical i
|
|
65
|
+
# loc[logical] = physical; which_log[physical] = logical
|
|
66
|
+
loc = list(range(n))
|
|
67
|
+
which_log = list(range(n))
|
|
68
|
+
|
|
69
|
+
out = QuantumCircuit(n, getattr(circuit, "n_clbits", 0))
|
|
70
|
+
out_insts: list[Instruction] = out._instructions # alias
|
|
71
|
+
|
|
72
|
+
for inst in circuit.instructions:
|
|
73
|
+
name, qs, ps, cs = inst.name, inst.qubits, inst.params, inst.clbits
|
|
74
|
+
|
|
75
|
+
if name in ("x", "sx", "rz"):
|
|
76
|
+
q = qs[0]
|
|
77
|
+
out_insts.append(Instruction(name, (loc[q],), ps, cs))
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if name == "measure":
|
|
81
|
+
q = qs[0]
|
|
82
|
+
out_insts.append(Instruction("measure", (loc[q],), ps, cs))
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if name == "cx":
|
|
86
|
+
c_log, t_log = qs
|
|
87
|
+
c_phy, t_phy = loc[c_log], loc[t_log]
|
|
88
|
+
|
|
89
|
+
# If directly connected, emit and continue
|
|
90
|
+
if (c_phy, t_phy) in tgt.coupling_map or (t_phy, c_phy) in tgt.coupling_map:
|
|
91
|
+
out_insts.append(Instruction("cx", (c_phy, t_phy), ()))
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Otherwise, find path and move control toward target with SWAPs
|
|
95
|
+
path = _shortest_path(g, c_phy, t_phy)
|
|
96
|
+
if not path or len(path) < 2:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"No path between qubits {c_phy} and {t_phy} for routing"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Walk path by swapping control forward one hop at a time (record hops)
|
|
102
|
+
current = c_phy
|
|
103
|
+
hops: list[tuple[int, int]] = []
|
|
104
|
+
for next_hop in path[1:-1]: # stop one before t_phy
|
|
105
|
+
_emit_swap_as_cx_triple(out_insts, current, next_hop)
|
|
106
|
+
# Update mapping
|
|
107
|
+
l_a = which_log[current]
|
|
108
|
+
l_b = which_log[next_hop]
|
|
109
|
+
which_log[current], which_log[next_hop] = l_b, l_a
|
|
110
|
+
loc[l_a], loc[l_b] = next_hop, current
|
|
111
|
+
hops.append((current, next_hop))
|
|
112
|
+
current = next_hop
|
|
113
|
+
|
|
114
|
+
# Now control is adjacent to target; emit CX with current (control) -> t_phy
|
|
115
|
+
c_phy = current
|
|
116
|
+
if not (
|
|
117
|
+
(c_phy, t_phy) in tgt.coupling_map or (t_phy, c_phy) in tgt.coupling_map
|
|
118
|
+
):
|
|
119
|
+
raise ValueError("Routing logic error: not adjacent after swaps")
|
|
120
|
+
out_insts.append(Instruction("cx", (c_phy, t_phy), ()))
|
|
121
|
+
|
|
122
|
+
# Swap BACK along the reverse path to restore the original logical->physical layout
|
|
123
|
+
for a, b in reversed(hops):
|
|
124
|
+
_emit_swap_as_cx_triple(out_insts, a, b)
|
|
125
|
+
# Undo the mapping change
|
|
126
|
+
l_a = which_log[a]
|
|
127
|
+
l_b = which_log[b]
|
|
128
|
+
which_log[a], which_log[b] = l_b, l_a
|
|
129
|
+
loc[l_a], loc[l_b] = b, a
|
|
130
|
+
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Anything else: copy as-is (basis_map should have eliminated non-native)
|
|
134
|
+
out_insts.append(inst)
|
|
135
|
+
|
|
136
|
+
return out
|