1212from numpy .typing import NDArray
1313from pasqal_cloud import SDK
1414from pasqal_cloud .batch import Batch
15+ from pasqal_cloud .device import BaseConfig , EmuTNConfig , EmulatorType
16+ from pasqal_cloud .job import Job
1517from pasqal_cloud .utils .filters import BatchFilters
18+ from pathlib import Path
1619import numpy as np
1720import pulser as pl
1821from 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:
534537SLEEP_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