Skip to content
84 changes: 76 additions & 8 deletions azure-quantum/azure/quantum/cirq/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
##
from typing import TYPE_CHECKING, Dict, Sequence
from typing import TYPE_CHECKING, Dict, Optional, Sequence

if TYPE_CHECKING:
import cirq
Expand All @@ -14,14 +14,16 @@ class Job:
Thin wrapper around an Azure Quantum Job that supports
returning results in Cirq format.
"""

def __init__(
self,
azure_job: "AzureJob",
program: "cirq.Circuit",
measurement_dict: dict = None
measurement_dict: dict = None,
target: Optional[object] = None,
):
"""Construct a Job.

:param azure_job: Job
:type azure_job: azure.quantum.job.Job
:param program: Cirq program
Expand All @@ -32,11 +34,17 @@ def __init__(
self._azure_job = azure_job
self._program = program
self._measurement_dict = measurement_dict
self._target = target

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

@property
def azure_job(self) -> "AzureJob":
"""Returns the underlying Azure Quantum job."""
return self._azure_job

def status(self) -> str:
"""Gets the current status of the job."""
self._azure_job.refresh()
Expand Down Expand Up @@ -66,15 +74,75 @@ def measurement_dict(self) -> Dict[str, Sequence[int]]:
"""Returns a dictionary of measurement keys to target qubit index."""
if self._measurement_dict is None:
from cirq import MeasurementGate
measurements = [op for op in self._program.all_operations() if isinstance(op.gate, MeasurementGate)]

measurements = [
op
for op in self._program.all_operations()
if isinstance(op.gate, MeasurementGate)
]
self._measurement_dict = {
meas.gate.key: [q.x for q in meas.qubits] for meas in measurements
}
return self._measurement_dict

def results(self, timeout_seconds: int = 7200) -> "cirq.Result":
"""Poll the Azure Quantum API for results."""
return self._azure_job.get_results(timeout_secs=timeout_seconds)
def results(
self,
timeout_seconds: int = 7200,
*,
param_resolver=None,
seed=None,
) -> "cirq.Result":
"""Poll the Azure Quantum API for results and return a Cirq result.

Provider targets may return different result payload shapes. This method
normalizes those payloads into a `cirq.Result` using the target-specific
`_to_cirq_result` implementation.
"""

import cirq

if param_resolver is None:
param_resolver = cirq.ParamResolver({})
else:
param_resolver = cirq.ParamResolver(param_resolver)

target = self._target
if target is None:
# Best-effort reconstruction for jobs created via `Workspace.get_job`.
try:
from azure.quantum.cirq.service import AzureQuantumService

service = AzureQuantumService(workspace=self._azure_job.workspace)
target = service.get_target(name=self._azure_job.details.target)
except Exception:
target = None

if target is None:
raise RuntimeError(
"Cirq Job is missing its target wrapper; use `azure_job.get_results()` for raw results."
)

# Generic QIR wrapper must use per-shot data.
try:
from azure.quantum.cirq.targets.generic import AzureGenericQirCirqTarget

is_generic_qir = isinstance(target, AzureGenericQirCirqTarget)
except Exception:
is_generic_qir = False

if is_generic_qir:
raw = self._azure_job.get_results_shots(timeout_secs=timeout_seconds)
extra_kwargs = {"measurement_dict": self.measurement_dict()}
else:
raw = self._azure_job.get_results(timeout_secs=timeout_seconds)
extra_kwargs = {}

return target._to_cirq_result(
result=raw,
param_resolver=param_resolver,
seed=seed,
**extra_kwargs,
)

def cancel(self):
"""Cancel the given job."""
Expand All @@ -85,4 +153,4 @@ def delete(self):
self._azure_job.workspace.cancel_job(self._azure_job)

def __str__(self) -> str:
return f'azure.quantum.cirq.Job(job_id={self.job_id()})'
return f"azure.quantum.cirq.Job(job_id={self.job_id()})"
150 changes: 109 additions & 41 deletions azure-quantum/azure/quantum/cirq/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import cirq
except ImportError:
raise ImportError(
"Missing optional 'cirq' dependencies. \
"Missing optional 'cirq' dependencies. \
To install run: pip install azure-quantum[cirq]"
)
)

from azure.quantum import Workspace
from azure.quantum.job.base_job import DEFAULT_TIMEOUT
from azure.quantum.cirq.targets import *
from azure.quantum.cirq.targets import *
from azure.quantum.cirq.targets.generic import AzureGenericQirCirqTarget
from azure.quantum.cirq.targets.target import Target as CirqTargetBase

