From b1a43cf1c2f0a57ffc4c2547a85f5d9eeb2a3b06 Mon Sep 17 00:00:00 2001 From: G Adam Cox Date: Wed, 16 Nov 2022 17:11:36 -0800 Subject: [PATCH 1/4] Adds base class option to initiate PBInd with different instruction set resolution --- src/qt3utils/pulsers/pulseblaster.py | 38 +++++++++++++++++++--------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/qt3utils/pulsers/pulseblaster.py b/src/qt3utils/pulsers/pulseblaster.py index 66249b61..841878da 100644 --- a/src/qt3utils/pulsers/pulseblaster.py +++ b/src/qt3utils/pulsers/pulseblaster.py @@ -13,6 +13,9 @@ class PulseBlaster(ExperimentPulser): + def __init__(self, instruction_set_resolution_in_ns = 50): + self.instruction_set_resolution_in_ns = instruction_set_resolution_in_ns + def start(self): self.open() ret = pulseblaster.spinapi.pb_start() @@ -55,10 +58,15 @@ def open(self): raise PulseBlasterInitError(f'{ret}: {pulseblaster.spinapi.pb_get_error()}') pulseblaster.spinapi.pb_core_clock(100*pulseblaster.spinapi.MHz) -class PulseBlasterArb(PulseBlaster): + def PBInd(self, *args, **kwargs): + kwargs['res'] = kwargs.get('res', self.instruction_set_resolution_in_ns) + self._PBInd = PBInd(*args, **kwargs) + return self._PBInd - def __init__(self, pb_board_number = 1): +class PulseBlasterArb(PulseBlaster): + def __init__(self, pb_board_number = 1, *args, **kwargs): + super().__init__(*args, **kwargs) self.pb_board_number = pb_board_number self.reset() @@ -120,13 +128,13 @@ def program_pulser_state(self, *args, **kwargs): which is the number of clock "ticks" for each full pulse sequence cycle. This useful for a data acquisition device that utilizes the clock signal. - If no clock channel has been specified, will return 0. + If no clock channel has been specified, will return 0. ''' hardware_pins = self.clock_channels + [s['channel'] for s in self.channel_settings] self.open() - pb = PBInd(pins = hardware_pins, on_time = int(self.full_cycle_width*1e9)) + pb = self.PBInd(pins = hardware_pins, on_time = int(self.full_cycle_width*1e9)) self.start_programming() for clock_channel in self.clock_channels: @@ -163,13 +171,13 @@ class PulseBlasterHoldAOM(PulseBlasterArb): ''' def __init__(self, pb_board_number = 1, aom_channel = 0, - cycle_width = 10e-3): + cycle_width = 10e-3, *args, **kwargs): """ pb_board_number - the board number (0, 1, ...) aom_channel output controls the AOM by holding a positive voltage cycle_width - the length of the programmed pulse. Since aom channel is held on, this value is arbitrary """ - super().__init__(pb_board_number) + super().__init__(pb_board_number, *args, **kwargs) self.add_channels(aom_channel, 0, cycle_width) class PulseBlasterCWODMR(PulseBlaster): @@ -189,7 +197,7 @@ def __init__(self, pb_board_number = 1, trigger_channel = 3, rf_pulse_duration = 5e-6, clock_period = 200e-9, - trigger_width = 500e-9): + trigger_width = 500e-9, *args, **kwargs): """ pb_board_number - the board number (0, 1, ...) aom_channel output controls the AOM by holding a positive voltage @@ -197,6 +205,8 @@ def __init__(self, pb_board_number = 1, clock_channel output provides a clock input to the NI DAQ card trigger_channel output provides a rising edge trigger for the NI DAQ card """ + super().__init__(*args, **kwargs) + self.pb_board_number = pb_board_number self.aom_channel = aom_channel self.rf_channel = rf_channel @@ -223,7 +233,7 @@ def program_pulser_state(self, rf_pulse_duration = None, *args, **kwargs): self.clock_channel, self.trigger_channel] self.open() - pb = PBInd(pins = hardware_pins, on_time = int(cycle_length*1e9)) + pb = self.PBInd(pins = hardware_pins, on_time = int(cycle_length*1e9)) self.start_programming() pb.on(self.trigger_channel, 0, int(self.trigger_width*1e9)) @@ -281,13 +291,15 @@ def __init__(self, pb_board_number = 1, pre_rf_pad = 100e-9, post_rf_pad = 100e-9, full_cycle_width = 30e-6, - rf_pulse_justify = 'center'): + rf_pulse_justify = 'center', *args, **kwargs): """ pb_board_number - the board number (0, 1, ...) rf_channel output controls a RF switch clock_channel output provides a clock input to the NI DAQ card trigger_channel output provides a rising edge trigger for the NI DAQ card """ + super().__init__(*args, **kwargs) + self.pb_board_number = pb_board_number self.aom_channel = aom_channel self.rf_channel = rf_channel @@ -334,7 +346,7 @@ def program_pulser_state(self, rf_pulse_duration = None, *args, **kwargs): self.clock_channel, self.trigger_channel] self.open() - pb = PBInd(pins = hardware_pins, on_time = int(self.full_cycle_width*1e9)) + pb = self.PBInd(pins = hardware_pins, on_time = int(self.full_cycle_width*1e9)) self.start_programming() pb.on(self.trigger_channel, 0, int(self.trigger_width*1e9)) @@ -405,7 +417,7 @@ def __init__(self, pb_board_number = 1, pre_rf_pad = 100e-9, post_rf_pad = 100e-9, free_precession_time = 5e-6, - n_refocussing_pi_pulses = 0): + n_refocussing_pi_pulses = 0, *args, **kwargs): """ Hardware configuration @@ -435,6 +447,8 @@ def __init__(self, pb_board_number = 1, n_refocussing_pi_pulses - will likely be changed via calls program_pulser_state method. """ + super().__init__(*args, **kwargs) + self.pb_board_number = pb_board_number self.aom_channel = aom_channel self.rf_channel = rf_channel @@ -532,7 +546,7 @@ def program_pulser_state(self, free_precession_time = None, self.clock_channel, self.trigger_channel] self.open() - pb = PBInd(pins = hardware_pins, on_time = int(self.full_cycle_width*1e9)) + pb = self.PBInd(pins = hardware_pins, on_time = int(self.full_cycle_width*1e9)) self.start_programming() From 1d011c61cd3284738ca50687ac30365aa00e3554 Mon Sep 17 00:00:00 2001 From: G Adam Cox Date: Fri, 16 Dec 2022 11:26:06 -0800 Subject: [PATCH 2/4] adds ignore for pycharm .idea folder --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9fce3c12..0fd5d948 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ dist/* *.egg-info *__pycache__ *.ipynb_checkpoints -.idea* \ No newline at end of file +.idea/ From 2b1b17b9af30a31a7c36ab0b6f33db7acfc020fb Mon Sep 17 00:00:00 2001 From: G Adam Cox Date: Fri, 16 Dec 2022 11:31:32 -0800 Subject: [PATCH 3/4] Adds PulseBlasterT1 class. Makes a few modifications to PulserBlasterArb to separate resets of the channels and clock --- src/qt3utils/pulsers/pulseblaster.py | 99 ++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/src/qt3utils/pulsers/pulseblaster.py b/src/qt3utils/pulsers/pulseblaster.py index 841878da..52737dd6 100644 --- a/src/qt3utils/pulsers/pulseblaster.py +++ b/src/qt3utils/pulsers/pulseblaster.py @@ -13,8 +13,9 @@ class PulseBlaster(ExperimentPulser): - def __init__(self, instruction_set_resolution_in_ns = 50): + def __init__(self, pb_board_number=0, instruction_set_resolution_in_ns=50): self.instruction_set_resolution_in_ns = instruction_set_resolution_in_ns + self.pb_board_number = pb_board_number def start(self): self.open() @@ -65,16 +66,17 @@ def PBInd(self, *args, **kwargs): class PulseBlasterArb(PulseBlaster): - def __init__(self, pb_board_number = 1, *args, **kwargs): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.pb_board_number = pb_board_number - self.reset() + self.clear_channel_settings() + self.clear_clock_channels() + self.set_full_cycle_length(0) - def reset(self): + def clear_clock_channels(self): self.clock_channels = [] self.clock_period = None + def clear_channel_settings(self): self.channel_settings = [] - self.full_cycle_width = 0 def set_clock_channels(self, pulse_blaster_channels, clock_period): ''' @@ -177,9 +179,92 @@ def __init__(self, pb_board_number = 1, aom_channel output controls the AOM by holding a positive voltage cycle_width - the length of the programmed pulse. Since aom channel is held on, this value is arbitrary """ - super().__init__(pb_board_number, *args, **kwargs) + super().__init__(pb_board_number=pb_board_number, *args, **kwargs) self.add_channels(aom_channel, 0, cycle_width) +class PulseBlasterT1(PulseBlasterArb): + """ + Sets up the pulse blaster to emit TTL signals for measuring the T1 relaxation time of + a quantum state that can be optically initialized and read out. + + pulse sequence + aom on, aom off for time = ~full_cycle_width/2 - tau, aom on, aom off for time = tau, aom on, aom off for time = ~full_cycle_width/2 + + Note that since this cycle repeats (often for many thousands of times), the first aom on pulse is + our background signal because it occurs ~full_cycle_width/2 after the third aom pulse. + The second aom pulse occurs a time tau before the third aom pulse. + Thus, the third aom pulse acts as a readout that occurs time tau after initialization. + It also acts as the initialization for the first aom pulse, which is why we use the first aom pulse for background measuremnt + This all assumes, of course, that the ensemble of states can be fully initialized within the aom pulse duration. + Default is set to 10 microseconds, which should be sufficient with ~1mW of optical power. + + Also to note: our current reliance on zeeshawn/pulseblaster hinders our ability to create long + pulse blaster sequences and simultaneously short clock periods. There seems to be a limitation that + full_cycle_width / clock_period <= 2000. This is probably because 2000 clock ticks requires 4000 + programming instructions, based upon zeeshawn/pulseblaster code. The pulse blaster has a limit + of 4k memory for pulse instructions. + """ + def __init__(self, aom_channels=0, + clock_channels=2, + trigger_channels=3, + aom_pulse_duration_time=10e-6, + aom_response_time = 800e-9, + clock_period=0.25e-6, + trigger_pulse_duration=1e-6, + tau_readout_delay_default=10e-6, + full_cycle_width=0.5e-3, *args, **kwargs): + """ + pb_board_number - the board number (0, 1, ...) + aom_channel output controls the AOM by holding a positive voltage + full_cycle_width - the length of the programmed pulse. Since aom channel is held on, this value is arbitrary + """ + super().__init__(*args, **kwargs) + self.aom_channels = aom_channels + self.clock_channels = clock_channels + self.trigger_channels = trigger_channels + self.aom_pulse_duration_time = aom_pulse_duration_time + self.aom_response_time = aom_response_time + self.trigger_pulse_duration = trigger_pulse_duration + self.clock_period = clock_period + self.tau_readout_delay = tau_readout_delay_default + self.set_full_cycle_length(full_cycle_width) + self.clock_delay = 0 #artificial delay to handle issue where NIDAQ doesn't start acquireing data until a full clock cycle after the trigger + # this normally isn't an issue when the AOM response time is greater than a clock cycle. + + def program_pulser_state(self, tau_readout_delay=None, *args, **kwargs): + """ + tau_readout_delay + """ + + if tau_readout_delay is not None: + self.tau_readout_delay = np.round(tau_readout_delay,8) + + self.clear_channel_settings() + self.set_clock_channels(self.clock_channels, self.clock_period) + self.add_channels(self.trigger_channels, 0, self.trigger_pulse_duration) + + self.clock_delay = 2*self.clock_period # we have to delay signals such that the pulses arrive after subsequent clock signals to our DAQ, otherwise the readout appears phase shifted + # # first aom pulse + self.add_channels(self.aom_channels, self.clock_delay, self.aom_pulse_duration_time) + # delay + read_out_start_time = self.full_cycle_width / 2 + read_out_start_time -= self.tau_readout_delay + self.aom_pulse_duration_time + # second aom pulse + self.add_channels(self.aom_channels, self.clock_delay + read_out_start_time, self.aom_pulse_duration_time) + # third aom pulse + self.add_channels(self.aom_channels, self.clock_delay + self.full_cycle_width / 2, self.aom_pulse_duration_time) + + + self.raise_for_pulse_width(self.tau_readout_delay) + return super().program_pulser_state(*args, **kwargs) + + def raise_for_pulse_width(self, tau_readout_delay, *args, **kwargs): + min_required_length = 2 * (self.aom_pulse_duration_time + tau_readout_delay + self.aom_pulse_duration_time) + min_required_length += self.aom_response_time + + if self.full_cycle_width < min_required_length: + raise PulseTrainWidthError(f'Readout delay is too large: {tau_readout_delay:.2e}. Increase self.full_cycle_width to > {min_required_length:.2e}') + class PulseBlasterCWODMR(PulseBlaster): ''' Programs the pulse sequences needed for CWODMR. From 034dbfc82c807fd1a1ca69fdd3a339ecddc4505e Mon Sep 17 00:00:00 2001 From: G Adam Cox Date: Mon, 19 Dec 2022 15:35:30 -0800 Subject: [PATCH 4/4] Adds T1Coherence experiment class, to be used in conjunction with the PulseBlasterT1 class. --- src/qt3utils/experiments/t1coherence.py | 387 +++++++++--------------- 1 file changed, 144 insertions(+), 243 deletions(-) diff --git a/src/qt3utils/experiments/t1coherence.py b/src/qt3utils/experiments/t1coherence.py index 84b80f5a..4b41ee6f 100644 --- a/src/qt3utils/experiments/t1coherence.py +++ b/src/qt3utils/experiments/t1coherence.py @@ -1,36 +1,60 @@ import logging import numpy as np -import qt3utils.analysis.aggregation +import time + +import qt3utils.experiments.common +import qt3utils.errors logger = logging.getLogger(__name__) -class T1Coherence: +def signal_and_background(data_buffer, experiment): + trace = qt3utils.experiments.common.aggregate_sum(data_buffer, experiment) + + aom_width = int((experiment.pulser.aom_pulse_duration_time + experiment.pulser.aom_response_time) // experiment.pulser.clock_period) + aom_width += int(xperiment.pulser.clock_delay // experiment.pulser.clock_period) # we add two clock periods to handle the clock_delay in the experiment + + start_background = 0 + end_background = start_background + aom_width + background_counts = np.sum(trace[start_background:end_background]) + + start_signal = len(trace) // 2 + end_signal = start_signal + aom_width + signal_counts = np.sum(trace[start_signal:end_signal]) + + return [signal_counts, background_counts] +def contrast_calculator(data_buffer, experiment): + signal_counts, background_counts = signal_and_background(data_buffer, experiment) + contrast = signal_counts / background_counts + + return contrast - def __init__(self, pulser, edge_counter_config, - aom_pulser_channel = 'A' - photon_counter_nidaq_terminal = 'PFI12', - clock_pulser_channel = 'C', - clock_nidaq_terminal = 'PFI0', - trigger_pulser_channel = 'D', +def contrast_calculator_with_error(data_buffer, experiment): + signal_counts, background_counts = signal_and_background(data_buffer, experiment) + + contrast = signal_counts / background_counts + contrast_error = (np.sqrt(signal_counts) / signal_counts) ** 2 + contrast_error += (np.sqrt(background_counts) / background_counts) ** 2 + contrast_error = np.sqrt(contrast_error) * contrast + + return [contrast, contrast_error] + +class T1Coherence(qt3utils.experiments.common.Experiment): + + def __init__(self, t1pulser, edge_counter_config, + photon_counter_nidaq_terminal = 'PFI0', + clock_nidaq_terminal = 'PFI12', trigger_nidaq_terminal = 'PFI1', - laser_delay_low = 1e-6, - laser_delay_high = 100e-6, - laser_delay_step = 1e-6, - aom_width = 3e-6, - aom_response_time = 800e-9): + tau_delay_low=1e-6, + tau_delay_high=100e-6, + tau_delay_step=1e-6): ''' The input parameters to this object specify the conditions of an experiment and the hardware system setup. Hardware Settings - pulser - a qcsapphire.Pulser object (future: support for PulseBlaster) + t1pulser - a qt3utils.pulsers.interface.ODMRPulser object (such as qt3utils.pulsers.pulseblaster.PulserBlasterT1) edge_counter_config - a qt3utils.nidaq.config.EdgeCounter object - External Pulser Connections - * aom_pulser_channel output controls the AOM to provide laser pulses - * clock_pulser_channel output provides a clock input to the NI DAQ card - * trigger_pulser_channel output provides a rising edge trigger for the NI DAQ card - NI DAQ Connections * photon_counter_nidaq_terminal - terminal connected to TTL pulses that indicate a photon * clock_nidaq_terminal - terminal connected to the clock_pulser_channel @@ -38,13 +62,8 @@ def __init__(self, pulser, edge_counter_config, Experimental parameters - The laser delay parameters define the range and step size of the scan. - The scan is inclusive of laser_delay_low and laser_delay_high. - - Ancillary parameters + The tau parameters define the range and step size of the scan. - A cycle is one full sequence of the pulse train used in the experiment. - For T1 coherence measurements, a cycle is {AOM/laser on, AOM/laser off}. The user is responsible for analyzing the data. However, during acquisition, a callback function can be supplied in order to perform an analysis @@ -55,182 +74,53 @@ def __init__(self, pulser, edge_counter_config, prohibitive amounts of memory. ''' - - ## TODO: assert conditions on rf width low, high and step sizes - # to be compatible with pulser. - raise NotImplementedError('This class is old and was never finished. Need to reimplement.') - - self.laser_delay_low = np.round(laser_delay_low, 9) - self.laser_delay_high = np.round(laser_delay_high, 9) - self.laser_delay_step = np.round(laser_delay_step, 9) - self.aom_width = np.round(aom_width, 9) - self.aom_response_time = np.round(aom_response_time, 9) - - self.pulser = pulser - #assert (type(self.pulser) = qcsapphire.Pulser) or (type(self.pulser) = pulseblaster.Pulser) - #assert(type(self.rfsynth) = qt3rfsynthcontrol.QT3SynthHD) - self.aom_pulser_channel = aom_pulser_channel - self.clock_pulser_channel = clock_pulser_channel - self.trigger_pulser_channel = trigger_pulser_channel + self.tau_delay_low = tau_delay_low + self.tau_delay_high = tau_delay_high + self.tau_delay_step = tau_delay_step + self.pulser = t1pulser self.photon_counter_nidaq_terminal = photon_counter_nidaq_terminal self.clock_nidaq_terminal = clock_nidaq_terminal self.trigger_nidaq_terminal = trigger_nidaq_terminal - self.clock_period = 200e-9 self.edge_counter_config = edge_counter_config - raise Exception("This Class is still a work in progress. Not ready for usage.") - def experimental_conditions(self): ''' Returns a dictionary that captures the essential experimental conditions. ''' return { - 'laser_delay_low':self.laser_delay_low, - 'laser_delay_high':self.laser_delay_high, - 'laser_delay_step':self.laser_delay_step, - 'aom_width':self.aom_width, - 'aom_response_time':self.aom_response_time, - 'clock_period':self.clock_period + 'tau_delay_low':self.tau_delay_low, + 'tau_delay_high':self.tau_delay_high, + 'tau_delay_step':self.tau_delay_step, + 'pulser':self.pulser.experimental_conditions() } - def set_pulser_state(self, laser_delay): - ''' - Sets the pulser to generate a signals on all channels -- AOM channel, - RF channel, clock channel and trigger channel. - - Allows the user to set a different rf_pulse_duration after object instantiation. - - This method is used during the data aquisition phase (see self.run()), - but is also "public" to allow the user to setup the pulser and observe - the output signals before starting acquisition. - - Note that the pulser will be in the OFF state after calling this function. - Call pulser.system.state(1) for the QCSapphire to start the pulser. - - A cycle is one full sequence of the pulse train used in the experiment. - For Rabi, a cycle is {AOM on, AOM off/RF on, AOM on, AOM off/RF off}. - - returns - int: N_clock_ticks_per_cycle - - ''' - laser_delay = np.round(laser_delay, 9) - - assert np.isclose(laser_delay % self.clock_period, 0) - assert np.isclose(self.aom_width % self.clock_period, 0) - assert laser_delay >= self.clock_period - assert self.aom_width >= self.clock_period - - clock_width = self.clock_period / 2 - aom_dc_on = np.round(self.aom_width / self.clock_period).astype(int) - aom_dc_off = np.round(laser_delay / self.clock_period).astype(int) - - N_clock_ticks_per_cycle = aom_dc_on + aom_dc_off - - - self._setup_qcsapphire_pulser( self.clock_period, - self.aom_channel, - self.aom_width, - aom_delay, - aom_dc_on, - aom_dc_off, - aom_wait_count, - self.clock_channel, - clock_width, - self.trigger_channel, - clock_width) - - return int(N_clock_ticks_per_cycle) - - - def _setup_qcsapphire_pulser(self, period = 200e-9, - aom_channel = 'A', - aom_width = 1e-6, - aom_dc_on = 5, - aom_dc_off = 2, - aom_wait_count = 0, - clock_channel = 'C', - clock_width = 100e-9, - trigger_channel = 'D', - trigger_width = 1e-6): - - self.pulser.query('*RCL 0') #restores system default - self.pulser.system.period(period) - self.pulser.system.mode('normal') - - # force inputs not to exceed resolution of pulser - # should this be done inside the qcsapphire object? - aom_width = np.round(aom_width, 9) - aom_delay = np.round(aom_delay, 9) - clock_width = np.round(clock_width, 9) - - ch_aom = self.pulser.channel(aom_channel) - ch_aom.cmode('dcycle') - ch_aom.width(aom_width) - ch_aom.output.amplitude(5.0) - ch_aom.pcounter(aom_dc_on) - ch_aom.ocounter(aom_dc_off) - ch_aom.wcounter(aom_wait_count) - ch_aom.sync('T0') - self.pulser.multiplex([aom_channel], aom_channel) - ch_aom.state(1) - - ch_clock = self.pulser.channel(clock_channel) - ch_clock.width(clock_width) - ch_clock.sync('T0') - self.pulser.multiplex([clock_channel], clock_channel) - ch_clock.state(1) - - ch_trig = self.pulser.channel(trigger_channel) - ch_trig.width(trigger_width) - ch_trig.cmode('dcycle') - ch_trig.pcounter(1) - ch_trig.ocounter(2*aom_dc_on + 2*aom_dc_off - 1) - ch_trig.wcounter(0) - ch_trig.delay(0) - ch_trig.sync('T0') - self.pulser.multiplex([trigger_channel], trigger_channel) - ch_trig.state(1) - - def _stop_and_close_daq_tasks(self): - try: - self.edge_counter_config.counter_task.stop() - except: - pass - try: - self.edge_counter_config.counter_task.close() - except: - pass - - def run(self, N_cycles = 20000, - post_process_function = aggregate_data): + def run(self, N_cycles=50000, + post_process_function=contrast_calculator, + random_order=False, *args, **kwargs): ''' - Performs the scan over the specificed range of RF widths. + Performs the scan over the specificed range of readout delays. - For each AOM/laser delay with, some number of cycles of data are acquired. A cycle - is one full sequence of the pulse train used in the experiment. - For Rabi, a cycle is {AOM/laser on, AOM/laser off}. + For each tau_delay value, some number of cycles of data are acquired. A cycle + is one full sequence of the pulse train used in the experiment. For T1, + a cycle is {}. The N_cycles specifies the total number of these cycles to - acquire. Your choice depends on your desired resolution or signal-to-noise - ratio, your post-data acquisition processing choices, and the amount of memory - available on your computer. + acquire at each tau. The choice depends on the desired resolution or signal-to-noise + ratio, the post-data acquisition processing function, and the amount of memory + available on the computer. - For each width, the number of data read from the NI DAQ will be + For each tau_delay, the number of data points read from the NI DAQ will be N_clock_ticks_per_cycle * N_cycles, where N_clock_ticks_per_cycle - is the value returned by self.set_pulser_state(laser_delay). + is the value returned by self.pulser.program_pulser_state(). - Given the way our pulser is configured, N_clock_ticks_per_cycle will - grow linearly by laser_delay. - - The acquired data are stored in a data_buffer within this method. They + These data are found in a data_buffer within this method. They may be analyzed with a function passed to post_process_function, which is useful to reduce the required memory to hold the raw data. - After data acquisition for each width in the scan, - the post_process_function is called and takes two arguments: + After data acquisition for each tau_delay in the scan, + the post_process_function function is called and takes two arguments: 1) data_buffer: the full trace of data acquired 2) self: a reference to an instance of this object @@ -241,8 +131,7 @@ def run(self, N_cycles = 20000, The return from this function is a list. Each element of the list is a list of the following values - laser_delay, - N_clock_ticks_per_cycle, + tau_delay, data_post_processing_output (or raw data trace) The remaining (fixed) values for analysis can be obtained from the @@ -252,69 +141,81 @@ def run(self, N_cycles = 20000, self.N_cycles = int(N_cycles) - + # Because we fix the full cycle length for each tau delay, + # we do the following to extract the number of clock ticks per cycle, + # calculate the total number of data samples we acquire from the DAQ, + # and calculate the amount of time it will take to acquire data. + # We can thus configure the DAQ Task once at the beginning of the run + # without needing to close the resource + + self.N_clock_ticks_per_cycle = self.pulser.program_pulser_state(self.tau_delay_low) + self.pulser.start() # start the pulser + + self.N_clock_ticks_per_tau = int(self.N_clock_ticks_per_cycle * self.N_cycles) + self.daq_time = self.N_clock_ticks_per_tau * self.pulser.clock_period + + self.edge_counter_config.configure_counter_period_measure( + source_terminal=self.photon_counter_nidaq_terminal, + N_samples_to_acquire_or_buffer_size=self.N_clock_ticks_per_tau, + clock_terminal=self.clock_nidaq_terminal, + trigger_terminal=self.trigger_nidaq_terminal) + + self.edge_counter_config.create_counter_reader() data = [] - for laser_delay in np.arange(self.laser_delay_low, self.laser_delay_high + self.laser_delay_step, self.laser_delay_step): - - self.current_laser_delay = np.round(laser_delay, 9) - - self.N_clock_ticks_per_cycle = self.set_pulser_state(self.current_laser_delay) - self.pulser.system.state(1) #start the pulser - - # compute the total number of samples to be acquired and the DAQ time - # these will be the same for each RF frequency through the scan - self.N_clock_ticks_per_frequency = int(self.N_clock_ticks_per_cycle * self.N_cycles) - self.daq_time = self.N_clock_ticks_per_frequency * self.clock_period - - self._stop_and_close_daq_tasks() #be sure tasks are closed - - self.edge_counter_config.configure_counter_period_measure( - source_terminal = self.photon_counter_nidaq_terminal, - N_samples_to_acquire_or_buffer_size = self.N_clock_ticks_per_frequency, - clock_terminal = self.clock_nidaq_terminal, - trigger_terminal = self.trigger_nidaq_terminal) - - self.edge_counter_config.create_counter_reader() - - logger.info(f'Laser Delay: {self.current_laser_delay*1e-6} microseconds') - logger.info(f'Acquiring {self.N_clock_ticks_per_frequency} samples') - logger.info(f' Sample period of {self.clock_period} seconds') - logger.info(f' acquisition time of {daq_time} seconds') - - data_buffer = np.zeros(self.N_clock_ticks_per_frequency) - - self.edge_counter_config.counter_task.wait_until_done() - self.edge_counter_config.counter_task.start() - time.sleep(daq_time*1.1) #pause for acquisition - - read_samples = self.edge_counter_config.counter_reader.read_many_sample_double( - data_buffer, - number_of_samples_per_channel=self.N_clock_ticks_per_frequency, - timeout=5) - - #should we assert that we read all samples? read_samples == self.N_clock_ticks_per_frequency - - self._stop_and_close_daq_tasks() - - if post_process_function: - data_buffer = post_process_function(data_buffer, self) - - #should we make this a dictionary with self.current_laser_delay as the key? - data.append([self.current_laser_delay, - self.N_clock_ticks_per_cycle, - data_buffer]) - - return data - - -def aggregate_data(data_buffer, rabi): - ''' - Calls qt3utils.analysis.aggregation.reshape_sum_trace, where - cwodmr.N_cycles = N_rows - cwodmr.N_clock_ticks_per_cycle = N_samples_per_row - - ''' - return qt3utils.analysis.aggregation.reshape_sum_trace(data_buffer, - rabi.N_cycles, - rabi.N_clock_ticks_per_cycle) + taus_to_scan = np.arange(self.tau_delay_low, self.tau_delay_high + self.tau_delay_step, self.tau_delay_step) + if random_order: + np.random.shuffle(taus_to_scan) + try: + for tau_delay in taus_to_scan: + + self.current_tau = tau_delay + n_clock_ticks_per_cycle = self.pulser.program_pulser_state(self.current_tau) + self.pulser.start() + if n_clock_ticks_per_cycle != self.N_clock_ticks_per_cycle: + raise qt3utils.errors.PulseTrainWidthError('''N clock ticks for the pulse sequence + different from the beginning of the run. The pulser has unexpectedly changed. Exiting run.''') + + logger.info(f'tau {self.current_tau:.2e}') + logger.debug(f'Acquiring {self.N_clock_ticks_per_tau} samples') + logger.debug(f' Sample period of {self.pulser.clock_period} seconds') + logger.debug(f' acquisition time of {self.daq_time} seconds') + + data_buffer = np.zeros(self.N_clock_ticks_per_tau) + + self.edge_counter_config.counter_task.wait_until_done() + self.edge_counter_config.counter_task.start() + time.sleep(self.daq_time*1.1) #pause for acquisition + + read_samples = self.edge_counter_config.counter_reader.read_many_sample_double( + data_buffer, + number_of_samples_per_channel=self.N_clock_ticks_per_tau, + timeout=5) + + #should we assert that we read all samples? read_samples == self.N_clock_ticks_per_tau + self.edge_counter_config.counter_task.stop() + if post_process_function: + data_buffer = post_process_function(data_buffer, self) + + #should we make this a dictionary with self.current_tau as the key? + data.append([self.current_tau, + data_buffer]) + + except Exception as e: + logger.error(f'{type(e)}: {e}') + raise e + + finally: + try: + self.edge_counter_config.counter_task.stop() + except Exception as e: + pass + try: + self.edge_counter_config.counter_task.close() + except Exception as e: + pass + + return data + + def build_spectrum_animator(self): + pass