Skip to content

Commit 9fc0a9a

Browse files
committed
Beging BackendV2 migration
1 parent 99ede56 commit 9fc0a9a

File tree

4 files changed

+177
-43
lines changed

4 files changed

+177
-43
lines changed

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

Lines changed: 152 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,29 @@
1010

1111
logger = 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
1415
from azure.quantum.version import __version__
1516
from azure.quantum.qiskit.job import (
1617
MICROSOFT_OUTPUT_DATA_FORMAT,
17-
MICROSOFT_OUTPUT_DATA_FORMAT_V2,
1818
AzureQuantumJob,
1919
)
2020
from abc import abstractmethod
2121
from azure.quantum.job.session import SessionHost
2222

23+
if TYPE_CHECKING:
24+
from azure.quantum import Workspace
25+
2326
try:
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

@@ -70,6 +76,77 @@
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+
73150
class 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'\tprovider.get_backend("{self.name()}", input_data_format: "qir.v1").'
373+
f'\tprovider.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"]

azure-quantum/azure/quantum/qiskit/job.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,13 @@ def __init__(
5050
"""
5151
if azure_job is None:
5252
azure_job = Job.from_input_data(
53-
workspace=backend.provider().get_workspace(),
53+
workspace=backend.provider.get_workspace(),
5454
session_id=backend.get_latest_session_id(),
5555
**kwargs
5656
)
5757

5858
self._azure_job = azure_job
59-
self._workspace = backend.provider().get_workspace()
59+
self._workspace = backend.provider.get_workspace()
6060

6161
super().__init__(backend, self._azure_job.id, **kwargs)
6262

@@ -87,8 +87,8 @@ def result(self, timeout=None, sampler_seed=None):
8787
result_dict = {
8888
"results" : results if isinstance(results, list) else [results],
8989
"job_id" : self._azure_job.details.id,
90-
"backend_name" : self._backend.name(),
91-
"backend_version" : self._backend.version,
90+
"backend_name" : self._backend.name,
91+
"backend_version" : getattr(self._backend, "backend_version", None),
9292
"qobj_id" : self._azure_job.details.name,
9393
"success" : success,
9494
"error_data" : None if self._azure_job.details.error_data is None else self._azure_job.details.error_data.as_dict()

azure-quantum/azure/quantum/qiskit/provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
try:
1313
from qiskit.providers import ProviderV1 as Provider
1414
from qiskit.providers.exceptions import QiskitBackendNotFoundError
15-
from qiskit.providers import BackendV1 as Backend
15+
from qiskit.providers import BackendV2 as Backend
1616
from qiskit.exceptions import QiskitError
1717
except ImportError:
1818
raise ImportError(
@@ -152,7 +152,7 @@ def _is_available_in_ws(
152152
self, allowed_targets: List[Tuple[str, str]], backend: Backend
153153
):
154154
for name, provider in allowed_targets:
155-
if backend.name() == name:
155+
if backend.name == name:
156156
config = backend.configuration().to_dict()
157157
if "azure" in config and "provider_id" in config["azure"]:
158158
if config["azure"]["provider_id"] == provider:
@@ -192,7 +192,7 @@ def _init_backends(self) -> Dict[str, List[Backend]]:
192192
backend_instance: Backend = self._get_backend_instance(
193193
backend_cls, name
194194
)
195-
backend_name: str = backend_instance.name()
195+
backend_name: str = backend_instance.name
196196
instances.setdefault(backend_name, []).append(backend_instance)
197197

198198
return instances

0 commit comments

Comments
 (0)