|
21 | 21 | from azure.quantum.job.session import SessionHost
|
22 | 22 |
|
23 | 23 | try:
|
24 |
| - from qiskit import QuantumCircuit, transpile |
| 24 | + from qiskit import QuantumCircuit |
25 | 25 | from qiskit.providers import BackendV1 as Backend
|
26 | 26 | from qiskit.providers import Options
|
27 | 27 | from qiskit.providers import Provider
|
28 | 28 | from qiskit.providers.models import BackendConfiguration
|
29 | 29 | from qiskit.qobj import QasmQobj, PulseQobj
|
30 |
| - from pyqir import Module |
31 |
| - from qiskit_qir import to_qir_module |
| 30 | + import pyqir as pyqir |
| 31 | + from qsharp.interop.qiskit import QSharpBackend |
| 32 | + from qsharp import TargetProfile |
32 | 33 |
|
33 | 34 | except ImportError:
|
34 | 35 | raise ImportError(
|
35 | 36 | "Missing optional 'qiskit' dependencies. \
|
36 | 37 | To install run: pip install azure-quantum[qiskit]"
|
37 | 38 | )
|
38 | 39 |
|
| 40 | +# barrier is handled by an extra flag which will transpile |
| 41 | +# them away if the backend doesn't support them. This has |
| 42 | +# to be done as a special pass as the transpiler will not |
| 43 | +# remove barriers by default. |
39 | 44 | QIR_BASIS_GATES = [
|
40 |
| - "x", |
41 |
| - "y", |
42 |
| - "z", |
| 45 | + "measure", |
| 46 | + "reset", |
| 47 | + "ccx", |
| 48 | + "cx", |
| 49 | + "cy", |
| 50 | + "cz", |
43 | 51 | "rx",
|
| 52 | + "rxx", |
| 53 | + "crx", |
44 | 54 | "ry",
|
| 55 | + "ryy", |
| 56 | + "cry", |
45 | 57 | "rz",
|
| 58 | + "rzz", |
| 59 | + "crz", |
46 | 60 | "h",
|
47 |
| - "swap", |
48 |
| - "cx", |
49 |
| - "cz", |
50 |
| - "reset", |
51 | 61 | "s",
|
52 | 62 | "sdg",
|
| 63 | + "swap", |
53 | 64 | "t",
|
54 | 65 | "tdg",
|
55 |
| - "measure", |
| 66 | + "x", |
| 67 | + "y", |
| 68 | + "z", |
| 69 | + "id", |
| 70 | + "ch", |
56 | 71 | ]
|
57 | 72 |
|
58 | 73 |
|
@@ -391,98 +406,141 @@ def _prepare_job_metadata(self, circuits: List[QuantumCircuit]) -> Dict[str, str
|
391 | 406 | return {}
|
392 | 407 |
|
393 | 408 | def _generate_qir(
|
394 |
| - self, circuits, targetCapability, **to_qir_kwargs |
395 |
| - ) -> Tuple[Module, List[str]]: |
| 409 | + self, circuits: List[QuantumCircuit], target_profile: TargetProfile, **kwargs |
| 410 | + ) -> pyqir.Module: |
| 411 | + |
| 412 | + if len(circuits) == 0: |
| 413 | + raise ValueError("No QuantumCircuits provided") |
396 | 414 |
|
397 | 415 | config = self.configuration()
|
398 | 416 | # Barriers aren't removed by transpilation and must be explicitly removed in the Qiskit to QIR translation.
|
399 |
| - emit_barrier_calls = "barrier" in config.basis_gates |
400 |
| - return to_qir_module( |
401 |
| - circuits, |
402 |
| - targetCapability, |
403 |
| - emit_barrier_calls=emit_barrier_calls, |
404 |
| - **to_qir_kwargs, |
| 417 | + supports_barrier = "barrier" in config.basis_gates |
| 418 | + skip_transpilation = kwargs.pop("skip_transpilation", False) |
| 419 | + |
| 420 | + backend = QSharpBackend( |
| 421 | + qiskit_pass_options={"supports_barrier": supports_barrier}, |
| 422 | + target_profile=target_profile, |
| 423 | + skip_transpilation=skip_transpilation, |
| 424 | + **kwargs, |
405 | 425 | )
|
406 | 426 |
|
407 |
| - def _get_qir_str(self, circuits, targetCapability, **to_qir_kwargs) -> str: |
408 |
| - module, _ = self._generate_qir(circuits, targetCapability, **to_qir_kwargs) |
| 427 | + name = "batch" |
| 428 | + if len(circuits) == 1: |
| 429 | + name = circuits[0].name |
| 430 | + |
| 431 | + if isinstance(circuits, list): |
| 432 | + for value in circuits: |
| 433 | + if not isinstance(value, QuantumCircuit): |
| 434 | + raise ValueError("Input must be List[QuantumCircuit]") |
| 435 | + else: |
| 436 | + raise ValueError("Input must be List[QuantumCircuit]") |
| 437 | + |
| 438 | + context = pyqir.Context() |
| 439 | + llvm_module = pyqir.qir_module(context, name) |
| 440 | + for circuit in circuits: |
| 441 | + qir_str = backend.qir(circuit) |
| 442 | + module = pyqir.Module.from_ir(context, qir_str) |
| 443 | + entry_point = next(filter(pyqir.is_entry_point, module.functions)) |
| 444 | + entry_point.name = circuit.name |
| 445 | + llvm_module.link(module) |
| 446 | + err = llvm_module.verify() |
| 447 | + if err is not None: |
| 448 | + raise Exception(err) |
| 449 | + |
| 450 | + return llvm_module |
| 451 | + |
| 452 | + def _get_qir_str( |
| 453 | + self, |
| 454 | + circuits: List[QuantumCircuit], |
| 455 | + target_profile: TargetProfile, |
| 456 | + **to_qir_kwargs, |
| 457 | + ) -> str: |
| 458 | + module = self._generate_qir(circuits, target_profile, **to_qir_kwargs) |
409 | 459 | return str(module)
|
410 | 460 |
|
411 | 461 | def _translate_input(
|
412 |
| - self, circuits: List[QuantumCircuit], input_params: Dict[str, Any] |
| 462 | + self, circuits: Union[QuantumCircuit, List[QuantumCircuit]], input_params: Dict[str, Any] |
413 | 463 | ) -> bytes:
|
414 | 464 | """Translates the input values to the QIR expected by the Backend."""
|
415 | 465 | logger.info(f"Using QIR as the job's payload format.")
|
416 |
| - config = self.configuration() |
| 466 | + if not (isinstance(circuits, list)): |
| 467 | + circuits = [circuits] |
417 | 468 |
|
418 |
| - # Override QIR translation parameters |
419 |
| - # We will record the output by default, but allow the backend to override this, and allow the user to override the backend. |
420 |
| - to_qir_kwargs = input_params.pop( |
421 |
| - "to_qir_kwargs", config.azure.get("to_qir_kwargs", {"record_output": True}) |
422 |
| - ) |
423 |
| - targetCapability = input_params.pop( |
424 |
| - "targetCapability", |
425 |
| - self.options.get("targetCapability", "AdaptiveExecution"), |
426 |
| - ) |
| 469 | + target_profile = self._get_target_profile(input_params) |
427 | 470 |
|
428 | 471 | if logger.isEnabledFor(logging.DEBUG):
|
429 |
| - qir = self._get_qir_str(circuits, targetCapability, **to_qir_kwargs) |
| 472 | + qir = self._get_qir_str(circuits, target_profile, skip_transpilation=True) |
430 | 473 | logger.debug(f"QIR:\n{qir}")
|
431 | 474 |
|
432 | 475 | # We'll transpile automatically to the supported gates in QIR unless explicitly skipped.
|
433 |
| - if not input_params.pop("skipTranspile", False): |
434 |
| - # Set of gates supported by QIR targets. |
435 |
| - circuits = transpile( |
436 |
| - circuits, basis_gates=config.basis_gates, optimization_level=0 |
437 |
| - ) |
| 476 | + skip_transpilation = input_params.pop("skipTranspile", False) |
| 477 | + |
| 478 | + module = self._generate_qir( |
| 479 | + circuits, target_profile, skip_transpilation=skip_transpilation |
| 480 | + ) |
| 481 | + |
| 482 | + def get_func_name(func: pyqir.Function) -> str: |
| 483 | + return func.name |
| 484 | + |
| 485 | + entry_points = list( |
| 486 | + map(get_func_name, filter(pyqir.is_entry_point, module.functions)) |
| 487 | + ) |
| 488 | + |
| 489 | + if not skip_transpilation: |
438 | 490 | # We'll only log the QIR again if we performed a transpilation.
|
439 | 491 | if logger.isEnabledFor(logging.DEBUG):
|
440 |
| - qir = self._get_qir_str(circuits, targetCapability, **to_qir_kwargs) |
| 492 | + qir = str(module) |
441 | 493 | logger.debug(f"QIR (Post-transpilation):\n{qir}")
|
442 | 494 |
|
443 |
| - (module, entry_points) = self._generate_qir( |
444 |
| - circuits, targetCapability, **to_qir_kwargs |
445 |
| - ) |
446 |
| - |
447 |
| - if not "items" in input_params: |
| 495 | + if "items" not in input_params: |
448 | 496 | arguments = input_params.pop("arguments", [])
|
449 | 497 | input_params["items"] = [
|
450 | 498 | {"entryPoint": name, "arguments": arguments} for name in entry_points
|
451 | 499 | ]
|
452 | 500 |
|
453 |
| - return module.bitcode |
| 501 | + return str(module).encode("utf-8") |
454 | 502 |
|
455 |
| - def _estimate_cost_qir(self, circuits, shots, options={}): |
| 503 | + def _estimate_cost_qir( |
| 504 | + self, circuits: Union[QuantumCircuit, List[QuantumCircuit]], shots, options={} |
| 505 | + ): |
456 | 506 | """Estimate the cost for the given circuit."""
|
457 |
| - config = self.configuration() |
458 | 507 | input_params = self._get_input_params(options, shots=shots)
|
459 | 508 |
|
460 | 509 | if not (isinstance(circuits, list)):
|
461 | 510 | circuits = [circuits]
|
462 |
| - |
463 |
| - to_qir_kwargs = input_params.pop( |
464 |
| - "to_qir_kwargs", config.azure.get("to_qir_kwargs", {"record_output": True}) |
465 |
| - ) |
466 |
| - targetCapability = input_params.pop( |
467 |
| - "targetCapability", |
468 |
| - self.options.get("targetCapability", "AdaptiveExecution"), |
469 |
| - ) |
470 |
| - |
471 |
| - if not input_params.pop("skipTranspile", False): |
472 |
| - # Set of gates supported by QIR targets. |
473 |
| - circuits = transpile( |
474 |
| - circuits, basis_gates=config.basis_gates, optimization_level=0 |
475 |
| - ) |
476 |
| - |
477 | 511 |
|
478 |
| - (module, _) = self._generate_qir( |
479 |
| - circuits, targetCapability, **to_qir_kwargs |
| 512 | + skip_transpilation = input_params.pop("skipTranspile", False) |
| 513 | + target_profile = self._get_target_profile(input_params) |
| 514 | + module = self._generate_qir( |
| 515 | + circuits, target_profile, skip_transpilation=skip_transpilation |
480 | 516 | )
|
481 |
| - |
| 517 | + |
482 | 518 | workspace = self.provider().get_workspace()
|
483 | 519 | target = workspace.get_targets(self.name())
|
484 | 520 | return target.estimate_cost(module, shots=shots)
|
485 | 521 |
|
| 522 | + def _get_target_profile(self, input_params) -> TargetProfile: |
| 523 | + # Default to Adaptive_RI if not specified on the backend |
| 524 | + # this is really just a safeguard in case the backend doesn't have a default |
| 525 | + default_profile = self.options.get("target_profile", TargetProfile.Adaptive_RI) |
| 526 | + |
| 527 | + # If the user is using the old targetCapability parameter, we'll warn them |
| 528 | + # and use that value for now. This will be removed in the future. |
| 529 | + if "targetCapability" in input_params: |
| 530 | + warnings.warn( |
| 531 | + "The 'targetCapability' parameter is deprecated and will be ignored in the future. " |
| 532 | + "Please, use 'target_profile' parameter instead.", |
| 533 | + category=DeprecationWarning, |
| 534 | + ) |
| 535 | + cap = input_params.pop("targetCapability") |
| 536 | + if cap == "AdaptiveExecution": |
| 537 | + default_profile = TargetProfile.Adaptive_RI |
| 538 | + else: |
| 539 | + default_profile = TargetProfile.Base |
| 540 | + # If the user specifies a target profile, use that. |
| 541 | + # Otherwise, use the profile we got from the backend/targetCapability. |
| 542 | + return input_params.pop("target_profile", default_profile) |
| 543 | + |
486 | 544 |
|
487 | 545 | class AzureBackend(AzureBackendBase):
|
488 | 546 | """Base class for interfacing with a backend in Azure Quantum"""
|
|
0 commit comments