Source code for qlauncher.routines.qiskit.backends.qiskit_backend

  1"""Base backend class for Qiskit routines."""
  2
  3import types
  4import typing
  5from typing import Literal
  6from warnings import warn
  7
  8import qiskit
  9from qiskit import QuantumCircuit, qasm2
 10from qiskit.primitives import BackendEstimatorV2, BackendSamplerV2, Sampler, StatevectorEstimator, StatevectorSampler
 11from qiskit.providers import BackendV1, BackendV2
 12from qiskit.quantum_info import SparsePauliOp
 13from qiskit_ibm_runtime import Options
 14
 15from qlauncher.base import Backend
 16from qlauncher.routines.circuits import CIRCUIT_FORMATS
 17from qlauncher.routines.qiskit.adapters import SamplerV2ToSamplerV1Adapter, TranslatingSampler
 18from qlauncher.routines.qiskit.backends.gate_circuit_backend import GateCircuitBackend
 19from qlauncher.routines.qiskit.backends.utils import (
 20    AUTO_TRANSPILE_ESTIMATOR_TYPE,
 21    AUTO_TRANSPILE_SAMPLER_TYPE,
 22    set_estimator_auto_run_behavior,
 23    set_sampler_auto_run_behavior,
 24)
 25from qlauncher.routines.qiskit.mitigation_suppression.base import CircuitExecutionMethod
 26from qlauncher.routines.qiskit.mitigation_suppression.mitigation import NoMitigation
 27
 28
[docs] 29class QiskitBackend(GateCircuitBackend[QuantumCircuit]): 30 """ 31 Base class for backends compatible with qiskit. 32 33 Attributes: 34 name (str): The name of the backend. 35 options (Options | None, optional): The options for the backend. Defaults to None. 36 backendv1v2 (BackendV1 | BackendV2 | None, optional): Predefined backend to use with name 'backendv1v2'. Defaults to None. 37 sampler (BaseSamplerV2): The sampler used for sampling. 38 estimator (BaseEstimatorV2): The estimator used for estimation. 39 """ 40 41 basis_gates = ['x', 'y', 'z', 'cx', 'h', 'rx', 'ry', 'rz', 'u'] 42 43 def __init__( 44 self, 45 name: Literal['local_simulator', 'backendv1v2'] | str = 'local_simulator', 46 options: Options | None = None, 47 backendv1v2: BackendV1 | BackendV2 | None = None, 48 auto_transpile_level: Literal[0, 1, 2, 3] | None = None, 49 error_mitigation_strategy: CircuitExecutionMethod | None = None, 50 ) -> None: 51 """ 52 Args: 53 **name (Literal['local_simulator', 'backendv1v2'] | str)**: Name or mode of operation. Defaults to local_simulator, 54 'backendv1v2' allows for using a specific backend simulator. 55 **options (Options | None, optional)**: Defaults to None. 56 **backendv1v2 (BackendV1 | BackendV2 | None, optional)**: 57 Used with name 'backendv1v2', sampler and estimator will use it. Defaults to None. 58 **auto_transpile_level (Literal[0, 1, 2, 3] | None, optional)**: 59 Optimization level for automatic transpilation of circuits. 60 - None: Don't transpile. 61 - 0: No optimization (only transpile to compatible gates). 62 - 1: Light optimization. 63 - 2: Heavy optimization. 64 - 3: Heaviest optimization. 65 Defaults to None. 66 """ 67 super().__init__(name) 68 self.options = options 69 self.backendv1v2 = backendv1v2 70 self._auto_transpile_level = auto_transpile_level 71 self._auto_assign = False 72 self._samplerV1: Sampler | None = None 73 if error_mitigation_strategy is None: 74 self._mitigation_strategy = NoMitigation() 75 else: 76 if backendv1v2 is None: 77 warn('Running mitigation without a set backendv1v2 is not supported, ignoring.') 78 self._mitigation_strategy = NoMitigation() 79 else: 80 self._mitigation_strategy = error_mitigation_strategy 81 self._set_primitives_on_backend_name() 82 83 @property 84 def samplerV1(self) -> Sampler: 85 if self._samplerV1 is None: 86 self._samplerV1 = SamplerV2ToSamplerV1Adapter(self.sampler) 87 return self._samplerV1 88 89 def _set_primitives_on_backend_name(self) -> None: 90 if self.name == 'local_simulator': 91 self.estimator = StatevectorEstimator() 92 self.sampler = StatevectorSampler() 93 elif self.name == 'backendv1v2': 94 if self.backendv1v2 is None: 95 raise AttributeError('Please indicate a backend when in backendv1v2 mode.') 96 self.estimator = BackendEstimatorV2(backend=self.backendv1v2) 97 self.sampler = BackendSamplerV2(backend=self.backendv1v2) 98 99 else: 100 raise ValueError(f"Unsupported mode for this backend:'{self.name}'") 101 102 self._configure_auto_behavior() 103 self.sampler = TranslatingSampler(self.sampler, self.compatible_circuit) 104 105 def _configure_auto_behavior(self) -> None: 106 """ 107 Set auto transpilation and/or auto assignment if turned on, on estimator and sampler if compatible. 108 """ 109 do_transpile, level = ( 110 self._auto_transpile_level is not None, 111 int(self._auto_transpile_level if self._auto_transpile_level is not None else 0), 112 ) 113 if isinstance(self.estimator, AUTO_TRANSPILE_ESTIMATOR_TYPE.__constraints__): 114 self.estimator = set_estimator_auto_run_behavior( 115 self.estimator, auto_transpile=do_transpile, auto_transpile_level=level, auto_assign=self._auto_assign 116 ) 117 if isinstance(self.sampler, AUTO_TRANSPILE_SAMPLER_TYPE.__constraints__): 118 self.sampler = set_sampler_auto_run_behavior( 119 self.sampler, auto_transpile=do_transpile, auto_transpile_level=level, auto_assign=self._auto_assign 120 ) 121
[docs] 122 @staticmethod 123 def to_qasm(circuit: qiskit.QuantumCircuit) -> str: 124 return qasm2.dumps(circuit)
125
[docs] 126 @staticmethod 127 def from_qasm(qasm: str, name: str = 'Qasm Circuit') -> qiskit.QuantumCircuit: 128 circuit = qiskit.QuantumCircuit.from_qasm_str(qasm) 129 circuit.name = name 130 return circuit
131
[docs] 132 def sample_circuit(self, circuit: CIRCUIT_FORMATS, shots: int = 1024) -> dict[str, int]: 133 compatible_circuit = self._mitigation_strategy.compatible_circuit 134 if not isinstance(circuit, compatible_circuit): 135 if isinstance(compatible_circuit, types.UnionType): 136 compatible_circuit = typing.get_args(compatible_circuit)[0] 137 circuit = GateCircuitBackend.get_translation(circuit, compatible_circuit) 138 139 return self._mitigation_strategy.sample(circuit, self, shots)
140
[docs] 141 def estimate_energy(self, circuit: QuantumCircuit, observable: SparsePauliOp) -> float: 142 return self._mitigation_strategy.estimate(circuit, observable, self)