Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#005 adding qdlscan and qdlscope first implementation #10

Merged
merged 24 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7c38883
Base implementation of the qdlscan scan controller
nyama8 Oct 26, 2024
666e9e5
Tested and bugfixed application controller
nyama8 Oct 26, 2024
fa8d72e
Created YAML file and controller GUI
nyama8 Oct 26, 2024
459f892
Added backend for single-axis line scans.
nyama8 Oct 26, 2024
bfa70ca
Implemented single line scan and image scan features. Image scan stil…
nyama8 Oct 27, 2024
43bab5d
Implemented save, pause, and continue functions. Tested basic functio…
nyama8 Oct 27, 2024
445cc88
Created example for loading data from qdlscan
nyama8 Oct 27, 2024
9ccc360
Added some basic documentation.
nyama8 Oct 27, 2024
11c1813
Basic implementation of qdlscope controller.
nyama8 Oct 30, 2024
68bbe01
Created basic framework for scope gui and main.
nyama8 Oct 31, 2024
f80b000
Base working version of qdlscope
nyama8 Oct 31, 2024
78d42e5
Testing timing of the sampling loop
nyama8 Oct 31, 2024
1594df1
Basic scope function implementation is complete.
nyama8 Oct 31, 2024
9cd54d9
Removed the int type casting in the save function of qdlscan
nyama8 Oct 31, 2024
99819ad
Added documentation to qdlscope.
nyama8 Oct 31, 2024
979f9eb
Added right click event handling, linked to qdlscope.
nyama8 Nov 1, 2024
b66c776
Added image scan normalization.
nyama8 Nov 1, 2024
5403a9a
Added documentation for new functions.
nyama8 Nov 1, 2024
3ac1859
nick's changes for backscanning
NnguyenHTommy Nov 6, 2024
abc76c2
Fixes to repump and scanning consistency in qdlple
nyama8 Nov 8, 2024
b6a9631
Small bug fix to patch input buffering on qdlmove.
nyama8 Nov 9, 2024
38a1fa4
Removed old test file
nyama8 Nov 12, 2024
0f94779
Updates to QDL scope to fix saving and adjust initial sample rate
nyama8 Nov 12, 2024
60609ca
Fix open scope callback on line scan, created gaussian optimization o…
nyama8 Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 301 additions & 0 deletions examples/qdlscan/confocal_scan_loader.ipynb

Large diffs are not rendered by default.

