From 2329c00bf31f2c45c6922c1461135b0761747d61 Mon Sep 17 00:00:00 2001 From: Zulfat Nutfullin Date: Thu, 22 Jan 2026 10:32:38 -0800 Subject: [PATCH 1/7] Fix checks if job is not succeeded --- azure-quantum/azure/quantum/job/job.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/azure-quantum/azure/quantum/job/job.py b/azure-quantum/azure/quantum/job/job.py index c018bd5f..ad4f657d 100644 --- a/azure-quantum/azure/quantum/job/job.py +++ b/azure-quantum/azure/quantum/job/job.py @@ -64,6 +64,13 @@ def has_completed(self) -> bool: or self.details.status == "Failed" or self.details.status == "Cancelled" ) + + def has_succeeded(self) -> bool: + """Check if the job has succeeded.""" + return ( + self.details.status == "Completed" + or self.details.status == "Succeeded" + ) def wait_until_completed( self, @@ -125,7 +132,7 @@ def get_results(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if not self.details.status == "Succeeded" and not self.details.status == "Completed": + if not self.has_succeeded(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: @@ -205,7 +212,7 @@ def get_results_histogram(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if not self.details.status == "Succeeded" or self.details.status == "Completed": + if not self.has_succeeded(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: @@ -288,7 +295,7 @@ def get_results_shots(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if not self.details.status == "Succeeded" or self.details.status == "Completed": + if not self.has_succeeded(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: From 397cd7a20d73b87b1ca4de2d233f351b7619d93d Mon Sep 17 00:00:00 2001 From: Zulfat Nutfullin Date: Thu, 22 Jan 2026 12:15:35 -0800 Subject: [PATCH 2/7] Handle errors in shots when returning results --- azure-quantum/azure/quantum/job/job.py | 4 +- .../tests/unit/local/test_job_results.py | 68 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/azure-quantum/azure/quantum/job/job.py b/azure-quantum/azure/quantum/job/job.py index ad4f657d..d6ee4bd5 100644 --- a/azure-quantum/azure/quantum/job/job.py +++ b/azure-quantum/azure/quantum/job/job.py @@ -349,8 +349,10 @@ def _process_outcome(self, histogram_results): def _convert_tuples(self, data): if isinstance(data, dict): + if "Error" in data: + return data # Check if the dictionary represents a tuple - if all(isinstance(k, str) and k.startswith("Item") for k in data.keys()): + elif all(isinstance(k, str) and k.startswith("Item") for k in data.keys()): # Convert the dictionary to a tuple return tuple(self._convert_tuples(data[f"Item{i+1}"]) for i in range(len(data))) else: diff --git a/azure-quantum/tests/unit/local/test_job_results.py b/azure-quantum/tests/unit/local/test_job_results.py index ccbde0e9..c59ed308 100644 --- a/azure-quantum/tests/unit/local/test_job_results.py +++ b/azure-quantum/tests/unit/local/test_job_results.py @@ -325,6 +325,74 @@ def test_job_for_microsoft_quantum_results_shots_v2_tuple_success(): assert job_results[2] == [1] +def test_job_for_microsoft_quantum_results_shots_v2_error_in_shots(): + output = """ + { + "DataFormat": "microsoft.quantum-results.v2", + "Results": [ + { + "Histogram": [ + { + "Outcome": [10], + "Display": "[10]", + "Count": 3 + }, + { + "Outcome": { + "Error": { + "Code": "0x20", + "Name": "TestErrorThirtyTwo" + } + }, + "Display": "Error 0x20: TestErrorThirtyTwo", + "Count": 1 + }, + { + "Outcome": { + "Error": { + "Code": "0x40", + "Name": "TestErrorSixtyFour" + } + }, + "Display": "Error 0x40: TestErrorSixtyFour", + "Count": 1 + } + ], + "Shots": [ + [10], + { + "Error": { + "Code": "0x20", + "Name": "TestErrorThirtyTwo", + "Foo": "42", + "Bar": "baz" + } + }, + [10], + { + "Error": { + "Code": "0x40", + "Name": "TestErrorSixtyFour", + "Arg0": "99", + "Arg1": "33" + } + }, + [10] + ] + } + ] + } + """ + + job_results = _get_job_results_shots("microsoft.quantum-results.v2", output) + assert len(job_results) == 5 + assert job_results[0] == [10] + assert job_results[1] == {"Error": {"Code": "0x20", "Name": "TestErrorThirtyTwo", "Foo": "42", "Bar": "baz"}} + assert job_results[2] == [10] + assert job_results[3] == {"Error": {"Code": "0x40", "Name": "TestErrorSixtyFour", "Arg0": "99", "Arg1": "33"}} + assert job_results[4] == [10] + + def test_job_for_microsoft_quantum_results_shots_v2_wrong_type_raises_exception(): try: _get_job_results_shots( From 07378c0ac0c8219011c7e1916373bf565389da9f Mon Sep 17 00:00:00 2001 From: Zulfat Nutfullin Date: Thu, 22 Jan 2026 12:27:54 -0800 Subject: [PATCH 3/7] Add tests for job results --- .../tests/unit/local/test_job_results.py | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/azure-quantum/tests/unit/local/test_job_results.py b/azure-quantum/tests/unit/local/test_job_results.py index c59ed308..464cba0b 100644 --- a/azure-quantum/tests/unit/local/test_job_results.py +++ b/azure-quantum/tests/unit/local/test_job_results.py @@ -42,13 +42,13 @@ def _get_job_results(output_data_format: str, results_as_json_str: str, status: return job.get_results() -def _get_job_results_histogram(output_data_format: str, results_as_json_str: str): - job = _mock_job(output_data_format, results_as_json_str) +def _get_job_results_histogram(output_data_format: str, results_as_json_str: str, status: str = "Succeeded"): + job = _mock_job(output_data_format, results_as_json_str, status) return job.get_results_histogram() -def _get_job_results_shots(output_data_format: str, results_as_json_str: str): - job = _mock_job(output_data_format, results_as_json_str) +def _get_job_results_shots(output_data_format: str, results_as_json_str: str, status: str = "Succeeded"): + job = _mock_job(output_data_format, results_as_json_str, status) return job.get_results_shots() @@ -99,6 +99,66 @@ def test_job_get_results_with_cancelled_status_raises_runtime_error(): ) +def test_job_get_results_histogram_with_completed_status(): + job_results = _get_job_results_histogram( + "microsoft.quantum-results.v2", + '{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}', + "Completed", + ) + assert len(job_results.keys()) == 2 + assert job_results["[0]"]["count"] == 2 + assert job_results["[1]"]["count"] == 2 + + +def test_job_get_results_histogram_with_failed_status_raises_runtime_error(): + with pytest.raises(RuntimeError, match="Cannot retrieve results as job execution failed"): + _get_job_results_histogram( + "microsoft.quantum-results.v2", + '{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}', + "Failed", + ) + + +def test_job_get_results_histogram_with_cancelled_status_raises_runtime_error(): + with pytest.raises(RuntimeError, match="Cannot retrieve results as job execution failed"): + _get_job_results_histogram( + "microsoft.quantum-results.v2", + '{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}', + "Cancelled", + ) + + +def test_job_get_results_shots_with_completed_status(): + job_results = _get_job_results_shots( + "microsoft.quantum-results.v2", + '{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}', + "Completed", + ) + assert len(job_results) == 4 + assert job_results[0] == [0] + assert job_results[1] == [1] + assert job_results[2] == [1] + assert job_results[3] == [0] + + +def test_job_get_results_shots_with_failed_status_raises_runtime_error(): + with pytest.raises(RuntimeError, match="Cannot retrieve results as job execution failed"): + _get_job_results_shots( + "microsoft.quantum-results.v2", + '{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}', + "Failed", + ) + + +def test_job_get_results_shots_with_cancelled_status_raises_runtime_error(): + with pytest.raises(RuntimeError, match="Cannot retrieve results as job execution failed"): + _get_job_results_shots( + "microsoft.quantum-results.v2", + '{"DataFormat": "microsoft.quantum-results.v2", "Results": [{"Histogram": [{"Outcome": [0], "Display": "[0]", "Count": 2}, {"Outcome": [1], "Display": "[1]", "Count": 2}], "Shots": [[0], [1], [1], [0]]}]}', + "Cancelled", + ) + + def test_job_for_microsoft_quantum_results_v1_no_histogram_returns_raw_result(): job_result_raw = '{"NotHistogramProperty": ["[0]", 0.50, "[1]", 0.50]}' job_result = _get_job_results("microsoft.quantum-results.v1", job_result_raw) From d0e20af2755ac1f3233b78b917dab8cea29dbae2 Mon Sep 17 00:00:00 2001 From: Zulfat Nutfullin Date: Thu, 22 Jan 2026 12:50:35 -0800 Subject: [PATCH 4/7] Add test for get results histogram with error in output --- .../tests/unit/local/test_job_results.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/azure-quantum/tests/unit/local/test_job_results.py b/azure-quantum/tests/unit/local/test_job_results.py index 464cba0b..9e1d15d9 100644 --- a/azure-quantum/tests/unit/local/test_job_results.py +++ b/azure-quantum/tests/unit/local/test_job_results.py @@ -453,6 +453,75 @@ def test_job_for_microsoft_quantum_results_shots_v2_error_in_shots(): assert job_results[4] == [10] +def test_job_for_microsoft_quantum_results_histogram_v2_error_in_histogram(): + output = """ + { + "DataFormat": "microsoft.quantum-results.v2", + "Results": [ + { + "Histogram": [ + { + "Outcome": [10], + "Display": "[10]", + "Count": 3 + }, + { + "Outcome": { + "Error": { + "Code": "0x20", + "Name": "TestErrorThirtyTwo" + } + }, + "Display": "Error 0x20: TestErrorThirtyTwo", + "Count": 1 + }, + { + "Outcome": { + "Error": { + "Code": "0x40", + "Name": "TestErrorSixtyFour" + } + }, + "Display": "Error 0x40: TestErrorSixtyFour", + "Count": 1 + } + ], + "Shots": [ + [10], + { + "Error": { + "Code": "0x20", + "Name": "TestErrorThirtyTwo", + "Foo": "42", + "Bar": "baz" + } + }, + [10], + { + "Error": { + "Code": "0x40", + "Name": "TestErrorSixtyFour", + "Arg0": "99", + "Arg1": "33" + } + }, + [10] + ] + } + ] + } + """ + + job_results = _get_job_results_histogram("microsoft.quantum-results.v2", output) + assert len(job_results.keys()) == 3 + assert job_results["[10]"]["count"] == 3 + assert job_results["Error 0x20: TestErrorThirtyTwo"]["count"] == 1 + assert job_results["Error 0x40: TestErrorSixtyFour"]["count"] == 1 + assert job_results["[10]"]["outcome"] == [10] + assert job_results["Error 0x20: TestErrorThirtyTwo"]["outcome"] == {"Error": {"Code": "0x20", "Name": "TestErrorThirtyTwo"}} + assert job_results["Error 0x40: TestErrorSixtyFour"]["outcome"] == {"Error": {"Code": "0x40", "Name": "TestErrorSixtyFour"}} + + def test_job_for_microsoft_quantum_results_shots_v2_wrong_type_raises_exception(): try: _get_job_results_shots( From 4b6411eddfb44ced35a903427d363b62c2fd990f Mon Sep 17 00:00:00 2001 From: Zulfat Nutfullin Date: Thu, 22 Jan 2026 13:11:15 -0800 Subject: [PATCH 5/7] Improve name for workspace api version used in ARM requests --- azure-quantum/azure/quantum/_constants.py | 2 +- azure-quantum/azure/quantum/_mgmt_client.py | 2 +- azure-quantum/tests/unit/local/test_mgmt_client.py | 2 +- azure-quantum/tests/unit/test_mgmt_client.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-quantum/azure/quantum/_constants.py b/azure-quantum/azure/quantum/_constants.py index 991c1e9c..7b3ca580 100644 --- a/azure-quantum/azure/quantum/_constants.py +++ b/azure-quantum/azure/quantum/_constants.py @@ -61,7 +61,7 @@ class ConnectionConstants: ARM_CREDENTIAL_SCOPE = "https://management.azure.com/.default" DEFAULT_ARG_API_VERSION = "2021-03-01" - DEFAULT_WORKSPACE_API_VERSION = "2025-11-01-preview" + DEFAULT_ARM_WORKSPACE_API_VERSION = "2025-12-15-preview" MSA_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad" diff --git a/azure-quantum/azure/quantum/_mgmt_client.py b/azure-quantum/azure/quantum/_mgmt_client.py index d76c883b..0a084150 100644 --- a/azure-quantum/azure/quantum/_mgmt_client.py +++ b/azure-quantum/azure/quantum/_mgmt_client.py @@ -179,7 +179,7 @@ def load_workspace_from_arm(self, connection_params: WorkspaceConnectionParams) if not all([connection_params.subscription_id, connection_params.resource_group, connection_params.workspace_name]): raise ValueError("Missing required connection parameters to load workspace details from ARM.") - api_version = connection_params.api_version or ConnectionConstants.DEFAULT_WORKSPACE_API_VERSION + api_version = connection_params.api_version or ConnectionConstants.DEFAULT_ARM_WORKSPACE_API_VERSION url = ( f"/subscriptions/{connection_params.subscription_id}" diff --git a/azure-quantum/tests/unit/local/test_mgmt_client.py b/azure-quantum/tests/unit/local/test_mgmt_client.py index cad4e4c8..21ee60ab 100644 --- a/azure-quantum/tests/unit/local/test_mgmt_client.py +++ b/azure-quantum/tests/unit/local/test_mgmt_client.py @@ -588,7 +588,7 @@ def test_load_workspace_from_arm_uses_default_api_version(): call_args = mock_send.call_args request = call_args[0][0] - assert ConnectionConstants.DEFAULT_WORKSPACE_API_VERSION in request.url + assert ConnectionConstants.DEFAULT_ARM_WORKSPACE_API_VERSION in request.url def test_load_workspace_from_arg_constructs_correct_url(): diff --git a/azure-quantum/tests/unit/test_mgmt_client.py b/azure-quantum/tests/unit/test_mgmt_client.py index 291b759e..7381602e 100644 --- a/azure-quantum/tests/unit/test_mgmt_client.py +++ b/azure-quantum/tests/unit/test_mgmt_client.py @@ -386,7 +386,7 @@ def test_load_workspace_from_arm_uses_default_api_version(self, mgmt_client, con # Verify the default API version was used call_args = mock_send.call_args request = call_args[0][0] - assert ConnectionConstants.DEFAULT_WORKSPACE_API_VERSION in request.url + assert ConnectionConstants.DEFAULT_ARM_WORKSPACE_API_VERSION in request.url def test_load_workspace_from_arg_constructs_correct_url(self, mgmt_client, connection_params): """Test that ARG request uses correct URL.""" From 4bfd71eba548e4754111e0c3ce744051dee2e9ea Mon Sep 17 00:00:00 2001 From: Zulfat Nutfullin Date: Thu, 22 Jan 2026 13:23:53 -0800 Subject: [PATCH 6/7] Minor improvement --- azure-quantum/azure/quantum/job/job.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/azure-quantum/azure/quantum/job/job.py b/azure-quantum/azure/quantum/job/job.py index d6ee4bd5..d082da34 100644 --- a/azure-quantum/azure/quantum/job/job.py +++ b/azure-quantum/azure/quantum/job/job.py @@ -65,11 +65,11 @@ def has_completed(self) -> bool: or self.details.status == "Cancelled" ) - def has_succeeded(self) -> bool: - """Check if the job has succeeded.""" + def has_failed(self) -> bool: + """Check if the job has failed.""" return ( - self.details.status == "Completed" - or self.details.status == "Succeeded" + not self.details.status == "Completed" + and not self.details.status == "Succeeded" ) def wait_until_completed( @@ -132,7 +132,7 @@ def get_results(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if not self.has_succeeded(): + if self.has_failed(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: @@ -212,7 +212,7 @@ def get_results_histogram(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if not self.has_succeeded(): + if self.has_failed(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: @@ -295,7 +295,7 @@ def get_results_shots(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if not self.has_succeeded(): + if self.has_failed(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: From 6868f0e1497ace8d66af6a1b5f9d092a813dc3a8 Mon Sep 17 00:00:00 2001 From: Zulfat Nutfullin Date: Thu, 22 Jan 2026 13:37:40 -0800 Subject: [PATCH 7/7] Revert "Minor improvement" This reverts commit 4bfd71eba548e4754111e0c3ce744051dee2e9ea. --- azure-quantum/azure/quantum/job/job.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/azure-quantum/azure/quantum/job/job.py b/azure-quantum/azure/quantum/job/job.py index d082da34..d6ee4bd5 100644 --- a/azure-quantum/azure/quantum/job/job.py +++ b/azure-quantum/azure/quantum/job/job.py @@ -65,11 +65,11 @@ def has_completed(self) -> bool: or self.details.status == "Cancelled" ) - def has_failed(self) -> bool: - """Check if the job has failed.""" + def has_succeeded(self) -> bool: + """Check if the job has succeeded.""" return ( - not self.details.status == "Completed" - and not self.details.status == "Succeeded" + self.details.status == "Completed" + or self.details.status == "Succeeded" ) def wait_until_completed( @@ -132,7 +132,7 @@ def get_results(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if self.has_failed(): + if not self.has_succeeded(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: @@ -212,7 +212,7 @@ def get_results_histogram(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if self.has_failed(): + if not self.has_succeeded(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: @@ -295,7 +295,7 @@ def get_results_shots(self, timeout_secs: float = DEFAULT_TIMEOUT): if not self.has_completed(): self.wait_until_completed(timeout_secs=timeout_secs) - if self.has_failed(): + if not self.has_succeeded(): if self.details.status == "Failed" and self._allow_failure_results(): job_blob_properties = self.download_blob_properties(self.details.output_data_uri) if job_blob_properties.size > 0: