Skip to content

Commit 47fd836

Browse files
authored
Implement RemoteEmuMPSEmulator (#45)
1 parent 633e421 commit 47fd836

3 files changed

Lines changed: 207 additions & 27 deletions

examples/tutorial 1 - Using a Quantum Device to Extract Machine-Learning Features.ipynb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@
156156
"\n",
157157
"if HAVE_PASQAL_ACCOUNT:\n",
158158
" # Use the QPU Extractor.\n",
159-
" extractor = qek_extractors.QPUExtractor(\n",
159+
" extractor = qek_extractors.RemoteQPUExtractor(\n",
160160
" # Once computing is complete, data will be saved in this file.\n",
161161
" path=\"saved_data.json\",\n",
162162
" compiler = compiler,\n",
@@ -191,10 +191,11 @@
191191
"\n",
192192
"There are two main ways to deal with this:\n",
193193
"\n",
194-
"1. `QPUExtractor` can be attached to an ongoing job from batch ids, so that you can resume your work\n",
194+
"1. `RemoteQPUExtractor` can be attached to an ongoing job from batch ids, so that you can resume your work\n",
195195
" e.g. after turning off your computer.\n",
196196
"2. Pasqal CLOUD offers access to high-performance hardware-based emulators, with dramatically\n",
197-
" shorter waiting lines.\n",
197+
" shorter waiting lines. For instance, in the snippet above, you may replace `RemoteQPUExtractor`\n",
198+
" with `RemoteEmuMPSExtractor` to use the emu-mps emulator.\n",
198199
"\n",
199200
"See [the documentation](https://pqs.pages.pasqal.com/quantum-evolution-kernel/) for more details."
200201
]

examples/tutorial 1a - Using a Quantum Device to Extract Machine-Learning Features - low-level.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"source": [
3838
"# Load the original PTC-FM dataset\n",
3939
"import torch_geometric.datasets as pyg_dataset\n",
40-
"og_ptcfm = [data for data in pyg_dataset.TUDataset(root=\"dataset\", name=\"PTC_FM\")]\n",
40+
"og_ptcfm = pyg_dataset.TUDataset(root=\"dataset\", name=\"PTC_FM\")\n",
4141
"\n",
4242
"display(\"Loaded %s samples\" % (len(og_ptcfm), ))"
4343
]

qek/data/extractors.py

Lines changed: 202 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
from numpy.typing import NDArray
1313
from pasqal_cloud import SDK
1414
from pasqal_cloud.batch import Batch
15+
from pasqal_cloud.device import BaseConfig, EmuTNConfig, EmulatorType
16+
from pasqal_cloud.job import Job
1517
from pasqal_cloud.utils.filters import BatchFilters
18+
from pathlib import Path
1619
import numpy as np
1720
import pulser as pl
1821
from pulser.devices import Device
@@ -134,14 +137,14 @@ def features(self, size_max: int | None) -> list[Feature]:
134137

135138
return [Feature(dataset.dist_excitation(state, size_max)) for state in self.states]
136139

137-
def save_dataset(self, file_path: str) -> None:
140+
def save_dataset(self, file_path: Path) -> None:
138141
"""Saves the processed dataset to a JSON file.
139142
140143
Note: This does NOT attempt to save the graphs.
141144
142145
Args:
143146
dataset: The dataset to be saved.
144-
file_path (str): The path where the dataset will be saved as a JSON
147+
file_path: The path where the dataset will be saved as a JSON
145148
file.
146149
147150
Note:
@@ -249,7 +252,7 @@ class BaseExtractor(abc.ABC, Generic[GraphType]):
249252
"""
250253

251254
def __init__(
252-
self, device: Device, compiler: BaseGraphCompiler[GraphType], path: str | None = None
255+
self, device: Device, compiler: BaseGraphCompiler[GraphType], path: Path | None = None
253256
) -> None:
254257
self.path = path
255258