Binary file added examples/qdlscan/image.hdf5
Binary file not shown.
Binary file added examples/qdlscan/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/qdlscan/x_scan.hdf5
Binary file not shown.
Binary file added examples/qdlscan/x_scan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/qdlscan/y_scan.hdf5
Binary file not shown.
Binary file added examples/qdlscan/y_scan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/qdlscan/z_scan.hdf5
Binary file not shown.
Binary file added examples/qdlscan/z_scan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion src/qdlutils/applications/qdlmove/application_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,23 @@ class MovementController:
'''
def __init__(self, positioners: dict = {}):
self.positioners = positioners # dictionary of the controller instantiations
self.busy = False


def move_axis(self, axis_controller_name: str, position: float):
if self.busy:
logger.warning('Movement controller busy')
return None
self.busy = True
# Move the axis specified by the axis_controller_name
self.positioners[axis_controller_name].go_to_position(position)
self.busy = False

def step_axis(self, axis_controller_name: str, dx: float):
if self.busy:
logger.warning('Movement controller busy')
return None
self.busy = True
# Step the axis specified by the axis_controller_name
self.positioners[axis_controller_name].step_position(dx)
self.positioners[axis_controller_name].step_position(dx)
self.busy = False
5 changes: 2 additions & 3 deletions src/qdlutils/applications/qdlmove/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,8 @@ def configure_from_yaml(self, afile: str) -> None:
This method loads a YAML file to configure the qdlmove hardware
based on yaml file indicated by argument `afile`.

This method configures the wavelength controller and the readers
based off of the YAML file data, and then generates a class
consctrutor for the main application controller.
This method constructs the positioners and configures them, then
stores them in a dictionary which is saved in the application.
'''
with open(afile, 'r') as file:
# Log selection
Expand Down
14 changes: 9 additions & 5 deletions src/qdlutils/applications/qdlple/application_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,15 @@ def configure_scan(self,
self.sample_step_size_down = (min - max) / (n_pixels_down * n_subpixels) # Should be negative

# Calculate the output voltage readings
self.pixel_voltages_up = np.arange(min, max, self.pixel_step_size_up)
self.pixel_voltages_down = np.arange(max, min, self.pixel_step_size_down)
#self.pixel_voltages_up = np.arange(min, max, self.pixel_step_size_up)
#self.pixel_voltages_down = np.arange(max, min, self.pixel_step_size_down)
self.pixel_voltages_up = np.linspace(min, max - self.pixel_step_size_up, n_pixels_up)
self.pixel_voltages_down = np.linspace(max, min + self.pixel_step_size_down, n_pixels_down)
# Calculate the sample voltage readings
self.sample_voltages_up = np.arange(min, max, self.sample_step_size_up)
self.sample_voltages_down = np.arange(max, min, self.sample_step_size_down)
#self.sample_voltages_up = np.arange(min, max, self.sample_step_size_up)
#self.sample_voltages_down = np.arange(max, min, self.sample_step_size_down)
self.sample_voltages_up = np.linspace(min, max - self.sample_step_size_up, n_pixels_up * n_subpixels)
self.sample_voltages_down = np.linspace(max, min - self.sample_step_size_down, n_pixels_down * n_subpixels)

# Calculate time per pixel
self.pixel_time_up = time_up / n_pixels_up
Expand Down Expand Up @@ -500,7 +504,7 @@ def repump(self):
# Wait for repump time
time.sleep(self.time_repump * 0.001)
# Turn off the pump laser
repump_controller.go_to_voltage(voltage=repump_controller.max_voltage)
repump_controller.go_to_voltage(voltage=repump_controller.min_voltage)


def __del__(self):
Expand Down
5 changes: 4 additions & 1 deletion src/qdlutils/applications/qdlple/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ def scan_thread_function(self) -> None:
self.current_scan.view.data_viewport.update_image_and_plot(self.current_scan.application_controller)
self.current_scan.view.canvas.draw()

# Set repump to toggle value
self.toggle_repump_laser()

logger.info('Scan complete.')
self.current_scan.application_controller.stop()

Expand Down Expand Up @@ -524,7 +527,7 @@ def save_data(self, event=None) -> None:
'scan_id',
'timestamp',
'original_name'], dtype='S'))
ds.attrs['application'] = 'qdlutils.qt3ple'
ds.attrs['application'] = 'qdlutils.qdlple'
ds.attrs['qdlutils_version'] = qdlutils.__version__
ds.attrs['scan_id'] = self.id
ds.attrs['timestamp'] = self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
Expand Down
Empty file.
301 changes: 301 additions & 0 deletions src/qdlutils/applications/qdlscan/application_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import logging
import time

import numpy as np

from qdlutils.hardware.nidaq.counters.nidaqtimedratecounter import NidaqTimedRateCounter
from qdlutils.hardware.nidaq.analogoutputs.nidaqposition import NidaqPositionController

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)


class ScanController:
'''
This is the main class which coordinates the scaning and collection of data.
'''

def __init__(self,
x_axis_controller: NidaqPositionController,
y_axis_controller: NidaqPositionController,
z_axis_controller: NidaqPositionController,
counter_controller: NidaqTimedRateCounter,
inter_scan_settle_time: float=0.01):

self.x_axis_controller = x_axis_controller
self.y_axis_controller = y_axis_controller
self.z_axis_controller = z_axis_controller
self.counter_controller = counter_controller
self.inter_scan_settle_time = inter_scan_settle_time

