Skip to content

Commit f0b9de2

Browse files
committed
Remove BackendConfig
1 parent 87ba931 commit f0b9de2

File tree

6 files changed

+359
-135
lines changed

6 files changed

+359
-135
lines changed

azure-quantum/azure/quantum/qiskit/backends/backend.py

Lines changed: 209 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
##
5+
import copy
56
import os
67
import json
78

@@ -10,8 +11,9 @@
1011

1112
logger = logging.getLogger(__name__)
1213

14+
from dataclasses import dataclass, field
1315
from functools import lru_cache
14-
from typing import Any, Dict, Union, List, Optional, TYPE_CHECKING
16+
from typing import Any, Dict, Union, List, Optional, TYPE_CHECKING, Tuple, Mapping
1517
from azure.quantum.version import __version__
1618
from azure.quantum.qiskit.job import (
1719
MICROSOFT_OUTPUT_DATA_FORMAT,
@@ -20,6 +22,9 @@
2022
from abc import abstractmethod
2123
from azure.quantum.job.session import SessionHost
2224

25+
BackendConfigurationType = None
26+
QOBJ_TYPES: Tuple[type, ...] = tuple()
27+
2328
if TYPE_CHECKING:
2429
from azure.quantum import Workspace
2530

@@ -28,19 +33,29 @@
2833
from qiskit.providers import BackendV2 as Backend
2934
from qiskit.providers import Options
3035
from qiskit.providers import Provider
31-
from qiskit.providers.models import BackendConfiguration
32-
from qiskit.qobj import QasmQobj, PulseQobj
3336
from qiskit.transpiler import Target
3437
from qiskit.circuit import Instruction, Parameter
3538
from qiskit.circuit.library.standard_gates import get_standard_gate_name_mapping
3639
from qsharp.interop.qiskit import QSharpBackend
3740
from qsharp import TargetProfile
38-
39-
except ImportError:
41+
except ImportError as exc:
4042
raise ImportError(
4143
"Missing optional 'qiskit' dependencies. \
4244
To install run: pip install azure-quantum[qiskit]"
43-
)
45+
) from exc
46+
47+
try: # Qiskit 1.x legacy support
48+
from qiskit.providers.models import BackendConfiguration # type: ignore
49+
BackendConfigurationType = BackendConfiguration
50+
51+
from qiskit.qobj import QasmQobj, PulseQobj # type: ignore
52+
except ImportError: # Qiskit 2.0 removes qobj module
53+
QasmQobj = None # type: ignore
54+
PulseQobj = None # type: ignore
55+
56+
QOBJ_TYPES = tuple(
57+
obj_type for obj_type in (QasmQobj, PulseQobj) if obj_type is not None
58+
)
4459

4560
# barrier is handled by an extra flag which will transpile
4661
# them away if the backend doesn't support them. This has
@@ -148,6 +163,141 @@ def _resolve_instruction(gate_name: str) -> Optional[Instruction]:
148163
return Instruction(gate_name, 1, 0, params=[])
149164

150165

