Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: support for microsoft.dft target #540

Merged
merged 5 commits into from
Jan 2, 2024
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
1 change: 1 addition & 0 deletions azure-quantum/azure/quantum/job/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
##

from azure.quantum.job.job import Job
from azure.quantum.job.job_failed_with_results_error import JobFailedWithResultsError
from azure.quantum.job.workspace_item import WorkspaceItem
from azure.quantum.job.workspace_item_factory import WorkspaceItemFactory
from azure.quantum.job.session import Session, SessionHost
Expand Down
56 changes: 41 additions & 15 deletions azure-quantum/azure/quantum/job/base_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from typing import Any, Dict, Optional, TYPE_CHECKING
from azure.storage.blob import BlobClient

from azure.quantum.storage import upload_blob, download_blob, ContainerClient
from azure.quantum.storage import upload_blob, download_blob, download_blob_properties, ContainerClient
from azure.quantum._client.models import JobDetails
from azure.quantum.job.workspace_item import WorkspaceItem

Expand All @@ -26,6 +26,7 @@

class ContentType(str, Enum):
json = "application/json"
text_plain = "text/plain"

class BaseJob(WorkspaceItem):
# Optionally override these to create a Provider-specific Job subclass
Expand Down Expand Up @@ -261,6 +262,7 @@ def upload_input_data(
)
return uploaded_blob_uri


def download_data(self, blob_uri: str) -> dict:
"""Download file from blob uri

Expand All @@ -269,23 +271,26 @@ def download_data(self, blob_uri: str) -> dict:
:return: Payload from blob
:rtype: dict
"""
url = urlparse(blob_uri)
if url.query.find("se=") == -1:
# blob_uri does not contains SAS token,
# get sas url from service
blob_client = BlobClient.from_blob_url(
blob_uri
)
blob_uri = self.workspace._get_linked_storage_sas_uri(
blob_client.container_name, blob_client.blob_name
)
payload = download_blob(blob_uri)
else:
# blob_uri contains SAS token, use it
payload = download_blob(blob_uri)

blob_uri_with_sas_token = self._get_blob_uri_with_sas_token(blob_uri)
payload = download_blob(blob_uri_with_sas_token)

return payload


def download_blob_properties(self, blob_uri: str):
"""Download Blob properties

:param blob_uri: Blob URI
:type blob_uri: str
:return: Blob properties
:rtype: dict
"""

blob_uri_with_sas_token = self._get_blob_uri_with_sas_token(blob_uri)
return download_blob_properties(blob_uri_with_sas_token)