from typing import Optional, Union, List, TYPE_CHECKING

Expand All @@ -30,25 +32,29 @@ class AzureQuantumService:
Class for interfacing with the Azure Quantum service
using Cirq quantum circuits
"""

def __init__(
self,
workspace: Workspace = None,
default_target: Optional[str] = None,
**kwargs
**kwargs,
):
"""AzureQuantumService class

:param workspace: Azure Quantum workspace. If missing it will create a new Workspace passing `kwargs` to the constructor. Defaults to None.
:param workspace: Azure Quantum workspace. If missing it will create a new Workspace passing `kwargs` to the constructor. Defaults to None.
:type workspace: Workspace
:param default_target: Default target name, defaults to None
:type default_target: Optional[str]
"""
if kwargs is not None and len(kwargs) > 0:
from warnings import warn
warn(f"""Consider passing \"workspace\" argument explicitly.
The ability to initialize AzureQuantumService with arguments {', '.join(f'"{argName}"' for argName in kwargs)} is going to be deprecated in future versions.""",
DeprecationWarning,
stacklevel=2)

warn(
f"""Consider passing \"workspace\" argument explicitly.
The ability to initialize AzureQuantumService with arguments {', '.join(f'"{argName}"' for argName in kwargs)} is going to be deprecated in future versions.""",
DeprecationWarning,
stacklevel=2,
)

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

target_factory = TargetFactory(
base_cls=Target,
workspace=self._workspace,
default_targets=DEFAULT_TARGETS
base_cls=Target, workspace=self._workspace, default_targets=DEFAULT_TARGETS
)

return target_factory

def targets(
self,
name: str = None,
provider_id: str = None,
**kwargs
self, name: str = None, provider_id: str = None, **kwargs
) -> Union["CirqTarget", List["CirqTarget"]]:
"""Get all quantum computing targets available in the Azure Quantum Workspace.

