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.