def upload_attachment(
self,
name: str,
Expand Down Expand Up @@ -345,3 +350,24 @@ def download_attachment(
blob_client = container_client.get_blob_client(name)
response = blob_client.download_blob().readall()
return response


def _get_blob_uri_with_sas_token(self, blob_uri: str) -> str:
"""Get Blob URI with SAS-token if one was not specified in blob_uri parameter
:param blob_uri: Blob URI
:type blob_uri: str
:return: Blob URI with SAS-token
:rtype: str
"""
url = urlparse(blob_uri)
if url.query.find("se=") == -1:
# blob_uri does not contains SAS token,
# get sas url from service
blob_client = BlobClient.from_blob_url(
blob_uri
)
blob_uri = self.workspace._get_linked_storage_sas_uri(
blob_client.container_name, blob_client.blob_name
)

return blob_uri
20 changes: 20 additions & 0 deletions azure-quantum/azure/quantum/job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import TYPE_CHECKING

from azure.quantum._client.models import JobDetails
from azure.quantum.job.job_failed_with_results_error import JobFailedWithResultsError
from azure.quantum.job.base_job import BaseJob, ContentType, DEFAULT_TIMEOUT
from azure.quantum.job.filtered_job import FilteredJob

Expand Down Expand Up @@ -117,6 +118,12 @@ def get_results(self, timeout_secs: float = DEFAULT_TIMEOUT):
self.wait_until_completed(timeout_secs=timeout_secs)

if not self.details.status == "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:
job_failure_data = self.download_data(self.details.output_data_uri)
raise JobFailedWithResultsError("An error occurred during job execution.", job_failure_data)

raise RuntimeError(
f'{"Cannot retrieve results as job execution failed"}'
+ f"(status: {self.details.status}."
Expand All @@ -130,3 +137,16 @@ def get_results(self, timeout_secs: float = DEFAULT_TIMEOUT):
except:
# If errors decoding the data, return the raw payload:
return payload


@classmethod
def _allow_failure_results(cls) -> bool:
"""
Allow to download job results even if the Job status is "Failed".

This method can be overridden in derived classes to alter the default
behaviour.

The default is False.
"""
return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import json
from typing import Any, Dict, Union

class JobFailedWithResultsError(RuntimeError):
"""
Error produced when Job completes with status "Failed" and the Job
supports producing failure results.

The failure results can be accessed with get_failure_results() method
"""

def __init__(self, message: str, failure_results: Any, *args: object) -> None:
self._set_error_details(message, failure_results)
super().__init__(message, *args)


def _set_error_details(self, message: str, failure_results: Any) -> None:
self._message = message
try:
decoded_failure_results = failure_results.decode("utf8")
self._failure_results: Dict[str, Any] = json.loads(decoded_failure_results)
except:
self._failure_results = failure_results


def get_message(self) -> str:
"""
Get error message.
"""
return self._message


def get_failure_results(self) -> Union[Dict[str, Any], str]:
"""
Get failure results produced by the job.
"""
return self._failure_results


def __str__(self) -> str:
return f"{self._message}\nFailure results: {self._failure_results}"
3 changes: 2 additions & 1 deletion azure-quantum/azure/quantum/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
generate_blob_sas,
generate_container_sas,
BlobType,
BlobProperties
)
from datetime import datetime, timedelta
from enum import Enum
Expand Down Expand Up @@ -196,7 +197,7 @@ def download_blob(blob_url: str) -> Any:
return response


def download_blob_properties(blob_url: str) -> Dict[str, str]:
def download_blob_properties(blob_url: str) -> BlobProperties:
"""Downloads the blob properties from Azure for the given blob URI"""
blob_client = BlobClient.from_blob_url(blob_url)
logger.info(
Expand Down
1 change: 1 addition & 0 deletions azure-quantum/azure/quantum/target/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .quantinuum import Quantinuum
from .rigetti import Rigetti
from .pasqal import Pasqal
from .microsoft.elements.dft import MicrosoftElementsDft, MicrosoftElementsDftJob

# Default targets to use when there is no target class
# associated with a given target ID
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .target import MicrosoftElementsDft
from .job import MicrosoftElementsDftJob

__all__ = ["MicrosoftElementsDft", "MicrosoftElementsDftJob"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import collections.abc
from typing import Any, Dict, Union
from azure.quantum.job import JobFailedWithResultsError
from azure.quantum.job.job import Job, DEFAULT_TIMEOUT
from azure.quantum._client.models import JobDetails

class MicrosoftElementsDftJob(Job):
"""
A dedicated job class for jobs from the microsoft.dft target.
"""

def __init__(self, workspace, job_details: JobDetails, **kwargs):
super().__init__(workspace, job_details, **kwargs)


def get_results(self, timeout_secs: float = DEFAULT_TIMEOUT) -> Dict[str, Any]:
try:
job_results = super().get_results(timeout_secs)
return job_results["results"]
except JobFailedWithResultsError as e:
failure_results = e.get_failure_results()
if MicrosoftElementsDftJob._is_dft_failure_results(failure_results):
error = failure_results["results"][0]["error"]
message = f'{e.get_message()} Error type: {error["error_type"]}. Message: {error["error_message"]}'
ashwinmayya marked this conversation as resolved.
Show resolved Hide resolved
raise JobFailedWithResultsError(message, failure_results) from None


@classmethod
def _allow_failure_results(cls) -> bool:
"""
Allow to download job results even if the Job status is "Failed".
"""
return True


@staticmethod
def _is_dft_failure_results(failure_results: Union[Dict[str, Any], str]) -> bool:
return isinstance(failure_results, dict) \
and "results" in failure_results \
and isinstance(failure_results["results"], collections.abc.Sequence) \
and len(failure_results["results"]) > 0 \
ashwinmayya marked this conversation as resolved.
Show resolved Hide resolved
and isinstance(failure_results["results"][0], dict) \
and "error" in failure_results["results"][0] \
and isinstance(failure_results["results"][0]["error"], dict) \
and "error_type" in failure_results["results"][0]["error"] \
and "error_message" in failure_results["results"][0]["error"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from azure.quantum.job.base_job import ContentType
from azure.quantum.job.job import Job
from azure.quantum.target.target import Target
from azure.quantum.workspace import Workspace
from azure.quantum.target.params import InputParams
from typing import Any, Dict, Type, Union
from .job import MicrosoftElementsDftJob


class MicrosoftElementsDft(Target):
"""
Microsoft Elements Dft target from the microsoft-elements provider.
"""

target_names = [
"microsoft.dft"
]


def __init__(
self,
workspace: "Workspace",
name: str = "microsoft.dft",
**kwargs
):
# There is only a single target name for this target
assert name == self.target_names[0]

# make sure to not pass argument twice
kwargs.pop("provider_id", None)

super().__init__(
workspace=workspace,
name=name,
input_data_format="microsoft.xyz.v1",
output_data_format="microsoft.dft-results.v1",
provider_id="microsoft-elements",
content_type=ContentType.text_plain,
**kwargs
)


def submit(self,
input_data: Any,
name: str = "azure-quantum-dft-job",
input_params: Union[Dict[str, Any], InputParams, None] = None,
**kwargs) -> MicrosoftElementsDftJob:

return super().submit(input_data, name, input_params, **kwargs)


@classmethod
def _get_job_class(cls) -> Type[Job]:
return MicrosoftElementsDftJob
62 changes: 62 additions & 0 deletions azure-quantum/tests/unit/molecule.xyz
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
60

O 97.873900000 103.017000000 100.816000000
H 98.128600000 103.038000000 99.848800000
H 97.173800000 102.317000000 100.960000000
O 100.645000000 100.169000000 95.891500000
H 101.491000000 100.305000000 96.406200000
H 99.888700000 100.618000000 96.367800000
O 99.814000000 100.835000000 101.232000000
H 99.329200000 99.976800000 101.063000000
H 99.151600000 101.561000000 101.414000000
O 98.804000000 98.512200000 97.758100000
H 99.782100000 98.646900000 97.916700000
H 98.421800000 99.326500000 97.321300000
O 100.747000000 100.164000000 103.736000000
H 100.658000000 100.628000000 102.855000000
H 100.105000000 99.398600000 103.776000000
O 98.070300000 98.516900000 100.438000000
H 97.172800000 98.878600000 100.690000000
H 98.194000000 98.592200000 99.448100000
O 98.548000000 101.265000000 97.248600000
H 98.688900000 102.140000000 97.711000000
H 97.919900000 101.391000000 96.480800000
O 103.898000000 98.427900000 99.984500000
H 103.015000000 98.654900000 99.573700000
H 104.128000000 97.477300000 99.776100000
O 99.166600000 96.442100000 101.723000000
H 98.843200000 97.206600000 101.166000000
H 99.643900000 95.783700000 101.141000000
O 102.891000000 100.842000000 97.477600000
H 103.837000000 100.662000000 97.209700000
H 102.868000000 101.166000000 98.423400000
O 96.227200000 100.990000000 101.698000000
H 96.148800000 100.422000000 102.517000000
H 95.313600000 101.237000000 101.375000000
O 98.864800000 98.222500000 103.917000000
H 98.949800000 97.463000000 103.272000000
H 99.054800000 97.896400000 104.843000000
O 104.578000000 100.035000000 101.952000000
H 104.419000000 101.011000000 101.802000000
H 104.206000000 99.514900000 101.184000000
O 102.429000000 104.060000000 101.348000000
H 101.757000000 103.665000000 101.974000000
H 102.209000000 105.021000000 101.185000000
O 98.708200000 103.752000000 98.244300000
H 98.397100000 104.234000000 97.425400000
H 99.598500000 104.111000000 98.524400000
O 95.630300000 99.996600000 98.245400000
H 96.540400000 100.410000000 98.268900000
H 94.982900000 100.638000000 97.834500000
O 102.360000000 101.551000000 99.964500000
H 102.675000000 102.370000000 100.444000000
H 101.556000000 101.180000000 100.430000000
O 101.836000000 97.446700000 102.110000000
H 100.860000000 97.397400000 101.898000000
H 101.991000000 97.133400000 103.047000000
O 101.665000000 98.316100000 98.319400000
H 101.904000000 99.233800000 98.002000000
H 102.224000000 97.640900000 97.837700000
O 99.984700000 103.272000000 102.307000000
H 99.640700000 103.104000000 103.231000000
H 99.216500000 103.453000000 101.693000000
Loading