Skip to content

Commit 36e339b

Browse files
authored
Bug fixes related to getting job results (#715)
* Fix checks if job is not succeeded * Handle errors in shots when returning results * Add tests for job results * Add test for get results histogram with error in output * Improve name for workspace api version used in ARM requests * Minor improvement * Revert "Minor improvement" This reverts commit 4bfd71e.
1 parent c841730 commit 36e339b

6 files changed

Lines changed: 218 additions & 12 deletions

File tree

azure-quantum/azure/quantum/_constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class ConnectionConstants:
6161
ARM_CREDENTIAL_SCOPE = "https://management.azure.com/.default"
6262

6363
DEFAULT_ARG_API_VERSION = "2021-03-01"
64-
DEFAULT_WORKSPACE_API_VERSION = "2025-11-01-preview"
64+
DEFAULT_ARM_WORKSPACE_API_VERSION = "2025-12-15-preview"
6565

6666
MSA_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad"
6767

azure-quantum/azure/quantum/_mgmt_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def load_workspace_from_arm(self, connection_params: WorkspaceConnectionParams)
179179
if not all([connection_params.subscription_id, connection_params.resource_group, connection_params.workspace_name]):
180180
raise ValueError("Missing required connection parameters to load workspace details from ARM.")
181181

182-
api_version = connection_params.api_version or ConnectionConstants.DEFAULT_WORKSPACE_API_VERSION
182+
api_version = connection_params.api_version or ConnectionConstants.DEFAULT_ARM_WORKSPACE_API_VERSION
183183

184184
url = (
185185
f"/subscriptions/{connection_params.subscription_id}"

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ def has_completed(self) -> bool:
6464
or self.details.status == "Failed"
6565
or self.details.status == "Cancelled"
6666
)
67+
68+
def has_succeeded(self) -> bool:
69+
"""Check if the job has succeeded."""
70+
return (
71+
self.details.status == "Completed"
72+
or self.details.status == "Succeeded"
73+
)
6774

6875
def wait_until_completed(
6976
self,
@@ -125,7 +132,7 @@ def get_results(self, timeout_secs: float = DEFAULT_TIMEOUT):
125132
if not self.has_completed():
126133
self.wait_until_completed(timeout_secs=timeout_secs)
127134

128-
if not self.details.status == "Succeeded" and not self.details.status == "Completed":
135+
if not self.has_succeeded():
129136
if self.details.status == "Failed" and self._allow_failure_results():
130137
job_blob_properties = self.download_blob_properties(self.details.output_data_uri)
131138
if job_blob_properties.size > 0:
@@ -205,7 +212,7 @@ def get_results_histogram(self, timeout_secs: float = DEFAULT_TIMEOUT):
205212
if not self.has_completed():
206213
self.wait_until_completed(timeout_secs=timeout_secs)
207214

208-
if not self.details.status == "Succeeded" or self.details.status == "Completed":
215+
if not self.has_succeeded():
209216
if self.details.status == "Failed" and self._allow_failure_results():
210217
job_blob_properties = self.download_blob_properties(self.details.output_data_uri)
211218
if job_blob_properties.size > 0:
@@ -288,7 +295,7 @@ def get_results_shots(self, timeout_secs: float = DEFAULT_TIMEOUT):
288295
if not self.has_completed():
289296
self.wait_until_completed(timeout_secs=timeout_secs)
290297

291-
if not self.details.status == "Succeeded" or self.details.status == "Completed":
298+
if not self.has_succeeded():
292299
if self.details.status == "Failed" and self._allow_failure_results():
293300
job_blob_properties = self.download_blob_properties(self.details.output_data_uri)
294301
if job_blob_properties.size > 0:
@@ -342,8 +349,10 @@ def _process_outcome(self, histogram_results):
342349

343350
def _convert_tuples(self, data):
344351
if isinstance(data, dict):
352+
if "Error" in data:
353+
return data
345354
# Check if the dictionary represents a tuple
346-
if all(isinstance(k, str) and k.startswith("Item") for k in data.keys()):
355+
elif all(isinstance(k, str) and k.startswith("Item") for k in data.keys()):
347356
# Convert the dictionary to a tuple
348357
return tuple(self._convert_tuples(data[f"Item{i+1}"]) for i in range(len(data)))
349358
else:

azure-quantum/tests/unit/local/test_job_results.py

Lines changed: 201 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ def _get_job_results(output_data_format: str, results_as_json_str: str, status:
4242
return job.get_results()
4343

4444

45-
def _get_job_results_histogram(output_data_format: str, results_as_json_str: str):
46-
job = _mock_job(output_data_format, results_as_json_str)
45+
def _get_job_results_histogram(output_data_format: str, results_as_json_str: str, status: str = "Succeeded"):
46+
job = _mock_job(output_data_format, results_as_json_str, status)
4747
return job.get_results_histogram()
4848

4949

50-
def _get_job_results_shots(output_data_format: str, results_as_json_str: str):
51-
job = _mock_job(output_data_format, results_as_json_str)
50+
def _get_job_results_shots(output_data_format: str, results_as_json_str: str, status: str = "Succeeded"):
51+
job = _mock_job(output_data_format, results_as_json_str, status)
5252
return job.get_results_shots()
5353

5454

@@ -99,6 +99,66 @@ def test_job_get_results_with_cancelled_status_raises_runtime_error():
9999
)
100100

101101

102+
def test_job_get_results_histogram_with_completed_status():
103+
job_results = _get_job_results_histogram(
104+
"microsoft.quantum-results.v2",
105+
'{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}',
106+
"Completed",
107+
)
108+
assert len(job_results.keys()) == 2
109+
assert job_results["[0]"]["count"] == 2
110+
assert job_results["[1]"]["count"] == 2
111+
112+
113+
def test_job_get_results_histogram_with_failed_status_raises_runtime_error():
114+
with pytest.raises(RuntimeError, match="Cannot retrieve results as job execution failed"):
115+
_get_job_results_histogram(
116+
"microsoft.quantum-results.v2",
117+
'{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}',
118+
"Failed",
119+
)
120+
121+
122+
def test_job_get_results_histogram_with_cancelled_status_raises_runtime_error():
123+
with pytest.raises(RuntimeError, match="Cannot retrieve results as job execution failed"):
124+
_get_job_results_histogram(
125+
"microsoft.quantum-results.v2",
126+
'{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}',
127+
"Cancelled",
128+
)
129+
130+
131+
def test_job_get_results_shots_with_completed_status():
132+
job_results = _get_job_results_shots(
133+
"microsoft.quantum-results.v2",
134+
'{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}',
135+
"Completed",
136+
)
137+
assert len(job_results) == 4
138+
assert job_results[0] == [0]
139+
assert job_results[1] == [1]
140+
assert job_results[2] == [1]
141+
assert job_results[3] == [0]
142+
143+
144+
def test_job_get_results_shots_with_failed_status_raises_runtime_error():
145+
with pytest.raises(RuntimeError, match="Cannot retrieve results as job execution failed"):
146+
_get_job_results_shots(
147+
"microsoft.quantum-results.v2",
148+
'{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}',
149+
"Failed",
150+
)
151+
152+
153+
def test_job_get_results_shots_with_cancelled_status_raises_runtime_error():
154+
with pytest.raises(RuntimeError, match="Cannot retrieve results as job execution failed"):
155+
_get_job_results_shots(
156+
"microsoft.quantum-results.v2",
157+
'{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}',
158+
"Cancelled",
159+
)
160+
161+
102162
def test_job_for_microsoft_quantum_results_v1_no_histogram_returns_raw_result():
103163
job_result_raw = '{"NotHistogramProperty": ["[0]", 0.50, "[1]", 0.50]}'
104164
job_result = _get_job_results("microsoft.quantum-results.v1", job_result_raw)
@@ -325,6 +385,143 @@ def test_job_for_microsoft_quantum_results_shots_v2_tuple_success():
325385
assert job_results[2] == [1]
326386

327387

388+
def test_job_for_microsoft_quantum_results_shots_v2_error_in_shots():
389+
output = """
390+
{
391+
"DataFormat": "microsoft.quantum-results.v2",
392+
"Results": [
393+
{
394+
"Histogram": [
395+
{
396+
"Outcome": [10],
397+
"Display": "[10]",
398+
"Count": 3
399+
},
400+
{
401+
"Outcome": {
402+
"Error": {
403+
"Code": "0x20",
404+
"Name": "TestErrorThirtyTwo"
405+
}
406+
},
407+
"Display": "Error 0x20: TestErrorThirtyTwo",
408+
"Count": 1
409+
},
410+
{
411+
"Outcome": {
412+
"Error": {
413+
"Code": "0x40",
414+
"Name": "TestErrorSixtyFour"
415+
}
416+
},
417+
"Display": "Error 0x40: TestErrorSixtyFour",
418+
"Count": 1
419+
}
420+
],
421+
"Shots": [
422+
[10],
423+
{
424+
"Error": {
425+
"Code": "0x20",
426+
"Name": "TestErrorThirtyTwo",
427+
"Foo": "42",
428+
"Bar": "baz"
429+
}
430+
},
431+
[10],
432+
{
433+
"Error": {
434+
"Code": "0x40",
435+
"Name": "TestErrorSixtyFour",
436+
"Arg0": "99",
437+
"Arg1": "33"
438+
}
439+
},
440+
[10]
441+
]
442+
}
443+
]
444+
}
445+
"""
446+
447+
job_results = _get_job_results_shots("microsoft.quantum-results.v2", output)
448+
assert len(job_results) == 5
449+
assert job_results[0] == [10]
450+
assert job_results[1] == {"Error": {"Code": "0x20", "Name": "TestErrorThirtyTwo", "Foo": "42", "Bar": "baz"}}
451+
assert job_results[2] == [10]
452+
assert job_results[3] == {"Error": {"Code": "0x40", "Name": "TestErrorSixtyFour", "Arg0": "99", "Arg1": "33"}}
453+
assert job_results[4] == [10]
454+
455+
456+
def test_job_for_microsoft_quantum_results_histogram_v2_error_in_histogram():
457+
output = """
458+
{
459+
"DataFormat": "microsoft.quantum-results.v2",
460+
"Results": [
461+
{
462+
"Histogram": [
463+
{
464+
"Outcome": [10],
465+
"Display": "[10]",
466+
"Count": 3
467+
},
468+
{
469+
"Outcome": {
470+
"Error": {
471+
"Code": "0x20",
472+
"Name": "TestErrorThirtyTwo"
473+
}
474+
},
475+
"Display": "Error 0x20: TestErrorThirtyTwo",
476+
"Count": 1
477+
},
478+
{
479+
"Outcome": {
480+
"Error": {
481+
"Code": "0x40",
482+
"Name": "TestErrorSixtyFour"
483+
}
484+
},
485+
"Display": "Error 0x40: TestErrorSixtyFour",
486+
"Count": 1
487+
}
488+
],
489+
"Shots": [
490+
[10],
491+
{
492+
"Error": {
493+
"Code": "0x20",
494+
"Name": "TestErrorThirtyTwo",
495+
"Foo": "42",
496+
"Bar": "baz"
497+
}
498+
},
499+
[10],
500+
{
501+
"Error": {
502+
"Code": "0x40",
503+
"Name": "TestErrorSixtyFour",
504+
"Arg0": "99",
505+
"Arg1": "33"
506+
}
507+
},
508+
[10]
509+
]
510+
}
511+
]
512+
}
513+
"""
514+
515+
job_results = _get_job_results_histogram("microsoft.quantum-results.v2", output)
516+
assert len(job_results.keys()) == 3
517+
assert job_results["[10]"]["count"] == 3
518+
assert job_results["Error 0x20: TestErrorThirtyTwo"]["count"] == 1
519+
assert job_results["Error 0x40: TestErrorSixtyFour"]["count"] == 1
520+
assert job_results["[10]"]["outcome"] == [10]
521+
assert job_results["Error 0x20: TestErrorThirtyTwo"]["outcome"] == {"Error": {"Code": "0x20", "Name": "TestErrorThirtyTwo"}}
522+
assert job_results["Error 0x40: TestErrorSixtyFour"]["outcome"] == {"Error": {"Code": "0x40", "Name": "TestErrorSixtyFour"}}
523+
524+
328525
def test_job_for_microsoft_quantum_results_shots_v2_wrong_type_raises_exception():
329526
try:
330527
_get_job_results_shots(

azure-quantum/tests/unit/local/test_mgmt_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ def test_load_workspace_from_arm_uses_default_api_version():
588588

589589
call_args = mock_send.call_args
590590
request = call_args[0][0]
591-
assert ConnectionConstants.DEFAULT_WORKSPACE_API_VERSION in request.url
591+
assert ConnectionConstants.DEFAULT_ARM_WORKSPACE_API_VERSION in request.url
592592

593593

594594
def test_load_workspace_from_arg_constructs_correct_url():

azure-quantum/tests/unit/test_mgmt_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ def test_load_workspace_from_arm_uses_default_api_version(self, mgmt_client, con
386386
# Verify the default API version was used
387387
call_args = mock_send.call_args
388388
request = call_args[0][0]
389-
assert ConnectionConstants.DEFAULT_WORKSPACE_API_VERSION in request.url
389+
assert ConnectionConstants.DEFAULT_ARM_WORKSPACE_API_VERSION in request.url
390390

391391
def test_load_workspace_from_arg_constructs_correct_url(self, mgmt_client, connection_params):
392392
"""Test that ARG request uses correct URL."""

0 commit comments

Comments
 (0)