Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ['3.11']
python-version: ['3.12']
action: ['lint', 'type', 'format']
steps:
- name: Checkout code
Expand Down
23 changes: 20 additions & 3 deletions integration/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pytket import Circuit
from pytket.backends.backendinfo import BackendInfo
from pytket.backends.backendresult import BackendResult
from quantinuum_schemas.models.backend_config import BaseBackendConfig
from quantinuum_schemas.models.hypertket_config import HyperTketConfig

import qnexus as qnx
Expand All @@ -33,7 +34,10 @@ def test_job_get_all(
assert isinstance(my_job_db_matches.count(), int)
assert isinstance(my_job_db_matches.summarize(), pd.DataFrame)

assert isinstance(next(my_job_db_matches), JobRef)
for job_ref in my_job_db_matches.list():
assert isinstance(job_ref, JobRef)
assert job_ref.backend_config is not None
assert isinstance(job_ref.backend_config, BaseBackendConfig)


def test_job_get_by_id(
Expand All @@ -45,12 +49,14 @@ def test_job_get_by_id(

my_compile_job = qnx.jobs.get(name_like=qa_compile_job_name)
assert isinstance(my_compile_job, CompileJobRef)
assert isinstance(my_compile_job.backend_config, BaseBackendConfig)

my_compile_job_2 = qnx.jobs.get(id=my_compile_job.id)
assert my_compile_job == my_compile_job_2

my_execute_job = qnx.jobs.get(name_like=qa_execute_job_name)
assert isinstance(my_execute_job, ExecuteJobRef)
assert isinstance(my_execute_job.backend_config, BaseBackendConfig)

my_execute_job_2 = qnx.jobs.get(id=my_execute_job.id)
assert my_execute_job == my_execute_job_2
Expand Down Expand Up @@ -99,11 +105,13 @@ def test_submit_compile(

my_proj = qnx.projects.get(name_like=qa_project_name)

config = qnx.AerConfig()

compile_job_ref = qnx.start_compile_job(
circuits=[_authenticated_nexus_circuit_ref],
name=f"qnexus_integration_test_compile_job_{datetime.now()}",
project=my_proj,
backend_config=qnx.AerConfig(),
backend_config=config,
)

assert isinstance(compile_job_ref, CompileJobRef)
Expand All @@ -125,6 +133,9 @@ def test_submit_compile(
assert isinstance(first_pass_data.get_output(), CircuitRef)
assert isinstance(first_pass_data.pass_name, str)

cj_ref = qnx.jobs.get(id=compile_job_ref.id)
assert cj_ref.backend_config == config


def test_compile(
_authenticated_nexus_circuit_ref: CircuitRef,
Expand Down Expand Up @@ -194,14 +205,16 @@ def test_submit_execute(
"""Test that we can run an execute job in Nexus, wait for the job to complete and
obtain the results from the execution."""

config = qnx.AerConfig()

my_proj = qnx.projects.get(name_like=qa_project_name)
my_circ = qnx.circuits.get(name_like=qa_circuit_name, project=my_proj)

execute_job_ref = qnx.start_execute_job(
circuits=[my_circ],
name=f"qnexus_integration_test_execute_job_{datetime.now()}",
project=my_proj,
backend_config=qnx.AerConfig(),
backend_config=config,
n_shots=[10],
)

Expand All @@ -219,6 +232,9 @@ def test_submit_execute(

assert isinstance(execute_results[0].download_backend_info(), BackendInfo)

pj_ref = qnx.jobs.get(id=execute_job_ref.id)
assert pj_ref.backend_config == config


def test_execute(
_authenticated_nexus: None,
Expand All @@ -239,6 +255,7 @@ def test_execute(
)

assert len(backend_results) == 1
assert isinstance(backend_results[0], BackendResult)
assert isinstance(backend_results[0].get_counts(), Counter)


Expand Down
2,606 changes: 1,419 additions & 1,187 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ pytket = "^1.34"
pytket-qiskit = {version = ">=0.50", optional = true}
websockets = ">11,<14"
pydantic-settings = "^2"
quantinuum-schemas = "^2.0"
hugr = "^0.11.1"
quantinuum-schemas = "^2.1"
hugr = "^0.11.3"
guppylang = "^0.17.0"

[tool.poetry.group.dev.dependencies]
jupyter = "^1.0.0"
Expand Down
6 changes: 3 additions & 3 deletions qnexus/client/hugr.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ class Params(
# without changes. We expect HUGR team to make other formats available during
# March 2025.
ENVELOPE_CONFIG = EnvelopeConfig(
# As of hugr v0.11.1, the only format available is JSON
# As of hugr v0.11.3, the only format available is JSON
format=EnvelopeFormat.JSON,
# As of hugr v0.11.1, zstd compression is not supported
zstd=None,
# default zstd compression level
zstd=0,
)


Expand Down
24 changes: 17 additions & 7 deletions qnexus/client/jobs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from pytket.backends.backendresult import BackendResult
from pytket.backends.status import WAITING_STATUS, StatusEnum
from quantinuum_schemas.models.backend_config import config_name_to_class
from quantinuum_schemas.models.hypertket_config import HyperTketConfig
from websockets.client import connect
from websockets.exceptions import ConnectionClosed
Expand Down Expand Up @@ -51,13 +52,15 @@
CompileJobRef,
DataframableList,
ExecuteJobRef,
ExecutionProgram,
ExecutionResult,
ExecutionResultRef,
JobRef,
JobType,
ProjectRef,
WasmModuleRef,
)
from qnexus.models.utils import AllowNone, assert_never
from qnexus.models.utils import assert_never

EPOCH_START = datetime(1970, 1, 1, tzinfo=timezone.utc)

Expand Down Expand Up @@ -262,6 +265,12 @@ def _fetch_by_id(job_id: UUID | str, scope: ScopeFilterEnum | None) -> JobRef:
case _:
assert_never(job_data["attributes"]["job_type"])

backend_config_dict = job_data["data"]["attributes"]["definition"]["backend_config"]
backend_config_class = config_name_to_class[backend_config_dict["type"]]
backend_config: BackendConfig = backend_config_class( # type: ignore
**backend_config_dict
)

return job_type(
id=job_data["data"]["id"],
annotations=Annotations.from_dict(job_data["data"]["attributes"]),
Expand All @@ -273,6 +282,7 @@ def _fetch_by_id(job_id: UUID | str, scope: ScopeFilterEnum | None) -> JobRef:
job_data["data"]["attributes"]["status"]
).message,
project=project,
backend_config_store=backend_config,
)


Expand Down Expand Up @@ -319,10 +329,10 @@ async def listen_job_status(
# If we pass True into the websocket connection, it sets a default SSLContext.
# See: https://websockets.readthedocs.io/en/stable/reference/client.html
ssl_reconfigured: Union[bool, ssl.SSLContext] = True
# if not nexus_config.verify_session:
# ssl_reconfigured = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
# ssl_reconfigured.check_hostname = False
# ssl_reconfigured.verify_mode = ssl.CERT_NONE
if not get_config().httpx_verify:
ssl_reconfigured = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_reconfigured.check_hostname = False
ssl_reconfigured.verify_mode = ssl.CERT_NONE

extra_headers = {
# TODO, this cookie will expire frequently
Expand Down Expand Up @@ -478,7 +488,7 @@ def compile( # pylint: disable=redefined-builtin, too-many-positional-arguments

@merge_properties_from_context
def execute( # pylint: disable=too-many-locals, too-many-positional-arguments
circuits: Union[CircuitRef, list[CircuitRef]],
circuits: Union[ExecutionProgram, list[ExecutionProgram]],
n_shots: list[int] | list[None],
backend_config: BackendConfig,
name: str,
Expand All @@ -494,7 +504,7 @@ def execute( # pylint: disable=too-many-locals, too-many-positional-arguments
credential_name: str | None = None,
user_group: str | None = None,
timeout: float | None = 300.0,
) -> list[BackendResult]:
) -> list[ExecutionResult]:
"""
Utility method to run an execute job and return the results. Blocks until
the results are available. See ``qnexus.start_execute_job`` for a function
Expand Down
1 change: 1 addition & 0 deletions qnexus/client/jobs/_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def start_compile_job( # pylint: disable=too-many-arguments, too-many-locals, t
last_status=StatusEnum.SUBMITTED,
last_message="",
project=project,
backend_config_store=backend_config,
)


Expand Down
82 changes: 69 additions & 13 deletions qnexus/client/jobs/_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
from pytket.backends.backendinfo import BackendInfo
from pytket.backends.backendresult import BackendResult
from pytket.backends.status import StatusEnum
from quantinuum_schemas.models.result import QSysResult

import qnexus.exceptions as qnx_exc
from qnexus.client import circuits as circuit_api
from qnexus.client import get_nexus_client
from qnexus.client import hugr as hugr_api
from qnexus.context import get_active_project, merge_properties_from_context
from qnexus.models import BackendConfig, StoredBackendInfo
from qnexus.models.annotations import Annotations, CreateAnnotations, PropertiesDict
Expand All @@ -17,16 +19,20 @@
CircuitRef,
DataframableList,
ExecuteJobRef,
ExecutionProgram,
ExecutionResultRef,
HUGRRef,
JobType,
ProjectRef,
ResultType,
WasmModuleRef,
)
from qnexus.models.utils import assert_never


@merge_properties_from_context
def start_execute_job( # pylint: disable=too-many-arguments, too-many-locals, too-many-positional-arguments
circuits: Union[CircuitRef, list[CircuitRef]],
circuits: Union[ExecutionProgram, list[ExecutionProgram]],
n_shots: list[int] | list[None],
backend_config: BackendConfig,
name: str,
Expand All @@ -50,14 +56,14 @@ def start_execute_job( # pylint: disable=too-many-arguments, too-many-locals, t
project = project or get_active_project(project_required=True)
project = cast(ProjectRef, project)

circuit_ids = (
[str(circuits.id)]
if isinstance(circuits, CircuitRef)
else [str(c.id) for c in circuits]
program_ids = (
[str(p.id) for p in circuits]
if isinstance(circuits, list)
else [str(circuits.id)]
)

if len(n_shots) != len(circuit_ids):
raise ValueError("Number of circuits must equal number of n_shots.")
if len(n_shots) != len(program_ids):
raise ValueError("Number of programs must equal number of n_shots.")

attributes_dict = CreateAnnotations(
name=name,
Expand All @@ -81,8 +87,8 @@ def start_execute_job( # pylint: disable=too-many-arguments, too-many-locals, t
"wasm_module_id": str(wasm_module.id) if wasm_module else None,
"credential_name": credential_name,
"items": [
{"circuit_id": circuit_id, "n_shots": n_shot}
for circuit_id, n_shot in zip(circuit_ids, n_shots)
{"circuit_id": program_id, "n_shots": n_shot}
for program_id, n_shot in zip(program_ids, n_shots)
],
},
}
Expand All @@ -91,7 +97,7 @@ def start_execute_job( # pylint: disable=too-many-arguments, too-many-locals, t
"project": {"data": {"id": str(project.id), "type": "project"}},
"circuits": {
"data": [
{"id": str(circuit_id), "type": "circuit"} for circuit_id in circuit_ids
{"id": str(program_id), "type": "circuit"} for program_id in program_ids
]
},
}
Expand Down Expand Up @@ -119,6 +125,7 @@ def start_execute_job( # pylint: disable=too-many-arguments, too-many-locals, t
last_status=StatusEnum.SUBMITTED,
last_message="",
project=project,
backend_config_store=backend_config,
)


Expand All @@ -144,22 +151,35 @@ def _results(

for item in resp_data["attributes"]["definition"]["items"]:
if item["status"]["status"] == "COMPLETED":
result_type: ResultType

match item["result_type"]:
case ResultType.QSYS:
result_type = ResultType.QSYS
case ResultType.PYTKET:
result_type = ResultType.PYTKET
case _:
assert_never(item["result_type"])

result_ref = ExecutionResultRef(
id=item["result_id"],
annotations=execute_job.annotations,
project=execute_job.project,
result_type=result_type,
)

execute_results.append(result_ref)

return execute_results


def _fetch_execution_result(
handle: ExecutionResultRef,
def _fetch_pytket_execution_result(
result_ref: ExecutionResultRef,
) -> tuple[BackendResult, BackendInfo, CircuitRef]:
"""Get the results for an execute job item."""
res = get_nexus_client().get(f"/api/results/v1beta/{handle.id}")
assert result_ref.result_type == ResultType.PYTKET, "Incorrect result type"

res = get_nexus_client().get(f"/api/results/v1beta/{result_ref.id}")
if res.status_code != 200:
raise qnx_exc.ResourceFetchFailed(message=res.text, status_code=res.status_code)

Expand All @@ -186,3 +206,39 @@ def _fetch_execution_result(
).to_pytket_backend_info()

return (backend_result, backend_info, input_circuit)


def _fetch_qsys_execution_result(
result_ref: ExecutionResultRef,
) -> tuple[QSysResult, BackendInfo, HUGRRef]:
"""Get the results of a next-gen Qsys execute job."""
assert result_ref.result_type == ResultType.QSYS, "Incorrect result type"

res = get_nexus_client().get(f"/api/qsys_results/v1beta/{result_ref.id}")

if res.status_code != 200:
raise qnx_exc.ResourceFetchFailed(message=res.text, status_code=res.status_code)

res_dict = res.json()

input_hugr_id = res_dict["data"]["relationships"]["hugr_module"]["data"]["id"]

input_hugr = hugr_api._fetch_by_id( # pylint: disable=protected-access
input_hugr_id,
scope=None,
)

example_qsys_result = QSysResult(res_dict["data"]["attributes"]["results"])

backend_info_data = next(
data for data in res_dict["included"] if data["type"] == "backend_snapshot"
)
backend_info = StoredBackendInfo(
**backend_info_data["attributes"]
).to_pytket_backend_info()

return (
example_qsys_result,
backend_info,
input_hugr,
)
3 changes: 0 additions & 3 deletions qnexus/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ class Config(BaseSettings):
port: int = 443
httpx_verify: bool = True

# scientific - TODO consider options
# optimisation_level: int = 1

# auth
store_tokens: bool = True
token_path: str = ".qnx/auth"
Expand Down
Loading
Loading