From cad95e96709752731db938ff07d8c15f49012c47 Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Thu, 18 Jun 2026 00:21:26 +0200 Subject: [PATCH 1/9] use connection instead of sdk --- qek/target/backends.py | 74 +++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/qek/target/backends.py b/qek/target/backends.py index b8889cf..a1837e2 100644 --- a/qek/target/backends.py +++ b/qek/target/backends.py @@ -7,11 +7,12 @@ from typing import Counter, cast import os -from pasqal_cloud import SDK -from pasqal_cloud.device import BaseConfig, EmulatorType -from pasqal_cloud.job import Job +from emu_base import BackendConfig +from pasqal_cloud.device import DeviceTypeName from pulser import Sequence from pulser.devices import Device +from pulser.backend.remote import RemoteConnection, BatchStatus, RemoteResults, RemoteBackend +from pulser_pasqal import PasqalCloud from pulser_simulation import QutipEmulator from qek.data.extractors import deserialize_device @@ -95,10 +96,11 @@ class BaseRemoteBackend(BaseBackend): def __init__( self, - project_id: str, - username: str, + project_id: str = None, + username: str = None, device_name: str = "FRESNEL", password: str | None = None, + connection: RemoteConnection = None, ): """ Create a remote backend @@ -112,8 +114,13 @@ def __init__( the default value of "FRESNEL" represents the latest QPU available through the Pasqal Cloud API. """ + # validate None stuff + if connection is not None: + self._connection = connection + else: + assert project_id is not None and username is not None + self._connection = PasqalCloud(username=username, project_id=project_id, password=password) self.device_name = device_name - self._sdk = SDK(username=username, project_id=project_id, password=password) self._max_runs = 500 self._sequence = None self._device = None @@ -127,7 +134,7 @@ async def device(self) -> Device: # Fetch the latest list of QPUs # Implementation note: Currently sync, hopefully async in the future. - specs = self._sdk.get_device_specs_dict() + specs = self._connection.fetch_available_devices() self._device = cast(Device, deserialize_device(specs[self.device_name])) # As of this writing, the API doesn't support runs longer than 500 jobs. @@ -141,17 +148,17 @@ async def _run( self, register: targets.Register, pulse: targets.Pulse, - emulator: EmulatorType | None, - config: BaseConfig | None = None, + device_type_name: DeviceTypeName | None, + config: BackendConfig | None = None, sleep_sec: int = 2, - ) -> Job: + ) -> RemoteResults: """ Run the pulse + register. Arguments: register: A register to run. pulse: A pulse to execute. - emulator: The emulator to use, or None to run on a QPU. + device_type_name: The emulator to use, or None to run on a QPU. config: The backend-specific config. sleep_sec (optional): The amount of time to sleep when waiting for the remote server to respond, in seconds. Defaults to 2. @@ -167,26 +174,30 @@ async def _run( raise CompilationError(f"This register/pulse cannot be executed on the device: {e}") # Enqueue execution. - batch = self._sdk.create_batch( - serialized_sequence=sequence.to_abstract_repr(), - jobs=[{"runs": self._max_runs}], + class CloudBackend(RemoteBackend): + def __init__(self, device_type_name, *args, **kwargs): + self._device_type_name = device_type_name + super(*args, **kwargs) + + def _submit_kwargs(self) -> dict[str, Any]: + """Keyword arguments given to any call to RemoteConnection.submit().""" + return dict(batch_id=self._batch_id, device_type=self.device_type_name) + + + backend = CloudBackend(device_type_name, sequence, self._connection, config=config) + remote_results = backend.run( + jobs_params=[{"runs": self._max_runs}], wait=False, - emulator=emulator, - configuration=config, ) # Wait for execution to complete. while True: await asyncio.sleep(sleep_sec) # Currently sync, hopefully async in the future. - batch.refresh() - if batch.status in {"PENDING", "RUNNING"}: + if remote_results.get_batch_status() in {BatchStatus.PENDING, BatchStatus.RUNNING}: # Continue waiting. continue - job = next(iter(batch.jobs.values())) - if job.status == "ERROR": - raise Exception(f"Error while executing remote job: {job.errors}") - return job + return remote_results.results.final_bitstrings class RemoteQPUBackend(BaseRemoteBackend): @@ -198,10 +209,9 @@ class RemoteQPUBackend(BaseRemoteBackend): may be very long. You may use this Extractor to resume your workflow with a computation that has been previously started. """ - async def run(self, register: targets.Register, pulse: targets.Pulse) -> Counter[str]: - job = await self._run(register, pulse, emulator=None, config=None) - return cast(Counter[str], job.result) + remote_results = await self._run(register, pulse, device_type_name=None, config=None) + return remote_results.results.final_bitstrings class RemoteEmuMPSBackend(BaseRemoteBackend): @@ -209,15 +219,11 @@ class RemoteEmuMPSBackend(BaseRemoteBackend): A backend that uses a remote high-performance emulator (EmuMPS) published on Pasqal Cloud. """ - - async def run( - self, register: targets.Register, pulse: targets.Pulse, dt: int = 10 - ) -> Counter[str]: - job = await self._run(register, pulse, emulator=EmulatorType.EMU_MPS, config=None) - bag = cast(dict[str, dict[int, Counter[str]]], job.result) - - assert self._sequence is not None - return bag["counter"] + async def run(self, register: targets.Register, pulse: targets.Pulse) -> Counter[str]: + observable = emu_mps.BitStrings(evaluation_times=[1.0]) + config = emu_mps.MPSConfig(observables=[observable], dt=10) + remote_results = await self._run(register, pulse, device_type_name=DeviceTypeName.EMU_MPS, config=config) + return remote_results.results.final_bitstrings if os.name == "posix": From ad32f5060a95da200db5799d5c468acdacbb7e2e Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Thu, 18 Jun 2026 00:49:35 +0200 Subject: [PATCH 2/9] simplify --- qek/target/backends.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/qek/target/backends.py b/qek/target/backends.py index a1837e2..9b5aad7 100644 --- a/qek/target/backends.py +++ b/qek/target/backends.py @@ -7,12 +7,12 @@ from typing import Counter, cast import os -from emu_base import BackendConfig -from pasqal_cloud.device import DeviceTypeName from pulser import Sequence from pulser.devices import Device +from pulser.backend import QPUBackend from pulser.backend.remote import RemoteConnection, BatchStatus, RemoteResults, RemoteBackend from pulser_pasqal import PasqalCloud +from pulser_pasqal.backends import EmuMPSBackend as RemoteMPSBackend from pulser_simulation import QutipEmulator from qek.data.extractors import deserialize_device @@ -148,8 +148,7 @@ async def _run( self, register: targets.Register, pulse: targets.Pulse, - device_type_name: DeviceTypeName | None, - config: BackendConfig | None = None, + backend_class: RemoteBackend | None, sleep_sec: int = 2, ) -> RemoteResults: """ @@ -158,8 +157,7 @@ async def _run( Arguments: register: A register to run. pulse: A pulse to execute. - device_type_name: The emulator to use, or None to run on a QPU. - config: The backend-specific config. + backend_class: The backend to use sleep_sec (optional): The amount of time to sleep when waiting for the remote server to respond, in seconds. Defaults to 2. Raises: @@ -173,19 +171,7 @@ async def _run( except ValueError as e: raise CompilationError(f"This register/pulse cannot be executed on the device: {e}") - # Enqueue execution. - class CloudBackend(RemoteBackend): - def __init__(self, device_type_name, *args, **kwargs): - self._device_type_name = device_type_name - super(*args, **kwargs) - - def _submit_kwargs(self) -> dict[str, Any]: - """Keyword arguments given to any call to RemoteConnection.submit().""" - return dict(batch_id=self._batch_id, device_type=self.device_type_name) - - - backend = CloudBackend(device_type_name, sequence, self._connection, config=config) - remote_results = backend.run( + remote_results = backend_class(sequence, self._connection).run( jobs_params=[{"runs": self._max_runs}], wait=False, ) @@ -210,19 +196,17 @@ class RemoteQPUBackend(BaseRemoteBackend): with a computation that has been previously started. """ async def run(self, register: targets.Register, pulse: targets.Pulse) -> Counter[str]: - remote_results = await self._run(register, pulse, device_type_name=None, config=None) + remote_results = await self._run(register, pulse, backend_class=QPUBackend) return remote_results.results.final_bitstrings class RemoteEmuMPSBackend(BaseRemoteBackend): """ A backend that uses a remote high-performance emulator (EmuMPS) - published on Pasqal Cloud. + published on Pasqal Cloud or third party connection. """ async def run(self, register: targets.Register, pulse: targets.Pulse) -> Counter[str]: - observable = emu_mps.BitStrings(evaluation_times=[1.0]) - config = emu_mps.MPSConfig(observables=[observable], dt=10) - remote_results = await self._run(register, pulse, device_type_name=DeviceTypeName.EMU_MPS, config=config) + remote_results = await self._run(register, pulse, backend_class=RemoteMPSBackend) return remote_results.results.final_bitstrings From 55ecb1bdb9e4ee1bc8fbb287a9571e8801d7ee57 Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Fri, 19 Jun 2026 17:52:30 +0200 Subject: [PATCH 3/9] small fix --- qek/target/backends.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/qek/target/backends.py b/qek/target/backends.py index 9b5aad7..ac27779 100644 --- a/qek/target/backends.py +++ b/qek/target/backends.py @@ -4,13 +4,13 @@ import abc import asyncio -from typing import Counter, cast +from typing import Counter, cast, Type import os from pulser import Sequence from pulser.devices import Device from pulser.backend import QPUBackend -from pulser.backend.remote import RemoteConnection, BatchStatus, RemoteResults, RemoteBackend +from pulser.backend.remote import RemoteConnection, BatchStatus, RemoteBackend from pulser_pasqal import PasqalCloud from pulser_pasqal.backends import EmuMPSBackend as RemoteMPSBackend from pulser_simulation import QutipEmulator @@ -148,9 +148,9 @@ async def _run( self, register: targets.Register, pulse: targets.Pulse, - backend_class: RemoteBackend | None, + backend_class: Type[RemoteBackend] | None, sleep_sec: int = 2, - ) -> RemoteResults: + ) -> Counter[str]: """ Run the pulse + register. @@ -196,9 +196,7 @@ class RemoteQPUBackend(BaseRemoteBackend): with a computation that has been previously started. """ async def run(self, register: targets.Register, pulse: targets.Pulse) -> Counter[str]: - remote_results = await self._run(register, pulse, backend_class=QPUBackend) - return remote_results.results.final_bitstrings - + return await self._run(register, pulse, backend_class=QPUBackend) class RemoteEmuMPSBackend(BaseRemoteBackend): """ @@ -206,8 +204,7 @@ class RemoteEmuMPSBackend(BaseRemoteBackend): published on Pasqal Cloud or third party connection. """ async def run(self, register: targets.Register, pulse: targets.Pulse) -> Counter[str]: - remote_results = await self._run(register, pulse, backend_class=RemoteMPSBackend) - return remote_results.results.final_bitstrings + return self._run(register, pulse, backend_class=RemoteMPSBackend) if os.name == "posix": From 757e961305bbcea2e3b5c5830c3e7df56ed9bdbc Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Fri, 19 Jun 2026 17:52:39 +0200 Subject: [PATCH 4/9] update deps --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f34cfde..fe78483 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,13 +31,13 @@ classifiers = [ dependencies = [ "networkx", "numpy", - "pulser~=1.5", + "pulser~=1.8", "rdkit", "scikit-learn", "torch", "torch_geometric", "matplotlib", - "emu-mps~=2.2.0", + "emu-mps~=2.7", "pasqal-cloud", ] From de3390fad146bba2418178979edcc89ecaa7a227 Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Fri, 19 Jun 2026 17:52:59 +0200 Subject: [PATCH 5/9] create extractor v2 to handle arbitrary connection --- qek/data/extractors_v2.py | 400 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 qek/data/extractors_v2.py diff --git a/qek/data/extractors_v2.py b/qek/data/extractors_v2.py new file mode 100644 index 0000000..c43362a --- /dev/null +++ b/qek/data/extractors_v2.py @@ -0,0 +1,400 @@ +""" +High-Level API to compile raw data (graphs) and process it on a quantum device, either a local emulator, +a remote emulator or a physical QPI. +""" + +import abc +import asyncio +import logging +import time +from typing import Any, Generator, Generic, cast, Type +from pulser.backend import QPUBackend, Results +from pulser.backend.remote import RemoteConnection, BatchStatus, RemoteBackend, RemoteResults +from pulser_pasqal.backends import EmuMPSBackend as RemoteMPSBackend +from pathlib import Path +import pulser as pl +from pulser.devices import Device +from pulser.json.abstract_repr.deserializer import deserialize_device + +from qek.data.extractors import BaseExtracted, Compiled, SyncExtracted, GraphType, BaseExtractor +from qek.data.graphs import BaseGraph, BaseGraphCompiler +from qek.data.processed_data import ProcessedData + +logger = logging.getLogger(__name__) + +# How many seconds to sleep while waiting for the results from the cloud. +SLEEP_DELAY_S = 2 + + +class RemoteExtracted(BaseExtracted): + """ + Data extracted from remote API, i.e. we need wait for a remote server. + + Performance note: + If your code is meant to be executed as part of an interactive application or + a server, you should consider calling `await extracted` before your first call + to any of the methods of `extracted`. Otherwise, you will block the main thread. + + If you are running this as part of an experiment, a Jupyter notebook, etc. you + do not need to do so. + """ + + def __init__( + self, + compiled: list[Compiled], + batch_ids: list[str], + connection: RemoteConnection, + path: Path | None = None, + ): + """ + Prepare for reception of data. + + Arguments: + compiled: The result of compiling a set of graphs. + job_ids: The ids of the jobs on the cloud API, in the same order as `compiled`. + path: If provided, a path at which to save the results once they're available. + """ + self._compiled = compiled + self._batch_ids = batch_ids + self._results: SyncExtracted | None = None + self._path = path + self._connection = connection + + def _wait(self) -> None: + """ + Wait synchronously until remote execution is ready. + + This WILL BLOCK your main thread, possibly for a very long time. + """ + if self._results is not None: + # Results are already available. + return + pending_batch_ids: set[str] = set(self._batch_ids) + all_remote_results = {bid: RemoteResults(batch_id=bid,connection=self._connection) for bid in pending_batch_ids} + completed_batchs: dict[str, Results] = {} + while len(pending_batch_ids) > 0: + time.sleep(SLEEP_DELAY_S) + # Update their status. + for bid in pending_batch_ids: + remote_results = all_remote_results[bid] + batch_status = remote_results.get_batch_status() + if batch_status not in {BatchStatus.PENDING, BatchStatus.RUNNING}: + logger.debug("Batch %s is now complete", bid) + pending_batch_ids.discard(bid) + completed_batchs[bid] = remote_results + + # At this point, all jobs are complete. + self._ingest(completed_batchs) + + def __await__(self) -> Generator[Any, Any, None]: + """ + Wait asynchronously until remote execution is ready. + + This will NOT block your main thread, so this method is strongly recommended + for use on a server or an interactive application. + + Example: + await extracted + """ + if self._results is not None: + # Results are already available. + return + pending_batch_ids: set[str] = set(self._batch_ids) + all_remote_results = {bid: RemoteResults(batch_id=bid,connection=self._connection) for bid in pending_batch_ids} + completed_batchs: dict[str, Results] = {} + while len(pending_batch_ids) > 0: + yield from asyncio.sleep(SLEEP_DELAY_S).__await__() + # Update their status. + for bid in pending_batch_ids: + remote_results = all_remote_results[bid] + batch_status = remote_results.get_batch_status() + if batch_status not in {BatchStatus.PENDING, BatchStatus.RUNNING}: + logger.debug("Batch %s is now complete", bid) + pending_batch_ids.discard(bid) + completed_batchs[bid] = remote_results + + # At this point, all jobs are complete. + self._ingest(completed_batchs) + + def _ingest(self, completed_batch: dict[str, RemoteResults]) -> None: + """ + Ingest data received from the remote server. + + No I/O. + """ + assert len(completed_batch) == len(self._batch_ids) + + raw_data = [] + targets: list[int] = [] + sequences = [] + all_bitstrings = [] + for i, id in enumerate(self._batch_ids): + batch_results = completed_batch[id] + compiled = self._compiled[i] + results = list(batch_results.get_available_results().values()) + if len(results) == 1: + job_results = results[0] + bitstrings = self._state_extractor(job_results.final_bitstrings, compiled.sequence) + if bitstrings is None: + logger.warning( + "Job %s (graph %s) did not return a usable state, skipping", + i, + compiled.graph.id, + ) + continue + raw_data.append(compiled.graph) + if compiled.graph.target is not None: + targets.append(compiled.graph.target) + sequences.append(compiled.sequence) + all_bitstrings.append(bitstrings) + else: + # If some sequences failed, let's skip them and proceed as well as we can. + logger.warning( + "Job %s (graph %s) failed, skipping", + i, + compiled.graph.id + ) + self._results = SyncExtracted( + raw_data=raw_data, targets=targets, sequences=sequences, states=all_bitstrings + ) + if self._path is not None: + self.save_dataset(self._path) + + @property + def processed_data(self) -> list[ProcessedData]: + self._wait() + assert self._results is not None + return self._results.processed_data + + @property + def raw_data(self) -> list[BaseGraph]: + self._wait() + assert self._results is not None + return self._results.raw_data + + @property + def targets(self) -> list[int] | None: + self._wait() + assert self._results is not None + return self._results.targets + + @property + def sequences(self) -> list[pl.Sequence]: + self._wait() + assert self._results is not None + return self._results.sequences + + @property + def states(self) -> list[dict[str, int]]: + self._wait() + assert self._results is not None + return self._results.states + + +class BaseRemoteExtractorV2(BaseExtractor[GraphType], Generic[GraphType]): + """ + An Extractor that uses a remote Quantum Device published + on Pasqal Cloud, to run sequences compiled from graphs. + + Performance note (servers and interactive applications only): + If your code is meant to be executed as part of an interactive application or + a server, you should consider calling `await extracted` before your first call + to any of the methods of `extracted`. Otherwise, you will block the main thread. + + If you are running this as part of an experiment, a Jupyter notebook, etc. you + may ignore this performance note. + + Args: + path: Path to store the result of the run, for future uses. + To reload the result of a previous run, use `LoadExtractor`. + project_id: The ID of the project on the Pasqal Cloud API. + username: Your username on the Pasqal Cloud API. + password: Your password on the Pasqal Cloud API. If you leave + this to None, you will need to enter your password manually. + device_name: The name of the device to use. As of this writing, + the default value of "FRESNEL" represents the latest QPU + available through the Pasqal Cloud API. + batch_ids: Use this to resume a workflow e.g. after turning off + your computer while the QPU was executing your sequences. + Warning: A job started with one executor MUST NOT be resumed + with a different executor. + """ + + def __init__( + self, + compiler: BaseGraphCompiler[GraphType], + connection: RemoteConnection, + batch_ids: list[str] | None = None, + device_name: str = "FRESNEL", + path: Path | None = None, + ): + + # Fetch the latest list of QPUs + specs = connection.fetch_available_devices() + device = cast(Device, deserialize_device(specs[device_name])) + + super().__init__(device=device, compiler=compiler, path=path) + self._connection = connection + self._batch_ids: list[str] | None = batch_ids + + @property + def batch_ids(self) -> list[str] | None: + return self._batch_ids + + @abc.abstractmethod + def run( + self, + ) -> RemoteExtracted: + """ + Launch the extraction. + """ + raise NotImplementedError() + + def _run( + self, + backend_class: Type[RemoteBackend], + ) -> RemoteExtracted: + if len(self.sequences) == 0: + logger.warning("No sequences to run, did you forget to call compile()?") + return RemoteExtracted( + compiled=[], + batch_ids=[], + connection=self._connection, + path=self.path, + ) + + device: pl.devices.Device = self.sequences[0].sequence.device + # As of this writing, the API doesn't support runs longer than 500 jobs. + # If we want to add more runs, we'll need to split them across several jobs. + max_runs = device.max_runs if isinstance(device.max_runs, int) else 500 + + if self._batch_ids is None: + # Enqueue jobs. + self._batch_ids = [] + for compiled in self.sequences: + logger.debug("Enqueuing execution of compiled graph #%s", compiled.graph.id) + remote_results = backend_class(compiled.sequence, self._connection).run( + jobs_params=[{"runs": max_runs}], + wait=False, + ) + batch_id = remote_results.batch_id + logger.info( + "Remote execution of compiled graph #%s starting, job with id %s", + compiled.graph.id, + batch_id, + ) + self._batch_ids.append(batch_id) + logger.info( + "All %s jobs enqueued for remote execution, with ids %s", + len(self._batch_ids), + self._batch_ids, + ) + assert len(self._batch_ids) == len(self.sequences) + + return RemoteExtracted( + compiled=self.sequences, + batch_ids=self._batch_ids, + connection=self._connection, + path=self.path, + ) + + +class RemoteQPUExtractorV2(BaseRemoteExtractorV2[GraphType]): + """ + An Extractor that uses a remote QPU published + on Pasqal Cloud, to run sequences compiled from graphs. + + Performance note: + as of this writing, the waiting lines for a QPU + may be very long. You may use this Extractor to resume your workflow + with a computation that has been previously started. + + Performance note (servers and interactive applications only): + If your code is meant to be executed as part of an interactive application or + a server, you should consider calling `await extracted` before your first call + to any of the methods of `extracted`. Otherwise, you will block the main thread. + + If you are running this as part of an experiment, a Jupyter notebook, etc. you + may ignore this performance note. + + Args: + path: Path to store the result of the run, for future uses. + To reload the result of a previous run, use `LoadExtractor`. + project_id: The ID of the project on the Pasqal Cloud API. + username: Your username on the Pasqal Cloud API. + password: Your password on the Pasqal Cloud API. If you leave + this to None, you will need to enter your password manually. + device_name: The name of the device to use. As of this writing, + the default value of "FRESNEL" represents the latest QPU + available through the Pasqal Cloud API. + job_id: Use this to resume a workflow e.g. after turning off + your computer while the QPU was executing your sequences. + """ + + def __init__( + self, + compiler: BaseGraphCompiler[GraphType], + connection: RemoteConnection, + batch_ids: list[str] | None = None, + device_name: str = "FRESNEL", + path: Path | None = None, + ): + super().__init__( + compiler=compiler, + connection=connection, + batch_ids=batch_ids, + device_name=device_name, + path=path, + ) + + def run(self) -> RemoteExtracted: + return self._run(backend_class=QPUBackend) + + +class RemoteEmuMPSExtractorV2(BaseRemoteExtractorV2[GraphType]): + """ + An Extractor that uses a remote high-performance emulator (EmuMPS) + published on Pasqal Cloud, to run sequences compiled from graphs. + + Performance note (servers and interactive applications only): + If your code is meant to be executed as part of an interactive application or + a server, you should consider calling `await extracted` before your first call + to any of the methods of `extracted`. Otherwise, you will block the main thread. + + If you are running this as part of an experiment, a Jupyter notebook, etc. you + may ignore this performance note. + + Args: + path: Path to store the result of the run, for future uses. + To reload the result of a previous run, use `LoadExtractor`. + project_id: The ID of the project on the Pasqal Cloud API. + username: Your username on the Pasqal Cloud API. + password: Your password on the Pasqal Cloud API. If you leave + this to None, you will need to enter your password manually. + device_name: The name of the device to use. As of this writing, + the default value of "FRESNEL" represents the latest QPU + available through the Pasqal Cloud API. + job_id: Use this to resume a workflow e.g. after turning off + your computer while the QPU was executing your sequences. + """ + + def __init__( + self, + compiler: BaseGraphCompiler[GraphType], + connection: RemoteConnection, + batch_ids: list[str] | None = None, + device_name: str = "FRESNEL", + path: Path | None = None, + ): + super().__init__( + compiler=compiler, + connection=connection, + batch_ids=batch_ids, + device_name=device_name, + path=path, + ) + + def run(self) -> RemoteExtracted: + return self._run( + backend_class=RemoteMPSBackend, + ) From b258687df2935dc25e1f911b3ad08885a9265241 Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Mon, 14 Sep 2026 14:35:07 +0200 Subject: [PATCH 6/9] claude to polish stuff --- qek/data/extractors_v2.py | 270 ++++++++++++++++---------------------- 1 file changed, 112 insertions(+), 158 deletions(-) diff --git a/qek/data/extractors_v2.py b/qek/data/extractors_v2.py index c43362a..bc2fc4f 100644 --- a/qek/data/extractors_v2.py +++ b/qek/data/extractors_v2.py @@ -1,20 +1,22 @@ """ High-Level API to compile raw data (graphs) and process it on a quantum device, either a local emulator, -a remote emulator or a physical QPI. +a remote emulator or a physical QPU. + +Unlike `qek.data.extractors`, this module only speaks Pulser: any `pulser.backend.remote.RemoteConnection` +(e.g. `pulser_pasqal.PasqalCloud`) and any `RemoteBackend` will do, so nothing here depends on the +pasqal-cloud SDK. """ import abc import asyncio import logging import time -from typing import Any, Generator, Generic, cast, Type -from pulser.backend import QPUBackend, Results +from typing import Any, Generator, Generic, Type +from pulser.backend import QPUBackend from pulser.backend.remote import RemoteConnection, BatchStatus, RemoteBackend, RemoteResults -from pulser_pasqal.backends import EmuMPSBackend as RemoteMPSBackend from pathlib import Path import pulser as pl from pulser.devices import Device -from pulser.json.abstract_repr.deserializer import deserialize_device from qek.data.extractors import BaseExtracted, Compiled, SyncExtracted, GraphType, BaseExtractor from qek.data.graphs import BaseGraph, BaseGraphCompiler @@ -25,10 +27,13 @@ # How many seconds to sleep while waiting for the results from the cloud. SLEEP_DELAY_S = 2 +# Batch statuses that mean "come back later". +_PENDING_STATUSES = {BatchStatus.PENDING, BatchStatus.RUNNING} + class RemoteExtracted(BaseExtracted): """ - Data extracted from remote API, i.e. we need wait for a remote server. + Data extracted from a remote connection, i.e. we need to wait for a remote server. Performance note: If your code is meant to be executed as part of an interactive application or @@ -51,7 +56,9 @@ def __init__( Arguments: compiled: The result of compiling a set of graphs. - job_ids: The ids of the jobs on the cloud API, in the same order as `compiled`. + batch_ids: The ids of the batches on the remote connection, in the same + order as `compiled`, one batch per compiled graph. + connection: The connection on which the batches were submitted. path: If provided, a path at which to save the results once they're available. """ self._compiled = compiled @@ -60,31 +67,38 @@ def __init__( self._path = path self._connection = connection - def _wait(self) -> None: + def _poll(self) -> Generator[None, None, None]: """ - Wait synchronously until remote execution is ready. + Poll the remote connection until all batches are complete, ingesting the results. - This WILL BLOCK your main thread, possibly for a very long time. + Yields once per round, leaving it to the caller to wait between rounds (blocking + or not). Yields nothing at all if the results are already available. """ if self._results is not None: # Results are already available. return - pending_batch_ids: set[str] = set(self._batch_ids) - all_remote_results = {bid: RemoteResults(batch_id=bid,connection=self._connection) for bid in pending_batch_ids} - completed_batchs: dict[str, Results] = {} - while len(pending_batch_ids) > 0: - time.sleep(SLEEP_DELAY_S) - # Update their status. - for bid in pending_batch_ids: - remote_results = all_remote_results[bid] - batch_status = remote_results.get_batch_status() - if batch_status not in {BatchStatus.PENDING, BatchStatus.RUNNING}: + pending = { + bid: RemoteResults(batch_id=bid, connection=self._connection) for bid in self._batch_ids + } + completed: dict[str, RemoteResults] = {} + while len(pending) > 0: + yield + for bid, remote_results in list(pending.items()): + if remote_results.get_batch_status() not in _PENDING_STATUSES: logger.debug("Batch %s is now complete", bid) - pending_batch_ids.discard(bid) - completed_batchs[bid] = remote_results + completed[bid] = pending.pop(bid) + + # At this point, all batches are complete. + self._ingest(completed) - # At this point, all jobs are complete. - self._ingest(completed_batchs) + def _wait(self) -> None: + """ + Wait synchronously until remote execution is ready. + + This WILL BLOCK your main thread, possibly for a very long time. + """ + for _ in self._poll(): + time.sleep(SLEEP_DELAY_S) def __await__(self) -> Generator[Any, Any, None]: """ @@ -96,66 +110,51 @@ def __await__(self) -> Generator[Any, Any, None]: Example: await extracted """ - if self._results is not None: - # Results are already available. - return - pending_batch_ids: set[str] = set(self._batch_ids) - all_remote_results = {bid: RemoteResults(batch_id=bid,connection=self._connection) for bid in pending_batch_ids} - completed_batchs: dict[str, Results] = {} - while len(pending_batch_ids) > 0: + for _ in self._poll(): yield from asyncio.sleep(SLEEP_DELAY_S).__await__() - # Update their status. - for bid in pending_batch_ids: - remote_results = all_remote_results[bid] - batch_status = remote_results.get_batch_status() - if batch_status not in {BatchStatus.PENDING, BatchStatus.RUNNING}: - logger.debug("Batch %s is now complete", bid) - pending_batch_ids.discard(bid) - completed_batchs[bid] = remote_results - - # At this point, all jobs are complete. - self._ingest(completed_batchs) - def _ingest(self, completed_batch: dict[str, RemoteResults]) -> None: + def _ingest(self, completed: dict[str, RemoteResults]) -> None: """ Ingest data received from the remote server. No I/O. """ - assert len(completed_batch) == len(self._batch_ids) + assert len(completed) == len(self._batch_ids) raw_data = [] targets: list[int] = [] sequences = [] - all_bitstrings = [] + states = [] for i, id in enumerate(self._batch_ids): - batch_results = completed_batch[id] compiled = self._compiled[i] - results = list(batch_results.get_available_results().values()) - if len(results) == 1: - job_results = results[0] - bitstrings = self._state_extractor(job_results.final_bitstrings, compiled.sequence) - if bitstrings is None: - logger.warning( - "Job %s (graph %s) did not return a usable state, skipping", - i, - compiled.graph.id, - ) - continue - raw_data.append(compiled.graph) - if compiled.graph.target is not None: - targets.append(compiled.graph.target) - sequences.append(compiled.sequence) - all_bitstrings.append(bitstrings) - else: + # We submit exactly one job per compiled graph. + results = list(completed[id].get_available_results().values()) + if len(results) != 1: # If some sequences failed, let's skip them and proceed as well as we can. logger.warning( - "Job %s (graph %s) failed, skipping", - i, - compiled.graph.id + "Batch %s (graph %s) returned %s results instead of 1, skipping", + id, + compiled.graph.id, + len(results), ) + continue + try: + bitstrings = results[0].final_bitstrings + except RuntimeError as e: + logger.warning( + "Batch %s (graph %s) did not return a usable state (%s), skipping", + id, + compiled.graph.id, + e, + ) + continue + raw_data.append(compiled.graph) + if compiled.graph.target is not None: + targets.append(compiled.graph.target) + sequences.append(compiled.sequence) + states.append(bitstrings) self._results = SyncExtracted( - raw_data=raw_data, targets=targets, sequences=sequences, states=all_bitstrings + raw_data=raw_data, targets=targets, sequences=sequences, states=states ) if self._path is not None: self.save_dataset(self._path) @@ -193,8 +192,8 @@ def states(self) -> list[dict[str, int]]: class BaseRemoteExtractorV2(BaseExtractor[GraphType], Generic[GraphType]): """ - An Extractor that uses a remote Quantum Device published - on Pasqal Cloud, to run sequences compiled from graphs. + An Extractor that runs sequences compiled from graphs on a remote Quantum Device, + reachable through any Pulser `RemoteConnection`. Performance note (servers and interactive applications only): If your code is meant to be executed as part of an interactive application or @@ -205,33 +204,33 @@ class BaseRemoteExtractorV2(BaseExtractor[GraphType], Generic[GraphType]): may ignore this performance note. Args: - path: Path to store the result of the run, for future uses. - To reload the result of a previous run, use `LoadExtractor`. - project_id: The ID of the project on the Pasqal Cloud API. - username: Your username on the Pasqal Cloud API. - password: Your password on the Pasqal Cloud API. If you leave - this to None, you will need to enter your password manually. - device_name: The name of the device to use. As of this writing, - the default value of "FRESNEL" represents the latest QPU - available through the Pasqal Cloud API. + compiler: A graph compiler, in charge of converting graphs to Pulser Sequences. + connection: An open connection to the remote API, e.g. `pulser_pasqal.PasqalCloud`. + device: The device to compile for. If unspecified, fetch `device_name` from + `connection`. + device_name: The name of the device to fetch from `connection`. As of this writing, + the default value of "FRESNEL" represents the latest QPU available through + the Pasqal Cloud API. Ignored if `device` is specified. batch_ids: Use this to resume a workflow e.g. after turning off your computer while the QPU was executing your sequences. - Warning: A job started with one executor MUST NOT be resumed - with a different executor. + Warning: A batch started with one extractor MUST NOT be resumed + with a different extractor. + path: Path to store the result of the run, for future uses. + To reload the result of a previous run, use `LoadExtractor`. """ def __init__( self, compiler: BaseGraphCompiler[GraphType], connection: RemoteConnection, - batch_ids: list[str] | None = None, + device: Device | None = None, device_name: str = "FRESNEL", + batch_ids: list[str] | None = None, path: Path | None = None, ): - - # Fetch the latest list of QPUs - specs = connection.fetch_available_devices() - device = cast(Device, deserialize_device(specs[device_name])) + if device is None: + # Fetch the latest specs of the device. + device = connection.fetch_available_devices()[device_name] super().__init__(device=device, compiler=compiler, path=path) self._connection = connection @@ -253,6 +252,7 @@ def run( def _run( self, backend_class: Type[RemoteBackend], + **backend_kwargs: Any, ) -> RemoteExtracted: if len(self.sequences) == 0: logger.warning("No sequences to run, did you forget to call compile()?") @@ -269,23 +269,22 @@ def _run( max_runs = device.max_runs if isinstance(device.max_runs, int) else 500 if self._batch_ids is None: - # Enqueue jobs. + # Enqueue one batch per compiled graph. self._batch_ids = [] for compiled in self.sequences: logger.debug("Enqueuing execution of compiled graph #%s", compiled.graph.id) - remote_results = backend_class(compiled.sequence, self._connection).run( - jobs_params=[{"runs": max_runs}], - wait=False, - ) + remote_results = backend_class( + compiled.sequence, self._connection, **backend_kwargs + ).run(job_params=[{"runs": max_runs}], wait=False) batch_id = remote_results.batch_id logger.info( - "Remote execution of compiled graph #%s starting, job with id %s", + "Remote execution of compiled graph #%s starting, batch with id %s", compiled.graph.id, batch_id, ) self._batch_ids.append(batch_id) logger.info( - "All %s jobs enqueued for remote execution, with ids %s", + "All %s batches enqueued for remote execution, with ids %s", len(self._batch_ids), self._batch_ids, ) @@ -299,15 +298,19 @@ def _run( ) -class RemoteQPUExtractorV2(BaseRemoteExtractorV2[GraphType]): +class RemoteExtractorV2(BaseRemoteExtractorV2[GraphType]): """ - An Extractor that uses a remote QPU published - on Pasqal Cloud, to run sequences compiled from graphs. + An Extractor that runs sequences compiled from graphs on a remote backend. + + By default, it runs on a QPU (`QPUBackend`). To run on a remote emulator instead, pass + the corresponding backend class, e.g.: + + RemoteExtractorV2(compiler, connection, backend_class=pulser_pasqal.EmuMPSBackend) Performance note: as of this writing, the waiting lines for a QPU may be very long. You may use this Extractor to resume your workflow - with a computation that has been previously started. + with a computation that has been previously started, by passing `batch_ids`. Performance note (servers and interactive applications only): If your code is meant to be executed as part of an interactive application or @@ -318,83 +321,34 @@ class RemoteQPUExtractorV2(BaseRemoteExtractorV2[GraphType]): may ignore this performance note. Args: - path: Path to store the result of the run, for future uses. - To reload the result of a previous run, use `LoadExtractor`. - project_id: The ID of the project on the Pasqal Cloud API. - username: Your username on the Pasqal Cloud API. - password: Your password on the Pasqal Cloud API. If you leave - this to None, you will need to enter your password manually. - device_name: The name of the device to use. As of this writing, - the default value of "FRESNEL" represents the latest QPU - available through the Pasqal Cloud API. - job_id: Use this to resume a workflow e.g. after turning off - your computer while the QPU was executing your sequences. - """ + backend_class: The Pulser remote backend to execute the sequences on. It must be + compatible with `connection`. + backend_kwargs: Any additional arguments for `backend_class`, e.g. `config`. - def __init__( - self, - compiler: BaseGraphCompiler[GraphType], - connection: RemoteConnection, - batch_ids: list[str] | None = None, - device_name: str = "FRESNEL", - path: Path | None = None, - ): - super().__init__( - compiler=compiler, - connection=connection, - batch_ids=batch_ids, - device_name=device_name, - path=path, - ) - - def run(self) -> RemoteExtracted: - return self._run(backend_class=QPUBackend) - - -class RemoteEmuMPSExtractorV2(BaseRemoteExtractorV2[GraphType]): - """ - An Extractor that uses a remote high-performance emulator (EmuMPS) - published on Pasqal Cloud, to run sequences compiled from graphs. - - Performance note (servers and interactive applications only): - If your code is meant to be executed as part of an interactive application or - a server, you should consider calling `await extracted` before your first call - to any of the methods of `extracted`. Otherwise, you will block the main thread. - - If you are running this as part of an experiment, a Jupyter notebook, etc. you - may ignore this performance note. - - Args: - path: Path to store the result of the run, for future uses. - To reload the result of a previous run, use `LoadExtractor`. - project_id: The ID of the project on the Pasqal Cloud API. - username: Your username on the Pasqal Cloud API. - password: Your password on the Pasqal Cloud API. If you leave - this to None, you will need to enter your password manually. - device_name: The name of the device to use. As of this writing, - the default value of "FRESNEL" represents the latest QPU - available through the Pasqal Cloud API. - job_id: Use this to resume a workflow e.g. after turning off - your computer while the QPU was executing your sequences. + See `BaseRemoteExtractorV2` for the other arguments. """ def __init__( self, compiler: BaseGraphCompiler[GraphType], connection: RemoteConnection, - batch_ids: list[str] | None = None, + backend_class: Type[RemoteBackend] = QPUBackend, + device: Device | None = None, device_name: str = "FRESNEL", + batch_ids: list[str] | None = None, path: Path | None = None, + **backend_kwargs: Any, ): super().__init__( compiler=compiler, connection=connection, - batch_ids=batch_ids, + device=device, device_name=device_name, + batch_ids=batch_ids, path=path, ) + self._backend_class = backend_class + self._backend_kwargs = backend_kwargs def run(self) -> RemoteExtracted: - return self._run( - backend_class=RemoteMPSBackend, - ) + return self._run(backend_class=self._backend_class, **self._backend_kwargs) From be1a10ececec807dfdc2438659abe531a485b9e9 Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Mon, 14 Sep 2026 22:13:07 +0200 Subject: [PATCH 7/9] use connection instead of sdk in doc and tutorial --- ...to Extract Machine-Learning Features.ipynb | 32 +++++++++++++------ ...achine-Learning Features - low-level.ipynb | 26 +++++++-------- ...VM QEK - low-level - generic dataset.ipynb | 24 +++++++------- qek/target/backends.py | 9 +++--- tests/test_backends.py | 20 +++++++++++- 5 files changed, 71 insertions(+), 40 deletions(-) diff --git a/examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb b/examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb index 8446d36..61732ad 100644 --- a/examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb +++ b/examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb @@ -167,8 +167,10 @@ "## Creating and executing a feature extractor on a physical QPU\n", "\n", "Once you have checked that low qubit sequences provide the results you expect on an emulator, you will generally want to move to a QPU.\n", - "For this, you will need either physical access to a QPU, or an account with [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides\n", - "you remote access to QPUs built and hosted by Pasqal. In this section, we'll see how to use the latter.\n", + "For this, you will need a connection to a remote quantum device. QEK accepts any Pulser\n", + "[`RemoteConnection`](https://pulser.readthedocs.io/en/stable/apidoc/backend.html), so it works with any provider that\n", + "ships one. In this section, we'll use [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides remote access to\n", + "QPUs built and hosted by Pasqal, through Pulser's `PasqalCloud` connection.\n", "\n", "If you don't have an account, just skip to the next section!" ] @@ -180,19 +182,28 @@ "outputs": [], "source": [ "from pathlib import Path\n", + "import qek.data.extractors_v2 as qek_extractors_v2\n", "\n", "\n", "HAVE_PASQAL_ACCOUNT = False # If you have a PASQAL Cloud account, fill in the details and set this to `True`.\n", "\n", "if HAVE_PASQAL_ACCOUNT:\n", - " # Use the QPU Extractor.\n", - " extractor = qek_extractors.RemoteQPUExtractor(\n", + " from pulser_pasqal import PasqalCloud\n", + "\n", + " # Open a connection to the remote device. Any Pulser `RemoteConnection` will do here.\n", + " connection = PasqalCloud(\n", + " project_id = \"XXXX\", # Replace this with your project id on the PASQAL Cloud\n", + " username = \"XXX\", # Replace this with your username on PASQAL Cloud\n", + " # Security note: we deliberately do not pass a password here. Leaving it out means\n", + " # you will be prompted for it, instead of writing it down in your code.\n", + " )\n", + "\n", + " # Use the remote extractor. By default, it runs on a QPU.\n", + " extractor = qek_extractors_v2.RemoteExtractorV2(\n", " # Once computing is complete, data will be saved in this file.\n", " path=Path(\"saved_data.json\"),\n", " compiler = compiler,\n", - " project_id = \"XXXX\", # Replace this with your project id on the PASQAL Cloud\n", - " username = \"XXX\", # Replace this with your username on PASQAL Cloud\n", - " password = None, # Replace this with your password on PASQAL Cloud or enter it on the command-line\n", + " connection = connection,\n", " )\n", "\n", " # Add the graphs, exactly as above.\n", @@ -221,11 +232,12 @@ "\n", "There are two main ways to deal with this:\n", "\n", - "1. `RemoteQPUExtractor` can be attached to an ongoing job from batch ids, so that you can resume your work\n", + "1. `RemoteExtractorV2` can be attached to an ongoing job by passing `batch_ids`, so that you can resume your work\n", " e.g. after turning off your computer.\n", "2. Pasqal CLOUD offers access to high-performance hardware-based emulators, with dramatically\n", - " shorter waiting lines. For instance, in the snippet above, you may replace `RemoteQPUExtractor`\n", - " with `RemoteEmuMPSExtractor` to use the emu-mps emulator.\n", + " shorter waiting lines. For instance, in the snippet above, you may pass\n", + " `backend_class=pulser_pasqal.EmuMPSBackend` to run on the emu-mps emulator instead of a QPU.\n", + " Any Pulser remote backend compatible with your connection works the same way.\n", "\n", "See [the documentation](https://pqs.pages.pasqal.com/quantum-evolution-kernel/) for more details." ] diff --git a/examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb b/examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb index faefaff..bb49d04 100644 --- a/examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb +++ b/examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb @@ -240,7 +240,7 @@ "Once you have checked that the compiled graphs work on an emulator, you will probably want to move to a QPU. Execution on a QPU takes\n", "resources polynomial in the number of qubits, which hopefully means an almost exponential speedup for large number of qubits.\n", "\n", - "To experiment with a QPU, you will need either physical access to a QPU, or an account with [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides you remote access to QPUs built and hosted by Pasqal. In this section, we'll see how to use the latter.\n", + "To experiment with a QPU, you will need a connection to a remote quantum device: QEK accepts any Pulser [`RemoteConnection`](https://pulser.readthedocs.io/en/stable/apidoc/backend.html). In this section, we'll use [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides remote access to QPUs built and hosted by Pasqal, through Pulser's `PasqalCloud` connection.\n", "\n", "If you don't have an account, just skip to the next section!" ] @@ -253,20 +253,20 @@ "source": [ "HAVE_PASQAL_ACCOUNT = False # If you have a PASQAL Cloud account, fill in the details and set this to `True`.\n", "\n", - "if HAVE_PASQAL_ACCOUNT: \n", + "if HAVE_PASQAL_ACCOUNT:\n", + " from pulser_pasqal import PasqalCloud\n", " from qek.target.backends import RemoteQPUBackend\n", " processed_dataset = []\n", "\n", - " # Initialize connection\n", + " # Open a connection to the remote device. Any Pulser `RemoteConnection` will do here.\n", + " connection = PasqalCloud(\n", + " project_id = \"your_project_id\", # Replace this value with your project_id on the PASQAL platform.\n", + " username = \"your_username\", # Replace this value with your username or email on the PASQAL platform.\n", + " # Security note: we deliberately do not pass a password here. Leaving it out means\n", + " # you will be prompted for it, instead of writing it down in your code.\n", + " )\n", "\n", - " my_project_id = \"your_project_id\"# Replace this value with your project_id on the PASQAL platform.\n", - " my_username = \"your_username\" # Replace this value with your username or email on the PASQAL platform.\n", - " my_password = \"your_password\" # Replace this value with your password on the PASQAL platform.\n", - " # Security note: In real life, you probably don't want to write your password in the code.\n", - " # See the documentation of PASQAL Cloud for other ways to provide your password.\n", - "\n", - " # Initialize the cloud client\n", - " backend = RemoteQPUBackend(username=my_username, project_id=my_project_id, password=my_password)\n", + " backend = RemoteQPUBackend(connection=connection)\n", "\n", " # Fetch the specification of our QPU\n", " device = await backend.device()\n", @@ -301,9 +301,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "There are other ways to use the SDK. For instance, you can enqueue a job and check later whether it has completed. Also, to work around the long waiting lines, Pasqal provides high-performance distributed and hardware-accelerated emulators, which you can access through the SDK.\n", + "There are other ways to use a remote connection. For instance, you can enqueue a job and check later whether it has completed. Also, to work around the long waiting lines, Pasqal provides high-performance distributed and hardware-accelerated emulators, which you can reach over the same connection by picking another Pulser remote backend (e.g. `pulser_pasqal.EmuMPSBackend`).\n", "\n", - "For more details, [take a look at the documentation of the SDK](https://docs.pasqal.com/cloud).\n" + "For more details, [take a look at the Pulser documentation](https://pulser.readthedocs.io/en/stable/apidoc/backend.html).\n" ] }, { diff --git a/examples/tutorial 1b - Training SVM QEK - low-level - generic dataset.ipynb b/examples/tutorial 1b - Training SVM QEK - low-level - generic dataset.ipynb index 1545d79..5e86009 100644 --- a/examples/tutorial 1b - Training SVM QEK - low-level - generic dataset.ipynb +++ b/examples/tutorial 1b - Training SVM QEK - low-level - generic dataset.ipynb @@ -318,11 +318,11 @@ "Once you have checked that the pulses work on an emulator, you will probably want to move to a QPU. Execution on a QPU takes\n", "resources polynomial in the number of qubits, which hopefully means an almost exponential speedup for large number of qubits.\n", "\n", - "To experiment with a QPU, you will need either physical access to a QPU, or an account with [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides you remote access to QPUs built and hosted by Pasqal. In this section, we'll see how to use the latter.\n", + "To experiment with a QPU, you will need a connection to a remote quantum device: QEK accepts any Pulser [`RemoteConnection`](https://pulser.readthedocs.io/en/stable/apidoc/backend.html). In this section, we'll use [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides remote access to QPUs built and hosted by Pasqal, through Pulser's `PasqalCloud` connection.\n", "\n", "If you don't have an account, just skip to the next section!\n", "\n", - "> There are other ways to use the SDK. For instance, you can enqueue a job and check later whether it has completed. Also, to work around the long waiting lines, Pasqal provides high-performance distributed and hardware-accelerated emulators, which you can access through the SDK. For more details, [take a look at the documentation of the SDK](https://docs.pasqal.com/cloud)." + "> There are other ways to use a remote connection. For instance, you can enqueue a job and check later whether it has completed. Also, to work around the long waiting lines, Pasqal provides high-performance distributed and hardware-accelerated emulators, which you can reach over the same connection by picking another Pulser remote backend (e.g. `pulser_pasqal.EmuMPSBackend`). For more details, [take a look at the Pulser documentation](https://pulser.readthedocs.io/en/stable/apidoc/backend.html)." ] }, { @@ -333,20 +333,20 @@ "source": [ "HAVE_PASQAL_ACCOUNT = False # If you have a PASQAL Cloud account, fill in the details and set this to `True`.\n", "\n", - "if HAVE_PASQAL_ACCOUNT: \n", + "if HAVE_PASQAL_ACCOUNT:\n", + " from pulser_pasqal import PasqalCloud\n", " from qek.target.backends import RemoteQPUBackend\n", " processed_dataset = []\n", "\n", - " # Initialize connection\n", + " # Open a connection to the remote device. Any Pulser `RemoteConnection` will do here.\n", + " connection = PasqalCloud(\n", + " project_id = \"your_project_id\", # Replace this value with your project_id on the PASQAL platform.\n", + " username = \"your_username\", # Replace this value with your username or email on the PASQAL platform.\n", + " # Security note: we deliberately do not pass a password here. Leaving it out means\n", + " # you will be prompted for it, instead of writing it down in your code.\n", + " )\n", "\n", - " my_project_id = \"your_project_id\"# Replace this value with your project_id on the PASQAL platform.\n", - " my_username = \"your_username\" # Replace this value with your username or email on the PASQAL platform.\n", - " my_password = \"your_password\" # Replace this value with your password on the PASQAL platform.\n", - " # Security note: In real life, you probably don't want to write your password in the code.\n", - " # See the documentation of PASQAL Cloud for other ways to provide your password.\n", - "\n", - " # Initialize the cloud client\n", - " executor = RemoteQPUBackend(username=my_username, project_id=my_project_id, password=my_password)\n", + " executor = RemoteQPUBackend(connection=connection)\n", "\n", " # Fetch the specification of our QPU\n", " device = await executor.device()\n", diff --git a/qek/target/backends.py b/qek/target/backends.py index ac27779..6453bc0 100644 --- a/qek/target/backends.py +++ b/qek/target/backends.py @@ -15,7 +15,6 @@ from pulser_pasqal.backends import EmuMPSBackend as RemoteMPSBackend from pulser_simulation import QutipEmulator -from qek.data.extractors import deserialize_device from qek.shared.error import CompilationError from qek.shared._utils import make_sequence from qek.target import targets @@ -134,8 +133,9 @@ async def device(self) -> Device: # Fetch the latest list of QPUs # Implementation note: Currently sync, hopefully async in the future. + # A Pulser `RemoteConnection` already hands us deserialized `Device`s. specs = self._connection.fetch_available_devices() - self._device = cast(Device, deserialize_device(specs[self.device_name])) + self._device = cast(Device, specs[self.device_name]) # As of this writing, the API doesn't support runs longer than 500 jobs. # If we want to add more runs, we'll need to split them across several jobs. @@ -172,7 +172,7 @@ async def _run( raise CompilationError(f"This register/pulse cannot be executed on the device: {e}") remote_results = backend_class(sequence, self._connection).run( - jobs_params=[{"runs": self._max_runs}], + job_params=[{"runs": self._max_runs}], wait=False, ) @@ -183,7 +183,8 @@ async def _run( if remote_results.get_batch_status() in {BatchStatus.PENDING, BatchStatus.RUNNING}: # Continue waiting. continue - return remote_results.results.final_bitstrings + # We submit exactly one job, so exactly one `Results`. + return remote_results.results[0].final_bitstrings class RemoteQPUBackend(BaseRemoteBackend): diff --git a/tests/test_backends.py b/tests/test_backends.py index e39259e..835e4c7 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -1,14 +1,17 @@ from typing import cast +from unittest.mock import patch import os import pulser as pl import pytest import torch_geometric.data as pyg_data import torch_geometric.datasets as pyg_dataset +from pulser_pasqal import PasqalCloud from qek.target import targets -from qek.target.backends import CompilationError, QutipBackend, BaseBackend +from qek.target.backends import CompilationError, QutipBackend, BaseBackend, RemoteQPUBackend import qek.data.graphs as qek_graphs from qek.shared.retrier import PygRetrier +from tests.mock_cloud_sdk import MockSDK if os.name == "posix": # As of this writing, emu-mps only works under Unix. @@ -62,3 +65,18 @@ async def test_async_emulators() -> None: assert v >= 0 for c in k: assert c in {"0", "1"} + + +@pytest.mark.asyncio +async def test_async_remote_backend_device() -> None: + """ + A remote backend fed any Pulser `RemoteConnection` (as the tutorials do) must be + able to fetch its device specs. + """ + with patch("pasqal_cloud.SDK", return_value=MockSDK()): + connection = PasqalCloud(username="placeholder", project_id="placeholder") + + backend = RemoteQPUBackend(connection=connection) + device = await backend.device() + assert isinstance(device, pl.devices.Device) + assert device.name == "Fresnel" From 1e49af93288a99679e994d2fa7a6581cef4a74ac Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Mon, 14 Sep 2026 22:26:17 +0200 Subject: [PATCH 8/9] lint --- qek/target/backends.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/qek/target/backends.py b/qek/target/backends.py index 6453bc0..7b7497b 100644 --- a/qek/target/backends.py +++ b/qek/target/backends.py @@ -118,7 +118,9 @@ def __init__( self._connection = connection else: assert project_id is not None and username is not None - self._connection = PasqalCloud(username=username, project_id=project_id, password=password) + self._connection = PasqalCloud( + username=username, project_id=project_id, password=password + ) self.device_name = device_name self._max_runs = 500 self._sequence = None @@ -148,7 +150,7 @@ async def _run( self, register: targets.Register, pulse: targets.Pulse, - backend_class: Type[RemoteBackend] | None, + backend_class: Type[RemoteBackend], sleep_sec: int = 2, ) -> Counter[str]: """ @@ -184,7 +186,7 @@ async def _run( # Continue waiting. continue # We submit exactly one job, so exactly one `Results`. - return remote_results.results[0].final_bitstrings + return cast(Counter[str], remote_results.results[0].final_bitstrings) class RemoteQPUBackend(BaseRemoteBackend): @@ -196,16 +198,19 @@ class RemoteQPUBackend(BaseRemoteBackend): may be very long. You may use this Extractor to resume your workflow with a computation that has been previously started. """ + async def run(self, register: targets.Register, pulse: targets.Pulse) -> Counter[str]: return await self._run(register, pulse, backend_class=QPUBackend) + class RemoteEmuMPSBackend(BaseRemoteBackend): """ A backend that uses a remote high-performance emulator (EmuMPS) published on Pasqal Cloud or third party connection. """ + async def run(self, register: targets.Register, pulse: targets.Pulse) -> Counter[str]: - return self._run(register, pulse, backend_class=RemoteMPSBackend) + return await self._run(register, pulse, backend_class=RemoteMPSBackend) if os.name == "posix": From 2cfa7c9ece8a2a4a5e5ffcb591947f48d3c4335e Mon Sep 17 00:00:00 2001 From: Aurelien Nober Date: Mon, 14 Sep 2026 22:41:22 +0200 Subject: [PATCH 9/9] fix deps --- ...tum Device to Extract Machine-Learning Features.ipynb | 8 ++++---- ...o Extract Machine-Learning Features - low-level.ipynb | 8 ++++---- ... Training SVM QEK - low-level - generic dataset.ipynb | 8 ++++---- pyproject.toml | 2 +- qek/data/extractors_v2.py | 9 +++++---- qek/target/backends.py | 5 ++--- tests/test_backends.py | 6 +++--- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb b/examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb index 61732ad..ecc025b 100644 --- a/examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb +++ b/examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb @@ -170,7 +170,7 @@ "For this, you will need a connection to a remote quantum device. QEK accepts any Pulser\n", "[`RemoteConnection`](https://pulser.readthedocs.io/en/stable/apidoc/backend.html), so it works with any provider that\n", "ships one. In this section, we'll use [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides remote access to\n", - "QPUs built and hosted by Pasqal, through Pulser's `PasqalCloud` connection.\n", + "QPUs built and hosted by Pasqal, through `pasqal_cloud.PasqalCloudConnection`, which implements Pulser's `RemoteConnection`.\n", "\n", "If you don't have an account, just skip to the next section!" ] @@ -188,10 +188,10 @@ "HAVE_PASQAL_ACCOUNT = False # If you have a PASQAL Cloud account, fill in the details and set this to `True`.\n", "\n", "if HAVE_PASQAL_ACCOUNT:\n", - " from pulser_pasqal import PasqalCloud\n", + " from pasqal_cloud import PasqalCloudConnection\n", "\n", " # Open a connection to the remote device. Any Pulser `RemoteConnection` will do here.\n", - " connection = PasqalCloud(\n", + " connection = PasqalCloudConnection(\n", " project_id = \"XXXX\", # Replace this with your project id on the PASQAL Cloud\n", " username = \"XXX\", # Replace this with your username on PASQAL Cloud\n", " # Security note: we deliberately do not pass a password here. Leaving it out means\n", @@ -236,7 +236,7 @@ " e.g. after turning off your computer.\n", "2. Pasqal CLOUD offers access to high-performance hardware-based emulators, with dramatically\n", " shorter waiting lines. For instance, in the snippet above, you may pass\n", - " `backend_class=pulser_pasqal.EmuMPSBackend` to run on the emu-mps emulator instead of a QPU.\n", + " `backend_class=pasqal_cloud.RemoteMPSBackend` to run on the emu-mps emulator instead of a QPU.\n", " Any Pulser remote backend compatible with your connection works the same way.\n", "\n", "See [the documentation](https://pqs.pages.pasqal.com/quantum-evolution-kernel/) for more details." diff --git a/examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb b/examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb index bb49d04..f0b5d4c 100644 --- a/examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb +++ b/examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb @@ -240,7 +240,7 @@ "Once you have checked that the compiled graphs work on an emulator, you will probably want to move to a QPU. Execution on a QPU takes\n", "resources polynomial in the number of qubits, which hopefully means an almost exponential speedup for large number of qubits.\n", "\n", - "To experiment with a QPU, you will need a connection to a remote quantum device: QEK accepts any Pulser [`RemoteConnection`](https://pulser.readthedocs.io/en/stable/apidoc/backend.html). In this section, we'll use [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides remote access to QPUs built and hosted by Pasqal, through Pulser's `PasqalCloud` connection.\n", + "To experiment with a QPU, you will need a connection to a remote quantum device: QEK accepts any Pulser [`RemoteConnection`](https://pulser.readthedocs.io/en/stable/apidoc/backend.html). In this section, we'll use [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides remote access to QPUs built and hosted by Pasqal, through `pasqal_cloud.PasqalCloudConnection`, which implements Pulser's `RemoteConnection`.\n", "\n", "If you don't have an account, just skip to the next section!" ] @@ -254,12 +254,12 @@ "HAVE_PASQAL_ACCOUNT = False # If you have a PASQAL Cloud account, fill in the details and set this to `True`.\n", "\n", "if HAVE_PASQAL_ACCOUNT:\n", - " from pulser_pasqal import PasqalCloud\n", + " from pasqal_cloud import PasqalCloudConnection\n", " from qek.target.backends import RemoteQPUBackend\n", " processed_dataset = []\n", "\n", " # Open a connection to the remote device. Any Pulser `RemoteConnection` will do here.\n", - " connection = PasqalCloud(\n", + " connection = PasqalCloudConnection(\n", " project_id = \"your_project_id\", # Replace this value with your project_id on the PASQAL platform.\n", " username = \"your_username\", # Replace this value with your username or email on the PASQAL platform.\n", " # Security note: we deliberately do not pass a password here. Leaving it out means\n", @@ -301,7 +301,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "There are other ways to use a remote connection. For instance, you can enqueue a job and check later whether it has completed. Also, to work around the long waiting lines, Pasqal provides high-performance distributed and hardware-accelerated emulators, which you can reach over the same connection by picking another Pulser remote backend (e.g. `pulser_pasqal.EmuMPSBackend`).\n", + "There are other ways to use a remote connection. For instance, you can enqueue a job and check later whether it has completed. Also, to work around the long waiting lines, Pasqal provides high-performance distributed and hardware-accelerated emulators, which you can reach over the same connection by picking another Pulser remote backend (e.g. `pasqal_cloud.RemoteMPSBackend`).\n", "\n", "For more details, [take a look at the Pulser documentation](https://pulser.readthedocs.io/en/stable/apidoc/backend.html).\n" ] diff --git a/examples/tutorial 1b - Training SVM QEK - low-level - generic dataset.ipynb b/examples/tutorial 1b - Training SVM QEK - low-level - generic dataset.ipynb index 5e86009..9714c0f 100644 --- a/examples/tutorial 1b - Training SVM QEK - low-level - generic dataset.ipynb +++ b/examples/tutorial 1b - Training SVM QEK - low-level - generic dataset.ipynb @@ -318,11 +318,11 @@ "Once you have checked that the pulses work on an emulator, you will probably want to move to a QPU. Execution on a QPU takes\n", "resources polynomial in the number of qubits, which hopefully means an almost exponential speedup for large number of qubits.\n", "\n", - "To experiment with a QPU, you will need a connection to a remote quantum device: QEK accepts any Pulser [`RemoteConnection`](https://pulser.readthedocs.io/en/stable/apidoc/backend.html). In this section, we'll use [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides remote access to QPUs built and hosted by Pasqal, through Pulser's `PasqalCloud` connection.\n", + "To experiment with a QPU, you will need a connection to a remote quantum device: QEK accepts any Pulser [`RemoteConnection`](https://pulser.readthedocs.io/en/stable/apidoc/backend.html). In this section, we'll use [PASQAL Cloud](https://docs.pasqal.com/cloud), which provides remote access to QPUs built and hosted by Pasqal, through `pasqal_cloud.PasqalCloudConnection`, which implements Pulser's `RemoteConnection`.\n", "\n", "If you don't have an account, just skip to the next section!\n", "\n", - "> There are other ways to use a remote connection. For instance, you can enqueue a job and check later whether it has completed. Also, to work around the long waiting lines, Pasqal provides high-performance distributed and hardware-accelerated emulators, which you can reach over the same connection by picking another Pulser remote backend (e.g. `pulser_pasqal.EmuMPSBackend`). For more details, [take a look at the Pulser documentation](https://pulser.readthedocs.io/en/stable/apidoc/backend.html)." + "> There are other ways to use a remote connection. For instance, you can enqueue a job and check later whether it has completed. Also, to work around the long waiting lines, Pasqal provides high-performance distributed and hardware-accelerated emulators, which you can reach over the same connection by picking another Pulser remote backend (e.g. `pasqal_cloud.RemoteMPSBackend`). For more details, [take a look at the Pulser documentation](https://pulser.readthedocs.io/en/stable/apidoc/backend.html)." ] }, { @@ -334,12 +334,12 @@ "HAVE_PASQAL_ACCOUNT = False # If you have a PASQAL Cloud account, fill in the details and set this to `True`.\n", "\n", "if HAVE_PASQAL_ACCOUNT:\n", - " from pulser_pasqal import PasqalCloud\n", + " from pasqal_cloud import PasqalCloudConnection\n", " from qek.target.backends import RemoteQPUBackend\n", " processed_dataset = []\n", "\n", " # Open a connection to the remote device. Any Pulser `RemoteConnection` will do here.\n", - " connection = PasqalCloud(\n", + " connection = PasqalCloudConnection(\n", " project_id = \"your_project_id\", # Replace this value with your project_id on the PASQAL platform.\n", " username = \"your_username\", # Replace this value with your username or email on the PASQAL platform.\n", " # Security note: we deliberately do not pass a password here. Leaving it out means\n", diff --git a/pyproject.toml b/pyproject.toml index fe78483..fb5de1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "torch_geometric", "matplotlib", "emu-mps~=2.7", - "pasqal-cloud", + "pasqal-cloud>=0.23.0", ] [tool.hatch.metadata] diff --git a/qek/data/extractors_v2.py b/qek/data/extractors_v2.py index bc2fc4f..a669d57 100644 --- a/qek/data/extractors_v2.py +++ b/qek/data/extractors_v2.py @@ -3,8 +3,8 @@ a remote emulator or a physical QPU. Unlike `qek.data.extractors`, this module only speaks Pulser: any `pulser.backend.remote.RemoteConnection` -(e.g. `pulser_pasqal.PasqalCloud`) and any `RemoteBackend` will do, so nothing here depends on the -pasqal-cloud SDK. +(e.g. `pasqal_cloud.PasqalCloudConnection`) and any `RemoteBackend` will do, so nothing here +depends on the pasqal-cloud SDK's own job API. """ import abc @@ -205,7 +205,8 @@ class BaseRemoteExtractorV2(BaseExtractor[GraphType], Generic[GraphType]): Args: compiler: A graph compiler, in charge of converting graphs to Pulser Sequences. - connection: An open connection to the remote API, e.g. `pulser_pasqal.PasqalCloud`. + connection: An open connection to the remote API, e.g. + `pasqal_cloud.PasqalCloudConnection`. device: The device to compile for. If unspecified, fetch `device_name` from `connection`. device_name: The name of the device to fetch from `connection`. As of this writing, @@ -305,7 +306,7 @@ class RemoteExtractorV2(BaseRemoteExtractorV2[GraphType]): By default, it runs on a QPU (`QPUBackend`). To run on a remote emulator instead, pass the corresponding backend class, e.g.: - RemoteExtractorV2(compiler, connection, backend_class=pulser_pasqal.EmuMPSBackend) + RemoteExtractorV2(compiler, connection, backend_class=pasqal_cloud.RemoteMPSBackend) Performance note: as of this writing, the waiting lines for a QPU diff --git a/qek/target/backends.py b/qek/target/backends.py index 7b7497b..131383b 100644 --- a/qek/target/backends.py +++ b/qek/target/backends.py @@ -11,8 +11,7 @@ from pulser.devices import Device from pulser.backend import QPUBackend from pulser.backend.remote import RemoteConnection, BatchStatus, RemoteBackend -from pulser_pasqal import PasqalCloud -from pulser_pasqal.backends import EmuMPSBackend as RemoteMPSBackend +from pasqal_cloud import PasqalCloudConnection, RemoteMPSBackend from pulser_simulation import QutipEmulator from qek.shared.error import CompilationError @@ -118,7 +117,7 @@ def __init__( self._connection = connection else: assert project_id is not None and username is not None - self._connection = PasqalCloud( + self._connection = PasqalCloudConnection( username=username, project_id=project_id, password=password ) self.device_name = device_name diff --git a/tests/test_backends.py b/tests/test_backends.py index 835e4c7..76d8d25 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -6,7 +6,7 @@ import pytest import torch_geometric.data as pyg_data import torch_geometric.datasets as pyg_dataset -from pulser_pasqal import PasqalCloud +from pasqal_cloud import PasqalCloudConnection from qek.target import targets from qek.target.backends import CompilationError, QutipBackend, BaseBackend, RemoteQPUBackend import qek.data.graphs as qek_graphs @@ -73,8 +73,8 @@ async def test_async_remote_backend_device() -> None: A remote backend fed any Pulser `RemoteConnection` (as the tutorials do) must be able to fetch its device specs. """ - with patch("pasqal_cloud.SDK", return_value=MockSDK()): - connection = PasqalCloud(username="placeholder", project_id="placeholder") + with patch("pasqal_cloud.pasqal_cloud_connection.PasqalCloudClient", return_value=MockSDK()): + connection = PasqalCloudConnection(username="placeholder", project_id="placeholder") backend = RemoteQPUBackend(connection=connection) device = await backend.device()