|
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