microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/replace-qsharp-with-qdk-python-tests

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/pip/qsharp/interop/qiskit/backends/neutral_atom_backend.py

288lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3
4import logging
5from typing import Any, Dict, List, Literal, Optional, Union
6from uuid import uuid4
7
8from qiskit import QuantumCircuit
9from qiskit.providers import Options
10from qiskit.transpiler.target import Target
11
12from .... import Result, TargetProfile
13from .. import OutputSemantics
14from ..execution import DetaultExecutor
15from ..jobs import QsSimJob, QsJobSet
16from .backend_base import BackendBase
17from .compilation import Compilation
18from .errors import Errors
19from .neutral_atom_target import NeutralAtomTarget
20
21logger = logging.getLogger(__name__)
22
23
24def _bitstring_has_qubit_loss(bitstring: str) -> bool:
25 """Return True if the bitstring contains a qubit-loss marker.
26
27 Lost qubits may be represented using non-binary markers (e.g. '-', '2').
28 We treat any shot containing those markers as lost-qubit affected.
29 """
30 return "-" in bitstring or "2" in bitstring
31
32
33class NeutralAtomBackend(BackendBase):
34 """A Qiskit backend that simulates circuits using the NeutralAtomDevice pipeline.
35
36 Circuits are transpiled to OpenQASM 3 using the device's native gate set
37 (Rz, SX, CZ), compiled to QIR via the Q# compiler, then run through the
38 NeutralAtomDevice compilation and simulation pipeline.
39 The device handles single-qubit gate optimization and qubit movement scheduling.
40 An optional noise model can be applied to model realistic device behavior.
41
42 The native gate set target ensures Qiskit's transpiler decomposes all non-native
43 gates before simulation, so noise configured on native gates (``noise.rz``,
44 ``noise.sx``, ``noise.cz``, ``noise.mresetz``) behaves as expected.
45
46 The simulator backend (Clifford, CPU full-state, or GPU full-state) is
47 selected automatically unless overridden via the ``simulator_type`` option.
48
49 Example::
50
51 from qiskit import QuantumCircuit
52 from qsharp.interop.qiskit import NeutralAtomBackend
53 from qsharp._simulation import NoiseConfig
54
55 qc = QuantumCircuit(2)
56 qc.h(0)
57 qc.cx(0, 1)
58 qc.measure_all()
59
60 # Noiseless simulation
61 backend = NeutralAtomBackend()
62 job = backend.run(qc, shots=1000)
63 print(job.result().get_counts())
64
65 # Noisy simulation
66 noise = NoiseConfig()
67 noise.cz.set_depolarizing(1e-3)
68 noise.mresetz.set_bitflip(1e-3)
69
70 job = backend.run(qc, shots=1000, noise=noise, seed=42)
71 print(job.result().get_counts())
72 """
73
74 def __init__(
75 self,
76 device=None,
77 target: Optional[Target] = None,
78 qiskit_pass_options: Optional[Dict[str, Any]] = None,
79 transpile_options: Optional[Dict[str, Any]] = None,
80 qasm_export_options: Optional[Dict[str, Any]] = None,
81 skip_transpilation: bool = False,
82 **options,
83 ):
84 """
85 :param device: The NeutralAtomDevice instance to use for compilation and simulation.
86 A default-configured device is created automatically if not provided.
87 Pass a custom device to control the qubit layout (column count, zone dimensions, etc.).
88 :type device: NeutralAtomDevice
89 :param target: Qiskit transpiler target. Defaults to the NeutralAtomDevice native
90 gate set ``{rz, sx, cz, measure, reset}``. Override only if you need a custom
91 decomposition strategy.
92 :param qiskit_pass_options: Options forwarded to Qiskit pre-transpilation passes.
93 :type qiskit_pass_options: Dict
94 :param transpile_options: Options forwarded to ``qiskit.transpile()``.
95 :type transpile_options: Dict
96 :param qasm_export_options: Options forwarded to the Qiskit QASM3 exporter.
97 :type qasm_export_options: Dict
98 :param skip_transpilation: Skip Qiskit transpilation. Useful when the circuit is
99 already expressed in terms of the target gate set.
100 :type skip_transpilation: bool
101 :param **options: Default option overrides. These can also be overridden per-call via
102 :meth:`run`. Common options:
103
104 - ``name`` (str): Backend name for job metadata. Defaults to the circuit name.
105 - ``shots`` (int): Number of shots. Defaults to ``1024``.
106 - ``seed`` (int): Random seed for reproducibility. Defaults to ``None``.
107 - ``noise`` (NoiseConfig): Optional per-gate noise model. Defaults to ``None`` (noiseless).
108 - ``simulator_type`` (str): Simulator to use — ``"clifford"`` (Clifford only),
109 ``"cpu"`` (CPU full-state), ``"gpu"`` (GPU full-state), or ``None`` to
110 auto-select (GPU if available, CPU otherwise).
111 - ``output_semantics`` (OutputSemantics): QIR output encoding. Defaults to ``OutputSemantics.Qiskit``.
112 - ``executor``: Executor for async job submission.
113 """
114 self._device = device
115 super().__init__(
116 target,
117 qiskit_pass_options,
118 transpile_options,
119 qasm_export_options,
120 skip_transpilation,
121 **options,
122 )
123
124 def _get_device(self):
125 """Return the NeutralAtomDevice, creating a default one on first access."""
126 if self._device is None:
127 from qsharp._device._atom import NeutralAtomDevice
128
129 self._device = NeutralAtomDevice()
130 return self._device
131
132 def _build_target(self) -> Target:
133 """Return a target restricted to the NeutralAtomDevice native gate set.
134
135 Limiting the target to ``{rz, sx, cz, measure, reset}`` ensures Qiskit's
136 transpiler decomposes all non-native gates before QASM3 export, so the
137 circuit that reaches the simulator already uses only native gates.
138 """
139 return NeutralAtomTarget.build_target(num_qubits=None)
140
141 @classmethod
142 def _default_options(cls):
143 return Options(
144 search_path=".",
145 shots=1024,
146 seed=None,
147 noise=None,
148 simulator_type=None,
149 output_semantics=OutputSemantics.Qiskit,
150 executor=DetaultExecutor(),
151 )
152
153 def run(
154 self,
155 run_input: Union[QuantumCircuit, List[QuantumCircuit]],
156 **options,
157 ) -> Union[QsSimJob, QsJobSet]:
158 """Simulate the given circuit(s) using the NeutralAtomDevice pipeline.
159
160 :param run_input: A single ``QuantumCircuit`` or a list of them.
161 :param **options: Per-call option overrides. Common options:
162
163 - ``name`` (str): Backend name for job metadata. Defaults to the circuit name.
164 - ``shots`` (int): Number of shots. Defaults to ``1024``.
165 - ``seed`` (int): Random seed for reproducibility. Defaults to ``None``.
166 - ``noise`` (NoiseConfig): Optional per-gate noise model. Defaults to ``None`` (noiseless).
167 - ``simulator_type`` (str): Simulator to use — ``"clifford"`` (Clifford only),
168 ``"cpu"`` (CPU full-state), ``"gpu"`` (GPU full-state), or ``None`` to
169 auto-select (GPU if available, CPU otherwise).
170 - ``output_semantics`` (OutputSemantics): QIR output encoding. Defaults to ``OutputSemantics.Qiskit``.
171 - ``executor``: Executor for async job submission.
172 :return: A job object whose ``.result()`` returns a Qiskit ``Result``.
173 :rtype: QsSimJob
174 :raises ValueError: If ``run_input`` is not a ``QuantumCircuit`` or list thereof,
175 or if a ``target_profile`` other than ``TargetProfile.Base`` is provided.
176 """
177 run_input = self._validate_quantum_circuits(run_input)
178 return self._run(run_input, **options)
179
180 def _map_result_bit(self, v) -> str:
181 """Override: unknown values are qubit-loss markers (``"-"``)."""
182 if v == Result.One:
183 return "1"
184 if v == Result.Zero:
185 return "0"
186 return "-"
187
188 def _execute(self, programs: List[Compilation], **input_params) -> Dict[str, Any]:
189 device = self._get_device()
190
191 shots = input_params.get("shots")
192 if shots is None:
193 raise ValueError(str(Errors.MISSING_NUMBER_OF_SHOTS))
194
195 noise = input_params.get("noise")
196 simulator_type: Optional[Literal["clifford", "cpu", "gpu"]] = input_params.get(
197 "simulator_type"
198 )
199 seed: Optional[int] = input_params.get("seed")
200 search_path: str = input_params.get("search_path", ".")
201 output_semantics = input_params.get("output_semantics")
202
203 # NeutralAtomDevice always requires base-profile QIR — the device's
204 # compilation pipeline validates that no conditional branches exist.
205 # Raise explicitly if the caller passed a non-Base profile so the
206 # error is immediate and clear rather than silently ignored.
207 target_profile = input_params.get("target_profile")
208 if target_profile is not None and target_profile != TargetProfile.Base:
209 raise ValueError(
210 "NeutralAtomBackend only supports TargetProfile.Base. "
211 "The NeutralAtomDevice compilation pipeline does not support "
212 f"conditional branches produced by {target_profile}."
213 )
214
215 job_results = []
216 for program in programs:
217 name = input_params.get("name", program.circuit.name)
218
219 # Compile QASM3 → QIR (base profile).
220 qir = self._qasm_to_qir(
221 program.qasm,
222 name=name,
223 target_profile=TargetProfile.Base,
224 output_semantics=output_semantics,
225 search_path=search_path,
226 )
227
228 # Run through NeutralAtomDevice compilation + simulation pipeline.
229 sim_results = device.simulate(
230 qir,
231 shots=shots,
232 noise=noise,
233 type=simulator_type,
234 seed=seed,
235 )
236
237 raw_memory = [self._shot_to_bitstring(shot) for shot in sim_results]
238
239 # Separate accepted shots (no loss markers) from raw shots.
240 # Qiskit-compatible fields (counts, memory, probabilities)
241 # contain only clean {0,1} outcomes; raw_* fields retain the
242 # full picture including loss.
243 memory = [s for s in raw_memory if not _bitstring_has_qubit_loss(s)]
244 accepted_total_count = len(memory)
245 raw_total_count = len(raw_memory)
246
247 raw_counts: Dict[str, int] = {}
248 counts: Dict[str, int] = {}
249 for bs in raw_memory:
250 raw_counts[bs] = raw_counts.get(bs, 0) + 1
251 if not _bitstring_has_qubit_loss(bs):
252 counts[bs] = counts.get(bs, 0) + 1
253
254 raw_probabilities = (
255 {}
256 if raw_total_count == 0
257 else {bs: c / raw_total_count for bs, c in raw_counts.items()}
258 )
259 probabilities = (
260 {}
261 if accepted_total_count == 0
262 else {bs: c / accepted_total_count for bs, c in counts.items()}
263 )
264
265 job_results.append(
266 {
267 "data": {
268 # Qiskit-compatible fields: loss shots excluded.
269 "counts": counts,
270 "probabilities": probabilities,
271 "memory": memory,
272 # Raw fields: all shots, including loss markers.
273 "raw_counts": raw_counts,
274 "raw_probabilities": raw_probabilities,
275 "raw_memory": raw_memory,
276 },
277 "success": True,
278 "header": {
279 "metadata": {"qasm": program.qasm},
280 "name": program.circuit.name,
281 "compilation_time_taken": program.time_taken,
282 },
283 # shots reflects accepted (non-loss) count.
284 "shots": accepted_total_count,
285 }
286 )
287
288 return {"results": job_results, "qobj_id": str(uuid4()), "success": True}
289