|
| 1 | +from __future__ import annotations |
| 2 | +from typing import TYPE_CHECKING |
| 3 | +import logging |
| 4 | + |
| 5 | +from qiskit_aer import AerSimulator |
| 6 | +from qiskit_aer.noise import NoiseModel |
| 7 | +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager |
| 8 | +from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService |
| 9 | + |
| 10 | +from graphix_ibmq.compile_options import IBMQCompileOptions |
| 11 | +from graphix_ibmq.compiler import IBMQPatternCompiler, IBMQCompiledCircuit |
| 12 | +from graphix_ibmq.job import IBMQJob |
| 13 | + |
| 14 | +if TYPE_CHECKING: |
| 15 | + from graphix.pattern import Pattern |
| 16 | + from qiskit.providers.backend import BackendV2 |
| 17 | + |
| 18 | +logger = logging.getLogger(__name__) |
| 19 | + |
| 20 | + |
| 21 | +class IBMQBackend: |
| 22 | + """ |
| 23 | + Manages compilation and execution on IBMQ simulators or hardware. |
| 24 | +
|
| 25 | + This class configures the execution target and provides methods to compile |
| 26 | + a graphix Pattern and submit it as a job. Instances should be created using |
| 27 | + the `from_simulator` or `from_hardware` classmethods. |
| 28 | + """ |
| 29 | + |
| 30 | + def __init__(self, backend: BackendV2 | None = None, options: IBMQCompileOptions | None = None) -> None: |
| 31 | + if backend is None or options is None: |
| 32 | + raise TypeError( |
| 33 | + "IBMQBackend cannot be instantiated directly. " |
| 34 | + "Please use the classmethods `IBMQBackend.from_simulator()` " |
| 35 | + "or `IBMQBackend.from_hardware()`." |
| 36 | + ) |
| 37 | + self._backend: BackendV2 = backend |
| 38 | + self._options: IBMQCompileOptions = options |
| 39 | + |
| 40 | + @classmethod |
| 41 | + def from_simulator( |
| 42 | + cls, |
| 43 | + noise_model: NoiseModel | None = None, |
| 44 | + from_backend: BackendV2 | None = None, |
| 45 | + options: IBMQCompileOptions | None = None, |
| 46 | + ) -> IBMQBackend: |
| 47 | + """Creates an instance with a local Aer simulator as the backend. |
| 48 | +
|
| 49 | + Parameters |
| 50 | + ---------- |
| 51 | + noise_model : NoiseModel, optional |
| 52 | + A custom noise model for the simulation. |
| 53 | + from_backend : BackendV2, optional |
| 54 | + A hardware backend to base the noise model on. |
| 55 | + Ignored if `noise_model` is provided. |
| 56 | + options : IBMQCompileOptions, optional |
| 57 | + Compilation and execution options. |
| 58 | + """ |
| 59 | + if noise_model is None and from_backend is not None: |
| 60 | + noise_model = NoiseModel.from_backend(from_backend) |
| 61 | + |
| 62 | + aer_backend = AerSimulator(noise_model=noise_model) |
| 63 | + compile_options = options if options is not None else IBMQCompileOptions() |
| 64 | + |
| 65 | + logger.info("Backend set to local AerSimulator.") |
| 66 | + return cls(backend=aer_backend, options=compile_options) |
| 67 | + |
| 68 | + @classmethod |
| 69 | + def from_hardware( |
| 70 | + cls, |
| 71 | + name: str | None = None, |
| 72 | + min_qubits: int = 1, |
| 73 | + options: IBMQCompileOptions | None = None, |
| 74 | + ) -> IBMQBackend: |
| 75 | + """Creates an instance with a real IBM Quantum hardware device as the backend. |
| 76 | +
|
| 77 | + Parameters |
| 78 | + ---------- |
| 79 | + name : str, optional |
| 80 | + The specific name of the device (e.g., 'ibm_brisbane'). If None, |
| 81 | + the least busy backend with at least `min_qubits` will be selected. |
| 82 | + min_qubits : int |
| 83 | + The minimum number of qubits required. |
| 84 | + options : IBMQCompileOptions, optional |
| 85 | + Compilation and execution options. |
| 86 | + """ |
| 87 | + service = QiskitRuntimeService() |
| 88 | + if name: |
| 89 | + hw_backend = service.backend(name) |
| 90 | + else: |
| 91 | + hw_backend = service.least_busy(min_num_qubits=min_qubits, operational=True) |
| 92 | + |
| 93 | + compile_options = options if options is not None else IBMQCompileOptions() |
| 94 | + |
| 95 | + logger.info("Selected hardware backend: %s", hw_backend.name) |
| 96 | + return cls(backend=hw_backend, options=compile_options) |
| 97 | + |
| 98 | + @staticmethod |
| 99 | + def compile(pattern: Pattern, save_statevector: bool = False) -> IBMQCompiledCircuit: |
| 100 | + """Compiles a graphix pattern into a Qiskit QuantumCircuit. |
| 101 | +
|
| 102 | + This method is provided as a staticmethod because it does not depend |
| 103 | + on the backend's state. |
| 104 | +
|
| 105 | + Parameters |
| 106 | + ---------- |
| 107 | + pattern : Pattern |
| 108 | + The graphix pattern to compile. |
| 109 | + save_statevector : bool |
| 110 | + If True, saves the statevector before the final measurement. |
| 111 | +
|
| 112 | + Returns |
| 113 | + ------- |
| 114 | + IBMQCompiledCircuit |
| 115 | + An object containing the compiled circuit and related metadata. |
| 116 | + """ |
| 117 | + compiler = IBMQPatternCompiler(pattern) |
| 118 | + return compiler.compile(save_statevector=save_statevector) |
| 119 | + |
| 120 | + def submit_job(self, compiled_circuit: IBMQCompiledCircuit, shots: int = 1024) -> IBMQJob: |
| 121 | + """ |
| 122 | + Submits the compiled circuit to the configured backend for execution. |
| 123 | +
|
| 124 | + Parameters |
| 125 | + ---------- |
| 126 | + compiled_circuit : IBMQCompiledCircuit |
| 127 | + The compiled circuit object from the `compile` method. |
| 128 | + shots : int, optional |
| 129 | + The number of execution shots. Defaults to 1024. |
| 130 | +
|
| 131 | + Returns |
| 132 | + ------- |
| 133 | + IBMQJob |
| 134 | + A job object to monitor execution and retrieve results. |
| 135 | + """ |
| 136 | + pass_manager = generate_preset_pass_manager( |
| 137 | + backend=self._backend, |
| 138 | + optimization_level=self._options.optimization_level, |
| 139 | + ) |
| 140 | + transpiled_circuit = pass_manager.run(compiled_circuit.circuit) |
| 141 | + |
| 142 | + sampler = Sampler(mode=self._backend) |
| 143 | + job = sampler.run([transpiled_circuit], shots=shots) |
| 144 | + |
| 145 | + return IBMQJob(job, compiled_circuit) |
0 commit comments