@@ -380,7 +383,7 @@ def __init__(
380383
self,
381384
compiler: BaseGraphCompiler[GraphType],
382385
device: Device = pl.devices.AnalogDevice,
383-
path: str | None = None,
386+
path: Path | None = None,
384387
):
385388
super().__init__(path=path, device=device, compiler=compiler)
386389
self.graphs: list[BaseGraph]
@@ -463,7 +466,7 @@ def __init__(
463466
self,
464467
compiler: BaseGraphCompiler[GraphType],
465468
device: Device = pl.devices.AnalogDevice,
466-
path: str | None = None,
469+
path: Path | None = None,
467470
):
468471
super().__init__(device=device, compiler=compiler, path=path)
469472
self.graphs: list[BaseGraph]
@@ -534,7 +537,7 @@ def run(self, max_qubits: int = 10, dt: int = 10) -> BaseExtracted:
534537
SLEEP_DELAY_S = 2
535538

536539

537-
class CloudExtracted(BaseExtracted):
540+
class PasqalCloudExtracted(BaseExtracted):
538541
"""
539542
Data extracted from the cloud API, i.e. we need wait for a remote server.
540543
@@ -548,13 +551,29 @@ class CloudExtracted(BaseExtracted):
548551
"""
549552

550553
def __init__(
551-
self, compiled: list[Compiled], batch_ids: list[str], sdk: SDK, path: str | None = None
554+
self,
555+
compiled: list[Compiled],
556+
batch_ids: list[str],
557+
sdk: SDK,
558+
state_extractor: Callable[[Job, pl.Sequence], dict[str, int] | None],
559+
path: Path | None = None,
552560
):
561+
"""
562+
Prepare for reception of data.
563+
564+
Arguments:
565+
compiled: The result of compiling a set of graphs.
566+
batch_ids: The ids of the batches on the cloud API, in the same order as `compiled`.
567+
state_extractor: A callback used to extract the counter from a job.
568+
Used as various cloud back-ends return different formats.
569+
path: If provided, a path at which to save the results once they're available.
570+
"""
553571
self._compiled = compiled
554572
self._batch_ids = batch_ids
555573
self._results: SyncExtracted | None = None
556574
self._path = path
557575
self._sdk = sdk
576+
self._state_extractor = state_extractor
558577

559578
def _wait(self) -> None:
560579
"""
@@ -643,10 +662,16 @@ def _ingest(self, batches: dict[str, Batch]) -> None:
643662
compiled = self._compiled[i]
644663
# Note: There's only one job per batch.
645664
assert len(batch.jobs) == 1
646-
for _, job in batch.jobs.items():
665+
for job in batch.jobs.values():
647666
if job.status == "DONE":
648-
state_dict = job.result
649-
assert state_dict is not None
667+
state_dict = self._state_extractor(job, compiled.sequence)
668+
if state_dict is None:
669+
logger.warning(
670+
"Batch %s (graph %s) did not return a usable state, skipping",
671+
i,
672+
compiled.graph.id,
673+
)
674+
continue
650675
raw_data.append(compiled.graph)
651676
if compiled.graph.target is not None:
652677
targets.append(compiled.graph.target)
@@ -698,14 +723,18 @@ def states(self) -> list[dict[str, int]]:
698723
return self._results.states
699724

700725

701-
class QPUExtractor(BaseExtractor[GraphType]):
726+
class BaseRemoteExtractor(BaseExtractor[GraphType], Generic[GraphType]):
702727
"""
703-
A Extractor that uses a distant physical QPU to run sequences
704-
compiled from graphs.
728+
An Extractor that uses a remote Quantum Device published
729+
on Pasqal Cloud, to run sequences compiled from graphs.
705730
706-
Performance note: as of this writing, the waiting lines for a QPU
707-
may be very long. You may use this Extractor to resume your workflow
708-
with a computation that has been previously started.
731+
Performance note (servers and interactive applications only):
732+
If your code is meant to be executed as part of an interactive application or
733+
a server, you should consider calling `await extracted` before your first call
734+
to any of the methods of `extracted`. Otherwise, you will block the main thread.
735+
736+
If you are running this as part of an experiment, a Jupyter notebook, etc. you
737+
may ignore this performance note.
709738
710739
Args:
711740
path: Path to store the result of the run, for future uses.
@@ -726,10 +755,10 @@ def __init__(
726755
compiler: BaseGraphCompiler[GraphType],
727756
project_id: str,
728757
username: str,
758+
device_name: str,
729759
password: str | None = None,
730-
device_name: str = "FRESNEL",
731760
batch_ids: list[str] | None = None,
732-
path: str | None = None,
761+
path: Path | None = None,
733762
):
734763
sdk = SDK(username=username, project_id=project_id, password=password)
735764

