Skip to content

Commit 4d049ce

Browse files
authored
Adding support for precompiled OPX measurements in sweeps (#90)
* Using context manager to handle precompiled OPX measurements * cleaning up and safer conditional logic * fixes in cleaning up measurement * fixes in cleanup * adding IF overrides * making if overrides into dictionary * adding documentation * fix typo * adding info about overriding waveforms * adding more print statements
1 parent d161e0a commit 4d049ce

File tree

1 file changed

+139
-24
lines changed

1 file changed

+139
-24
lines changed

labcore/instruments/opx/sweep.py

Lines changed: 139 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
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
2323
config: 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

2829
logger = 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

Comments
 (0)