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.
Files changed (76) hide show
  1. qsys/__init__.py +31 -0
  2. qsys/__pycache__/__init__.cpython-313.pyc +0 -0
  3. qsys/backends/base.py +39 -0
  4. qsys/backends/local5q.py +8 -0
  5. qsys/circuit/__init__.py +20 -0
  6. qsys/circuit/__pycache__/__init__.cpython-313.pyc +0 -0
  7. qsys/circuit/__pycache__/quantum_circuit.cpython-313.pyc +0 -0
  8. qsys/circuit/quantum_circuit.py +103 -0
  9. qsys/cli/__init__.py +2 -0
  10. qsys/cli/io_cli.py +45 -0
  11. qsys/errors/__init__.py +32 -0
  12. qsys/errors/__pycache__/__init__.cpython-313.pyc +0 -0
  13. qsys/io/__init__.py +21 -0
  14. qsys/io/json_io.py +84 -0
  15. qsys/io/text_io.py +58 -0
  16. qsys/ir/__init__.py +1 -0
  17. qsys/ir/__pycache__/__init__.cpython-313.pyc +0 -0
  18. qsys/ir/__pycache__/types.cpython-313.pyc +0 -0
  19. qsys/ir/from_payload.py +27 -0
  20. qsys/ir/types.py +9 -0
  21. qsys/logging.py +14 -0
  22. qsys/runtime/__init__.py +4 -0
  23. qsys/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
  24. qsys/runtime/__pycache__/execute.cpython-313.pyc +0 -0
  25. qsys/runtime/execute.py +155 -0
  26. qsys/target.py +44 -0
  27. qsys/targets.py +63 -0
  28. qsys/transpiler/__init__.py +6 -0
  29. qsys/transpiler/basis.py +39 -0
  30. qsys/transpiler/opt1q.py +101 -0
  31. qsys/transpiler/passes.py +57 -0
  32. qsys/transpiler/routing.py +136 -0
  33. qsys/transpiler/validate.py +132 -0
  34. qsys/viz/__init__.py +1 -0
  35. qsys/viz/text_drawer.py +89 -0
  36. simulator_statevector/__init__.py +5 -0
  37. simulator_statevector/__pycache__/__init__.cpython-313.pyc +0 -0
  38. simulator_statevector/simulator_statevector.pyd +0 -0
  39. zena/__init__.py +7 -0
  40. zena/__pycache__/__init__.cpython-312.pyc +0 -0
  41. zena/__pycache__/__init__.cpython-313.pyc +0 -0
  42. zena/__pycache__/execute.cpython-312.pyc +0 -0
  43. zena/__pycache__/execute.cpython-313.pyc +0 -0
  44. zena/circuit/__init__.py +2 -0
  45. zena/circuit/__pycache__/__init__.cpython-312.pyc +0 -0
  46. zena/circuit/__pycache__/__init__.cpython-313.pyc +0 -0
  47. zena/circuit/__pycache__/quantum_circuit.cpython-312.pyc +0 -0
  48. zena/circuit/__pycache__/quantum_circuit.cpython-313.pyc +0 -0
  49. zena/circuit/__pycache__/register.cpython-312.pyc +0 -0
  50. zena/circuit/__pycache__/register.cpython-313.pyc +0 -0
  51. zena/circuit/quantum_circuit.py +218 -0
  52. zena/circuit/register.py +28 -0
  53. zena/compiler/__init__.py +72 -0
  54. zena/compiler/__pycache__/__init__.cpython-312.pyc +0 -0
  55. zena/compiler/__pycache__/__init__.cpython-313.pyc +0 -0
  56. zena/dist/zena_sdk-0.1.0-py3-none-any.whl +0 -0
  57. zena/dist/zena_sdk-0.1.0.tar.gz +0 -0
  58. zena/execute.py +35 -0
  59. zena/providers/__init__.py +5 -0
  60. zena/providers/__pycache__/__init__.cpython-312.pyc +0 -0
  61. zena/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  62. zena/providers/__pycache__/aer.cpython-312.pyc +0 -0
  63. zena/providers/__pycache__/aer.cpython-313.pyc +0 -0
  64. zena/providers/__pycache__/backend.cpython-312.pyc +0 -0
  65. zena/providers/__pycache__/backend.cpython-313.pyc +0 -0
  66. zena/providers/__pycache__/job.cpython-312.pyc +0 -0
  67. zena/providers/__pycache__/job.cpython-313.pyc +0 -0
  68. zena/providers/aer.py +71 -0
  69. zena/providers/backend.py +18 -0
  70. zena/providers/job.py +24 -0
  71. zena/visualization/__init__.py +28 -0
  72. zena/visualization/__pycache__/__init__.cpython-312.pyc +0 -0
  73. zena/visualization/__pycache__/__init__.cpython-313.pyc +0 -0
  74. zena_sdk-0.1.4.dist-info/METADATA +70 -0
  75. zena_sdk-0.1.4.dist-info/RECORD +76 -0
  76. zena_sdk-0.1.4.dist-info/WHEEL +4 -0
@@ -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
@@ -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
@@ -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