2121# --- Options that need to be set by the user for the OPX to work ---
2222# config object that when called returns the config dictionary as expected by the OPX
2323config : Optional [QMConfig ] = None # OPX config dictionary
24- qmachine_mgr = None # Quantum machine manager
25- qmachine = None # Quantum machine
24+
25+ # WARNING: DO NOT TOUCH THIS VARIABLE. IT IS GLOBAL AND HANDLED BY THE CONTEXT MANAGER.
26+ _qmachine_context = None
2627
2728
2829logger = logging .getLogger (__name__ )
@@ -38,28 +39,51 @@ class QuantumMachineContext:
3839 [your measurement code here]
3940 ```
4041
41- This does not need to be used, but if measurements are done repeatedly, it saves some time.
42+ This does not need to be used, but if measurements are done repeatedly and precompiling with the OPX is
43+ desired, it saves some time.
4244
4345 Warning: Using a context manager doesn't let you update the config of the OPX. If you want to change the
44- config, you need to open a new quantum machine.
46+ config, you need to open a new quantum machine or use precompiled measurements to overwrite aspects of the
47+ measurement on the fly.
4548 """
49+
50+ def __init__ (self , wf_overrides : Optional [Dict ] = None , if_overrides : Optional [Dict ] = None , * args , ** kwargs ):
51+ """
52+ Initializes the context manager with a function to be executed, its arguments, and optional overrides.
53+
54+ :param fun: The function to be executed in the quantum machine.
55+ :param args: Positional arguments for the function.
56+ :param wf_overrides: Optional dictionary of overrides for the waveforms.
57+ :param if_overrides: Optional dictionary of overrides for the intermediate frequencies.
58+ :param kwargs: Keyword arguments for the function.
59+ """
60+ global config
61+ self .wf_overrides = wf_overrides
62+ self .if_overrides = if_overrides
63+ self .kwargs = kwargs
64+
65+ self ._qmachine_mgr = None
66+ self ._qmachine = None
67+ self ._program_id = None
68+
4669 def __enter__ (self ):
47- global qmachine_mgr , qmachine , config
48- qmachine_mgr = QuantumMachinesManager (
70+ global config , _qmachine_context
71+ _qmachine_mgr = QuantumMachinesManager (
4972 host = config .opx_address ,
5073 port = config .opx_port ,
5174 cluster_name = config .cluster_name ,
5275 octave = config .octave
5376 )
54- qmachine = qmachine_mgr .open_qm (config (), close_other_machines = False )
55- return qmachine
77+
78+ self ._qmachine = _qmachine_mgr .open_qm (config (), close_other_machines = False )
79+ _qmachine_context = self
80+ return self
5681
5782 def __exit__ (self , exc_type , exc_value , traceback ):
58- global qmachine , qmachine_mgr , config
59- if qmachine is not None :
60- qmachine .close ()
61- qmachine = None
62- qmachine_mgr = None
83+ global _qmachine_context
84+ if self ._qmachine is not None :
85+ self ._qmachine .close ()
86+ _qmachine_context = None
6387
6488
6589@dataclass
@@ -102,10 +126,10 @@ def setup(self, fun, *args, **kwargs) -> None:
102126 the module variable global_config. It saves the result handles and saves initial values to the communicator
103127 dictionary.
104128 """
105- global qmachine , qmachine_mgr
129+ global _qmachine_context
106130 self .communicator ["self_managed" ] = False
107131 # Start the measurement in the OPX.
108- if qmachine_mgr is None and qmachine is None :
132+ if _qmachine_context is None :
109133 qmachine_mgr = QuantumMachinesManager (
110134 host = config .opx_address ,
111135 port = config .opx_port ,
@@ -116,6 +140,9 @@ def setup(self, fun, *args, **kwargs) -> None:
116140 qmachine = qmachine_mgr .open_qm (config (), close_other_machines = False )
117141 logger .info (f"current QM: { qmachine } , { qmachine .id } " )
118142 self .communicator ["self_managed" ] = True
143+ else :
144+ qmachine_mgr = _qmachine_context ._qmachine_mgr
145+ qmachine = _qmachine_context ._qmachine
119146
120147 job = qmachine .execute (fun (* args , ** kwargs ))
121148 result_handles = job .result_handles
@@ -177,21 +204,20 @@ def cleanup(self):
177204 """
178205 Functions in charge of cleaning up any software tools that needs cleanup.
179206
180- Currently, manually closes the qmachine in the OPT so that simultaneous measurements can occur.
207+ Currently, manually closes the _qmachine in the OPT so that simultaneous measurements can occur.
181208 """
182- global qmachine , qmachine_mgr
183209 logger .info ('Cleaning up' )
184210
185- open_machines = qmachine_mgr .list_open_quantum_machines ()
186- logger .info (f"currently open QMs: { open_machines } " )
187211 if self .communicator ["self_managed" ]:
188- machine_id = qmachine .id
189- qmachine .close ()
212+ open_machines = self .communicator ["manager" ].list_open_quantum_machines ()
213+ logger .info (f"currently open QMs: { open_machines } " )
214+ machine_id = self .communicator ["qmachine_id" ]
215+ self .communicator ["qmachine" ].close ()
190216 logger .info (f"QM with ID { machine_id } closed." )
191217
192- qmachine = None
193- qmachine_mgr = None
194-
218+ self . communicator [ " qmachine" ] = None
219+ self . communicator [ "manager" ] = None
220+
195221
196222
197223 def collect (self , batchsize : int = 100 ) -> Generator [Dict , None , None ]:
@@ -282,3 +308,92 @@ def get_data_from_handle(name, up_to):
282308
283309 finally :
284310 self .cleanup ()
311+
312+
313+ class RecordPrecompiledOPXdata (RecordOPXdata ):
314+ """
315+ Implementation of AsyncRecord for use with precompiled OPX programs.
316+
317+ To pass either waveform or IF overrides, use the QuantumMachineContext and set the overrides as attributes.
318+ The overrides must be passed as dictionaries.
319+
320+ For the waveform overrides the keys are the names of the waveforms as defined in the OPX config file, and
321+ the values are the new waveform arrays. For an arbitrary (as defined in the qmconfig) waveform to be overridable,
322+ the waveform must have `"is_overridable": True` set. Constant waveforms do not need to be set as such: the override
323+ will simply be a constant value. Other waveform types are not overridable.
324+
325+ For the IF overrides the keys are the names of the elements as defined in the OPX config file, and
326+ the values are the new intermediate frequencies in Hz.
327+
328+ Usage example:
329+ ```
330+ def create_readout_wf(amp):
331+ wf_samples = [0.0] * int(params.q01.readout.short.buffer()) + [amp] * int(
332+ params.q01.readout.short.len()
333+ - 2 * params.q01.readout.short.buffer()
334+ ) + [0.0] * int(params.q01.readout.short.buffer())
335+ return wf_samples
336+
337+ def create_drive_wf(amp):
338+ return amp
339+
340+ with QuantumMachineContext() as qmc:
341+ loc = measure_time_rabi()
342+
343+ qmc.wf_overrides = {
344+ "waveforms": {
345+ f"q01_short_readout_wf": create_readout_wf(),
346+ f"q01_square_pi_pulse_iwf": create_drive_wf()
347+ }
348+ }
349+ qmc.if_overrides = {
350+ "q01": 80e6,
351+ "q01_readout": 80e6
352+ }
353+
354+ loc = measure_time_rabi()
355+ ```
356+ This will perform a time Rabi measurement, redefine the IF and waveforms of the drive and readout elements,
357+ and then execute the same measurement with the new settings.
358+
359+ There is no need to create a new quantum machine or recompile in between measurements.
360+ """
361+
362+ def setup (self , fun , * args , ** kwargs ):
363+ """
364+ Starts the measurement using a provided _program_id. Compilation only happens if the _program_id is None.
365+ """
366+ global _qmachine_context
367+ self .communicator ["self_managed" ] = False
368+
369+ if _qmachine_context is None :
370+ raise RuntimeError ("No quantum machine manager or quantum machine found. "
371+ "Please use a context manager for precompiled measurements." )
372+
373+ if _qmachine_context ._program_id is None :
374+ _qmachine_context ._program_id = _qmachine_context ._qmachine .compile (fun (* args , ** kwargs ))
375+ if _qmachine_context .wf_overrides is not None :
376+ print (f"Using waveform overrides: { _qmachine_context .wf_overrides } " )
377+ pending_job = _qmachine_context ._qmachine .queue .add_compiled (_qmachine_context ._program_id , overrides = _qmachine_context .wf_overrides )
378+ else :
379+ print ("No waveform overrides provided, using default waveforms." )
380+ pending_job = _qmachine_context ._qmachine .queue .add_compiled (_qmachine_context ._program_id )
381+
382+ if _qmachine_context .if_overrides is not None :
383+ print (f"Using IF overrides: { _qmachine_context .if_overrides } " )
384+ for element , frequency in _qmachine_context .if_overrides .items ():
385+ _qmachine_context ._qmachine .set_intermediate_frequency (element , frequency )
386+ _qmachine_context .if_overrides = None
387+ else :
388+ print ("No IF overrides provided, using default IFs." )
389+
390+ job = pending_job .wait_for_execution ()
391+ result_handles = job .result_handles
392+
393+ self .communicator ["result_handles" ] = result_handles
394+ self .communicator ["active" ] = True
395+ self .communicator ["counter" ] = 0
396+ self .communicator ["manager" ] = _qmachine_context ._qmachine_mgr
397+ self .communicator ["qmachine" ] = _qmachine_context ._qmachine
398+ self .communicator ["qmachine_id" ] = _qmachine_context ._qmachine .id
399+
0 commit comments