# On initialization move all position controllers to zero.
# WARNING: it is assumed that the zero is a valid position of the DAQ
try:
# Set the positions to zero on start up, this is necessary as it establishes
# a `last_write_value` for the controllers so that the position is defined.
self._set_axis(axis_controller=self.x_axis_controller, position=0)
self._set_axis(axis_controller=self.y_axis_controller, position=0)
self._set_axis(axis_controller=self.z_axis_controller, position=0)
except Exception as e:
logger.warning(f'Could not zero axes on startup: {e}')


# This is a flag to keep track of if the controller is currently busy
# Must turn on whenever a scan is being performed and remain on until
# the scanner is free to perform another operation.
# The external applications are required to flag this when in use.
self.busy = False

# This is a flag to keep track of if a scan is currently in progress
# Must turn on whenever a scan is actively running (via the class
# method `self.scan_axis()`).
# Use this flag to tell the controller to stop in the middle of a scan
self.scanning = False

# This is a flag set by external applications to request the application
# controller to stop scanning.
self.stop_scan = False

def get_position(self):
'''
Returns the position based off of the last write values of the controllers
'''
x = self.x_axis_controller.last_write_value
y = self.y_axis_controller.last_write_value
z = self.z_axis_controller.last_write_value
return x,y,z

def set_axis(self, axis: str, position: float):
'''
Outward facing method for moving the position of an axis specified
by a string `axis`.
'''
# Block action if busy
if self.busy:
raise RuntimeError('Application controller is currently in use.')
# Reserve the controller
self.busy = True

# Get the axis controller depending on which axis is requested
if axis == 'x':
axis_controller = self.x_axis_controller
elif axis == 'y':
axis_controller = self.y_axis_controller
elif axis == 'z':
axis_controller = self.z_axis_controller
else:
raise ValueError(f'Requested axis {axis} is invalid.')

# Call the internal movement function
try:
self._set_axis(axis_controller=axis_controller, position=position)
except Exception as e:
logger.warning(f'Movement of axis {axis} failed due to exception: {e}')
# Free up the controller
self.busy = False

def _set_axis(self, axis_controller: NidaqPositionController, position: float):
'''
Internal function for moving the axis controlled by the given controller.
This avoids logic required to determine which axis is to be moved, for example
in the case of scans where only one axis is used repeatedly.
'''
logger.debug(f'Attempting to move to position {position}.')
# Move the axis
axis_controller.go_to_position(position=position)

def scan_axis(self,
axis: str,
start: float,
stop: float,
n_pixels: int,
scan_time: float):
'''
Outward facing scan function.
Scans the designated axis between the `start` and `stop` positions in
`n_pixels` over `scan_time` seconds. Returns the counts at each pixel
not normalized to time.
'''
# Block action if busy
if self.busy:
raise RuntimeError('Application controller is currently in use.')
# Block the controller from additional external commands
self.busy=True
# Start the counter
logger.info('Starting counter task on DAQ.')
self.counter_controller.start()

# Get the axis controller depending on which axis is requested
if axis == 'x':
axis_controller = self.x_axis_controller
elif axis == 'y':
axis_controller = self.y_axis_controller
elif axis == 'z':
axis_controller = self.z_axis_controller
else:
raise ValueError(f'Requested axis {axis} is invalid.')

# Go to start position
self._set_axis(axis_controller=axis_controller, position=start)
# Let the axis settle before next scan
time.sleep(self.inter_scan_settle_time)

data = self._scan_axis(axis_controller=axis_controller,
start=start,
stop=stop,
n_pixels=n_pixels,
scan_time=scan_time)

# Free up the controller
self.stop()
return data

def _scan_axis(self,
axis_controller: str,
start: float,
stop: float,
n_pixels: int,
scan_time: float):
'''
Internal scanning function
'''
# Set the scanning flag
self.scanning = True