@@ -745,13 +774,33 @@ def __init__(
745774
def batch_ids(self) -> list[str] | None:
746775
return self._batch_ids
747776

748-
def run(self) -> CloudExtracted:
777+
@abc.abstractmethod
778+
def run(
779+
self,
780+
) -> PasqalCloudExtracted:
781+
"""
782+
Launch the extraction.
783+
"""
784+
raise Exception("Not implemented")
785+
786+
def _run(
787+
self,
788+
state_extractor: Callable[[Job, pl.Sequence], dict[str, int] | None],
789+
emulator: EmulatorType | None,
790+
config: BaseConfig | None,
791+
) -> PasqalCloudExtracted:
749792
if len(self.sequences) == 0:
750793
logger.warning("No sequences to run, did you forget to call compile()?")
751-
return CloudExtracted(compiled=[], batch_ids=[], sdk=self._sdk, path=self.path)
794+
return PasqalCloudExtracted(
795+
compiled=[],
796+
batch_ids=[],
797+
sdk=self._sdk,
798+
path=self.path,
799+
state_extractor=state_extractor,
800+
)
752801

753802
device: pl.devices.Device = self.sequences[0].sequence.device
754-
# As of this writing, the API doesn't support run longer than 500 jobs.
803+
# As of this writing, the API doesn't support runs longer than 500 jobs.
755804
# If we want to add more runs, we'll need to split them across several jobs.
756805
max_runs = device.max_runs if isinstance(device.max_runs, int) else 500
757806

@@ -764,6 +813,8 @@ def run(self) -> CloudExtracted:
764813
compiled.sequence.to_abstract_repr(),
765814
jobs=[{"runs": max_runs}],
766815
wait=False,
816+
emulator=emulator,
817+
configuration=config,
767818
)
768819
logger.info(
769820
"Remote execution of compiled graph #%s starting, batched with id %s",
@@ -778,6 +829,134 @@ def run(self) -> CloudExtracted:
778829
)
779830
assert len(self._batch_ids) == len(self.sequences)
780831

