Create an ImSwitch Extension

Create an Extension for ImSwitch: A Tutorial

In the openUC2 ImSwitch fork we created a new feature. => You don’t need to understand the whole code base anymore, only the 3 main elements to interact with imswitch. :innocent:
In this tutorial, we will create an adapter for the Arkitekt Experiment Manager to be used within ImSwitch. We will expose API-generated functions to a service and implement the Controller, Manager, and Widget for it.

To simplify the process, we will use a cookiecutter template that provides a skeleton for the plugin. Follow these steps to get started.

1. Setting Up Your Environment

  1. Open a terminal and navigate to the directory where you want to develop the plugin, e.g., ~/Downloads:

    cd ~/Downloads
    
  2. Clone the cookiecutter template from the GitHub repository:

    git clone https://github.com/openUC2/cookiecutter-imswitch
    
  3. Activate your Python environment or use your system Python:

    pip install cookiecutter
    
  4. Use the cookiecutter to generate the plugin skeleton and follow the prompts:

    cookiecutter https://github.com/openUC2/cookiecutter-imswitch/
    

    Example responses for the prompts:

    full_name (ImSwitch Developer): Benedict
    email (yourname@example.com): bene.d@gmx.de
    github_username_or_organization (githubuser): beniroquai
    plugin_name (imswitch-foobar): imswitch-arkitekt
    Select github_repository_url: 2
    module_name (imswitch_arkitekt): [press Enter]
    display_name (FooBar Camera): ImSwitch Arkitekt
    short_description (A simple plugin to use a camera from XYZ within ImSwitch): This module will connect ImSwitch to Arkitekt
    include_controller (y): y
    include_manager_plugin (y): y
    include_widget_plugin (y): y
    include_info_plugin (y): y
    use_git_tags_for_versioning (n): n
    install_precommit (n): n
    Select license: 2
    

This process generates a project structure with files such as imswitch_arkitekt_manager.py, imswitch_arkitekt_widget.py, and imswitch_arkitekt_controller.py.

2. Setting Up Dependencies

Add any dependencies your plugin requires in the setup.cfg file. For our example, we need the arkitekt package:

[options]
packages = find:
install_requires =
    arkitekt

3. Registering the Plugin

In setup.cfg, register the entry points for the plugin:

[options.entry_points]
imswitch.manifest =
    imswitch-arkitekt = imswitch_arkitekt:imswitch.yaml
imswitch.implugins =
    imswitch_arkitekt_controller = imswitch_arkitekt:imswitch_arkitekt_controller
    imswitch_arkitekt_widget = imswitch_arkitekt:imswitch_arkitekt_widget

These entry points will be loaded inside ImSwitch.

4. Implementing the Widget

The widget needs to be derived from one of the base widgets. Here’s an example implementation in imswitch_arkitekt_widget.py:

from imswitch.imcontrol.view.widgets.basewidgets import Widget
from imswitch.imcommon.model import initLogger
from arkitekt.qt.magic_bar import MagicBar
from arkitekt.builders import publicscheduleqt
from qtpy import QtCore, QtWidgets

class imswitch_arkitekt_widget(Widget):
    """Linked to the Arkitekt Controller."""

    sigLoadParamsFromHDF5 = QtCore.Signal()
    sigPickSetup = QtCore.Signal()
    sigClosing = QtCore.Signal()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Initialize the logger
        self.__logger = initLogger(self)
        self.__logger.debug("Initializing")

        # Create the main widget layout
        self.cwidget = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout()
        self.cwidget.setLayout(layout)

        # Initialize the Magic Bar (Arkitekt) to have the menu available
        identifier = "github.io.jhnnsrs.mikro-imswitch"
        version = "latest"
        logo = "https://avatars.githubusercontent.com/u/127734217?s=200&v=4"
        settings = QtCore.QSettings("imswitch", f"{identifier}:{version}")
        self.global_app = publicscheduleqt(
            identifier, version, parent=self.cwidget, logo=logo, settings=settings
        )
        self.magic_bar = MagicBar(self.global_app, dark_mode=True)

        self.loadParamsButton = QtWidgets.QPushButton("Load Parameters")
        self.loadParamsButton.clicked.connect(self.sigLoadParamsFromHDF5.emit)

        self.pickSetupButton = QtWidgets.QPushButton("Pick Setup")
        self.pickSetupButton.clicked.connect(self.sigPickSetup.emit)

        self.fileSelectButton = QtWidgets.QPushButton("Select File")
        self.fileSelectButton.clicked.connect(self.sigPickSetup.emit)

        layout.addWidget(self.magic_bar)
        layout.addWidget(self.loadParamsButton)
        layout.addWidget(self.pickSetupButton)
        layout.addWidget(self.fileSelectButton)
        layout.addStretch()

        self.setLayout(layout)

