Skip to content

Commit

Permalink
Update IQM backend to work with new client and server (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
cqc-alec authored Feb 7, 2025
1 parent 125dfa0 commit 2c0a593
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 139 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ on:
- cron: '0 4 * * 2'

env:
PYTKET_REMOTE_IQM_AUTH_SERVER_URL: ${{ secrets.PYTKET_REMOTE_IQM_AUTH_SERVER_URL }}
PYTKET_REMOTE_IQM_USERNAME: ${{ secrets.PYTKET_REMOTE_IQM_USERNAME }}
PYTKET_REMOTE_IQM_PASSWORD: ${{ secrets.PYTKET_REMOTE_IQM_APIKEY }}
PYTKET_REMOTE_IQM_API_TOKEN: ${{ secrets.PYTKET_REMOTE_IQM_API_TOKEN }}

jobs:
iqm-checks:
Expand Down
33 changes: 13 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,7 @@ compile it and run. Here is a small example of running a GHZ state circuit:
from pytket.extensions.iqm import IQMBackend
from pytket.circuit import Circuit

backend = IQMBackend(
url="https://demo.qc.iqm.fi/cocos",
auth_server_url="https://demo.qc.iqm.fi/auth",
username="USERNAME",
password="PASSWORD",
)
backend = IQMBackend(device="garnet", api_token="API_TOKEN")

circuit = Circuit(3, 3)
circuit.H(0)
Expand All @@ -56,21 +51,23 @@ result = backend.run_circuit(compiled_circuit, n_shots=100)
print(result.get_shots())
```

Note that the API token can be provided explicitly as an argument when
constructing the backend; alternatively it can be stored in pytket config (see
`IQMConfig.set_iqm_config()`); or it can be stored in a file whose location is
given by the environment variable `IQM_TOKENS_FILE`.

The IQM Client documentation includes the [set of currently supported
instructions]
(https://iqm-finland.github.io/iqm-client/api/iqm_client.iqm_client.html).
(https://iqm-finland.github.io/iqm-client/api/iqm.iqm_client.models.Instruction.html).
`pytket-iqm` retrieves the set from the IQM backend during the initialisation;
then `get_compiled_circuit()` takes care of compiling the circuit into the
form suitable to run on the backend.

During the backend initialisation, `pytket-iqm` also retrieves the names of
physical qubits and qubit connectivity. You can override the qubit connectivity
by providing the `arch` parameter to the `IQMBackend` constructor, but it generally
does not make sense, since the IQM server reports the valid quantum architecture
relevant to the given backend URL.
physical qubits and qubit connectivity.

(Note: At the moment IQM does not provide a quantum computing service open to the
general public. Please contact our [sales team](https://www.meetiqm.com/contact/)
general public. Please contact their [sales team](https://www.meetiqm.com/contact/)
to set up your access to an IQM quantum computer.)

## Bugs and feature requests
Expand Down Expand Up @@ -128,14 +125,10 @@ pytest
```

By default, the remote tests, which run against the real backend server, are
skipped. To enable them, set the following environment variables:

```shell
export PYTKET_RUN_REMOTE_TESTS=1
export PYTKET_REMOTE_IQM_AUTH_SERVER_URL=https://demo.qc.iqm.fi/auth
export PYTKET_REMOTE_IQM_USERNAME=YOUR_USERNAME
export PYTKET_REMOTE_IQM_PASSWORD=YOUR_PASSWORD
```
skipped. To enable them, set the environment variable
`PYTKET_RUN_REMOTE_TESTS=1` and make sure you have your API token stored either
in pytket config or in a file whose location is given by the environment
variable `IQM_TOKENS_FILE`.

When adding a new feature, please add a test for it. When fixing a bug, please
add a test that demonstrates the fix.
4 changes: 3 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Changelog
0.16.0 (unreleased)
-------------------

* Updated pytket version requirement to 1.39.
* Updated pytket version requirement to 1.40.
* Bring up to date with latest IQM client and server.
* Change parameters used to initialize IQMBackend.

0.15.0 (October 2024)
---------------------
Expand Down
3 changes: 1 addition & 2 deletions pytket/extensions/iqm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Backends for processing pytket circuits with IQM devices
"""
"""Backends for processing pytket circuits with IQM devices"""

# _metadata.py is copied to the folder after installation.
from ._metadata import __extension_version__, __extension_name__
Expand Down
3 changes: 1 addition & 2 deletions pytket/extensions/iqm/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Backends for processing pytket circuits with IQM devices
"""
"""Backends for processing pytket circuits with IQM devices"""

from .iqm import IQMBackend
32 changes: 9 additions & 23 deletions pytket/extensions/iqm/backends/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,18 @@ class IQMConfig(PytketExtConfig):

ext_dict_key: ClassVar[str] = "iqm"

auth_server_url: Optional[str]
username: Optional[str]
password: Optional[str]
api_token: Optional[str]

@classmethod
def from_extension_dict(
def from_extension_dict( # type: ignore
cls: Type["IQMConfig"], ext_dict: Dict[str, Any]
) -> "IQMConfig":
return cls(
ext_dict.get("auth_server_url"),
ext_dict.get("username"),
ext_dict.get("password"),
)


def set_iqm_config(
auth_server_url: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
) -> None:
return cls(ext_dict.get("api_token"))


def set_iqm_config(api_token: Optional[str] = None) -> None:
"""Set default value for IQM API token."""
config = IQMConfig.from_default_config_file()
if auth_server_url is not None:
config.auth_server_url = auth_server_url
if username is not None:
config.username = username
if password is not None:
config.password = password
config: IQMConfig = IQMConfig.from_default_config_file()
if api_token is not None:
config.api_token = api_token
config.update_default_config_file()
103 changes: 50 additions & 53 deletions pytket/extensions/iqm/backends/iqm.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
import os
from typing import cast, Dict, List, Optional, Sequence, Tuple, Union
from uuid import UUID
from iqm.iqm_client.iqm_client import Circuit as IQMCircuit
from iqm.iqm_client.iqm_client import Instruction, IQMClient, Metadata, Status
from iqm.iqm_client.iqm_client import IQMClient
from iqm.iqm_client.models import Circuit as IQMCircuit
from iqm.iqm_client.models import Instruction, Metadata, Status
import numpy as np
from pytket.backends import Backend, CircuitStatus, ResultHandle, StatusEnum
from pytket.backends.backend import KwargTypes
Expand Down Expand Up @@ -59,12 +60,14 @@

# Mapping of natively supported instructions' names to members of Pytket OpType
_IQM_PYTKET_OP_MAP = {
"phased_rx": OpType.PhasedX,
"prx": OpType.PhasedX,
"cz": OpType.CZ,
"measurement": OpType.Measure,
"measure": OpType.Measure,
"barrier": OpType.Barrier,
}

_SERVER_URL = "https://cocos.resonance.meetiqm.com"


class IqmAuthenticationError(Exception):
"""Raised when there is no IQM access credentials available."""
Expand All @@ -73,6 +76,13 @@ def __init__(self) -> None:
super().__init__("No IQM access credentials provided or found in config file.")


class IqmDeviceUnsupportedError(Exception):
"""Raised when we are unable to support a requested device."""

def __init__(self, reason: str) -> None:
super().__init__(reason)


class IQMBackend(Backend):
"""
Interface to an IQM device or simulator.
Expand All @@ -85,62 +95,47 @@ class IQMBackend(Backend):

def __init__(
self,
url: str,
arch: Optional[List[List[str]]] = None,
auth_server_url: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
device: str,
api_token: Optional[str] = None,
):
"""
Construct a new IQM backend.
Requires _either_ a valid auth server URL, username and password, _or_ a tokens
file.
Requires either a valid API token or a tokens file.
Auth server URL, username and password can either be provided as parameters or
set in config using :py:meth:`pytket.extensions.iqm.set_iqm_config`.
API token can either be provided as a parameter or set in config using
:py:meth:`pytket.extensions.iqm.set_iqm_config`.
Path to the tokens file is read from the environmment variable
``IQM_TOKENS_FILE``. If set, this overrides any other credentials provided as
arguments.
:param url: base URL for requests
:param arch: Optional list of couplings between the qubits defined, if
not set the default value from the server is used.
:param auth_server_url: base URL of authentication server
:param username: IQM username
:param password: IQM password
:param device: Name of device, e.g. "garnet"
:param api_token: API token
"""
super().__init__()
self._url = url
config = IQMConfig.from_default_config_file()
config: IQMConfig = IQMConfig.from_default_config_file()

if auth_server_url is None:
auth_server_url = config.auth_server_url
if api_token is None:
api_token = config.api_token
tokens_file = os.getenv("IQM_TOKENS_FILE")
if username is None:
username = config.username
if password is None:
password = config.password
if (username is None or password is None) and tokens_file is None:
if api_token is None and tokens_file is None:
raise IqmAuthenticationError()

url = f"{_SERVER_URL}/{device}"
if tokens_file is None:
self._client = IQMClient(
self._url,
auth_server_url=auth_server_url,
username=username,
password=password,
)
self._client = IQMClient(url=url, token=api_token)
else:
self._client = IQMClient(self._url, tokens_file=tokens_file)
self._client = IQMClient(url=url, tokens_file=tokens_file)
_iqmqa = self._client.get_quantum_architecture()
# TODO We don't currently support resonator qubits or the "move" operation.
if "move" in _iqmqa.operations:
raise IqmDeviceUnsupportedError(
"Unable to support device with computational resonator"
)
self._operations = [_IQM_PYTKET_OP_MAP[op] for op in _iqmqa.operations]
self._qubits = [_as_node(qb) for qb in _iqmqa.qubits]
self._n_qubits = len(self._qubits)
if arch is None:
arch = _iqmqa.qubit_connectivity
coupling = [(_as_node(a), _as_node(b)) for (a, b) in arch]
coupling = [(_as_node(a), _as_node(b)) for (a, b) in _iqmqa.qubit_connectivity]
if any(qb not in self._qubits for couple in coupling for qb in couple):
raise ValueError("Architecture contains qubits not in device")
self._arch = Architecture(coupling)
Expand Down Expand Up @@ -263,9 +258,7 @@ def circuit_status(self, handle: ResultHandle) -> CircuitStatus:
run_id = UUID(bytes=cast(bytes, handle[0]))
run_result = self._client.get_run(run_id)
status = run_result.status
if status == Status.PENDING_EXECUTION:
return CircuitStatus(StatusEnum.SUBMITTED)
elif status == Status.READY:
if status == Status.READY:
measurements = cast(dict, run_result.measurements)[0]
shots = OutcomeArray.from_readouts(
np.array(
Expand All @@ -281,9 +274,10 @@ def circuit_status(self, handle: ResultHandle) -> CircuitStatus:
handle, {"result": BackendResult(shots=shots, ppcirc=ppcirc)}
)
return CircuitStatus(StatusEnum.COMPLETED)
else:
assert status == Status.FAILED
elif status in [Status.FAILED, Status.ABORTED]:
return CircuitStatus(StatusEnum.ERROR, cast(str, run_result.message))
else:
return CircuitStatus(StatusEnum.SUBMITTED)

def get_result(self, handle: ResultHandle, **kwargs: KwargTypes) -> BackendResult:
"""
Expand Down Expand Up @@ -339,10 +333,13 @@ def get_metadata(self, handle: ResultHandle, **kwargs: KwargTypes) -> Metadata:


def _as_node(qname: str) -> Node:
assert qname.startswith("QB")
x = int(qname[2:])
assert x >= 1
return Node(x - 1)
if qname == "COMP_R":
return Node(0)
else:
assert qname.startswith("QB")
x = int(qname[2:])
assert x >= 1
return Node(x - 1)


def _as_name(qnode: Node) -> str:
Expand All @@ -360,23 +357,23 @@ def _translate_iqm(circ: Circuit) -> Tuple[Instruction, ...]:
optype = op.type
params = op.params
if optype == OpType.PhasedX:
instr = Instruction(
name="phased_rx",
instr = Instruction( # type: ignore
name="prx",
implementation=None,
qubits=(str(qbs[0]),),
args={"angle_t": 0.5 * params[0], "phase_t": 0.5 * params[1]},
)
elif optype == OpType.CZ:
instr = Instruction(
instr = Instruction( # type: ignore
name="cz",
implementation=None,
qubits=(str(qbs[0]), str(qbs[1])),
args={},
)
else:
assert optype == OpType.Measure
instr = Instruction(
name="measurement",
instr = Instruction( # type: ignore
name="measure",
implementation=None,
qubits=(str(qbs[0]),),
args={"key": str(cbs[0])},
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
author_email="[email protected]",
python_requires=">=3.10",
project_urls={
"Documentation": "https://tket.quantinuum.com/extensions/pytket-iqm/index.html",
"Documentation": "https://docs.quantinuum.com/tket/extensions/pytket-iqm/index.html",
"Source": "https://github.com/CQCL/pytket-iqm",
"Tracker": "https://github.com/CQCL/pytket-iqm/issues",
},
Expand All @@ -42,7 +42,7 @@
license="Apache 2",
packages=find_namespace_packages(include=["pytket.*"]),
include_package_data=True,
install_requires=["pytket >= 1.39.0", "iqm-client ~= 15.2"],
install_requires=["pytket >= 1.40.0", "iqm-client >= 20.15"],
classifiers=[
"Environment :: Console",
"Programming Language :: Python :: 3.10",
Expand Down
Loading

0 comments on commit 2c0a593

Please sign in to comment.