Skip to content

Commit

Permalink
Merge pull request #7 from CQCL/release/0.6.0
Browse files Browse the repository at this point in the history
Release/0.6.0
  • Loading branch information
cqc-melf authored Jul 26, 2022
2 parents 6997521 + 85bf742 commit cd7390f
Show file tree
Hide file tree
Showing 8 changed files with 666 additions and 146 deletions.
14 changes: 14 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
Changelog
~~~~~~~~~

0.6.0 (July 2022)
-----------------

* Changed batching interface: `process_circuits` no longer batches, use
`start_batching` and `add_to_batch` methods to explicitly start and append to
batches.
* New `submit_qasm` backend method to enable direct submission of a QASM program.

0.5.0 (July 2022)
-----------------

* Updated pytket version requirement to 1.4.
* Add support for multi-factor authentication and microsoft federated login.

0.4.0 (June 2022)
-----------------

Expand Down
34 changes: 31 additions & 3 deletions docs/intro.txt
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,42 @@ The ``process_circuits`` method for the QuantinuumBackend accepts the following
* ``noisy_simulation`` : boolean flag to specify whether the simulator should
perform noisy simulation with an error model (default value is ``True``).
* ``group`` : string identifier of a collection of jobs, can be used for usage tracking.
* ``max_batch_cost``: maximum HQC usable by submitted batch, default is 500.
* ``batch_id`` : first ``jobid`` of the batch to which this batch of circuits should be submitted. Job IDs can be retrieved from ResultHandle using ``backend.get_jobid(handle)``.
* ``close_batch`` : boolean flag to close the batch after the last circuit in the job (default value is ``True``).

For the Quantinuum ``Backend``, ``process_circuits`` returns a ``ResultHandle`` object containing a ``job_id`` and a postprocessing ( ``ppcirc``) circuit if there is one.

The ``logout()`` method clears stored JSON web tokens and the user will have to sign in again to access the Quantinuum API.

Batching
--------
Quantinuum backends (except syntax checkers) support batching of jobs (circuits). To create
a batch of jobs, users submit the first job, then signal that subsequent jobs should
be added to the same batch using the handle of the first. The backend queue
management system will start the batch as soon as the first job reaches the
front of the queue and ensure subsequent batch jobs are run one after the other,
until the end of the batch is reached or there are no new jobs added to the batch
for ~1 min (at which point the batch expires and any subsequent jobs will be
added to the standard queue).

The standard ``process_circuits`` method **no
longer batches by default**. To use batching first start the batch with
``start_batch``, which has a similar interface to ``process_circuit`` but with
an extra first argument `max_batch_cost`:

::

h1 = backend.start_batch(max_batch_cost=300, circuit=circuit, n_shots=100)

Add to the batch with subsequent calls of ``add_to_batch`` which takes as first
argument the handle of the first job of the batch, and has the optional keyword
argument `batch_end` to signal the end of a batch (default `False`).

::

h2 = backend.add_to_batch(h1, circuit_2, n_shots=100)
h3 = backend.add_to_batch(h1, circuit_3, n_shots=100, batch_end=True)



.. toctree::
api.rst
changelog.rst
63 changes: 62 additions & 1 deletion pytket/extensions/quantinuum/backends/api_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from .config import QuantinuumConfig
from .credential_storage import MemoryCredentialStorage
from .federated_login import microsoft_login

# This is necessary for use in Jupyter notebooks to allow for nested asyncio loops
try:
Expand Down Expand Up @@ -72,12 +73,20 @@ class QuantinuumAPI:

DEFAULT_API_URL = "https://qapi.quantinuum.com/"

AZURE_PROVIDER = "microsoft"

# Quantinuum API error codes
# mfa verification code is required during login
ERROR_CODE_MFA_REQUIRED = 73

