Skip to content

Commit 4cbe286

Browse files
Added Generic Cirq Target (#728)
* Added Generic Cirq Target, WIP * clean up service.py a bit * Cleaned up generic.py a bit and replaced the random sampling with accessing the actual per-shot results * updated unit tests * preserve non-binary results * Make job.results() produce a cirq result type object * drop error shots and keep a raw_shots and raw_mesasurements lazy eval function * drop `shot_index` * missed something
1 parent 5df57c7 commit 4cbe286

7 files changed

Lines changed: 1032 additions & 56 deletions

File tree

azure-quantum/azure/quantum/cirq/job.py

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
##
5-
from typing import TYPE_CHECKING, Dict, Sequence
5+
from typing import TYPE_CHECKING, Dict, Optional, Sequence
66

77
if TYPE_CHECKING:
88
import cirq
@@ -14,14 +14,16 @@ class Job:
1414
Thin wrapper around an Azure Quantum Job that supports
1515
returning results in Cirq format.
1616
"""
17+
1718
def __init__(
1819
self,
1920
azure_job: "AzureJob",
2021
program: "cirq.Circuit",
21-
measurement_dict: dict = None
22+
measurement_dict: dict = None,
23+
target: Optional[object] = None,
2224
):
2325
"""Construct a Job.
24-
26+
2527
:param azure_job: Job
2628
:type azure_job: azure.quantum.job.Job
2729
:param program: Cirq program
@@ -32,11 +34,17 @@ def __init__(
3234
self._azure_job = azure_job
3335
self._program = program
3436
self._measurement_dict = measurement_dict
37+
self._target = target
3538

3639
def job_id(self) -> str:
3740
"""Returns the job id (UID) for the job."""
3841
return self._azure_job.id
3942

43+
@property
44+
def azure_job(self) -> "AzureJob":
45+
"""Returns the underlying Azure Quantum job."""
46+
return self._azure_job
47+
4048
def status(self) -> str:
4149
"""Gets the current status of the job."""
4250
self._azure_job.refresh()
@@ -66,15 +74,75 @@ def measurement_dict(self) -> Dict[str, Sequence[int]]:
6674
"""Returns a dictionary of measurement keys to target qubit index."""
6775
if self._measurement_dict is None:
6876
from cirq import MeasurementGate
69-
measurements = [op for op in self._program.all_operations() if isinstance(op.gate, MeasurementGate)]
77+
78+
measurements = [
79+
op
80+
for op in self._program.all_operations()
81+
if isinstance(op.gate, MeasurementGate)
82+
]
7083
self._measurement_dict = {
7184
meas.gate.key: [q.x for q in meas.qubits] for meas in measurements
7285
}
7386
return self._measurement_dict
7487

75-
def results(self, timeout_seconds: int = 7200) -> "cirq.Result":
76-
"""Poll the Azure Quantum API for results."""
77-
return self._azure_job.get_results(timeout_secs=timeout_seconds)
88+
def results(
89+
self,
90+
timeout_seconds: int = 7200,
91+
*,
92+
param_resolver=None,
93+
seed=None,
94+
) -> "cirq.Result":
95+
"""Poll the Azure Quantum API for results and return a Cirq result.
96+
97+
Provider targets may return different result payload shapes. This method
98+
normalizes those payloads into a `cirq.Result` using the target-specific
99+
`_to_cirq_result` implementation.
100+
"""
101+
102+
import cirq
103+
104+
if param_resolver is None:
105+
param_resolver = cirq.ParamResolver({})
106+
else:
107+
param_resolver = cirq.ParamResolver(param_resolver)
108+
109+
target = self._target
110+
if target is None:
111+
# Best-effort reconstruction for jobs created via `Workspace.get_job`.
112+
try:
113+
from azure.quantum.cirq.service import AzureQuantumService
114+
115+
service = AzureQuantumService(workspace=self._azure_job.workspace)
116+
target = service.get_target(name=self._azure_job.details.target)
117+
except Exception:
118+
target = None
119+
120+
if target is None:
121+
raise RuntimeError(
122+
"Cirq Job is missing its target wrapper; use `azure_job.get_results()` for raw results."
123+
)
124+
125+
# Generic QIR wrapper must use per-shot data.
126+
try:
127+
from azure.quantum.cirq.targets.generic import AzureGenericQirCirqTarget
128+
129+
is_generic_qir = isinstance(target, AzureGenericQirCirqTarget)
130+
except Exception:
131+
is_generic_qir = False
132+
133+
if is_generic_qir:
134+
raw = self._azure_job.get_results_shots(timeout_secs=timeout_seconds)
135+
extra_kwargs = {"measurement_dict": self.measurement_dict()}
136+
else:
137+
raw = self._azure_job.get_results(timeout_secs=timeout_seconds)
138+
extra_kwargs = {}
139+
140+
return target._to_cirq_result(
141+
result=raw,
142+
param_resolver=param_resolver,
143+
seed=seed,
144+
**extra_kwargs,
145+
)
78146

79147
def cancel(self):
80148
"""Cancel the given job."""
@@ -85,4 +153,4 @@ def delete(self):
85153
self._azure_job.workspace.cancel_job(self._azure_job)
86154

87155
def __str__(self) -> str:
88-
return f'azure.quantum.cirq.Job(job_id={self.job_id()})'
156+
return f"azure.quantum.cirq.Job(job_id={self.job_id()})"

azure-quantum/azure/quantum/cirq/service.py

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
import cirq
77
except ImportError:
88
raise ImportError(
9-
"Missing optional 'cirq' dependencies. \
9+
"Missing optional 'cirq' dependencies. \
1010
To install run: pip install azure-quantum[cirq]"
11-
)
11+
)
1212

1313
from azure.quantum import Workspace
1414
from azure.quantum.job.base_job import DEFAULT_TIMEOUT
15-
from azure.quantum.cirq.targets import *
15+
from azure.quantum.cirq.targets import *
16+
from azure.quantum.cirq.targets.generic import AzureGenericQirCirqTarget
17+
from azure.quantum.cirq.targets.target import Target as CirqTargetBase
1618

1719
from typing import Optional, Union, List, TYPE_CHECKING
1820

@@ -30,25 +32,29 @@ class AzureQuantumService:
3032
Class for interfacing with the Azure Quantum service
3133
using Cirq quantum circuits
3234
"""
35+
3336
def __init__(
3437
self,
3538
workspace: Workspace = None,
3639
default_target: Optional[str] = None,
37-
**kwargs
40+
**kwargs,
3841
):
3942
"""AzureQuantumService class
4043
41-
:param workspace: Azure Quantum workspace. If missing it will create a new Workspace passing `kwargs` to the constructor. Defaults to None.
44+
:param workspace: Azure Quantum workspace. If missing it will create a new Workspace passing `kwargs` to the constructor. Defaults to None.
4245
:type workspace: Workspace
4346
:param default_target: Default target name, defaults to None
4447
:type default_target: Optional[str]
4548
"""
4649
if kwargs is not None and len(kwargs) > 0:
4750
from warnings import warn
48-
warn(f"""Consider passing \"workspace\" argument explicitly.
49-
The ability to initialize AzureQuantumService with arguments {', '.join(f'"{argName}"' for argName in kwargs)} is going to be deprecated in future versions.""",
50-
DeprecationWarning,
51-
stacklevel=2)
51+
52+
warn(
53+
f"""Consider passing \"workspace\" argument explicitly.
54+
The ability to initialize AzureQuantumService with arguments {', '.join(f'"{argName}"' for argName in kwargs)} is going to be deprecated in future versions.""",
55+
DeprecationWarning,
56+
stacklevel=2,
57+
)
5258

5359
if workspace is None:
5460
workspace = Workspace(**kwargs)
@@ -64,18 +70,13 @@ def _target_factory(self):
6470
from azure.quantum.cirq.targets import Target, DEFAULT_TARGETS
6571

6672
target_factory = TargetFactory(
67-
base_cls=Target,
68-
workspace=self._workspace,
69-
default_targets=DEFAULT_TARGETS
73+
base_cls=Target, workspace=self._workspace, default_targets=DEFAULT_TARGETS
7074
)
7175

7276
return target_factory
7377

7478
def targets(
75-
self,
76-
name: str = None,
77-
provider_id: str = None,
78-
**kwargs
79+
self, name: str = None, provider_id: str = None, **kwargs
7980
) -> Union["CirqTarget", List["CirqTarget"]]:
8081
"""Get all quantum computing targets available in the Azure Quantum Workspace.
8182
@@ -84,10 +85,27 @@ def targets(
8485
:return: Target instance or list thereof
8586
:rtype: typing.Union[Target, typing.List[Target]]
8687
"""
87-
return self._target_factory.get_targets(
88-
name=name,
89-
provider_id=provider_id
90-
)
88+
89+
target_statuses = self._workspace._get_target_status(name, provider_id)
90+
91+
cirq_targets: List["CirqTarget"] = []
92+
for pid, status in target_statuses:
93+
target = self._target_factory.from_target_status(pid, status, **kwargs)
94+
95+
if isinstance(target, CirqTargetBase):
96+
cirq_targets.append(target)
97+
continue
98+
99+
cirq_targets.append(
100+
AzureGenericQirCirqTarget.from_target_status(
101+
self._workspace, pid, status, **kwargs
102+
)
103+
)
104+
105+
# Back-compat with TargetFactory.get_targets return type.
106+
if name is not None:
107+
return cirq_targets[0] if cirq_targets else None
108+
return cirq_targets
91109

92110
def get_target(self, name: str = None, **kwargs) -> "CirqTarget":
93111
"""Get target with the specified name
@@ -114,25 +132,51 @@ def get_job(self, job_id: str, *args, **kwargs) -> Union["CirqJob", "CirqIonqJob
114132
:rtype: azure.quantum.cirq.Job
115133
"""
116134
job = self._workspace.get_job(job_id=job_id)
117-
target : CirqTarget = self._target_factory.create_target(
118-
provider_id=job.details.provider_id,
119-
name=job.details.target
135+
# Recreate a Cirq-capable target wrapper for this job's target.
136+
target = self.targets(
137+
name=job.details.target, provider_id=job.details.provider_id
120138
)
121-
return target._to_cirq_job(azure_job=job, *args, **kwargs)
139+
140+
if target is None:
141+
raise RuntimeError(
142+
f"Job '{job_id}' exists, but no Cirq target wrapper could be created for target '{job.details.target}' (provider '{job.details.provider_id}'). "
143+
"AzureQuantumService.get_job only supports jobs submitted to Cirq-capable targets (provider-specific Cirq targets or the generic Cirq-to-QIR wrapper). "
144+
"For non-Cirq jobs, use Workspace.get_job(job_id)."
145+
)
146+
147+
# Avoid misrepresenting arbitrary workspace jobs as Cirq jobs when using the
148+
# generic Cirq-to-QIR wrapper. The workspace target status APIs generally do
149+
# not expose supported input formats, so we rely on Cirq-stamped metadata.
150+
if isinstance(target, AzureGenericQirCirqTarget):
151+
metadata = job.details.metadata or {}
152+
cirq_flag = str(metadata.get("cirq", "")).strip().lower() == "true"
153+
if not cirq_flag:
154+
raise RuntimeError(
155+
f"Job '{job_id}' targets '{job.details.target}' but does not appear to be a Cirq job. "
156+
"Use Workspace.get_job(job_id) to work with this job."
157+
)
158+
159+
try:
160+
return target._to_cirq_job(azure_job=job, *args, **kwargs)
161+
except Exception as exc:
162+
raise RuntimeError(
163+
f"Job '{job_id}' exists but could not be represented as a Cirq job for target '{job.details.target}' (provider '{job.details.provider_id}'). "
164+
"Use Workspace.get_job(job_id) to work with the raw job."
165+
) from exc
122166

123167
def create_job(
124168
self,
125169
program: cirq.Circuit,
126170
repetitions: int,
127171
name: str = DEFAULT_JOB_NAME,
128172
target: str = None,
129-
param_resolver: cirq.ParamResolverOrSimilarType = cirq.ParamResolver({})
173+
param_resolver: cirq.ParamResolverOrSimilarType = cirq.ParamResolver({}),
130174
) -> Union["CirqJob", "CirqIonqJob"]:
131175
"""Create job to run the given `cirq` program in Azure Quantum
132176
133177
:param program: Cirq program or circuit
134178
:type program: cirq.Circuit
135-
:param repetitions: Number of measurements
179+
:param repetitions: Number of measurements
136180
:type repetitions: int
137181
:param name: Program name
138182
:type name: str
@@ -146,18 +190,27 @@ def create_job(
146190
# Get target
147191
_target = self.get_target(name=target)
148192
if not _target:
193+
# If the target exists in the workspace but was filtered out, provide
194+
# a more actionable error message.
149195
target_name = target or self._default_target
150-
raise RuntimeError(f"Could not find target '{target_name}'. \
151-
Please make sure the target name is valid and that the associated provider is added to your Workspace. \
152-
To add a provider to your quantum workspace on the Azure Portal, \
153-
see https://aka.ms/AQ/Docs/AddProvider")
196+
ws_statuses = self._workspace._get_target_status(target_name)
197+
if ws_statuses:
198+
pid, status = ws_statuses[0]
199+
raise RuntimeError(
200+
f"Target '{target_name}' exists in your workspace (provider '{pid}') and appears QIR-capable, but no Cirq-capable target could be created. "
201+
"If you're using the generic Cirq-to-QIR path, ensure `qsharp` is installed: pip install azure-quantum[cirq,qsharp]."
202+
)
203+
204+
raise RuntimeError(
205+
f"Could not find target '{target_name}'. "
206+
"Please make sure the target name is valid and that the associated provider is added to your Workspace. "
207+
"To add a provider to your quantum workspace on the Azure Portal, see https://aka.ms/AQ/Docs/AddProvider"
208+
)
154209
# Resolve parameters
155210
resolved_circuit = cirq.resolve_parameters(program, param_resolver)
156211
# Submit job to Azure
157212
return _target.submit(
158-
program=resolved_circuit,
159-
repetitions=repetitions,
160-
name=name
213+
program=resolved_circuit, repetitions=repetitions, name=name
161214
)
162215

163216
def run(
@@ -194,23 +247,38 @@ def run(
194247
repetitions=repetitions,
195248
name=name,
196249
target=target,
197-
param_resolver=param_resolver
250+
param_resolver=param_resolver,
198251
)
199-
# Get raw job results
252+
target_obj = self.get_target(name=target)
253+
254+
# For SDK Cirq job wrappers, Job.results() already returns a Cirq result.
255+
try:
256+
from azure.quantum.cirq.job import Job as CirqJob
257+
258+
if isinstance(job, CirqJob):
259+
return job.results(
260+
timeout_seconds=timeout_seconds,
261+
param_resolver=param_resolver,
262+
seed=seed,
263+
)
264+
except Exception:
265+
pass
266+
267+
# Otherwise, preserve provider-specific behavior (e.g., cirq_ionq.Job).
200268
try:
201269
result = job.results(timeout_seconds=timeout_seconds)
202270
except RuntimeError as e:
203271
# Catch errors from cirq_ionq.Job.results
204272
if "Job was not completed successful. Instead had status: " in str(e):
205-
raise TimeoutError(f"The wait time has exceeded {timeout_seconds} seconds. \
206-
Job status: '{job.status()}'.")
273+
raise TimeoutError(
274+
f"The wait time has exceeded {timeout_seconds} seconds. \
275+
Job status: '{job.status()}'."
276+
)
207277
else:
208278
raise e
209279

210-
# Convert to Cirq Result
211-
target = self.get_target(name=target)
212-
return target._to_cirq_result(
280+
return target_obj._to_cirq_result(
213281
result=result,
214282
param_resolver=param_resolver,
215-
seed=seed
283+
seed=seed,
216284
)

azure-quantum/azure/quantum/cirq/targets/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88
from azure.quantum.cirq.targets.target import Target
99
from azure.quantum.cirq.targets.quantinuum import QuantinuumTarget
1010
from azure.quantum.cirq.targets.ionq import IonQTarget
11+
from azure.quantum.cirq.targets.generic import AzureGenericQirCirqTarget
1112

12-
__all__ = ["Target", "QuantinuumTarget", "IonQTarget"]
13+
__all__ = [
14+
"Target",
15+
"QuantinuumTarget",
16+
"IonQTarget",
17+
"AzureGenericQirCirqTarget",
18+
]
1319

1420
# Default targets to use when there is no target class
1521
# associated with a given target ID

0 commit comments

Comments
 (0)