microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
billti/qdk_package

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

563lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3
4from abc import ABC, abstractmethod
5import datetime
6import logging
7from time import monotonic
8from typing import Dict, Any, List, Optional, Union
9from warnings import warn
10
11from qiskit import transpile
12from qiskit.circuit import (
13 QuantumCircuit,
14)
15from qiskit.version import get_version_info
16
17from qiskit.qasm3.exporter import Exporter
18from qiskit.providers import BackendV2, Options
19from qiskit.result import Result
20from qiskit.transpiler import PassManager
21from qiskit.transpiler.passes import RemoveBarriers, RemoveResetInZeroState
22from qiskit.transpiler.target import Target
23
24from .compilation import Compilation
25from .errors import Errors
26from .qirtarget import QirTarget
27from ..jobs import QsJob
28from ..passes import RemoveDelays
29from .... import TargetProfile
30
31logger = logging.getLogger(__name__)
32
33_QISKIT_NON_GATE_INSTRUCTIONS = [
34 "control_flow",
35 "if_else",
36 "switch_case",
37 "while_loop",
38 "break",
39 "continue",
40 "store",
41 "for_loop",
42 "measure",
43 "reset",
44]
45
46_QISKIT_STDGATES = [
47 "p",
48 "x",
49 "y",
50 "z",
51 "h",
52 "s",
53 "sdg",
54 "t",
55 "tdg",
56 "sx",
57 "rx",
58 "ry",
59 "rz",
60 "cx",
61 "cy",
62 "cz",
63 "cp",
64 "crx",
65 "cry",
66 "crz",
67 "ch",
68 "swap",
69 "ccx",
70 "cswap",
71 "cu",
72 "CX",
73 "phase",
74 "cphase",
75 "id",
76 "u1",
77 "u2",
78 "u3",
79 "U",
80]
81
82
83def filter_kwargs(func, **kwargs) -> Dict[str, Any]:
84 import inspect
85
86 sig = inspect.signature(func)
87 supported_args = set(sig.parameters.keys())
88 extracted_kwargs = {
89 k: kwargs.get(k) for k in list(kwargs.keys()) if k in supported_args
90 }
91 return extracted_kwargs
92
93
94def get_transpile_options(**kwargs) -> Dict[str, Any]:
95 args = filter_kwargs(transpile, **kwargs)
96 return args
97
98
99def get_exporter_options(**kwargs) -> Dict[str, Any]:
100 return filter_kwargs(Exporter.__init__, **kwargs)
101
102
103class BackendBase(BackendV2, ABC):
104 """
105 A virtual backend for transpiling to a Q# ecosystem compatible format.
106 """
107
108 def __init__(
109 self,
110 target: Optional[Target] = None,
111 qiskit_pass_options: Optional[Dict[str, Any]] = None,
112 transpile_options: Optional[Dict[str, Any]] = None,
113 qasm_export_options: Optional[Dict[str, Any]] = None,
114 skip_transpilation: bool = False,
115 **fields,
116 ):
117 """
118 Parameters:
119 target (Target): The target to use for the backend.
120 qiskit_pass_options (Dict): Options for the Qiskit passes.
121 transpile_options (Dict): Options for the transpiler.
122 qasm_export_options (Dict): Options for the QASM3 exporter.
123 **options: Additional keyword arguments to pass to the
124 execution used by subclasses.
125 """
126 super().__init__(
127 name="QSharpBackend",
128 description="A virtual BackendV2 for transpiling to a Q# compatible format.",
129 backend_version="0.0.1",
130 )
131
132 if fields is not None:
133 # we need to rename the seed_simulator to seed. This
134 # is a convenience for aer users.
135 # if the user passes in seed_simulator, we will rename it to seed
136 # but only if the seed field is defined in the backend options.
137 if "seed_simulator" in fields and "seed" in self._options.data:
138 warn("seed_simulator passed, but field is called seed.")
139 fields["seed"] = fields.pop("seed_simulator")
140
141 # updates the options with the fields passed in, if the backend
142 # doesn't have the field, it will raise an error.
143 self.set_options(**fields)
144
145 self._qiskit_pass_options = Options(
146 supports_barrier=False,
147 supports_delay=False,
148 remove_reset_in_zero_state=True,
149 )
150 self._skip_transpilation = skip_transpilation
151
152 # we need to set the target after the options are set
153 # so that the target_profile can be used to determine
154 # which gates/instructions are available
155 if target is not None:
156 # update the properties so that we are internally consistent
157 self._qiskit_pass_options.update_options(
158 **{
159 "supports_barrier": target.instruction_supported("barrier"),
160 "supports_delay": target.instruction_supported("delay"),
161 "remove_reset_in_zero_state": True,
162 }
163 )
164
165 self._target = target
166 else:
167 self._target = self._create_target()
168
169 self._transpile_options = {}
170
171 basis_gates = None
172 if qasm_export_options is not None and "basis_gates" in qasm_export_options:
173 basis_gates = qasm_export_options.pop("basis_gates")
174 else:
175 # here we get the gates that are in the target but not in qasm's
176 # stdgates so that we can build the basis gates list for the exporter.
177 # A user can override this list by passing in a basis_gates list
178 # We also remove any non-gate instructions from the list.
179 target_gates = set(self.target.operation_names)
180 target_gates -= set(_QISKIT_NON_GATE_INSTRUCTIONS)
181 target_gates -= set(_QISKIT_STDGATES)
182 basis_gates = list(target_gates)
183
184 # set the default options for the exporter
185 self._qasm_export_options = {
186 "includes": ("stdgates.inc",),
187 "alias_classical_registers": False,
188 "allow_aliasing": False,
189 "disable_constants": True,
190 "basis_gates": basis_gates,
191 }
192
193 if qiskit_pass_options is not None:
194 self._qiskit_pass_options.update_options(**qiskit_pass_options)
195 if transpile_options is not None:
196 self._transpile_options.update(**transpile_options)
197 if qasm_export_options is not None:
198 self._qasm_export_options.update(**qasm_export_options)
199
200 def _create_target(self) -> Target:
201 supports_barrier = self._qiskit_pass_options["supports_barrier"]
202 supports_delay = self._qiskit_pass_options["supports_delay"]
203 return QirTarget(
204 target_profile=self._options["target_profile"],
205 supports_barrier=supports_barrier,
206 supports_delay=supports_delay,
207 )
208
209 @property
210 def target(self) -> Target:
211 """Returns the target of the Backend object."""
212 return self._target
213
214 @property
215 def max_circuits(self):
216 """
217 Returns the maximum number of circuits that can be executed simultaneously.
218 """
219 return None
220
221 @abstractmethod
222 def _execute(self, programs: List[Compilation], **input_params) -> Dict[str, Any]:
223 """Execute circuits on the backend.
224
225 Parameters:
226 programs (List of QuantumCompilation): simulator input.
227 input_params (Dict): configuration for simulation/compilation.
228
229 Returns:
230 dict: return a dictionary of results.
231 """
232
233 @abstractmethod
234 def run(
235 self,
236 run_input: Union[QuantumCircuit, List[QuantumCircuit]],
237 **options,
238 ) -> QsJob:
239 pass
240
241 def _run(
242 self,
243 run_input: List[QuantumCircuit],
244 **options,
245 ) -> QsJob:
246 if "name" not in options and len(run_input) == 1:
247 options["name"] = run_input[0].name
248
249 # Get out default options
250 # Look at all of the kwargs and see if they match any of the options
251 # If they do, set the option to the value of the kwarg as an override
252 # We only to remove the options that are in the backend options for
253 # the run so that other options can be passed to other calls.
254 input_params: Dict[str, Any] = vars(self.options).copy()
255 input_params.update(options)
256
257 return self._submit_job(run_input, **input_params)
258
259 def run_job(
260 self, run_input: List[QuantumCircuit], job_id: str, **options
261 ) -> Result:
262 start = monotonic()
263
264 compilations = self._compile(run_input, **options)
265
266 output = self._execute(compilations, **options)
267
268 if not isinstance(output, dict):
269 logger.error("%s: run failed.", self.name)
270 if output:
271 logger.error("Output: %s", output)
272 from .... import QSharpError
273
274 raise QSharpError(str(Errors.RUN_TERMINATED_WITHOUT_OUTPUT))
275
276 output["job_id"] = job_id
277 output["date"] = str(datetime.datetime.now().isoformat())
278 output["status"] = "COMPLETED"
279 output["backend_name"] = self.name
280 output["backend_version"] = self.backend_version
281
282 duration = monotonic() - start
283 output["time_taken"] = str(duration)
284 output["config"] = {
285 "qasm_export_options": str(self._build_qasm_export_options(**options)),
286 "qiskit_pass_options": str(self._build_qiskit_pass_options(**options)),
287 "transpile_options": str(self._build_transpile_options(**options)),
288 }
289 output["header"] = {}
290 return self._create_results(output)
291
292 @abstractmethod
293 def _submit_job(self, run_input: List[QuantumCircuit], **input_params) -> QsJob:
294 pass
295
296 def _compile(self, run_input: List[QuantumCircuit], **options) -> List[Compilation]:
297 # for each run input, convert to qasm
298 compilations = []
299 for circuit in run_input:
300 args = options.copy()
301 assert isinstance(
302 circuit, QuantumCircuit
303 ), "Input must be a QuantumCircuit."
304 start = monotonic()
305 qasm = self._qasm(circuit, **args)
306 end = monotonic()
307
308 time_taken = end - start
309 compilation = Compilation(circuit, qasm, time_taken)
310 compilations.append(compilation)
311 return compilations
312
313 @abstractmethod
314 def _create_results(self, output: Dict[str, Any]) -> Any:
315 pass
316
317 def _transpile(self, circuit: QuantumCircuit, **options) -> QuantumCircuit:
318 if options.get("skip_transpilation", self._skip_transpilation):
319 return circuit
320
321 circuit = self.run_qiskit_passes(circuit, options)
322
323 transpile_options = self._build_transpile_options(**options)
324 backend = transpile_options.pop("backend", self)
325 target = transpile_options.pop("target", self.target)
326 if get_version_info().startswith("1.2"):
327 # The older Qiskit version does not support the `qubits_initially_zero` option
328 transpiled_circuit = transpile(
329 circuit,
330 backend=backend,
331 target=target,
332 **transpile_options,
333 )
334 else:
335 transpiled_circuit = transpile(
336 circuit,
337 backend=backend,
338 target=target,
339 qubits_initially_zero=True,
340 **transpile_options,
341 )
342 return transpiled_circuit
343
344 def run_qiskit_passes(self, circuit, options):
345 pass_options = self._build_qiskit_pass_options(**options)
346
347 pass_manager = PassManager()
348 if not pass_options["supports_barrier"]:
349 pass_manager.append(RemoveBarriers())
350 if not pass_options["supports_delay"]:
351 pass_manager.append(RemoveDelays())
352 if pass_options["remove_reset_in_zero_state"]:
353 # when doing state initialization, qiskit will reset all qubits to 0
354 # As our semantics are different, we can remove these resets
355 # as it will double the number of qubits if we have to reset them
356 # before using them when using the base profile.
357 pass_manager.append(RemoveResetInZeroState())
358
359 circuit = pass_manager.run(circuit)
360 return circuit
361
362 def _build_qiskit_pass_options(self, **kwargs) -> Dict[str, Any]:
363 params: Dict[str, Any] = vars(self._qiskit_pass_options).copy()
364 for opt in params.copy():
365 if opt in kwargs:
366 params[opt] = kwargs.pop(opt)
367 if "supports_barrier" not in params:
368 params["supports_barrier"] = False
369 if "supports_delay" not in params:
370 params["supports_delay"] = False
371 if "remove_reset_in_zero_state" not in params:
372 params["remove_reset_in_zero_state"] = True
373
374 return params
375
376 def _build_transpile_options(self, **kwargs) -> Dict[str, Any]:
377 # create the default options from the backend
378 args = self._transpile_options.copy()
379 # gather any remaining options that are not in the default list
380 transpile_args = get_transpile_options(**kwargs)
381 args.update(transpile_args)
382 return args
383
384 def _build_qasm_export_options(self, **kwargs) -> Dict[str, Any]:
385 # Disable aliasing until we decide want to support it
386 # The exporter defaults to only having the U gate.
387 # When it sees the stdgates.inc in the default includes list, it adds
388 # bodyless symbols for that fixed gate set.
389 # We set the basis gates for any gates that we want that wouldn't
390 # be defined when stdgates.inc is included.
391
392 # any gates that are not in the stdgates.inc file need to be defined
393 # in the basis gates list passed to the exporter. The exporter doesn't
394 # know about the gates defined in the backend's target.
395 # Anything in the basis_gates gets added to the qasm builder's global
396 # namespace as an opaque gate. All parameter information comes from the
397 # gate object itself in the circuit.
398
399 # create the default options from the backend
400 args = self._qasm_export_options.copy()
401 # gather any remaining options that are not in the default list
402 exporter_args = get_exporter_options(**kwargs)
403 args.update(exporter_args)
404 return args
405
406 def transpile(self, circuit: QuantumCircuit, **options) -> QuantumCircuit:
407 transpiled_circuit = self._transpile(circuit, **options)
408 return transpiled_circuit
409
410 def _qasm(self, circuit: QuantumCircuit, **options) -> str:
411 """Converts a Qiskit QuantumCircuit to QASM 3 for the current backend.
412
413 Args:
414 circuit (QuantumCircuit): The QuantumCircuit to be executed.
415 **options: Additional options for the execution.
416 - Any options for the transpiler, exporter, or Qiskit passes
417 configuration. Defaults to backend config values. Common
418 values include: 'optimization_level', 'basis_gates',
419 'includes', 'search_path'.
420
421 Returns:
422 str: The converted QASM code as a string. Any supplied includes
423 are emitted as include statements at the top of the program.
424
425 :raises QasmError: If there is an error generating or parsing QASM.
426 """
427 transpiled_circuit = self.transpile(circuit, **options)
428 try:
429 export_options = self._build_qasm_export_options(**options)
430 exporter = Exporter(**export_options)
431 qasm3_source = exporter.dumps(transpiled_circuit)
432 # Qiskit QASM exporter doesn't handle experimental features correctly and always emits
433 # OPENQASM 3.0; even though switch case is not supported in QASM 3.0, so we bump
434 # the version to 3.1 for now.
435 qasm3_source = qasm3_source.replace("OPENQASM 3.0", "OPENQASM 3.1")
436 return qasm3_source
437 except Exception as ex:
438 from .. import QasmError
439
440 raise QasmError(str(Errors.FAILED_TO_EXPORT_QASM)) from ex
441
442 def _qsharp(self, circuit: QuantumCircuit, **kwargs) -> str:
443 """
444 Converts a Qiskit QuantumCircuit to Q# for the current backend.
445
446 The generated Q# code will not be idiomatic Q# code, but will be
447 a direct translation of the Qiskit circuit.
448
449 Args:
450 circuit (QuantumCircuit): The QuantumCircuit to be executed.
451 **options: Additional options for the execution. Defaults to backend config values.
452 - Any options for the transpiler, exporter, or Qiskit passes
453 configuration. Defaults to backend config values. Common
454 values include: 'optimization_level', 'basis_gates',
455 'includes', 'search_path'.
456 - output_semantics (OutputSemantics, optional): The output semantics for the compilation.
457 Returns:
458 str: The converted QASM code as a string. Any supplied includes
459 are emitted as include statements at the top of the program.
460
461 :raises QSharpError: If there is an error evaluating the source code.
462 :raises QasmError: If there is an error generating, parsing, or compiling QASM.
463 """
464
465 qasm_source = self._qasm(circuit, **kwargs)
466
467 args = {
468 "name": kwargs.get("name", circuit.name),
469 }
470
471 if search_path := kwargs.pop("search_path", "."):
472 args["search_path"] = search_path
473
474 if output_semantics := kwargs.pop(
475 "output_semantics", self.options.get("output_semantics", default=None)
476 ):
477 args["output_semantics"] = output_semantics
478
479 qsharp_source = self._qasm_to_qsharp(qasm_source, **args)
480 return qsharp_source
481
482 def qir(
483 self,
484 circuit: QuantumCircuit,
485 **kwargs,
486 ) -> str:
487 """
488 Converts a Qiskit QuantumCircuit to QIR (Quantum Intermediate Representation).
489
490 Args:
491 circuit ('QuantumCircuit'): The input Qiskit QuantumCircuit object.
492 **kwargs: Additional options for the execution.
493 - params (str, optional): The entry expression for the QIR conversion. Defaults to None.
494 - target_profile (TargetProfile, optional): The target profile for the backend. Defaults to backend config value.
495 - output_semantics (OutputSemantics, optional): The output semantics for the compilation. Defaults to backend config value.
496 - search_path (str, optional): The search path for the backend. Defaults to '.'.
497 Returns:
498 str: The converted QIR code as a string.
499
500 :raises QSharpError: If there is an error evaluating the source code.
501 :raises QasmError: If there is an error generating, parsing, or compiling QASM.
502 :raises ValueError: If the backend configuration does not support QIR generation.
503 """
504 name = kwargs.pop("name", circuit.name)
505 target_profile = kwargs.pop("target_profile", self.options.target_profile)
506 if target_profile == TargetProfile.Unrestricted:
507 raise ValueError(str(Errors.UNRESTRICTED_INVALID_QIR_TARGET))
508
509 qasm_source = self._qasm(circuit, **kwargs)
510
511 args = {
512 "name": name,
513 "target_profile": target_profile,
514 }
515
516 if search_path := kwargs.pop("search_path", "."):
517 args["search_path"] = search_path
518
519 if params := kwargs.pop("params", None):
520 args["params"] = params
521
522 if output_semantics := kwargs.pop(
523 "output_semantics", self.options.get("output_semantics", default=None)
524 ):
525 args["output_semantics"] = output_semantics
526
527 return self._qasm_to_qir(qasm_source, **args)
528
529 def _qasm_to_qir(
530 self,
531 source: str,
532 **kwargs,
533 ) -> str:
534 from ...._native import compile_qasm_program_to_qir
535 from ...._fs import read_file, list_directory, resolve
536 from ...._http import fetch_github
537
538 return compile_qasm_program_to_qir(
539 source,
540 read_file,
541 list_directory,
542 resolve,
543 fetch_github,
544 **kwargs,
545 )
546
547 def _qasm_to_qsharp(
548 self,
549 source: str,
550 **kwargs,
551 ) -> str:
552 from ...._native import compile_qasm_to_qsharp
553 from ...._fs import read_file, list_directory, resolve
554 from ...._http import fetch_github
555
556 return compile_qasm_to_qsharp(
557 source,
558 read_file,
559 list_directory,
560 resolve,
561 fetch_github,
562 **kwargs,
563 )