From fa6fdb1a67d75d97f851ce4eed26603440809cb3 Mon Sep 17 00:00:00 2001 From: Joe Knapper Date: Thu, 9 Oct 2025 12:22:42 +0100 Subject: [PATCH] Stack parameter coercion in create_stack_params Co-authored with Julian Stirling --- .../things/autofocus.py | 58 +++++++++++++++++-- .../things/smart_scan.py | 3 +- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 14990dc9..ececadaf 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -10,7 +10,7 @@ See repository root for licensing information. from contextlib import contextmanager import logging import time -from typing import Annotated, Mapping, Optional, Sequence, Literal, Type +from typing import Annotated, Mapping, Optional, Sequence, Literal import os from dataclasses import dataclass @@ -105,7 +105,7 @@ class StackParams(BaseModel): return self # Note MyPy doesn't support decorating properties. See MyPy Pull #16571 and issue #14461. - @computed_field # type: ignore[prop-decorator] + @computed_field # type: ignore[prop-decorator] @property def stack_z_range(self) -> int: """The range of the z stack, in steps. @@ -116,7 +116,7 @@ class StackParams(BaseModel): """ return self.stack_dz * (self.min_images_to_test - 1) - @computed_field # type: ignore[prop-decorator] + @computed_field # type: ignore[prop-decorator] @property def steps_undershoot(self) -> int: """The distance to deliberately undershoot the estimated optimal starting point.""" @@ -126,7 +126,7 @@ class StackParams(BaseModel): # requires extra +z movements and captures. return self.stack_dz * self.img_undershoot - @computed_field # type: ignore[prop-decorator] + @computed_field # type: ignore[prop-decorator] @property def max_images_to_test(self) -> int: """The maximum number of images that will be captured and tested in a stack. @@ -487,7 +487,7 @@ class AutofocusThing(lt.Thing): """ @lt.thing_action - def set_stack_params( + def create_stack_params( self, images_dir: str, autofocus_dz: int, @@ -503,6 +503,53 @@ class AutofocusThing(lt.Thing): :returns: A StackParams object with the required parameters. """ + # Coerce min_images_to_test parameter + min_images_to_test = self.stack_min_images_to_test + if min_images_to_test < MIN_TEST_IMAGE_COUNT: + logger.warning( + f"Cannot test only {min_images_to_test} image(s) as this will fail. " + "Setting min images to test to lowest possible value of" + f"{MIN_TEST_IMAGE_COUNT}." + ) + min_images_to_test = MIN_TEST_IMAGE_COUNT + elif min_images_to_test > MAX_TEST_IMAGE_COUNT: + logger.warning( + f"Testing {min_images_to_test} images will cause defocus. " + "Setting min images to test to highest possible value of " + f"{MAX_TEST_IMAGE_COUNT}." + ) + min_images_to_test = MAX_TEST_IMAGE_COUNT + elif min_images_to_test % 2 == 0: + min_images_to_test += 1 + logger.warning( + "Minimum number of images to test should be odd, setting to " + f"{min_images_to_test}." + ) + # Set the Thing property to the coerced value + self.stack_min_images_to_test = min_images_to_test + + # Coerce the images to save parameter to be positive, odd, and less than + # min_images_to_save + images_to_save = self.stack_images_to_save + if images_to_save <= 0: + logger.warning( + "At least 1 images must be saved. Setting images to save to 1." + ) + images_to_save = 1 + elif images_to_save > min_images_to_test: + logger.warning( + f"Cannot save {min_images_to_test} images as this above the minimum " + f"number to test. Setting images to save to {MAX_TEST_IMAGE_COUNT}." + ) + images_to_save = min_images_to_test + elif images_to_save % 2 == 0: + images_to_save += 1 + logger.warning( + f"Images to save should be odd, setting to {images_to_save}." + ) + # Set the Thing property to the coerced value + self.stack_images_to_save = images_to_save + return StackParams( stack_dz=self.stack_dz, images_to_save=self.stack_images_to_save, @@ -510,7 +557,6 @@ class AutofocusThing(lt.Thing): autofocus_dz=autofocus_dz, images_dir=images_dir, save_resolution=save_resolution, - logger=logger, ) @lt.thing_action diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 593237b2..a6ab415c 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -122,7 +122,6 @@ class SmartScanThing(lt.Thing): self._cam: Optional[CameraClient] = None self._csm: Optional[CSMDep] = None self._stack_params: Optional[StackParams] = None - self._ongoing_scan: Optional[scan_directories.ScanDirectory] = None self._scan_data: Optional[scan_directories.ScanData] = None self._preview_stitcher: Optional[stitching.PreviewStitcher] = None @@ -365,7 +364,7 @@ class SmartScanThing(lt.Thing): self._cam.start_streaming(main_resolution=(3280, 2464)) self._scan_data = self._collect_scan_data() self._ongoing_scan.save_scan_data(self._scan_data) - self._stack_params = self._autofocus.set_stack_params( + self._stack_params = self._autofocus.create_stack_params( images_dir=self._ongoing_scan.images_dir, autofocus_dz=self.autofocus_dz, save_resolution=self._scan_data.save_resolution,