Skip to content

Commit

Permalink
fix: napari init attrib bug, and add unit tests (#102)
Browse files Browse the repository at this point in the history
This makes sure the attribute is always set regardless of the config. It
also adds a basic unit test mechanism for the gui / business logic code
that we can start using to test all this stuff.

Tested by: Unit tests
  • Loading branch information
ianohara authored Feb 13, 2025
1 parent a921c16 commit b12634c
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 16 deletions.
45 changes: 30 additions & 15 deletions software/control/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,7 @@ def __init__(self, multiPointController):
self.z_range = self.multiPointController.z_range

self.microscope = self.multiPointController.parent
self.performance_mode = self.microscope.performance_mode
self.performance_mode = self.microscope and self.microscope.performance_mode

try:
self.model = self.microscope.segmentation_model
Expand All @@ -1431,8 +1431,7 @@ def __init__(self, multiPointController):
self.t_inf = []
self.t_over = []

if USE_NAPARI_FOR_MULTIPOINT:
self.init_napari_layers = False
self.init_napari_layers = not USE_NAPARI_FOR_MULTIPOINT

self.count = 0

Expand Down Expand Up @@ -2208,6 +2207,7 @@ def __init__(
self.deltat = 0
self.do_autofocus = False
self.do_reflection_af = False
self.focus_map = None
self.use_manual_focus_map = False
self.gen_focus_map = False
self.focus_map_storage = []
Expand Down Expand Up @@ -2410,7 +2410,7 @@ def run_acquisition(self):
self.usb_spectrometer_was_streaming = False

# set current tabs
if self.parent.performance_mode:
if self.parent and self.parent.performance_mode:
self.parent.imageDisplayTabs.setCurrentIndex(0)

elif self.parent is not None and not self.parent.live_only_mode:
Expand Down Expand Up @@ -3860,21 +3860,21 @@ def add_region(self, well_id, center_x, center_y, scan_size_mm, overlap_percent=
# Use scan_size_mm as height, width is 0.6 * height
height_mm = scan_size_mm
width_mm = scan_size_mm * 0.6

# Calculate steps for height and width separately
steps_height = math.floor(height_mm / step_size_mm)
steps_width = math.floor(width_mm / step_size_mm)

# Calculate actual dimensions
actual_scan_height_mm = (steps_height - 1) * step_size_mm + fov_size_mm
actual_scan_width_mm = (steps_width - 1) * step_size_mm + fov_size_mm

steps_height = max(1, steps_height)
steps_width = max(1, steps_width)

half_steps_height = (steps_height - 1) / 2
half_steps_width = (steps_width - 1) / 2

for i in range(steps_height):
row = []
y = center_y + (i - half_steps_height) * step_size_mm
Expand Down Expand Up @@ -3916,8 +3916,13 @@ def add_region(self, well_id, center_x, center_y, scan_size_mm, overlap_percent=
y = center_y + (i - half_steps) * step_size_mm
for j in range(steps):
x = center_x + (j - half_steps) * step_size_mm
if shape == "Square" or shape == "Rectangle" or (
shape == "Circle" and self._is_in_circle(x, y, center_x, center_y, radius_squared, fov_size_mm_half)
if (
shape == "Square"
or shape == "Rectangle"
or (
shape == "Circle"
and self._is_in_circle(x, y, center_x, center_y, radius_squared, fov_size_mm_half)
)
):
if self.validate_coordinates(x, y):
row.append((x, y))
Expand Down Expand Up @@ -4059,20 +4064,30 @@ def add_manual_region(self, shape_coords, overlap_percent):
# valid_points = grid_points[mask]

def corners(x_mm, y_mm, fov):
center_to_corner = fov/2
center_to_corner = fov / 2
return (
(x_mm + center_to_corner, y_mm + center_to_corner),
(x_mm - center_to_corner, y_mm + center_to_corner),
(x_mm - center_to_corner, y_mm - center_to_corner),
(x_mm + center_to_corner, y_mm - center_to_corner)
(x_mm + center_to_corner, y_mm - center_to_corner),
)

valid_points = []
for x_center, y_center in grid_points:
if not self.validate_coordinates(x_center, y_center):
self._log.debug(f"Manual coords: ignoring {x_center=},{y_center=} because it is outside our movement range.")
self._log.debug(
f"Manual coords: ignoring {x_center=},{y_center=} because it is outside our movement range."
)
continue
if not self._is_in_polygon(x_center, y_center, shape_coords) and not any([self._is_in_polygon(x_corner, y_corner, shape_coords) for (x_corner, y_corner) in corners(x_center, y_center, fov_size_mm)]):
self._log.debug(f"Manual coords: ignoring {x_center=},{y_center=} because no corners or center are in poly. (corners={corners(x_center, y_center, fov_size_mm)}")
if not self._is_in_polygon(x_center, y_center, shape_coords) and not any(
[
self._is_in_polygon(x_corner, y_corner, shape_coords)
for (x_corner, y_corner) in corners(x_center, y_center, fov_size_mm)
]
):
self._log.debug(
f"Manual coords: ignoring {x_center=},{y_center=} because no corners or center are in poly. (corners={corners(x_center, y_center, fov_size_mm)}"
)
continue

valid_points.append((x_center, y_center))
Expand Down
2 changes: 1 addition & 1 deletion software/setup_22.04.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ mkdir -p "$SQUID_SOFTWARE_ROOT/cache"
# install libraries
pip3 install qtpy pyserial pandas imageio crc==1.3.0 lxml numpy tifffile scipy napari pyreadline3
pip3 install opencv-python-headless opencv-contrib-python-headless
pip3 install napari[all] scikit-image dask_image ome_zarr aicsimageio basicpy pytest
pip3 install napari[all] scikit-image dask_image ome_zarr aicsimageio basicpy pytest pytest-qt gitpython

# install camera drivers
cd "$DAHENG_CAMERA_DRIVER_ROOT"
Expand Down
114 changes: 114 additions & 0 deletions software/tests/control/gui_test_stubs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import os
import pathlib

import control.core.core
import control.microcontroller
import control.lighting
import control.camera
import squid.stage.cephla
from squid.config import get_stage_config
import squid.abc
import git

import control._def


def get_test_microcontroller() -> control.microcontroller.Microcontroller:
return control.microcontroller.Microcontroller(control.microcontroller.SimSerial(), True)


def get_test_camera():
return control.camera.Camera_Simulation()


def get_test_live_controller(
camera, microcontroller, config_manager, illumination_controller
) -> control.core.core.LiveController:
controller = control.core.core.LiveController(camera, microcontroller, config_manager, illumination_controller)
controller.set_microscope_mode(config_manager.configurations[0])

return controller


def get_test_stage(microcontroller):
return squid.stage.cephla.CephlaStage(microcontroller=microcontroller, stage_config=get_stage_config())


def get_repo_root() -> pathlib.Path:
git_repo = git.Repo(os.getcwd(), search_parent_directories=True)
git_root = git_repo.git.rev_parse("--show-toplevel")

return pathlib.Path(git_root).absolute()


def get_test_configuration_manager_path() -> pathlib.Path:
return get_repo_root() / "channel_configurations.xml"


def get_test_configuration_manager() -> control.core.core.ConfigurationManager:
return control.core.core.ConfigurationManager(get_test_configuration_manager_path())


def get_test_illumination_controller(
microcontroller: control.microcontroller.Microcontroller,
) -> control.lighting.IlluminationController:
return control.lighting.IlluminationController(
microcontroller=microcontroller,
intensity_control_mode=control.lighting.IntensityControlMode.Software,
shutter_control_mode=control.lighting.ShutterControlMode.Software,
)


def get_test_autofocus_controller(
camera,
stage: squid.abc.AbstractStage,
live_controller: control.core.core.LiveController,
microcontroller: control.microcontroller.Microcontroller,
):
return control.core.core.AutoFocusController(
camera=camera, stage=stage, liveController=live_controller, microcontroller=microcontroller
)


def get_test_scan_coordinates(
objective_store: control.core.core.ObjectiveStore,
navigation_viewer: control.core.core.NavigationViewer,
stage: squid.abc.AbstractStage,
):
return control.core.core.ScanCoordinates(objective_store, navigation_viewer, stage)


def get_test_objective_store():
return control.core.core.ObjectiveStore(
objectives_dict=control._def.OBJECTIVES, default_objective=control._def.DEFAULT_OBJECTIVE
)


def get_test_navigation_viewer(objective_store: control.core.core.ObjectiveStore):
return control.core.core.NavigationViewer(objective_store)


def get_test_multi_point_controller() -> control.core.core.MultiPointController:
microcontroller = get_test_microcontroller()
camera = get_test_camera()
stage = get_test_stage(microcontroller)
config_manager = get_test_configuration_manager()
live_controller = get_test_live_controller(
camera, microcontroller, config_manager, get_test_illumination_controller(microcontroller)
)
objective_store = get_test_objective_store()

multi_point_controller = control.core.core.MultiPointController(
camera=camera,
stage=stage,
microcontroller=microcontroller,
liveController=live_controller,
autofocusController=get_test_autofocus_controller(camera, stage, live_controller, microcontroller),
configurationManager=get_test_configuration_manager(),
scanCoordinates=get_test_scan_coordinates(objective_store, get_test_navigation_viewer(objective_store), stage),
)

multi_point_controller.set_base_path("/tmp/")
multi_point_controller.start_new_experiment("unit test experiment")

return multi_point_controller
35 changes: 35 additions & 0 deletions software/tests/control/test_MultiPointWorker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import control.core.core
from control._def import *

import tests.control.gui_test_stubs as gts


# Make sure we can create a multi point controller and worker with out default config
def test_multi_point_worker_with_default_config(qtbot):
multi_point_controller = gts.get_test_multi_point_controller()
multi_point_controller.run_acquisition()
multi_point_controller.request_abort_aquisition()


def test_multi_point_worker_init_bugs(qtbot):
# We don't always init all our fields in __init__, which leads to some paths
# for some configs whereby we use instance attributes before initialization. This
# test documents cases of those by writing tests that hit them (then subsequent PR that
# fix them to make this test pass).

# The init_napari_layers field is dependent on USE_NAPARI_FOR_MULTIPOINT,
# so make sure that it is initialized regardless of that config value.

USE_NAPARI_FOR_MULTIPOINT = False
multi_point_controller_for_false = gts.get_test_multi_point_controller()
multi_point_controller_for_false.run_acquisition()
multi_point_controller_for_false.request_abort_aquisition()
# This will throw if the attribute doesn't exist
napari_layer_for_false = multi_point_controller_for_false.multiPointWorker.init_napari_layers

USE_NAPARI_FOR_MULTIPOINT = True
multi_point_controller_for_true = gts.get_test_multi_point_controller()
multi_point_controller_for_true.run_acquisition()
multi_point_controller_for_true.request_abort_aquisition()
# This will throw if the attribute doesn't exist
napari_layer_for_true = multi_point_controller_for_true.multiPointWorker.init_napari_layers

0 comments on commit b12634c

Please sign in to comment.