def __init__(
self,
token_store: Optional[MemoryCredentialStorage] = None,
api_url: Optional[str] = None,
api_version: int = 1,
use_websocket: bool = True,
provider: Optional[str] = None,
support_mfa: bool = True,
__user_name: Optional[str] = None,
__pwd: Optional[str] = None,
):
Expand All @@ -93,6 +102,9 @@ def __init__(
:type api_version: int, optional
:param use_websocket: Whether to use websocket to retrieve, defaults to True
:type use_websocket: bool, optional
:param support_mfa: Whether to wait for the user to input the auth code,
defaults to True
:type support_mfa: bool, optional
"""
self.config = QuantinuumConfig.from_default_config_file()

Expand All @@ -110,6 +122,8 @@ def __init__(

self.api_version = api_version
self.use_websocket = use_websocket
self.provider = provider
self.support_mfa = support_mfa

self.ws_timeout = 180
self.retry_timeout = 5
Expand All @@ -129,6 +143,25 @@ def _request_tokens(self, user: str, pwd: str) -> None:
f"{self.url}login",
json.dumps(body),
)

# handle mfa verification
if response.status_code == HTTPStatus.UNAUTHORIZED:
error_code = response.json()["error"]["code"]
if error_code == self.ERROR_CODE_MFA_REQUIRED:
if not self.support_mfa:
raise QuantinuumAPIError(
"This API instance does not support MFA login."
)
# get a mfa code from user input
mfa_code = input("Enter your MFA verification code: ")
body["code"] = mfa_code

# resend request to login
response = requests.post(
f"{self.url}login",
json.dumps(body),
)

self._response_check(response, "Login")
resp_dict = response.json()
self._cred_store.save_tokens(
Expand All @@ -140,6 +173,31 @@ def _request_tokens(self, user: str, pwd: str) -> None:
del pwd
del body

def _request_tokens_federated(self) -> None:
"""Method to perform federated login and save tokens."""

if self.provider is not None and self.provider.lower() == self.AZURE_PROVIDER:
_, token = microsoft_login()
else:
raise RuntimeError(
f"Unsupported provider for login", HTTPStatus.UNAUTHORIZED
)

body = {"provider-token": token}

try:
response = requests.post(
f"{self.url}login",
json.dumps(body),
)
self._response_check(response, "Login")
resp_dict = response.json()
self._cred_store.save_tokens(
resp_dict["id-token"], resp_dict["refresh-token"]
)
finally:
del body

def _refresh_id_token(self, refresh_token: str) -> None:
"""Method to refresh ID token using a refresh token."""
body = {"refresh-token": refresh_token}
Expand Down Expand Up @@ -184,7 +242,10 @@ def _get_credentials(self) -> Tuple[str, str]:

def full_login(self) -> None:
"""Ask for user credentials from std input and update JWT tokens"""
self._request_tokens(*self._get_credentials())
if self.provider is None:
self._request_tokens(*self._get_credentials())
else:
self._request_tokens_federated()

def login(self) -> str:
"""This methods checks if we have a valid (non-expired) id-token
Expand Down
78 changes: 78 additions & 0 deletions pytket/extensions/quantinuum/backends/federated_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright 2020-2022 Cambridge Quantum Computing
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Tuple
from http import HTTPStatus
import json
import msal # type: ignore

AZURE_AD_APP_ID = "4ae73294-a491-45b7-bab4-945c236ee67a"
AZURE_AD_AUTHORITY = "https://login.microsoftonline.com/common"
AZURE_AD_SCOPE = ["User.Read"]


def microsoft_login() -> Tuple[str, str]:
"""Allows a user to login via Microsoft Azure Active Directory"""

# Create a preferably long-lived app instance which maintains a token cache.
app = msal.PublicClientApplication(AZURE_AD_APP_ID, authority=AZURE_AD_AUTHORITY)

# initiate the device flow authorization. It will expire after 15 minutes
flow = app.initiate_device_flow(scopes=AZURE_AD_SCOPE)

# check if the device code is available in the flow
if "user_code" not in flow:
raise ValueError(
"Fail to create device flow. Err: %s" % json.dumps(flow, indent=4)
)

# this prompts the user to visit https://microsoft.com/devicelogin and
# enter the provided code on a separate browser/device
code = flow["user_code"]
link = flow["verification_uri"]

print("To sign in:")
print("1) Open a web browser (using any device)")
print("2) Visit " + link)
print("3) Enter code '" + code + "'")
print("4) Enter your Microsoft credentials")

# This will block until the we've reached the flow's expiration time
result = app.acquire_token_by_device_flow(flow)

# check if we have an ID Token
if "id_token" in result:
token = result["id_token"]
username = result["id_token_claims"]["preferred_username"]
print("Authentication successful")

else:

# Check if a timeout occurred
if "authorization_pending" in result.get("error"):
print("Authorization code expired. Please try again.")
else:
# some other error occurred
print(result.get("error"))
print(result.get("error_description"))
print(
result.get("correlation_id")
) # You may need this when reporting a bug

# a token was not returned (an error occurred or the request timed out)
raise RuntimeError(
f"Unable to authorize federated login", HTTPStatus.UNAUTHORIZED
)

return username, token
Loading

0 comments on commit cd7390f

Please sign in to comment.