166+
@dataclass
167+
class AzureBackendConfig:
168+
"""Lightweight configuration container for Azure Quantum backends."""
169+
170+
backend_name: Optional[str] = None
171+
backend_version: Optional[str] = None
172+
description: Optional[str] = None
173+
max_experiments: Optional[int] = None
174+
max_experiments_provided: bool = False
175+
n_qubits: Optional[int] = None
176+
dt: Optional[float] = None
177+
basis_gates: Tuple[str, ...] = field(default_factory=tuple)
178+
azure: Dict[str, Any] = field(default_factory=dict)
179+
metadata: Dict[str, Any] = field(default_factory=dict)
180+
181+
def __post_init__(self) -> None:
182+
self.azure = copy.deepcopy(self.azure or {})
183+
self.metadata = dict(self.metadata or {})
184+
if self.basis_gates is None:
185+
self.basis_gates = tuple()
186+
else:
187+
self.basis_gates = tuple(self.basis_gates)
188+
if not self.max_experiments_provided and self.max_experiments is not None:
189+
self.max_experiments_provided = True
190+
191+
@property
192+
def name(self) -> Optional[str]:
193+
return self.backend_name
194+
195+
@property
196+
def max_circuits(self) -> Optional[int]:
197+
return self.max_experiments
198+
199+
@property
200+
def num_qubits(self) -> Optional[int]:
201+
"""Backward-compatible alias for Qiskit's ``BackendConfiguration.num_qubits``."""
202+
return self.n_qubits
203+
204+
@num_qubits.setter
205+
def num_qubits(self, value: Optional[int]) -> None:
206+
self.n_qubits = value
207+
208+
def get(self, key: str, default: Any = None) -> Any:
209+
if key == "basis_gates":
210+
return list(self.basis_gates)
211+
if key == "azure":
212+
return copy.deepcopy(self.azure)
213+
if hasattr(self, key):
214+
return getattr(self, key)
215+
return self.metadata.get(key, default)
216+
217+
def __getattr__(self, name: str) -> Any:
218+
try:
219+
return self.__dict__[name]
220+
except KeyError as exc:
221+
if name in self.metadata:
222+
return self.metadata[name]
223+
raise AttributeError(
224+
f"'{type(self).__name__}' object has no attribute '{name}'"
225+
) from exc
226+
227+
def to_dict(self) -> Dict[str, Any]:
228+
config_dict: Dict[str, Any] = {
229+
"backend_name": self.backend_name,
230+
"backend_version": self.backend_version,
231+
"description": self.description,
232+
"max_experiments": self.max_experiments,
233+
"n_qubits": self.n_qubits,
234+
"dt": self.dt,
235+
"basis_gates": list(self.basis_gates),
236+
}
237+
238+
config_dict.update(self.metadata)
239+
240+
if self.azure:
241+
config_dict["azure"] = copy.deepcopy(self.azure)
242+
243+
return config_dict
244+
245+
@classmethod
246+
def from_dict(cls, data: Mapping[str, Any]) -> "AzureBackendConfig":
247+
raw = dict(data)
248+
azure_config = copy.deepcopy(raw.get("azure", {}))
249+
basis_gates = raw.get("basis_gates") or []
250+
251+
known_keys = {
252+
"backend_name",
253+
"backend_version",
254+
"description",
255+
"max_experiments",
256+
"n_qubits",
257+
"dt",
258+
"basis_gates",
259+
"azure",
260+
}
261+
262+
metadata = {k: v for k, v in raw.items() if k not in known_keys}
263+
264+
return cls(
265+
backend_name=raw.get("backend_name"),
266+
backend_version=raw.get("backend_version"),
267+
description=raw.get("description"),
268+
max_experiments=raw.get("max_experiments"),
269+
max_experiments_provided="max_experiments" in raw,
270+
n_qubits=raw.get("n_qubits"),
271+
dt=raw.get("dt"),
272+
basis_gates=tuple(basis_gates),
273+
azure=azure_config,
274+
metadata=metadata,
275+
)
276+
277+
@classmethod
278+
def from_backend_configuration(
279+
cls, configuration: Any
280+
) -> "AzureBackendConfig":
281+
return cls.from_dict(configuration.to_dict())
282+
283+
284+
def _ensure_backend_config(
285+
configuration: Any
286+
) -> AzureBackendConfig:
287+
if isinstance(configuration, AzureBackendConfig):
288+
return configuration
289+
290+
if BackendConfigurationType is not None and isinstance(
291+
configuration, BackendConfigurationType
292+
):
293+
return AzureBackendConfig.from_backend_configuration(configuration)
294+
295+
if isinstance(configuration, Mapping):
296+
return AzureBackendConfig.from_dict(configuration)
297+
298+
raise TypeError("Unsupported configuration type for Azure backends")
299+
300+
151301
class AzureBackendBase(Backend, SessionHost):
152302

