Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion azure-quantum/azure/quantum/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion azure-quantum/azure/quantum/_mgmt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
17 changes: 13 additions & 4 deletions azure-quantum/azure/quantum/job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -342,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:
Expand Down
205 changes: 201 additions & 4 deletions azure-quantum/tests/unit/local/test_job_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -325,6 +385,143 @@ 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_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(
Expand Down
2 changes: 1 addition & 1 deletion azure-quantum/tests/unit/local/test_mgmt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 1 addition & 1 deletion azure-quantum/tests/unit/test_mgmt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down