22# Copyright (c) Microsoft Corporation.
33# Licensed under the MIT License.
44##
5+ import copy
56import os
67import json
78
1011
1112logger = logging .getLogger (__name__ )
1213
14+ from dataclasses import dataclass , field
1315from 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
1517from azure .quantum .version import __version__
1618from azure .quantum .qiskit .job import (
1719 MICROSOFT_OUTPUT_DATA_FORMAT ,
2022from abc import abstractmethod
2123from azure .quantum .job .session import SessionHost
2224
25+ BackendConfigurationType = None
26+ QOBJ_TYPES : Tuple [type , ...] = tuple ()
27+
2328if TYPE_CHECKING :
2429 from azure .quantum import Workspace
2530
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+
151301class 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:
437598class 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