153303
# Name of the provider's input parameter which specifies number of shots for a submitted job.
@@ -157,47 +307,57 @@ class AzureBackendBase(Backend, SessionHost):
157307
@abstractmethod
158308
def __init__(
159309
self,
160-
configuration: BackendConfiguration,
310+
configuration: Any,
161311
provider: Provider = None,
162312
**fields
163313
):
164314
if configuration is None:
165315
raise ValueError("Backend configuration is required for Azure backends")
166316

167-
warnings.warn(
168-
"The BackendConfiguration parameter is deprecated and will be removed when the SDK "
169-
"adopts Qiskit 2.0. Future versions will expose a lightweight provider-specific "
170-
"configuration instead.",
171-
DeprecationWarning,
172-
stacklevel=2,
173-
)
317+
if BackendConfigurationType is not None and isinstance(
318+
configuration, BackendConfigurationType
319+
):
320+
warnings.warn(
321+
"The BackendConfiguration parameter is deprecated and will be removed when the SDK "
322+
"adopts Qiskit 2.0. Future versions will expose a lightweight provider-specific "
323+
"configuration instead.",
324+
DeprecationWarning,
325+
stacklevel=2,
326+
)
327+
328+
config = _ensure_backend_config(configuration)
329+
330+
self._config = config
331+
self._configuration = self._config # Backwards compatibility for legacy attribute access.
174332

175-
self._configuration = configuration
176-
self._max_circuits = getattr(configuration, "max_experiments", 1)
333+
if config.max_experiments_provided:
334+
self._max_circuits = config.max_experiments
335+
else:
336+
self._max_circuits = 1
177337
if self._max_circuits is None or self._max_circuits != 1:
178338
raise ValueError(
179339
"This backend only supports running a single circuit per job."
180340
)
181341

182342
super().__init__(
183343
provider=provider,
184-
name=getattr(configuration, "backend_name", None),
185-
description=getattr(configuration, "description", None),
186-
backend_version=getattr(configuration, "backend_version", None),
344+
name=config.backend_name,
345+
description=config.description,
346+
backend_version=config.backend_version,
187347
**fields,
188348
)
189349

190-
self._target = self._build_target(configuration)
350+
self._target = self._build_target(config)
191351

192-
def _build_target(self, configuration: BackendConfiguration) -> Target:
193-
num_qubits = getattr(configuration, "n_qubits", None)
352+
def _build_target(self, configuration: AzureBackendConfig) -> Target:
353+
num_qubits = configuration.n_qubits
194354
target = Target(
195-
description=getattr(configuration, "description", None),
355+
description=configuration.description,
196356
num_qubits=num_qubits,
197-
dt=getattr(configuration, "dt", None),
357+
dt=configuration.dt,
198358
)
199359

200-
basis_gates: List[str] = list(getattr(configuration, "basis_gates", []) or [])
360+
basis_gates: List[str] = list(configuration.basis_gates or [])
201361
for gate_name in dict.fromkeys(basis_gates + ["measure", "reset"]):
202362
instruction = _resolve_instruction(gate_name)
203363
if instruction is None:
@@ -254,14 +414,14 @@ def _default_options(cls) -> Options:
254414
def _azure_config(self) -> Dict[str, str]:
255415
pass
256416

257-
def configuration(self) -> BackendConfiguration:
417+
def configuration(self) -> AzureBackendConfig:
258418
warnings.warn(
259419
"AzureBackendBase.configuration() is deprecated and will be removed when the SDK "
260420
"switches to Qiskit 2.0.",
261421
DeprecationWarning,
262422
stacklevel=2,
263423
)
264-
return self._configuration
424+
return self._config
265425

266426
@property
267427
def target(self) -> Target:
@@ -276,9 +436,9 @@ def retrieve_job(self, job_id) -> AzureQuantumJob:
276436
return self.provider.get_job(job_id)
277437

278438
def _get_output_data_format(self, options: Dict[str, Any] = {}) -> str:
279-
config: BackendConfiguration = self.configuration()
439+
config: AzureBackendConfig = self._config
280440

