Microscope
Zaber Motion Library includes an API that controls Zaber's Nucleus™ series microscopes. The following guide demonstrates how to use it. The guide assumes the knowledge from the Getting Started section.
The API consists of the microscopy namespace/module that contains all the classes representing different microscope components.
The central
Microscope
class provides access to all the components of a single microscope.
You can create an instance of this class by calling the
find
method:
# from zaber_motion.microscopy import Microscope
with Connection.open_serial_port("COM10") as connection:
connection.detect_devices()
scope = Microscope.find(connection)
scope.initialize()
The
initialize
method call initializes all the components by,
for example, homing devices that don't have their reference position.
With the components initialized, you can start exploring each component and its operations.
Keep in mind that some components may not be present in your microscope and are therefore represented by
None
on the microscope instance.
Here is the list of all the properties of the
Microscope
class representing different components:
If a component is physically present but you don't see it on the microscope instance, check the configuration of your microscope in the Zaber Launcher Microscope application.
Alternatively, you can always instantiate each component individually by calling the corresponding constructor.
Illuminator
The
Illuminator
class represents the X-LCA device.
Call the
get_channel
method to get an
IlluminatorChannel
instance
representing a single illuminator channel (numbered from 1).
Using the channel instance, you can turn the channel on and off as well as set the light intensity.
The intensity is specified as a fraction of the maximum intensity.
To prevent unwanted exposure of the sample, we recommend setting the intensity first before turning the channel on.
channel = scope.illuminator.get_channel(1)
channel.set_intensity(0.5) # 50% intensity
print("Intensity:", channel.get_intensity())
channel.on()
time.sleep(1)
channel.off()
Alternatively, you can set the intensity as an absolute radiant flux in Watts using the lamp.flux setting.
channel.settings.set("lamp.flux", 0.8) # Watts
Filter Changer
The
FilterChanger
class typically represents the X-FCR device.
Call the
change
method to switch between your filters (numbered from 1).
The instance also allows to get the current filter and number of available filters.
changer = scope.filter_changer
print("Number of filters:", changer.get_number_of_filters())
changer.change(2)
time.sleep(1)
changer.change(1)
print("Current filter:", changer.get_current_filter())
Objective Changer
The
ObjectiveChanger
class allows to switch between different objectives
by synchronizing movement of the focus axis and the objective changer (X-MOR device).
Use the
change
method to change between objectives (numbered from 1).
Upon objective change, the focus axis moves to the datum position (defaults to 15 mm).
You can change the datum position using the
set_focus_datum
method.
Additionally, you can adjust the end position by specifying the
focus_offset
argument on per-change basis.
changer = scope.objective_changer
print("Number of objectives:", changer.get_number_of_objectives())
changer.change(2)
time.sleep(1)
changer.change(1, focus_offset=Measurement(2, 'mm'))
print("Current objective:", changer.get_current_objective())
Focus Axis
Use the
focus_axis
property on the microscope instance to access the focus axis.
It's an instance of the common
Axis
class.
scope.focus_axis.move_relative(1, 'mm')
time.sleep(1)
scope.focus_axis.move_relative(-1, 'mm')
Plate
The
plate
property provides access to the
AxisGroup
class instance that groups both X and Y axes of the plate.
It allows you to simultaneously move the axes to a specific [X, Y] position.
plate = scope.plate
plate.move_relative(Measurement(-10, 'mm'), Measurement(10, 'mm'))
time.sleep(1)
plate.move_relative(Measurement(10, 'mm'), Measurement(-10, 'mm'))
Additionally, you can also access each of the axes independently.
scope.x_axis.move_relative(10, 'mm')
time.sleep(1)
scope.x_axis.move_relative(-10, 'mm')
scope.y_axis.move_relative(10, 'mm')
time.sleep(1)
scope.y_axis.move_relative(-10, 'mm')
Camera Trigger
The
camera_trigger
property provides access to the
CameraTrigger
class instance that
abstracts over a digital output that triggers the camera.
The given digital output pin is typically connected to one of the camera's GPIO pins.
Calling the
trigger
method sends
a precise pulse of the specified duration to the camera.
scope.camera_trigger.trigger(50, 'ms')
You can often configure the camera to either take a picture with auto-exposure or to expose for the pulse duration. Note that with the auto-exposure, the camera may still be exposing after the call returns. You may need to wait for a safe time before moving the microscope.
Autofocus
If your microscope is equipped with WDI autofocus system, you can use the
Autofocus
class to programmatically control the autofocus.
Since the connection to the WDI autofocus is separate from the microscope,
you need to modify the initial code to establish the connection using
the
WdiAutofocusProvider
class.
# from zaber_motion.microscopy import Microscope, WdiAutofocusProvider, ThirdPartyComponents
with WdiAutofocusProvider.open_tcp("169.254.64.162") as wdi_connection:
with Connection.open_serial_port("COM10") as connection:
connection.detect_devices()
scope = Microscope.find(connection, ThirdPartyComponents(wdi_connection.provider_id))
Note that the IP address (or hostname) likely differs for your setup. The easiest way to find the correct address is to go to the "Component Setup" dialog in the Zaber Launcher Microscope application.
Once you establish the connection, you can access the autofocus instance from
the
autofocus
property.
There are a couple of methods available to control the autofocus.
The
set_focus_zero
method sets
the current focus position as the focus target for the autofocus.
Typically you would call this method after manually focusing the microscope.
scope.autofocus.set_focus_zero()
The
focus_once
method starts
the autofocus process. The focus axis will move in an attempt to reach the focus target.
The method returns once the autofocus process reaches the target.
scope.autofocus.focus_once()
The
start_focus_loop
method
starts the autofocus process in a loop. The focus axis will continuously move in an attempt to reach the focus target. In the code below the loop runs until the user presses the Enter key.
scope.autofocus.start_focus_loop()
input("Press Enter to stop focus loop")
scope.autofocus.stop_focus_loop()
Alternatively, you may let your program run the loop until e.g. Zaber Launcher moves the focus axis manually and interrupts the autofocus process:
scope.autofocus.start_focus_loop()
scope.autofocus.focus_axis.wait_until_idle()
All the methods above require the autofocus to be in range and within the autofocus limits.
Should the focus process run out of range, the methods will throw MovementFailedException containing the Limit Error (FE) in the message. You can always check the autofocus status using the
get_status
method:
status = scope.autofocus.get_status()
print("Status:", status)
# AutofocusStatus(in_focus=True, in_range=True)
You can also adjust the limits to avoid the objective hitting the sample or the plate:
scope.autofocus.set_limit_max(21.3, "mm")
Lastly, if your application requires it, you can temporarily disable the autofocus laser
by calling
disable_laser
and
enable_laser
methods:
wdi_connection.disable_laser()
time.sleep(1)
wdi_connection.enable_laser()
You can explore the rest of the methods and properties in the API reference:
Manual Instantiation
Alternatively, you can instantiate the microscope manually by populating a
MicroscopeConfig
.
This is useful if you have, for example, a custom-built microscope with some duplicated components.
config = MicroscopeConfig(
illuminator=1, # X-LCA address
filter_changer=2, # X-FCR address
objective_changer=3, # X-MOR address
focus_axis=AxisAddress(4, 1), # X-LDA address, axis 1
x_axis=AxisAddress(5, 1), # X-MCC address, axis 1
y_axis=AxisAddress(5, 2), # X-MCC address, axis 2
camera_trigger=ChannelAddress(5, 1), # X-MCC address, digital output channel 1
)
scope = Microscope(connection, config)
Additionally, you can also instantiate each component individually. For example:
illuminator = Illuminator(connection.get_device(2))
filter_changer = FilterChanger(connection.get_device(3))
Resources
Our Examples repo contains multiple example programs for different microscope aspects and applications.
Support
We are curious to hear about your experience with the microscope API. If you have any struggles or suggestions, please contact us at our gitlab page.