781-
return CloudExtracted(
782-
compiled=self.sequences, batch_ids=self._batch_ids, sdk=self._sdk, path=self.path
832+
return PasqalCloudExtracted(
833+
compiled=self.sequences,
834+
batch_ids=self._batch_ids,
835+
sdk=self._sdk,
836+
path=self.path,
837+
state_extractor=state_extractor,
838+
)
839+
840+
841+
class RemoteQPUExtractor(BaseRemoteExtractor[GraphType]):
842+
"""
843+
An Extractor that uses a remote QPU published
844+
on Pasqal Cloud, to run sequences compiled from graphs.
845+
846+
Performance note:
847+
as of this writing, the waiting lines for a QPU
848+
may be very long. You may use this Extractor to resume your workflow
849+
with a computation that has been previously started.
850+
851+
Performance note (servers and interactive applications only):
852+
If your code is meant to be executed as part of an interactive application or
853+
a server, you should consider calling `await extracted` before your first call
854+
to any of the methods of `extracted`. Otherwise, you will block the main thread.
855+
856+
If you are running this as part of an experiment, a Jupyter notebook, etc. you
857+
may ignore this performance note.
858+
859+
Args:
860+
path: Path to store the result of the run, for future uses.
861+
To reload the result of a previous run, use `LoadExtractor`.
862+
project_id: The ID of the project on the Pasqal Cloud API.
863+
username: Your username on the Pasqal Cloud API.
864+
password: Your password on the Pasqal Cloud API. If you leave
865+
this to None, you will need to enter your password manually.
866+
device_name: The name of the device to use. As of this writing,
867+
the default value of "FRESNEL" represents the latest QPU
868+
available through the Pasqal Cloud API.
869+
batch_id: Use this to resume a workflow e.g. after turning off
870+
your computer while the QPU was executing your sequences.
871+
"""
872+
873+
def __init__(
874+
self,
875+
compiler: BaseGraphCompiler[GraphType],
876+
project_id: str,
877+
username: str,
878+
device_name: str = "FRESNEL",
879+
password: str | None = None,
880+
batch_ids: list[str] | None = None,
881+
path: Path | None = None,
882+
):
883+
super().__init__(
884+
compiler=compiler,
885+
project_id=project_id,
886+
username=username,
887+
device_name=device_name,
888+
password=password,
889+
batch_ids=batch_ids,
890+
path=path,
891+
)
892+
893+
def run(self) -> PasqalCloudExtracted:
894+
return self._run(emulator=None, config=None, state_extractor=lambda job, _seq: job.result)
895+
896+
897+
class RemoteEmuMPSExtractor(BaseRemoteExtractor[GraphType]):
898+
"""
899+
An Extractor that uses a remote high-performance emulator (EmuMPS)
900+
published on Pasqal Cloud, to run sequences compiled from graphs.
901+
902+
Performance note (servers and interactive applications only):
903+
If your code is meant to be executed as part of an interactive application or
904+
a server, you should consider calling `await extracted` before your first call
905+
to any of the methods of `extracted`. Otherwise, you will block the main thread.
906+
907+
If you are running this as part of an experiment, a Jupyter notebook, etc. you
908+
may ignore this performance note.
909+
910+
Args:
911+
path: Path to store the result of the run, for future uses.
912+
To reload the result of a previous run, use `LoadExtractor`.
913+
project_id: The ID of the project on the Pasqal Cloud API.
914+
username: Your username on the Pasqal Cloud API.
915+
password: Your password on the Pasqal Cloud API. If you leave
916+
this to None, you will need to enter your password manually.
917+
device_name: The name of the device to use. As of this writing,
918+
the default value of "FRESNEL" represents the latest QPU
919+
available through the Pasqal Cloud API.
920+
batch_id: Use this to resume a workflow e.g. after turning off
921+
your computer while the QPU was executing your sequences.
922+
"""
923+
924+
def __init__(
925+
self,
926+
compiler: BaseGraphCompiler[GraphType],
927+
project_id: str,
928+
username: str,
929+
device_name: str = "FRESNEL",
930+
password: str | None = None,
931+
batch_ids: list[str] | None = None,
932+
path: Path | None = None,
933+
):
934+
super().__init__(
935+
compiler=compiler,
936+
project_id=project_id,
937+
username=username,
938+
device_name=device_name,
939+
password=password,
940+
batch_ids=batch_ids,
941+
path=path,
942+
)
943+
944+
def run(self, dt: int = 10) -> PasqalCloudExtracted:
945+
def extractor(job: Job, sequence: pl.Sequence) -> dict[str, int] | None:
946+
cutoff_duration = int(ceil(sequence.get_duration() / dt) * dt)
947+
full_result = job.full_result
948+
if full_result is None:
949+
return None
950+
result = full_result["bitstring"][cutoff_duration]
951+
if result is None:
952+
return None
953+
assert isinstance(result, dict)
954+
return result
955+
956+
return self._run(
957+
emulator=EmulatorType.EMU_TN,
958+
config=EmuTNConfig(
959+
dt=dt,
960+
),
961+
state_extractor=extractor,
783962
)

0 commit comments

Comments
 (0)