Expand All @@ -84,10 +85,27 @@ def targets(
:return: Target instance or list thereof
:rtype: typing.Union[Target, typing.List[Target]]
"""
return self._target_factory.get_targets(
name=name,
provider_id=provider_id
)

target_statuses = self._workspace._get_target_status(name, provider_id)

cirq_targets: List["CirqTarget"] = []
for pid, status in target_statuses:
target = self._target_factory.from_target_status(pid, status, **kwargs)

if isinstance(target, CirqTargetBase):
cirq_targets.append(target)
continue

cirq_targets.append(
AzureGenericQirCirqTarget.from_target_status(
self._workspace, pid, status, **kwargs
)
)

# Back-compat with TargetFactory.get_targets return type.
if name is not None:
return cirq_targets[0] if cirq_targets else None
return cirq_targets

def get_target(self, name: str = None, **kwargs) -> "CirqTarget":
"""Get target with the specified name
Expand All @@ -114,25 +132,51 @@ def get_job(self, job_id: str, *args, **kwargs) -> Union["CirqJob", "CirqIonqJob
:rtype: azure.quantum.cirq.Job
"""
job = self._workspace.get_job(job_id=job_id)
target : CirqTarget = self._target_factory.create_target(
provider_id=job.details.provider_id,
name=job.details.target
# Recreate a Cirq-capable target wrapper for this job's target.
target = self.targets(
name=job.details.target, provider_id=job.details.provider_id
)
return target._to_cirq_job(azure_job=job, *args, **kwargs)

if target is None:
raise RuntimeError(
f"Job '{job_id}' exists, but no Cirq target wrapper could be created for target '{job.details.target}' (provider '{job.details.provider_id}'). "
"AzureQuantumService.get_job only supports jobs submitted to Cirq-capable targets (provider-specific Cirq targets or the generic Cirq-to-QIR wrapper). "
"For non-Cirq jobs, use Workspace.get_job(job_id)."
)

# Avoid misrepresenting arbitrary workspace jobs as Cirq jobs when using the
# generic Cirq-to-QIR wrapper. The workspace target status APIs generally do
# not expose supported input formats, so we rely on Cirq-stamped metadata.
if isinstance(target, AzureGenericQirCirqTarget):
metadata = job.details.metadata or {}
cirq_flag = str(metadata.get("cirq", "")).strip().lower() == "true"
if not cirq_flag:
raise RuntimeError(
f"Job '{job_id}' targets '{job.details.target}' but does not appear to be a Cirq job. "
"Use Workspace.get_job(job_id) to work with this job."
)

try:
return target._to_cirq_job(azure_job=job, *args, **kwargs)
except Exception as exc:
raise RuntimeError(
f"Job '{job_id}' exists but could not be represented as a Cirq job for target '{job.details.target}' (provider '{job.details.provider_id}'). "
"Use Workspace.get_job(job_id) to work with the raw job."
) from exc

def create_job(
self,
program: cirq.Circuit,
repetitions: int,
name: str = DEFAULT_JOB_NAME,
target: str = None,
param_resolver: cirq.ParamResolverOrSimilarType = cirq.ParamResolver({})
param_resolver: cirq.ParamResolverOrSimilarType = cirq.ParamResolver({}),
) -> Union["CirqJob", "CirqIonqJob"]:
"""Create job to run the given `cirq` program in Azure Quantum

:param program: Cirq program or circuit
:type program: cirq.Circuit
:param repetitions: Number of measurements
:param repetitions: Number of measurements
:type repetitions: int
:param name: Program name
:type name: str
Expand All @@ -146,18 +190,27 @@ def create_job(
# Get target
_target = self.get_target(name=target)
if not _target:
# If the target exists in the workspace but was filtered out, provide
# a more actionable error message.
target_name = target or self._default_target
raise RuntimeError(f"Could not find target '{target_name}'. \
Please make sure the target name is valid and that the associated provider is added to your Workspace. \
To add a provider to your quantum workspace on the Azure Portal, \
see https://aka.ms/AQ/Docs/AddProvider")
ws_statuses = self._workspace._get_target_status(target_name)
if ws_statuses:
pid, status = ws_statuses[0]
raise RuntimeError(
f"Target '{target_name}' exists in your workspace (provider '{pid}') and appears QIR-capable, but no Cirq-capable target could be created. "
"If you're using the generic Cirq-to-QIR path, ensure `qsharp` is installed: pip install azure-quantum[cirq,qsharp]."
)

raise RuntimeError(
f"Could not find target '{target_name}'. "
"Please make sure the target name is valid and that the associated provider is added to your Workspace. "
"To add a provider to your quantum workspace on the Azure Portal, see https://aka.ms/AQ/Docs/AddProvider"
)
# Resolve parameters
resolved_circuit = cirq.resolve_parameters(program, param_resolver)
# Submit job to Azure
return _target.submit(
program=resolved_circuit,
repetitions=repetitions,
name=name
program=resolved_circuit, repetitions=repetitions, name=name
)

def run(
Expand Down Expand Up @@ -194,23 +247,38 @@ def run(
repetitions=repetitions,
name=name,
target=target,
param_resolver=param_resolver
param_resolver=param_resolver,
)
# Get raw job results
target_obj = self.get_target(name=target)

# For SDK Cirq job wrappers, Job.results() already returns a Cirq result.
try:
from azure.quantum.cirq.job import Job as CirqJob

if isinstance(job, CirqJob):
return job.results(
timeout_seconds=timeout_seconds,
param_resolver=param_resolver,
seed=seed,
)
except Exception:
pass

# Otherwise, preserve provider-specific behavior (e.g., cirq_ionq.Job).
try:
result = job.results(timeout_seconds=timeout_seconds)
except RuntimeError as e:
# Catch errors from cirq_ionq.Job.results
if "Job was not completed successful. Instead had status: " in str(e):
raise TimeoutError(f"The wait time has exceeded {timeout_seconds} seconds. \
Job status: '{job.status()}'.")
raise TimeoutError(
f"The wait time has exceeded {timeout_seconds} seconds. \
Job status: '{job.status()}'."
)
else:
raise e

# Convert to Cirq Result
target = self.get_target(name=target)
return target._to_cirq_result(
return target_obj._to_cirq_result(
result=result,
param_resolver=param_resolver,
seed=seed
seed=seed,
)
8 changes: 7 additions & 1 deletion azure-quantum/azure/quantum/cirq/targets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@
from azure.quantum.cirq.targets.target import Target
from azure.quantum.cirq.targets.quantinuum import QuantinuumTarget
from azure.quantum.cirq.targets.ionq import IonQTarget
from azure.quantum.cirq.targets.generic import AzureGenericQirCirqTarget

__all__ = ["Target", "QuantinuumTarget", "IonQTarget"]
__all__ = [
"Target",
"QuantinuumTarget",
"IonQTarget",
"AzureGenericQirCirqTarget",
]

# Default targets to use when there is no target class
# associated with a given target ID
Expand Down
Loading
Loading