Add a number of extra tests to ScanWorkflows
This commit is contained in:
parent
57b20782a0
commit
1bc6b9e0c4
3 changed files with 216 additions and 5 deletions
211
tests/unit_tests/test_scan_workflows.py
Normal file
211
tests/unit_tests/test_scan_workflows.py
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
"""Tests for ScanWorkflow things."""
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
import labthings_fastapi as lt
|
||||
from labthings_fastapi.testing import create_thing_without_server
|
||||
|
||||
from openflexure_microscope_server.stitching import StitchingSettings
|
||||
from openflexure_microscope_server.things.autofocus import SmartStackParams
|
||||
from openflexure_microscope_server.things.camera_stage_mapping import csm_img_to_stage
|
||||
from openflexure_microscope_server.things.scan_workflows import (
|
||||
HistoScanSettingsModel,
|
||||
HistoScanWorkflow,
|
||||
ScanWorkflow,
|
||||
)
|
||||
from openflexure_microscope_server.ui import PropertyControl
|
||||
|
||||
|
||||
def test_partial_base_classes():
|
||||
"""Create a partial class and check it raises the correct errors."""
|
||||
|
||||
class MinimalSetings(BaseModel):
|
||||
"""Some minimal settings for a workflow that doesn't work."""
|
||||
|
||||
foo: str = "bar"
|
||||
|
||||
class BadWorkflow(ScanWorkflow[MinimalSetings]):
|
||||
"""Can initialise. Other properties and methods error."""
|
||||
|
||||
display_name: str = lt.property(default="Bad Workflow", readonly=True)
|
||||
|
||||
bad_workflow = create_thing_without_server(BadWorkflow)
|
||||
|
||||
settings = MinimalSetings()
|
||||
with pytest.raises(NotImplementedError):
|
||||
bad_workflow.check_before_start(settings)
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
bad_workflow.ready
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
bad_workflow.all_settings("Fake Dir")
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
bad_workflow.pre_scan_routine(settings)
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
bad_workflow.new_scan_planner(settings, position={"x": 0, "y": 0, "z": 0})
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
bad_workflow.acquisition_routine(settings, xyz_pos=[0, 0, 0])
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
bad_workflow.settings_ui
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def histo_workflow():
|
||||
"""Return a HistoScanWorkflow thing with slots mocked."""
|
||||
return create_thing_without_server(HistoScanWorkflow, mock_all_slots=True)
|
||||
|
||||
|
||||
# Use itertools to iterate over every true/false permutation
|
||||
@pytest.mark.parametrize(
|
||||
("csm_calibrated", "skip_background", "bg_ready"),
|
||||
itertools.product([True, False], repeat=3),
|
||||
)
|
||||
def test_histo_workflow_ready(
|
||||
histo_workflow, csm_calibrated, skip_background, bg_ready, caplog
|
||||
):
|
||||
"""Check ready property and the pre-run check work for each permutation."""
|
||||
histo_workflow._csm.calibration_required = not csm_calibrated
|
||||
histo_workflow.skip_background = skip_background
|
||||
histo_workflow._background_detector.ready = bg_ready
|
||||
|
||||
if not csm_calibrated:
|
||||
# For all permutations not ready if CSM isn't calibrated.
|
||||
assert not histo_workflow.ready
|
||||
with pytest.raises(
|
||||
RuntimeError, match="Camera Stage Mapping is not calibrated."
|
||||
):
|
||||
histo_workflow.check_before_start("scan_name")
|
||||
elif not skip_background:
|
||||
# CSM is ready and background is not skipped: Always ready, but warns on check
|
||||
assert histo_workflow.ready
|
||||
with caplog.at_level(logging.WARNING):
|
||||
histo_workflow.check_before_start("scan_name")
|
||||
assert len(caplog.records) == 1
|
||||
elif not bg_ready:
|
||||
# Skipping background, but detector not ready. Raises error
|
||||
assert not histo_workflow.ready
|
||||
with pytest.raises(RuntimeError, match="Background is not set"):
|
||||
histo_workflow.check_before_start("scan_name")
|
||||
else:
|
||||
# Finally CSM calibrated, skipping background, and detector ready: This is
|
||||
# ready and should not warn.
|
||||
assert histo_workflow.ready
|
||||
with caplog.at_level(logging.WARNING):
|
||||
histo_workflow.check_before_start("scan_name")
|
||||
assert len(caplog.records) == 0
|
||||
|
||||
|
||||
def test_histo_workflow_settings_generation(histo_workflow, mocker):
|
||||
"""Check the settings models generate as expected."""
|
||||
mocker.patch.object(
|
||||
histo_workflow, "_calc_displacement_from_overlap", return_value=(123, 456)
|
||||
)
|
||||
workflow_settings, stitching_settings = histo_workflow.all_settings("/this/img_dir")
|
||||
## Check type
|
||||
assert isinstance(workflow_settings, HistoScanSettingsModel)
|
||||
assert isinstance(stitching_settings, StitchingSettings)
|
||||
assert isinstance(workflow_settings.smart_stack_params, SmartStackParams)
|
||||
|
||||
# Check stitching defaults
|
||||
assert stitching_settings.correlation_resize == 0.5
|
||||
assert stitching_settings.overlap == 0.45
|
||||
# Check some workflow defaults
|
||||
assert workflow_settings.overlap == 0.45
|
||||
assert workflow_settings.max_dist == 45000
|
||||
assert workflow_settings.skip_background
|
||||
assert workflow_settings.smart_stack_params.stack_dz == 50
|
||||
assert workflow_settings.smart_stack_params.images_to_save == 1
|
||||
assert workflow_settings.smart_stack_params.min_images_to_test == 9
|
||||
# Check values from calculating overlap are as expected (from above mock)
|
||||
assert workflow_settings.dx == 123
|
||||
assert workflow_settings.dy == 456
|
||||
# And that the input image dir is passed to stack the stack parameter for saving
|
||||
assert workflow_settings.smart_stack_params.images_dir == "/this/img_dir"
|
||||
|
||||
|
||||
# A CSM that is "normal" changing from camera maxtrix coordinates (y,x) to normal
|
||||
# (x, y) coordinates
|
||||
CSM_NORMAL = [
|
||||
[0.02, 0.25],
|
||||
[0.25, 0.01],
|
||||
]
|
||||
# A CSM that is "normal" changing from camera maxtrix coordinates (y,x) to normal
|
||||
# (x, y) coordinates, but the output y sign is flipped.
|
||||
CSM_FLIP_Y = [
|
||||
[0.02, -0.25],
|
||||
[0.25, 0.01],
|
||||
]
|
||||
# A CSM that is for a rotated compared to normal camera changing from camera maxtrix
|
||||
# thus dx (the x motor) controls y and the y motor controls x
|
||||
CSM_ROTATED = [
|
||||
[0.25, 0.02],
|
||||
[0.01, 0.25],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("csm_matrix", "overlap", "expected_steps"),
|
||||
[
|
||||
(CSM_NORMAL, 0.5, (100, 75)),
|
||||
(CSM_NORMAL, 0.25, (150, 112)),
|
||||
(CSM_FLIP_Y, 0.5, (100, -75)),
|
||||
(CSM_FLIP_Y, 0.25, (150, -112)),
|
||||
(CSM_ROTATED, 0.5, (75, 100)),
|
||||
(CSM_ROTATED, 0.25, (112, 150)),
|
||||
],
|
||||
)
|
||||
def test_histo_calculate_overlap(csm_matrix, overlap, expected_steps, histo_workflow):
|
||||
"""Check the dx and dy values are as expected for given overlap and matrix.
|
||||
|
||||
Note that the matrices on the dominant axes all have 0.25 magnitude. And the
|
||||
mock camera is set to be 800px in x and 600px in y. For 0 overlap the movement
|
||||
should be dx of 200 (due to the 0.25 factor).
|
||||
"""
|
||||
|
||||
def apply_csm(x: float, y: float, **_kwargs: float) -> dict[str, int]:
|
||||
"""Convert image coordinates to stage coordinates."""
|
||||
return csm_img_to_stage(csm_matrix, x=x, y=y)
|
||||
|
||||
histo_workflow._csm.convert_image_to_stage_coordinates.side_effect = apply_csm
|
||||
|
||||
# First check this error if CSM isn't calibrated:
|
||||
histo_workflow._csm.calibration_required = True
|
||||
histo_workflow._csm.image_resolution = None
|
||||
with pytest.raises(RuntimeError, match="CSM not set"):
|
||||
histo_workflow._calc_displacement_from_overlap(0.25)
|
||||
|
||||
histo_workflow._csm.calibration_required = False
|
||||
# the img resolution is in matrix coords, so (y, x) not (x, y)
|
||||
histo_workflow._csm.image_resolution = (600, 800)
|
||||
dx, dy = histo_workflow._calc_displacement_from_overlap(overlap)
|
||||
assert (dx, dy) == expected_steps
|
||||
|
||||
|
||||
def test_histo_workflow_settings_ui(histo_workflow):
|
||||
"""Check that the workflow specifies the expected controls."""
|
||||
ui = histo_workflow.settings_ui
|
||||
|
||||
assert len(ui) == 7
|
||||
for element in ui:
|
||||
assert isinstance(element, PropertyControl)
|
||||
|
||||
names = [el.property_name for el in ui]
|
||||
expected_names = [
|
||||
"overlap",
|
||||
"skip_background",
|
||||
"stack_images_to_save",
|
||||
"stack_min_images_to_test",
|
||||
"stack_dz",
|
||||
"autofocus_dz",
|
||||
"max_range",
|
||||
]
|
||||
assert names == expected_names
|
||||
|
|
@ -53,7 +53,7 @@ def smart_scan_thing():
|
|||
SmartScanThing,
|
||||
scans_folder=SCAN_DIR,
|
||||
default_workflow="mock-_all_workflows",
|
||||
mock_all_slots=True
|
||||
mock_all_slots=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue