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

Qi2lab throlabs kcube #861

Merged
3 commits merged into from
Apr 25, 2024
Merged
Changes from all commits
Commits
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

Large diffs are not rendered by default.

629 changes: 629 additions & 0 deletions src/navigate/model/devices/APIs/thorlabs/kcube_steppermotor.py

Large diffs are not rendered by default.

274 changes: 274 additions & 0 deletions src/navigate/model/devices/stages/stage_tl_kcube_steppermotor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# Copyright (c) 2021-2022 The University of Texas Southwestern Medical Center.
# All rights reserved.

# Redistribution and use in source and binary forms, with or without
# modification, are permitted for academic and research use only (subject to the
# limitations in the disclaimer below) provided that the following conditions are met:

# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.

# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.

# * Neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.

# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Builds from
stage:
hardware:
-
name: stage
type: KST101
serial_number: 26001318
axes: [f]
axes_mapping: [1]
device_units_per_mm: 20000000/9.957067
axes_channels: autofocus
max: 0
min: 25
"""
# Standard Library imports
import importlib
import logging
import time
from multiprocessing.managers import ListProxy
from numpy import round
# Local Imports
from navigate.model.devices.stages.stage_base import StageBase


# # Logger Setup
# p = __name__.split(".")[1]
# logger = logging.getLogger(p)


def build_TLKSTStage_connection(serialnum):
"""Connect to the Thorlabs KST Stage
Parameters
----------
serialnum : str
Serial number of the stage.
Returns
-------
kst_controller
Thorlabs KST Stage controller
"""
kst_controller = importlib.import_module(
"navigate.model.devices.APIs.thorlabs.kcube_steppermotor"
)

# Initialize
kst_controller.TLI_BuildDeviceList()

# Open the same serial number device if there are several devices connected to the
# computer
available_serialnum = kst_controller.TLI_GetDeviceListExt()
if not list(filter(lambda s: str(s) == str(serialnum), available_serialnum)):
print(
f"** Please make sure Thorlabs stage with serial number {serialnum} "
f"is connected to the computer!"
)
raise RuntimeError
kst_controller.KST_Open(str(serialnum))
return kst_controller


class TLKSTStage(StageBase):
"""Thorlabs KST Stage"""


def __init__(self, microscope_name, device_connection, configuration, device_id=0):
"""Initialize the stage.
Parameters
----------
microscope_name : str
Name of the microscope.
device_connection : str
Connection string for the device.
configuration : dict
Configuration dictionary for the device.
device_id : int
Device ID for the device.
"""
super().__init__(microscope_name, device_connection, configuration, device_id)

# only initialize the focus axis
self.axes_mapping = {"f": 1}

#: list: List of KST axes available.
self.KST_axes = list(self.axes_mapping.values())

device_config = configuration["configuration"]["microscopes"][microscope_name]["stage"]["hardware"]
if type(device_config) == ListProxy:
#: str: Serial number of the stage.
self.serial_number = str(device_config[device_id]["serial_number"])
self.device_unit_scale = device_config[device_id]["device_units_per_mm"]
else:
self.serial_number = device_config["serial_number"]
self.device_unit_scale = device_config["device_units_per_mm"]

if device_connection is not None:
#: object: Thorlabs KST Stage controller
self.kst_controller = device_connection
else:
self.kst_controller = build_TLKSTStage_connection(self.serial_number)


def __del__(self):
"""Delete the KST Connection"""
try:
self.stop()
self.kst_controller.KST_Close(self.serial_number)
except AttributeError:
pass


def report_position(self):
"""
Report the position of the stage.
Reports the position of the stage for all axes, and creates the hardware
position dictionary.
Returns
-------
position_dict : dict
Dictionary containing the current position of the stage.
"""

try:
pos = self.kst_controller.KST_GetCurrentPosition(self.serial_number)/self.device_unit_scale
setattr(self, f"f_pos", pos)
except (
self.kst_controller.TLFTDICommunicationError,
self.kst_controller.TLDLLError,
self.kst_controller.TLMotorDLLError,
):
pass

return self.get_position_dict()


def move_axis_absolute(self, axes, abs_pos, wait_until_done=False):
"""
Implement movement.
Parameters
----------
axis : str
An axis. For example, 'x', 'y', 'z', 'f', 'theta'.
abs_pos : float
Absolute position value
wait_until_done : bool
Block until stage has moved to its new spot.
Returns
-------
bool
Was the move successful?
"""
axis_abs = self.get_abs_position(axes, abs_pos)
if axis_abs == -1e50:
return False

self.kst_controller.KST_SetAbsolutePosition(self.serial_number, int(axis_abs*self.device_unit_scale))
self.kst_controller.KST_MoveAbsolute(self.serial_number)

if wait_until_done:
stage_pos, n_tries, i = -1e50, 1000, 0
target_pos = axis_abs
while (round(stage_pos, 6) != round(target_pos, 6)) and (i < n_tries):
stage_pos = self.kst_controller.KST_GetCurrentPosition(self.serial_number)/self.device_unit_scale
i += 1
time.sleep(0.01)
if stage_pos != target_pos:
return False
return True


def move_absolute(self, move_dictionary, wait_until_done=False):
"""Move stage along a single axis.
Parameters
----------
move_dictionary : dict
A dictionary of values required for movement. Includes 'x_abs', etc. for
one or more axes. Expects values in micrometers, except for theta, which is
in degrees.
wait_until_done : bool
Block until stage has moved to its new spot.
Returns
-------
success : bool
Was the move successful?
"""

result = True
result = (self.move_axis_absolute("f", move_dictionary["f_abs"], wait_until_done), result)

return result


def move_to_position(self, position, wait_until_done=False):
"""Perform a move to position
Parameters
----------
position : float
Stage position in mm.
wait_until_done : bool
Block until stage has moved to its new spot.
Returns
-------
success : bool
Was the move successful?
"""
self.kst_controller.KST_MoveToPosition(self.serial_number, position*self.device_unit_scale)

if wait_until_done:
stage_pos, n_tries, i = -1e50, 1000, 0
target_pos = position
while (round(stage_pos, 4) != round(target_pos, 4)) and (i < n_tries):
stage_pos = self.kst_controller.KST_GetCurrentPosition(self.serial_number)/self.device_unit_scale
i += 1
time.sleep(0.01)
if stage_pos != target_pos:
return False
else:
return True


def run_homing(self):
self.kst_controller.KST_HomeDevice(self.serial_number)
self.move_to_position(12.5, wait_until_done=True)



def stop(self):
"""
Stop all stage channels move
"""
self.kst_controller.KST_MoveStop(self.serial_number)
113 changes: 113 additions & 0 deletions src/navigate/model/devices/test_thorlabs_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Test the KST device controller
"""
from stages.stage_tl_kcube_steppermotor import build_TLKSTStage_connection
import time