# Calculate the time per pixel
sample_time = scan_time / n_pixels
# Configure the counter controller
self.counter_controller.configure_sample_time(sample_time=sample_time)

# Generate the positions to scan according to the usual
# numpy.linspace implementation.
# Note that this means that `stop` > `start` is technically valid and
# will scan from large to small, however the output datastream will be
# ordered accordingly and thus resulting images might be flipped.
# It is on the calling functions to manage these nuances.
positions = np.linspace(start=start, stop=stop, num=n_pixels)

# Create a buffer for the data
output = np.zeros(shape=n_pixels)

# Then iterate through the positions
for index, position in enumerate(positions):
# Move to the desired position
self._set_axis(axis_controller=axis_controller, position=position)
# Get the counts
counts = self.counter_controller.sample_batch_counts()
# Store in the buffer
output[index] = counts

# Set the scanning flag
self.scanning = False

# Return the buffered output
return output



def scan_image(self,
axis_1: str,
start_1: float,
stop_1: float,
n_pixels_1: int,
axis_2: str,
start_2: float,
stop_2: float,
n_pixels_2: int,
scan_time: float):
'''
This is an experimental implementation of the scanning function using the
`yield` keyword in Python. The method implements a generator which can be
queried like

>>> for line in scan_image(**kwargs): # do stuff with line

where each line is one scan along the `axis_1` defined by the start, stop,
and number of pixels. For each line the `axis_2` is moved to the next
pixels also defined by its start, stop, and step size. The speed of a scan
over each axis is determined by the scan time.
'''

# Block action if busy
if self.busy:
raise RuntimeError('Application controller is currently in use.')
# Reserve the controller
self.busy = True


# Get the axis controller depending on which axis is requested
if axis_1 == 'x':
axis_controller_1 = self.x_axis_controller
elif axis_1 == 'y':
axis_controller_1 = self.y_axis_controller
elif axis_1 == 'z':
axis_controller_1 = self.z_axis_controller
else:
raise ValueError(f'Requested axis_1 {axis_1} is invalid.')
# Get the axis controller depending on which axis is requested
if axis_2 == 'x':
axis_controller_2 = self.x_axis_controller
elif axis_2 == 'y':
axis_controller_2 = self.y_axis_controller
elif axis_2 == 'z':
axis_controller_2 = self.z_axis_controller
else:
raise ValueError(f'Requested axis_2 {axis_2} is invalid.')

# Start the counter
logger.info('Starting counter task on DAQ.')
self.counter_controller.start()

# Get the positions for the slow scan axis
positions_2 = np.linspace(start=start_2, stop=stop_2, num=n_pixels_2)

# Then iterate through the positions
for index, position in enumerate(positions_2):
# Move axis 2 to the desired position
self._set_axis(axis_controller=axis_controller_2, position=position)
# Let the axis settle before next scan
time.sleep(self.inter_scan_settle_time)
# Scan axis 1
single_scan = self._scan_axis(axis_controller=axis_controller_1,
start=start_1,
stop=stop_1,
n_pixels=n_pixels_1,
scan_time=scan_time)
# Update the buffer
#output[index] = single_scan

# Set back to original position on fast scan axis
#self._set_axis(axis_controller=axis_controller_1, position=start_1)
# Slow scan back to start for smooth scanning?
self._scan_axis(axis_controller=axis_controller_1,
start=stop_1,
stop=start_1,
n_pixels=n_pixels_1,
scan_time=self.inter_scan_settle_time)

# Yield a single scan
yield single_scan

# If the stop is requested then terminate and return the final scan
if self.stop_scan:
logger.info('Stopping scan.')
self.stop()
return single_scan

logger.info('Scan complete.')
self.stop()

def stop(self) -> None:
'''
Stop running scan
'''
self.scanning = False
# Stop the DAQ
self.counter_controller.stop()
logger.info(f'Stopping counter task on DAQ.')
# Free up the controller
self.busy = False
# Reset the stop scan flag
self.stop_scan = False
Loading