1010
1111logger = logging .getLogger (__name__ )
1212
13- from typing import Any , Dict , Union , List , Optional
13+ from functools import lru_cache
14+ from typing import Any , Dict , Union , List , Optional , TYPE_CHECKING
1415from azure .quantum .version import __version__
1516from azure .quantum .qiskit .job import (
1617 MICROSOFT_OUTPUT_DATA_FORMAT ,
17- MICROSOFT_OUTPUT_DATA_FORMAT_V2 ,
1818 AzureQuantumJob ,
1919)
2020from abc import abstractmethod
2121from azure .quantum .job .session import SessionHost
2222
23+ if TYPE_CHECKING :
24+ from azure .quantum import Workspace
25+
2326try :
2427 from qiskit import QuantumCircuit
25- from qiskit .providers import BackendV1 as Backend
28+ from qiskit .providers import BackendV2 as Backend
2629 from qiskit .providers import Options
2730 from qiskit .providers import Provider
2831 from qiskit .providers .models import BackendConfiguration
2932 from qiskit .qobj import QasmQobj , PulseQobj
33+ from qiskit .transpiler import Target
34+ from qiskit .circuit import Instruction , Parameter
35+ from qiskit .circuit .library .standard_gates import get_standard_gate_name_mapping
3036 from qsharp .interop .qiskit import QSharpBackend
3137 from qsharp import TargetProfile
3238
7076]
7177
7278
79+ @lru_cache (maxsize = None )
80+ def _standard_gate_map () -> Dict [str , Instruction ]:
81+ mapping = get_standard_gate_name_mapping ()
82+ # Include both canonical and lowercase keys for easier lookup
83+ lowered = {name .lower (): gate for name , gate in mapping .items ()}
84+ combined = {** mapping , ** lowered }
85+ return combined
86+
87+
88+ def _custom_instruction_builders () -> Dict [str , Instruction ]:
89+ """Provide Instruction stubs for backend-specific gates.
90+
91+ Azure Quantum targets expose native operations (for example, IonQ's
92+ GPI-family gates or Quantinuum's multi-controlled primitives) that are not
93+ part of Qiskit's standard gate catalogue. When we build a Target instance we
94+ still need Instruction objects for these names so transpilation and circuit
95+ validation can succeed. This helper returns lightweight Instruction
96+ definitions that mirror each provider's gate signatures, ensuring
97+ ``Target.add_instruction`` has the metadata it requires even though the
98+ operations themselves are executed remotely.
99+ """
100+ param = Parameter
101+ return {
102+ "gpi" : Instruction ("gpi" , 1 , 0 , params = [param ("phi" )]),
103+ "gpi2" : Instruction ("gpi2" , 1 , 0 , params = [param ("phi" )]),
104+ "ms" : Instruction (
105+ "ms" ,
106+ 2 ,
107+ 0 ,
108+ params = [param ("phi0" ), param ("phi1" ), param ("angle" )],
109+ ),
110+ "zz" : Instruction ("zz" , 2 , 0 , params = [param ("angle" )]),
111+ "v" : Instruction ("v" , 1 , 0 , params = []),
112+ "vdg" : Instruction ("vdg" , 1 , 0 , params = []),
113+ "vi" : Instruction ("vi" , 1 , 0 , params = []),
114+ "si" : Instruction ("si" , 1 , 0 , params = []),
115+ "ti" : Instruction ("ti" , 1 , 0 , params = []),
116+ "mcp" : Instruction ("mcp" , 3 , 0 , params = [param ("angle" )]),
117+ "mcphase" : Instruction ("mcphase" , 3 , 0 , params = [param ("angle" )]),
118+ "mct" : Instruction ("mct" , 3 , 0 , params = []),
119+ "mcx" : Instruction ("mcx" , 3 , 0 , params = []),
120+ "mcx_gray" : Instruction ("mcx_gray" , 3 , 0 , params = []),
121+ "pauliexp" : Instruction ("pauliexp" , 1 , 0 , params = [param ("time" )]),
122+ "paulievolution" : Instruction ("PauliEvolution" , 1 , 0 , params = [param ("time" )]),
123+ }
124+
125+
126+ def _resolve_instruction (gate_name : str ) -> Optional [Instruction ]:
127+ if not gate_name :
128+ return None
129+
130+ mapping = _standard_gate_map ()
131+ instruction = mapping .get (gate_name )
132+ if instruction is not None :
133+ return instruction .copy ()
134+
135+ lower_name = gate_name .lower ()
136+ instruction = mapping .get (lower_name )
137+ if instruction is not None :
138+ return instruction .copy ()
139+
140+ custom_map = _custom_instruction_builders ()
141+ if gate_name in custom_map :
142+ return custom_map [gate_name ]
143+ if lower_name in custom_map :
144+ return custom_map [lower_name ]
145+
146+ # Default to a single-qubit placeholder instruction.
147+ return Instruction (gate_name , 1 , 0 , params = [])
148+
149+
73150class AzureBackendBase (Backend , SessionHost ):
74151
75152 # Name of the provider's input parameter which specifies number of shots for a submitted job.
@@ -83,7 +160,54 @@ def __init__(
83160 provider : Provider = None ,
84161 ** fields
85162 ):
86- super ().__init__ (configuration , provider , ** fields )
163+ if configuration is None :
164+ raise ValueError ("Backend configuration is required for Azure backends" )
165+
166+ warnings .warn (
167+ "The BackendConfiguration parameter is deprecated and will be removed when the SDK "
168+ "adopts Qiskit 2.0. Future versions will expose a lightweight provider-specific "
169+ "configuration instead." ,
170+ DeprecationWarning ,
171+ stacklevel = 2 ,
172+ )
173+
174+ self ._configuration = configuration
175+ self ._max_circuits = getattr (configuration , "max_experiments" , 1 )
176+ if self ._max_circuits is None or self ._max_circuits != 1 :
177+ raise ValueError (
178+ "This backend only supports running a single circuit per job."
179+ )
180+
181+ super ().__init__ (
182+ provider = provider ,
183+ name = getattr (configuration , "backend_name" , None ),
184+ description = getattr (configuration , "description" , None ),
185+ backend_version = getattr (configuration , "backend_version" , None ),
186+ ** fields ,
187+ )
188+
189+ self ._target = self ._build_target (configuration )
190+
191+ def _build_target (self , configuration : BackendConfiguration ) -> Target :
192+ num_qubits = getattr (configuration , "n_qubits" , None )
193+ target = Target (
194+ description = getattr (configuration , "description" , None ),
195+ num_qubits = num_qubits ,
196+ dt = getattr (configuration , "dt" , None ),
197+ )
198+
199+ basis_gates : List [str ] = list (getattr (configuration , "basis_gates" , []) or [])
200+ for gate_name in dict .fromkeys (basis_gates + ["measure" , "reset" ]):
201+ instruction = _resolve_instruction (gate_name )
202+ if instruction is None :
203+ continue
204+ try :
205+ target .add_instruction (instruction )
206+ except AttributeError :
207+ # Instruction already registered; skip duplicates.
208+ continue
209+
210+ return target
87211
88212 @abstractmethod
89213 def run (
@@ -129,25 +253,34 @@ def _default_options(cls) -> Options:
129253 def _azure_config (self ) -> Dict [str , str ]:
130254 pass
131255
256+ def configuration (self ) -> BackendConfiguration :
257+ warnings .warn (
258+ "AzureBackendBase.configuration() is deprecated and will be removed when the SDK "
259+ "switches to Qiskit 2.0." ,
260+ DeprecationWarning ,
261+ stacklevel = 2 ,
262+ )
263+ return self ._configuration
264+
265+ @property
266+ def target (self ) -> Target :
267+ return self ._target
268+
269+ @property
270+ def max_circuits (self ) -> Optional [int ]:
271+ return self ._max_circuits
272+
132273 def retrieve_job (self , job_id ) -> AzureQuantumJob :
133274 """Returns the Job instance associated with the given id."""
134- return self ._provider .get_job (job_id )
275+ return self .provider .get_job (job_id )
135276
136277 def _get_output_data_format (self , options : Dict [str , Any ] = {}) -> str :
137278 config : BackendConfiguration = self .configuration ()
138- # output data format default depends on the number of experiments. QIR backends
139- # that don't define a default in their azure config will use this value
140- # Once more than one experiment is supported, we should always use the v2 format
141- default_output_data_format = (
142- MICROSOFT_OUTPUT_DATA_FORMAT
143- if config .max_experiments == 1
144- else MICROSOFT_OUTPUT_DATA_FORMAT_V2
145- )
146279
147280 azure_config : Dict [str , Any ] = config .azure
148281 # if the backend defines an output format, use that over the default
149282 azure_defined_override = azure_config .get (
150- "output_data_format" , default_output_data_format
283+ "output_data_format" , MICROSOFT_OUTPUT_DATA_FORMAT
151284 )
152285 # if the user specifies an output format, use that over the default azure config
153286 output_data_format = options .pop ("output_data_format" , azure_defined_override )
@@ -212,7 +345,7 @@ def _get_input_params(self, options: Dict[str, Any], shots: int = None) -> Dict[
212345 return input_params
213346
214347 def _run (self , job_name , input_data , input_params , metadata , ** options ):
215- logger .info (f"Submitting new job for backend { self .name () } " )
348+ logger .info (f"Submitting new job for backend { self .name } " )
216349
217350 # The default of these job parameters come from the AzureBackend configuration:
218351 config = self .configuration ()
@@ -237,13 +370,13 @@ def _run(self, job_name, input_data, input_params, metadata, **options):
237370 message += "To find a QIR capable backend, use the following code:"
238371 message += os .linesep
239372 message += (
240- f'\t provider.get_backend("{ self .name () } ", input_data_format: "qir.v1").'
373+ f'\t provider.get_backend("{ self .name } ", input_data_format: "qir.v1").'
241374 )
242375 raise ValueError (message )
243376
244377 job = AzureQuantumJob (
245378 backend = self ,
246- target = self .name () ,
379+ target = self .name ,
247380 name = job_name ,
248381 input_data = input_data ,
249382 blob_name = blob_name ,
@@ -291,10 +424,10 @@ def _normalize_run_input_params(self, run_input, **options):
291424 raise ValueError ("No input provided." )
292425
293426 def _get_azure_workspace (self ) -> "Workspace" :
294- return self .provider () .get_workspace ()
427+ return self .provider .get_workspace ()
295428
296429 def _get_azure_target_id (self ) -> str :
297- return self .name ()
430+ return self .name
298431
299432 def _get_azure_provider_id (self ) -> str :
300433 return self ._azure_config ()["provider_id" ]
0 commit comments