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)