# test build connection function
serial_number = 26001318

# perform callibration
dv_units = 20000000
real_units = 9.957067 # mm
dv_per_mm = dv_units / real_units

# Open connection to stage
kcube_connection = build_TLKSTStage_connection(serial_number)
time.sleep(2)

# Move the stage to middle of travel
kcube_connection.KST_MoveToPosition(str(serial_number), int(12.5*dv_per_mm))

time.sleep(5)

current_pos = kcube_connection.KST_GetCurrentPosition(str(serial_number))
print(f"Stage currently at:{current_pos} dvUnits")


def time_move(distance):
"""Test how long commands take to excecute move some distance
"""
start = kcube_connection.KST_GetCurrentPosition(str(serial_number))
final_position = start + distance

kcube_connection.KST_MoveToPosition(str(serial_number), int(final_position*dv_per_mm))
time.sleep(5)

tstart = time.time()
kcube_connection.KST_MoveToPosition(str(serial_number), start)

pos = None
while pos!=start:
pos = kcube_connection.KST_GetCurrentPosition(str(serial_number))
tend = time.time()

print(f"it takes {tend-tstart:.3f}s to move {distance:.3}mm")


def test_move(position):
""" Test setPosition()
:param position: stage position in mm
"""
# Move the stage to the position
kcube_connection.KST_MoveToPosition(str(serial_number), int(position*dv_per_mm))

