From 5a79e4ffd3eff2547567014f11f3a1663b943c5f Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Mon, 2 Mar 2026 19:24:20 +0000 Subject: [PATCH 1/4] Allow histo scans with equal dx and dy --- .../things/scan_workflows.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index 9bc61846..5934209f 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -370,6 +370,11 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]): * 200 for 20x """ + equal_distances: bool = lt.setting(default=False) + """Make the distances in x and y equal in motor steps, rather than in overlap. + + Uses the shorter distance (usually dy) as both dx and dy""" + # The noqa statement is because scan_name is unused but is needed for equivalence # with other workflows that may want to validate the scan name. def check_before_start(self, scan_name: str) -> None: # noqa: ARG002 @@ -408,7 +413,21 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]): return self._background_detector.ready def _build_scan_settings(self, base_kwargs: dict) -> HistoScanSettingsModel: - """Construct the SettingModel for all_settings.""" + """Construct the SettingModel for all_settings. + + Adjust dx and dy to be equal if `equal_distances` is set. + """ + # Make dx and dy equal if requested + if self.equal_distances: + dx = abs(base_kwargs.get("dx", 0)) + dy = abs(base_kwargs.get("dy", 0)) + min_displacement = min(dx, dy) + base_kwargs["dx"] = min_displacement + base_kwargs["dy"] = min_displacement + self.logger.info( + f"Equal distances requested: dx=dy={min_displacement} steps" + ) + return HistoScanSettingsModel( **base_kwargs, max_dist=self.max_range, @@ -576,7 +595,10 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]): self, "max_range", label="Maximum Distance (steps)", step=1000 ), property_control_for( - self, "skip_background", label="Detect and Skip Empty Fields " + self, "skip_background", label="Detect and Skip Empty Fields" + ), + property_control_for( + self, "equal_distances", label="Set x and y distances to equal" ), ] From 7d3427ad2a12168af3004a07a423284e224b86fb Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Mon, 2 Mar 2026 19:31:04 +0000 Subject: [PATCH 2/4] Test equal overlaps --- tests/unit_tests/test_scan_workflows.py | 33 ++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/test_scan_workflows.py b/tests/unit_tests/test_scan_workflows.py index 50c27cdc..7047ff21 100644 --- a/tests/unit_tests/test_scan_workflows.py +++ b/tests/unit_tests/test_scan_workflows.py @@ -133,6 +133,36 @@ def test_histo_workflow_settings_generation(histo_workflow, mocker): assert workflow_settings.capture_params.images_dir == "/this/img_dir" +def test_histo_workflow_settings_generation_equal_overlap(histo_workflow, mocker): + """Check the settings models generate as expected.""" + mocker.patch.object( + histo_workflow, "_calc_displacement_from_overlap", return_value=(123, 456) + ) + histo_workflow.equal_distances = True + 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) + # but are then set to the lesser of the two, as dx=dy was requested + assert workflow_settings.dx == 123 + assert workflow_settings.dy == 123 + # And that the input image dir is passed to stack the stack parameter for saving + assert workflow_settings.capture_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 = [ @@ -349,7 +379,7 @@ 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 + assert len(ui) == 8 for element in ui: assert isinstance(element, PropertyControl) @@ -362,5 +392,6 @@ def test_histo_workflow_settings_ui(histo_workflow): "autofocus_dz", "max_range", "skip_background", + "equal_distances", ] assert names == expected_names From c09164a4e3bf796e3ca347011bc8cac7ac5c5072 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Mon, 2 Mar 2026 20:18:59 +0000 Subject: [PATCH 3/4] Apply suggestions from code review of branch equal-offsets Co-authored-by: Julian Stirling --- .../things/scan_workflows.py | 2 +- tests/unit_tests/test_scan_workflows.py | 37 ++++++++----------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index 5934209f..7d34ec22 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -598,7 +598,7 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]): self, "skip_background", label="Detect and Skip Empty Fields" ), property_control_for( - self, "equal_distances", label="Set x and y distances to equal" + self, "equal_distances", label="Set Equal x and y Distances" ), ] diff --git a/tests/unit_tests/test_scan_workflows.py b/tests/unit_tests/test_scan_workflows.py index 7047ff21..483b1f99 100644 --- a/tests/unit_tests/test_scan_workflows.py +++ b/tests/unit_tests/test_scan_workflows.py @@ -134,33 +134,28 @@ def test_histo_workflow_settings_generation(histo_workflow, mocker): def test_histo_workflow_settings_generation_equal_overlap(histo_workflow, mocker): - """Check the settings models generate as expected.""" + """Check that setting x and y equal behaves as expected.""" mocker.patch.object( histo_workflow, "_calc_displacement_from_overlap", return_value=(123, 456) ) - histo_workflow.equal_distances = True - 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) - # but are then set to the lesser of the two, as dx=dy was requested + # Different when False + histo_workflow.equal_distances = False + workflow_settings, _stitching_settings = histo_workflow.all_settings( + "/this/img_dir" + ) + + assert workflow_settings.dx == 123 + assert workflow_settings.dy == 456 + + # Same when set True + histo_workflow.equal_distances = True + workflow_settings, _stitching_settings = histo_workflow.all_settings( + "/this/img_dir" + ) + assert workflow_settings.dx == 123 assert workflow_settings.dy == 123 - # And that the input image dir is passed to stack the stack parameter for saving - assert workflow_settings.capture_params.images_dir == "/this/img_dir" # A CSM that is "normal" changing from camera maxtrix coordinates (y,x) to normal From 1dce732a9a8316de7668561391056eb81d5074a4 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Mon, 2 Mar 2026 20:53:16 +0000 Subject: [PATCH 4/4] Always log dx and dy steps for Histoscan --- src/openflexure_microscope_server/things/scan_workflows.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/openflexure_microscope_server/things/scan_workflows.py b/src/openflexure_microscope_server/things/scan_workflows.py index 7d34ec22..9fc132c5 100644 --- a/src/openflexure_microscope_server/things/scan_workflows.py +++ b/src/openflexure_microscope_server/things/scan_workflows.py @@ -424,9 +424,10 @@ class HistoScanWorkflow(RectGridWorkflow[HistoScanSettingsModel]): min_displacement = min(dx, dy) base_kwargs["dx"] = min_displacement base_kwargs["dy"] = min_displacement - self.logger.info( - f"Equal distances requested: dx=dy={min_displacement} steps" - ) + + self.logger.info( + f"Scanning with steps of dx={base_kwargs['dx']} and dy={base_kwargs['dy']}." + ) return HistoScanSettingsModel( **base_kwargs,