#!/usr/bin/env python3
#
# Copyright 2002-2025 Barcelona Supercomputing Center (www.bsc.es)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Lazy, user-extensible registry of Qdislib backends.
Backends are registered by name -> (module path, class name) and imported only
on first use, so a backend's optional third-party dependencies (qiskit-aer,
iqm, ...) are not required until that backend is actually built.
"""
import importlib
import logging
from Qdislib.core.backend.base_backend import BaseBackend, BackendConfig
logger = logging.getLogger(__name__)
DEFAULT_BACKEND_NAME = "AER_BACKEND"
DEFAULT_NUM_QUBITS = 30
_BACKEND_REGISTRY: dict[str, tuple[str, str]] = {
"AER_BACKEND": ("Qdislib.core.backend.aer_backend", "AerBackend"),
"QIBO_BACKEND": ("Qdislib.core.backend.qibo_backend", "QiboBackend"),
"CUDAQ_BACKEND": ("Qdislib.core.backend.cudaq_backend", "CudaqBackend"),
"PENNYLANE_BACKEND": ("Qdislib.core.backend.pennylane_backend", "PennylaneBackend"),
"IQM_BACKEND": ("Qdislib.core.backend.iqm_backend", "IQMBackend"),
"BSC_QPU_BACKEND": ("Qdislib.core.backend.qpu_backends", "BSCQpuBackend"),
"IBM_BACKEND": ("Qdislib.core.backend.qpu_backends", "IBMBackend"),
}
[docs]
def register_backend(name: str, module_path: str, class_name: str) -> None:
"""Register a backend without importing it.
Intended for user-defined backends or third-party extensions::
register_backend("MY_BACKEND", "my_package.my_backend", "MyBackend")
"""
_BACKEND_REGISTRY[name] = (module_path, class_name)
logger.debug("Backend '%s' registered (imported on first use)", name)
[docs]
def available_backends() -> list[str]:
"""Return the names of all registered backends."""
return list(_BACKEND_REGISTRY)
#: Maps the algorithms' ``software=`` strings to registered backend names, so a
#: cutting call can also name any user-registered backend directly.
_SOFTWARE_ALIAS = {
"qibo": "QIBO_BACKEND",
"qiskit": "AER_BACKEND",
"cudaq": "CUDAQ_BACKEND",
"pennylane": "PENNYLANE_BACKEND",
}
[docs]
def build_execution_backend(
software: str | None = None,
num_qubits: int = 1,
*,
gpu: bool = False,
gpu_min_qubits: int | None = None,
qpu: bool = False,
qpu_dict: dict | None = None,
max_qubit: int | None = None,
batch=None,
ibm_backend=None,
**extra,
) -> "BaseBackend":
"""Select and build the backend that evaluates a cut subcircuit.
The single place that turns the cutting algorithms' execution knobs
(``software``, ``gpu``, ``qpu``/``qpu_dict``) into a concrete backend, so the
algorithms never branch on the backend/device. Selection order: a large-enough
configured QPU (BSC ``MN_Ona`` -> ``BSC_QPU_BACKEND``, ``IBM_Quantum`` ->
``IBM_BACKEND``), then GPU-eligible Aer, then the ``software`` backend
(``qibo``/``qiskit``/``cudaq`` or the name of any registered backend). Extra
keyword arguments (e.g. ``platform`` for Qibo) flow into the ``BackendConfig``.
"""
size = max(int(num_qubits or 1), 1)
qpu_dict = qpu_dict or {}
fit = max_qubit if max_qubit is not None else size
if qpu and "MN_Ona" in qpu_dict and qpu_dict["MN_Ona"] >= fit:
return build_backend(
"BSC_QPU_BACKEND", BackendConfig(num_qubits=size, extra=extra)
)
if qpu and "IBM_Quantum" in qpu_dict and qpu_dict["IBM_Quantum"] >= fit:
return build_backend(
"IBM_BACKEND",
BackendConfig(
num_qubits=size, extra={**extra, "batch": batch, "backend": ibm_backend}
),
)
if gpu and gpu_min_qubits is not None and gpu_min_qubits <= fit <= 30:
return build_backend(
"AER_BACKEND",
BackendConfig(num_qubits=size, extra={**extra, "use_gpu": True}),
)
name = _SOFTWARE_ALIAS.get(software, software or DEFAULT_BACKEND_NAME)
return build_backend(name, BackendConfig(num_qubits=size, extra=extra))
[docs]
def build_backend(name: str, config: BackendConfig) -> BaseBackend:
"""Lazily import and instantiate a registered backend.
The backend module -- and its third-party dependencies -- are imported only
at this point, not at registry import time.
"""
if name not in _BACKEND_REGISTRY:
raise ValueError(
f"Backend '{name}' is not registered. Available: {available_backends()}"
)
module_path, class_name = _BACKEND_REGISTRY[name]
try:
module = importlib.import_module(module_path)
except ImportError as exc:
raise ImportError(
f"Could not import backend '{name}' from '{module_path}'. "
f"Its optional dependency may be missing."
) from exc
cls: type[BaseBackend] = getattr(module, class_name)
return cls(config)