# needed to add a sleep call
time.sleep(5)

# Read the new position and varify it matches expectations
pos = kcube_connection.KST_GetCurrentPosition(str(serial_number))

print(f"Target position = {position} \n",
f"The final position in device units:{pos}, in real units:{pos/dv_per_mm}mm")


def test_jog():
""" Test MoveJog
"""
# get the initial position
start = kcube_connection.KST_GetCurrentPosition(str(serial_number))

# Test a short jog
kcube_connection.KST_MoveJog(str(serial_number), 1)
time.sleep(2)
kcube_connection.KST_MoveStop(str(serial_number))

time.sleep(2)
# read stage and make sure it moved
jog_pos = kcube_connection.KST_GetCurrentPosition(str(serial_number))
print(f"JogMove moved from {start} to {jog_pos}, starting jog back...")

kcube_connection.KST_MoveJog(str(serial_number), 2)
time.sleep(2)
kcube_connection.KST_MoveStop(str(serial_number))

time.sleep(2)
end = kcube_connection.KST_GetCurrentPosition(str(serial_number))
print(f"JogMove back moved from {jog_pos} to {end}")


def test_polling():
"""Start polling, then run the jog test
"""
print("testing polling")
# start polling
kcube_connection.KST_StartPolling(str(serial_number), 100)

# Run Jog during active polling
test_jog()

# End polling
kcube_connection.KST_StopPolling(str(serial_number))
# pos = kcube_connection.KST_GetCurrentPosition(str(serial_number))

# print(f"final position: {pos}")

test_move(12.5)
test_jog()
time_move(1.0)
test_polling()

# # close connection to stage
# kcube_connection.KST_Close(str(serial_number))
96 changes: 96 additions & 0 deletions src/navigate/model/devices/test_thorlabs_kcube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
Test stage Class
"""
from stages.stage_tl_kcube_steppermotor import TLKSTStage

# Create configuration for microscope stage
serial_number = 26001318
dv_units = 20000000
real_units = 9.957067 # mm
dv_per_mm = dv_units / real_units
mm_per_dv = real_units / dv_units
microscope_name = "test"
config = {"configuration":{"microscopes":{f"{microscope_name}":{"stage":{"hardware":{"serial_number":str(serial_number),
"axes":"f",
"axes_mapping":[1],
"device_units_per_mm":dv_per_mm,
"f_min":0,
"f_max":25
},
"f_min":0,
"f_max":25
}
}
}
}
}

# Create the stage controller class
stage = TLKSTStage(microscope_name=microscope_name,
device_connection=None,
configuration=config
)
stage.run_homing()


def test_move_axis_absolute(distance):
""" Test setPosition()
"""
# Get the current position
stage.report_position()
start = stage.f_pos
print(f"starting stage position = {start}")

# Move the target distance
target = start + distance
stage.move_axis_absolute("f", target, True)

# Read the position and report
stage.report_position()
end = stage.f_pos

print(f"The final position in device units:{end/dv_per_mm}, in real units:{end}mm,\n",
f"Distance moved = {(end-start)}mm")


def test_move_absolute(distance):
""" Test MoveAbsolute
"""
# Get the current position
stage.report_position()
start = stage.f_pos
print(f"starting stage position = {start}")

# Move the target distance
target = start + distance
stage.move_to_position(target, True)

# Read the position and report
stage.report_position()
end = stage.f_pos

print(f"The final position in device units:{end}, in real units:{end}mm,\n",
f"Distance moved = {(end-start)}mm")


def test_move_to_position(distance):
# Get the current position
stage.report_position()
start = stage.f_pos
print(f"starting stage position = {start:.4f}")

# move target distance, wait till done
stage.move_to_position(start + distance, True)

# get the final position
stage.report_position()
end = stage.f_pos
print(f"End stage position = {end:.4f}",
f"distance moved = {end-start:.6f}")


# Run tests
test_move_to_position(0.100)
test_move_axis_absolute(0.100)
test_move_absolute(0.200)