281-
azure_config: Dict[str, Any] = config.azure
441+
azure_config: Dict[str, Any] = config.azure or {}
282442
# if the backend defines an output format, use that over the default
283443
azure_defined_override = azure_config.get(
284444
"output_data_format", MICROSOFT_OUTPUT_DATA_FORMAT
@@ -349,12 +509,13 @@ def _run(self, job_name, input_data, input_params, metadata, **options):
349509
logger.info(f"Submitting new job for backend {self.name}")
350510

351511
# The default of these job parameters come from the AzureBackend configuration:
352-
config = self.configuration()
353-
blob_name = options.pop("blob_name", config.azure["blob_name"])
354-
content_type = options.pop("content_type", config.azure["content_type"])
355-
provider_id = options.pop("provider_id", config.azure["provider_id"])
512+
config = self._config
513+
azure_config = config.azure or {}
514+
blob_name = options.pop("blob_name", azure_config.get("blob_name"))
515+
content_type = options.pop("content_type", azure_config.get("content_type"))
516+
provider_id = options.pop("provider_id", azure_config.get("provider_id"))
356517
input_data_format = options.pop(
357-
"input_data_format", config.azure["input_data_format"]
518+
"input_data_format", azure_config.get("input_data_format")
358519
)
359520
output_data_format = self._get_output_data_format(options)
360521

@@ -437,7 +598,10 @@ def _get_azure_provider_id(self) -> str:
437598
class AzureQirBackend(AzureBackendBase):
438599
@abstractmethod
439600
def __init__(
440-
self, configuration: BackendConfiguration, provider: Provider = None, **fields
601+
self,
602+
configuration: AzureBackendConfig,
603+
provider: Provider = None,
604+
**fields,
441605
):
442606
super().__init__(configuration, provider, **fields)
443607

@@ -509,7 +673,9 @@ def run(
509673

510674
job = super()._run(job_name, input_data, input_params, metadata, **options)
511675
logger.info(
512-
f"Submitted job with id '{job.id()}' with shot count of {shots_count}:"
676+
"Submitted job with id '%s' with shot count of %s:",
677+
job.id(),
678+
shots_count,
513679
)
514680

515681
return job
@@ -527,8 +693,7 @@ def _prepare_job_metadata(self, circuit: QuantumCircuit) -> Dict[str, str]:
527693
def _get_qir_str(
528694
self, circuit: QuantumCircuit, target_profile: TargetProfile, **kwargs
529695
) -> str:
530-
531-
config = self.configuration()
696+
config = self._config
532697
# Barriers aren't removed by transpilation and must be explicitly removed in the Qiskit to QIR translation.
533698
supports_barrier = "barrier" in config.basis_gates
534699
skip_transpilation = kwargs.pop("skip_transpilation", False)
@@ -564,7 +729,7 @@ def _translate_input(
564729
circuit, target_profile, skip_transpilation=skip_transpilation
565730
)
566731

567-
entry_points = ["ENTTRYPOINT_main"]
732+
entry_points = ["ENTRYPOINT__main"]
568733

569734
if not skip_transpilation:
570735
# We'll only log the QIR again if we performed a transpilation.
@@ -608,7 +773,10 @@ class AzureBackend(AzureBackendBase):
608773

609774
@abstractmethod
610775
def __init__(
611-
self, configuration: BackendConfiguration, provider: Provider = None, **fields
776+
self,
777+
configuration: AzureBackendConfig,
778+
provider: Provider = None,
779+
**fields,
612780
):
613781
super().__init__(configuration, provider, **fields)
614782

@@ -647,7 +815,7 @@ def run(
647815

648816
# If the circuit was created using qiskit.assemble,
649817
# disassemble into QASM here
650-
if isinstance(circuit, QasmQobj) or isinstance(circuit, PulseQobj):
818+
if QOBJ_TYPES and isinstance(circuit, QOBJ_TYPES):
651819
from qiskit.assembler import disassemble
652820

653821
circuits, run, _ = disassemble(circuit)

0 commit comments

Comments
 (0)