From 686df38f264374a5e7503ec4d1e81a03454b0826 Mon Sep 17 00:00:00 2001 From: G Adam Cox Date: Fri, 31 Mar 2023 17:19:48 -0700 Subject: [PATCH 1/4] adds run_scan function and data access functions --- src/qt3utils/datagenerators/piezoscanner.py | 32 ++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/qt3utils/datagenerators/piezoscanner.py b/src/qt3utils/datagenerators/piezoscanner.py index a04ead2e..47b65d1a 100644 --- a/src/qt3utils/datagenerators/piezoscanner.py +++ b/src/qt3utils/datagenerators/piezoscanner.py @@ -133,7 +133,19 @@ def scan_axis(self, axis, min, max, step_size): def reset(self): self.scanned_raw_counts = [] self.scanned_count_rate = [] - + + def get_raw_counts(self): + """ + Returns a list of raw counts from the most resent scan + """ + return self.scanned_raw_counts + + def get_count_rate(self): + """ + Returns a list of count rates from the most resent scan + """ + return self.scanned_count_rate + def optimize_position(self, axis, center_position, width = 2, step_size = 0.25): ''' Performs a scan over a particular axis about `center_position`. @@ -189,3 +201,21 @@ def optimize_position(self, axis, center_position, width = 2, step_size = 0.25): return count_rates, axis_vals, optimal_position, coeff + + def run_scan(self, reset_starting_position=True): + """ + Runs a scan of the sample. + + To get the data, call get_raw_counts() or get_count_rate() + """ + + if reset_starting_position: + self.reset() + self.set_to_starting_position() + + self.start() + while self.still_scanning(): + self.scan_x() + self.move_y() + self.stop() + From 5af00972e1497925363e93935326b72d0ce19c3f Mon Sep 17 00:00:00 2001 From: G Adam Cox Date: Sat, 1 Apr 2023 20:45:08 -0700 Subject: [PATCH 2/4] Adds hyper-data acquisition function to piezoscanner. A number of changes have been made in this commit to support the addition of a hyper-data acquisition callback function. This function allows an external user to define a function to be called at each position of the scan. This function may also be executed in a separate thread if the 'in_parallel' attribute of the funciton is set. In support of this function, a number of other smaller changes were made. A run_scan function was added in order to facilitate use of CounterAndScanner objects. A callback function for that is also provided so the GUI application pizeoscan.py may update it's display upon completion of a raster line. Unnecessary checks for stage_controller != None were removed since this object does not support instantiation without a stage_controller. (Although, future updates should provide proper type hinting.) The position of the scan is no longer tracked by the CounterAndScan object, but instead relies upon the last_write_values attribute of the stage_controller object. --- src/applications/piezoscan.py | 27 ++- src/qt3utils/datagenerators/piezoscanner.py | 188 +++++++++++++++----- 2 files changed, 153 insertions(+), 62 deletions(-) diff --git a/src/applications/piezoscan.py b/src/applications/piezoscan.py index 95b17795..ba7c6e93 100644 --- a/src/applications/piezoscan.py +++ b/src/applications/piezoscan.py @@ -80,18 +80,18 @@ def __init__(self, mplcolormap = 'gray'): self.ax.set_ylabel('y position (um)') self.log_data = False - def update(self, model): + def update(self, scanner): if self.log_data: - data = np.log10(model.scanned_count_rate) + data = np.log10(scanner.get_count_rate()) data[np.isinf(data)] = 0 #protect against +-inf else: - data = model.scanned_count_rate + data = scanner.get_count_rate() - self.artist = self.ax.imshow(data, cmap=self.cmap, extent=[model.xmin, - model.xmax + model.step_size, - model.current_y + model.step_size, - model.ymin]) + self.artist = self.ax.imshow(data, cmap=self.cmap, extent=[scanner.xmin, + scanner.xmax + scanner.step_size, + scanner.get_current_position('y') + scanner.step_size, + scanner.ymin]) if self.cbar is None: self.cbar = self.fig.colorbar(self.artist, ax=self.ax) else: @@ -395,17 +395,12 @@ def scan_thread_function(self, xmin, xmax, ymin, ymax, step_size, N): self.counter_scanner.set_num_data_samples_per_batch(N) try: - self.counter_scanner.reset() #clears the data - self.counter_scanner.start() #starts the DAQ - self.counter_scanner.set_to_starting_position() #moves the stage to starting position - - while self.counter_scanner.still_scanning(): - self.counter_scanner.scan_x() - self.view.scan_view.update(self.counter_scanner) + def update_scan_view(counter_scanner): + self.view.scan_view.update(counter_scanner) self.view.canvas.draw() - self.counter_scanner.move_y() - self.counter_scanner.stop() + self.counter_scanner.run_scan(reset_starting_position=True, + line_scan_callback=update_scan_view) except nidaqmx.errors.DaqError as e: logger.info(e) diff --git a/src/qt3utils/datagenerators/piezoscanner.py b/src/qt3utils/datagenerators/piezoscanner.py index 47b65d1a..a9353d0a 100644 --- a/src/qt3utils/datagenerators/piezoscanner.py +++ b/src/qt3utils/datagenerators/piezoscanner.py @@ -1,7 +1,10 @@ -import numpy as np -import scipy.optimize import time import logging +import threading +from typing import Union, Callable + +import numpy as np +import scipy.optimize logger = logging.getLogger(__name__) @@ -11,10 +14,30 @@ def gauss(x, *p): class CounterAndScanner: - def __init__(self, rate_counter, stage_controller): + def __init__(self, rate_counter, stage_controller, hyper_data_acquisition_function: Callable = None): + """ + + :param rate_counter: a RateCounter object + :param stage_controller: a StageController object + :param hyper_data_acquisition_function: a function is called at each position of the scan + + Notes on hyper_data_acquisition_function: + + This function must have an attribute "in_parallel" that is True or False. + It must store its own data if generated + + The function will be passed a copy of this CounterAndScanner object as the argument, from which the caller + can extract the current position and other information that may be desired. + + If the function's "in_parallel" attribute is True, then the function will be called in a separate thread. + This is useful, for example, if the spectrometer is being used to acquire data at each position. + Both the spectrometer and the rate counter will acquire data simultaneously. + If the function uses NiDAQ or any of the other hardware also used during a scan, then "in_parallel" must be + False. Otherwise, both the scan and the function will be trying to use the same hardware simultaneously. + + """ self.running = False - self.current_y = 0 self.ymin = 0.0 self.ymax = 80.0 self.xmin = 0.0 @@ -27,7 +50,36 @@ def __init__(self, rate_counter, stage_controller): self.stage_controller = stage_controller self.rate_counter = rate_counter - self.num_daq_batches = 1 # could change to 10 if want 10x more samples for each position + self.num_daq_batches = 1 # could change to 10 if want 10x more samples for each position + + self.hyper_data_acquisition_function = None + self.set_hyper_data_acquisition_function(hyper_data_acquisition_function) + + def set_hyper_data_acquisition_function(self, hyper_data_acquisition_function: Callable): + """ + Sets the function that is called at each position of the scan + + This function must have an attribute "in_parallel" that is True or False. + If no attribute is provided, then it is assumed to be False. + + It must store its own data if generated + + The function will be passed a copy of this CounterAndScanner object as the argument, from which the caller + can extract the current position and other information that may be desired. + + If the function's "in_parallel" attribute is True, then the function will be called in a separate thread. + This is useful, for example, if the spectrometer is being used to acquire data at each position. + Both the spectrometer and the rate counter will acquire data simultaneously. + + If the function uses NiDAQ or any of the other hardware also used during a scan, then "in_parallel" must be + False. Otherwise, both the scan and the function will be trying to use the same hardware simultaneously. + """ + self.hyper_data_acquisition_function = hyper_data_acquisition_function + if self.hyper_data_acquisition_function is not None: + if hasattr(self.hyper_data_acquisition_function, 'in_parallel') is False: + logger.warning('hyper_data_acquisition_function does not have the attribute "in_parallel"') + logger.warning('setting hyper_data_acquisition_function.in_parallel = False') + self.hyper_data_acquisition_function.in_parallel = False def stop(self): self.rate_counter.stop() @@ -38,9 +90,10 @@ def start(self): self.rate_counter.start() def set_to_starting_position(self): - self.current_y = self.ymin - if self.stage_controller: - self.stage_controller.go_to_position(x = self.xmin, y = self.ymin) + """ + Move the stage to the starting position (xmin, ymin) + """ + self.stage_controller.go_to_position(x=self.xmin, y=self.ymin) def close(self): self.rate_counter.close() @@ -57,9 +110,11 @@ def sample_count_rate(self, data_counts=None): return self.rate_counter.sample_count_rate(data_counts) def set_scan_range(self, xmin, xmax, ymin, ymax): - if self.stage_controller: - self.stage_controller.check_allowed_position(xmin, ymin) - self.stage_controller.check_allowed_position(xmax, ymax) + """ + Sets the scan range + """ + self.stage_controller.check_allowed_position(xmin, ymin) + self.stage_controller.check_allowed_position(xmax, ymax) self.ymin = ymin self.ymax = ymax @@ -73,60 +128,95 @@ def get_scan_range(self) -> tuple: """ return self.xmin, self.xmax, self.ymin, self.ymax + def get_current_position(self, axis=None, use_last_write_values=True) -> Union[dict, float]: + """ + Returns the last position written to the stage controller. + + This is the CURRENT position of the stage. + + :param axis: 'x', 'y', or 'z'. If None, returns a dict of all axes + :param use_last_write_values: if True, then the last position written to the stage controller is returned. + otherwise, the current position is returned as determined by the read voltages from the stage controller. + + :return: dict of x, y, z positions or a single float of the position of the specified axis + """ + if use_last_write_values: + position = dict(zip(['x', 'y', 'z'], self.stage_controller.last_write_values)) + else: + position = dict(zip(['x', 'y', 'z'], self.stage_controller.get_position())) + + if axis is None: + return position + else: + return position[axis] + def get_completed_scan_range(self) -> tuple: """ Returns a tuple of the scan range that has been completed - :return: xmin, xmax, ymin, current_y + :return: xmin, xmax, ymin, get_current_position('y') """ - return self.xmin, self.xmax, self.ymin, self.current_y + return self.xmin, self.xmax, self.ymin, self.get_current_position('y') - def still_scanning(self): - if self.running == False: #this allows external process to stop scan + def _still_scanning(self): + if self.running == False: # this allows external process to stop scan return False - if self.current_y <= self.ymax: #stops scan when reaches final position + if self.get_current_position('y') < self.ymax: # stops scan when reaches final position return True else: self.running = False return False - def move_y(self): - self.current_y += self.step_size - if self.stage_controller and self.current_y <= self.ymax: + def _move_y(self): + if self.get_current_position('y') + self.step_size <= self.ymax: try: - self.stage_controller.go_to_position(y=self.current_y) + self.stage_controller.go_to_position(y=self.get_current_position('y') + self.step_size) except ValueError as e: logger.info(f'out of range\n\n{e}') - def scan_x(self): + def _scan_x(self): """ Scans the x axis from xmin to xmax in steps of step_size. Stores results in self.scanned_raw_counts and self.scanned_count_rate. """ - raw_counts_for_axis = self.scan_axis('x', self.xmin, self.xmax, self.step_size) + raw_counts_for_axis = self._scan_axis('x', self.xmin, self.xmax, self.step_size, + hyper_data_acquisition_function=self.hyper_data_acquisition_function) self.scanned_raw_counts.append(raw_counts_for_axis) self.scanned_count_rate.append([self.sample_count_rate(raw_counts) for raw_counts in raw_counts_for_axis]) - def scan_axis(self, axis, min, max, step_size): + def _scan_axis(self, axis, min, max, step_size, hyper_data_acquisition_function=None): """ Moves the stage along the specified axis from min to max in steps of step_size. Returns a list of raw counts from the scan in the shape [[[counts, clock_samples]], [[counts, clock_samples]], ...] where each [[counts, clock_samples]] is the result of a single call to sample_counts at each scan position along the axis. + + If hyper_data_acquisition_function is not None, it will be called at each scan position. + If hyper_data_acquisition_function.in_parallel is True, it will be called in a separate thread. """ raw_counts = [] - self.stage_controller.go_to_position(**{axis:min}) + self.stage_controller.go_to_position(**{axis: min}) time.sleep(self.raster_line_pause) for val in np.arange(min, max, step_size): - if self.stage_controller: - logger.info(f'go to position {axis}: {val:.2f}') - self.stage_controller.go_to_position(**{axis:val}) - _raw_counts = self.sample_counts() + logger.info(f'go to position {axis}: {val:.2f}') + self.stage_controller.go_to_position(**{axis: val}) + + if hyper_data_acquisition_function is not None: + if hyper_data_acquisition_function.in_parallel: + thread = threading.Thread(target=hyper_data_acquisition_function, args=(self,)) + thread.start() + _raw_counts = self.sample_counts() + thread.join() + else: + hyper_data_acquisition_function(self) + _raw_counts = self.sample_counts() + else: + _raw_counts = self.sample_counts() + raw_counts.append(_raw_counts) logger.info(f'raw counts, total clock samples: {_raw_counts}') - if self.stage_controller: - logger.info(f'current position: {self.stage_controller.get_current_position()}') + logger.info(f'current stage position: {self.get_current_position()}') return raw_counts @@ -147,7 +237,7 @@ def get_count_rate(self): return self.scanned_count_rate def optimize_position(self, axis, center_position, width = 2, step_size = 0.25): - ''' + """ Performs a scan over a particular axis about `center_position`. The scan ranges from center_position +- width and progresses with step_size. @@ -170,18 +260,15 @@ def optimize_position(self, axis, center_position, width = 2, step_size = 0.25): and the fit_coeff is set to None. When the fit is successful, x_optimal = mu. - ''' + This function does NOT call self.hyper_data_acquisition_function. + """ min_val = center_position - width max_val = center_position + width - if self.stage_controller: - min_val = np.max([min_val, self.stage_controller.minimum_allowed_position]) - max_val = np.min([max_val, self.stage_controller.maximum_allowed_position]) - else: - min_val = np.max([min_val, 0.0]) - max_val = np.min([max_val, 80.0]) + min_val = np.max([min_val, self.stage_controller.minimum_allowed_position]) + max_val = np.min([max_val, self.stage_controller.maximum_allowed_position]) self.start() - raw_counts = self.scan_axis(axis, min_val, max_val, step_size) + raw_counts = self._scan_axis(axis, min_val, max_val, step_size) self.stop() axis_vals = np.arange(min_val, max_val, step_size) count_rates = [self.sample_count_rate(count) for count in raw_counts] @@ -201,12 +288,19 @@ def optimize_position(self, axis, center_position, width = 2, step_size = 0.25): return count_rates, axis_vals, optimal_position, coeff - - def run_scan(self, reset_starting_position=True): + def run_scan(self, reset_starting_position=True, line_scan_callback=None): """ - Runs a scan of the sample. + Runs a scan across the range of set parameters: xmin, xmax, ymin, ymax, step_size + + To get the data after the scan, call get_raw_counts() or get_count_rate(). You may also + provide a callback function that will be called after each line scan is completed. + + :param reset_starting_position: if True, the stage will be reset to the starting position before the scan begins + :param line_scan_callback: a function that will be called after each line scan is completed. The function + should take a single argument, which will be an instance of this class. + i.e. line_scan_callback(obj: CounterAndScanner) + :return: None - To get the data, call get_raw_counts() or get_count_rate() """ if reset_starting_position: @@ -214,8 +308,10 @@ def run_scan(self, reset_starting_position=True): self.set_to_starting_position() self.start() - while self.still_scanning(): - self.scan_x() - self.move_y() + while self._still_scanning(): + self._scan_x() + self._move_y() + if line_scan_callback is not None: + line_scan_callback(self) self.stop() From 71324d87aac24e867c674ad3c8fdc81569cd2de2 Mon Sep 17 00:00:00 2001 From: G Adam Cox Date: Sun, 2 Apr 2023 14:37:23 -0700 Subject: [PATCH 3/4] Changes internal naming. A misconception about the hardware led to incorrect variable names and documentation. Initially, the piezo actuator that controls the position of the laser light impinging upon a sample was called a "stage" controller due to a misunderstanding of the hardware setup. This labeling perpetuated until this change. All usage of "stage" control has been replaced either with "position" control or actuator, where appropriate. Variable naming may still feel a bit off in places. Please fix if you have better ways of describing functionality. --- README.md | 2 +- pyproject.toml | 2 +- src/applications/piezoscan.py | 31 ++++++------ src/qt3utils/datagenerators/piezoscanner.py | 52 ++++++++++----------- 4 files changed, 43 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index b860efa8..de8ff03d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ various spin-control experiments on quantum systems, such as NV centers in diamo * Spin Core PulseBlaster * Excelitas SPCM for photon detection * NI-DAQ card (PCIx 6363) for data acquisition and control - * Jena System's Piezo Actuator Stage Control Amplifier + * Jena System's Piezo Actuator Control Amplifier * [Future] spectrometer The code in this package facilitates usages of these devices to perform diff --git a/pyproject.toml b/pyproject.toml index 74157e39..87eeba69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ description = "A package for performing experiments in the QT3 lab at UW." readme = "README.md" requires-python = ">=3.8" license = {file = "LICENSE"} -keywords = ["qt3", "confocal scan", "nidaqmx", "piezo", "stage", "control", "electron spin control"] +keywords = ["qt3", "confocal scan", "nidaqmx", "electron spin control", "quantum computing", "nitrogen-vacancy center"] authors = [ {name = "G. Adam Cox", email = "gadamc@gmail.com" }, diff --git a/src/applications/piezoscan.py b/src/applications/piezoscan.py index ba7c6e93..d5bc5692 100644 --- a/src/applications/piezoscan.py +++ b/src/applications/piezoscan.py @@ -294,8 +294,8 @@ class MainTkApplication(): def __init__(self, counter_scanner): self.root = tk.Tk() self.counter_scanner = counter_scanner - scan_range = [counter_scanner.stage_controller.minimum_allowed_position, - counter_scanner.stage_controller.maximum_allowed_position] + scan_range = [counter_scanner.actuator_controller.minimum_allowed_position, + counter_scanner.actuator_controller.maximum_allowed_position] self.view = MainApplicationView(self.root, scan_range) self.view.sidepanel.startButton.bind("