This may look as the following:

5. Implementing the Controller

The controller connects the widget with the model and handles interactions. Here’s an example implementation in imswitch_arkitekt_controller.py:

from imswitch.imcontrol.model.managers.detectors.DetectorManager import DetectorManager
from imswitch.imcontrol.controller.basecontrollers import ImConWidgetController
from mikro.api.schema import from_xarray
from arkitekt import App
from rekuest.qt.builders import qtinloopactifier, qtwithfutureactifier
from mikro.api.schema import RepresentationFragment, OmeroRepresentationInput, PhysicalSizeInput, ChannelInput
from koil.qt import QtFuture
from qtpy import QtCore

class imswitch_arkitekt_controller(ImConWidgetController):
    """Linked to Arkitekt Controller."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.__logger = initLogger(self)
        self.__logger.debug("Initializing imswitch arkitekt controller")

        self.__app = self._widget.global_app
        if not imswitch.IS_HEADLESS:
            self._widget.magic_bar.app_up.connect(self.on_app_up)
            self._widget.magic_bar.app_down.connect(self.on_app_down)

        self.qt_actifier = qtinloopactifier
        self.qt_future_activier = qtwithfutureactifier

        self.__app.rekuest.register(builder=self.qt_actifier)(self.call_run_scan)
        self.__app.rekuest.register(builder=self.qt_actifier)(self.set_laser_value)
        self.__app.rekuest.register()(self.snap_image)
        self.__app.rekuest.register(builder=self.qt_future_activier)(self.on_do_stuff_async)

    def on_do_stuff_async(self, qtfuture: QtFuture, hello: str) -> str:
        print(hello)
        qtfuture.resolve("42")
        return None

    def call_run_scan(self):
        controlMainController = self._MikroMainController__moduleMainControllers["imcontrol"]
        scanController = controlMainController.controllers["Scan"]
        scanController.runScan()

    def snap_image(self, xdims: int = 1000, ydims: int = 1000) -> RepresentationFragment:
        detectorName = self._master.detectorsManager.getAllDeviceNames()[0]
        detectorManager = self._controller._master.detectorsManager[detectorName]
        latestimg = detectorManager.getLatestFrame().copy()
        active_channels = [detectorName]
        if len(latestimg.shape) == 2:
            latestimg = latestimg[:, :, np.newaxis]
            active_channels = ["channel1"]

        metadata = OmeroRepresentationInput(
            physicalSize=PhysicalSizeInput(x=2, y=2),
            channels=[ChannelInput(name=channel) for channel in active_channels]
        )

        return from_xarray(
            xr.DataArray(latestimg, dims=["x", "y", "c"]),
            name="Randomly generated image",
            omero=metadata
        )

    def set_laser_value(self, lasername: str, value: int):
        allIlluNames = self._master.lasersManager.getAllDeviceNames()
        laserSource = self._master.lasersManager[allIlluNames[0]]
        laserSource.setEnabled(True)
        laserSource.setLaserValue(lasername, value)

    def on_app_up(self):
        print("App up")

    def on_app_down(self):
        print("App down")

    def closeEvent(self):
        self.__logger.debug("Shutting down")

6. Debugging Tips

  • Start ImSwitch each time you make changes to test the extension.
  • Keep the number of availableWidgets low to reduce startup times.

7. Installation

To install the plugin, navigate to the plugin directory and run:

pip install -e .

You should see:

Installing collected packages: imswitch-arkitekt
Successfully installed imswitch-arkitekt-0.0.1

8. Setting Up and Testing the Plugin

Add the plugin to the availableModules tag in your ImSwitch configuration:

"availableWidgets": [
  "Settings",
  "View",
  "Recording",
  "Image",
  "Laser",
  "Positioner",
  "Autofocus",
  "MCT",
  "ROIScan",
  "HistoScan",
  "Hypha",
 

 "imswitch_arkitekt"
]

9. Publishing

Feel free to update the README.md and then add your changes and publish it on github. Then - e.g. in case of this example plugin - you can install it using:

pip install https://github.com/openUC2/imswitch-arkitekt/archive/refs/heads/main.zip

in the same environment as imswitch. Then you can use the plugin.

10. Troubleshooting

  • Add any missing packages to setup.cfg.
  • Ensure the correct names are used in the configuration to avoid errors like:
--- Logging error ---
Traceback (most recent call last):
  ...
AttributeError: module 'imswitch.imcontrol.view.widgets' has no attribute 'arkitekt_controllerWidget'

With this guide, you should be able to create and set up an ImSwitch plugin successfully. Happy coding!