66 import cirq
77except 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
1313from azure .quantum import Workspace
1414from 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
1719from 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 )
0 commit comments