diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index 506cd8e5..b054677d 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -2153,17 +2153,26 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools print("OK. continue.") return - self.tomo_countingtime = self._get_val(" s", self.tomo_countingtime, float) - self.tomo_shellstep = self._get_val(" um", self.tomo_shellstep, float) + self.tomo_countingtime = self._get_val( + " s", self.tomo_countingtime, float, min_=0.001, max_=100.0 + ) + self.tomo_shellstep = self._get_val( + " um", self.tomo_shellstep, float, min_=0.025, max_=20.0 + ) self.lamni_piezo_range_x = self._get_val( - " um", self.lamni_piezo_range_x, float + " um", self.lamni_piezo_range_x, float, min_=0.0, max_=80.0 ) self.lamni_piezo_range_y = self._get_val( - " um", self.lamni_piezo_range_y, float + " um", self.lamni_piezo_range_y, float, min_=0.0, max_=80.0 ) self.lamni_stitch_x = self._get_val("", self.lamni_stitch_x, int) self.lamni_stitch_y = self._get_val("", self.lamni_stitch_y, int) - self.tomo_circfov = self._get_val(" um", self.tomo_circfov, float) + self.tomo_stitch_overlap = self._get_val( + " um", self.tomo_stitch_overlap, float, min_=0.0, max_=10.0 + ) + self.tomo_circfov = self._get_val( + " um", self.tomo_circfov, float, min_=0.0, max_=500.0 + ) self.ptycho_reconstruct_foldername = self._get_val( "Reconstruction queue", self.ptycho_reconstruct_foldername, str ) @@ -2243,8 +2252,22 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools ) @staticmethod - def _get_val(msg: str, default_value, data_type): - return data_type(input(f"{msg} ({default_value}): ") or default_value) + def _get_val(msg: str, default_value, data_type, min_=None, max_=None): + """Prompt for a value, casting to data_type. When min_/max_ are + given, re-prompts (rather than silently clamping) until the typed + value is in range, mirroring the Tomo Parameters GUI's spinbox + bounds (tomo_params.py) so tomo_parameters() enforces the same + limits instead of accepting anything of the right type. Mirrors + Flomni._get_val() exactly.""" + while True: + val = data_type(input(f"{msg} ({default_value}): ") or default_value) + if min_ is not None and val < min_: + print(f"Value must be >= {min_}. Please try again.") + continue + if max_ is not None and val > max_: + print(f"Value must be <= {max_}. Please try again.") + continue + return val def _confirm_sequence_override(self, warning: str, force: bool) -> bool: """Print *warning* and ask whether to proceed anyway. diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py index ec9eced8..caf9dcf6 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py @@ -2362,7 +2362,7 @@ SETUP_PROFILES: dict[str, dict[str, Any]] = { "has_180_mode": False, "has_single_point": False, "has_zero_deg_reference": True, - "fov_fields": [("tomo_circfov", "Circular FOV (µm, 0 = disabled)", 0.0, 200.0, 2)], + "fov_fields": [("tomo_circfov", "Circular FOV (µm, 0 = disabled)", 0.0, 500.0, 2)], "stitch_fields": [ ("lamni_stitch_x", "Stitch x", 0, 50), ("lamni_stitch_y", "Stitch y", 0, 50), @@ -2372,8 +2372,8 @@ SETUP_PROFILES: dict[str, dict[str, Any]] = { ("manual_shift_y", "Manual shift y (µm)", -1000.0, 1000.0, 3), ], "piezo_range_fields": [ - ("lamni_piezo_range_x", "Piezo range x (µm)", 0.0, 200.0, 2), - ("lamni_piezo_range_y", "Piezo range y (µm)", 0.0, 200.0, 2), + ("lamni_piezo_range_x", "Piezo range x (µm)", 0.0, 80.0, 2), + ("lamni_piezo_range_y", "Piezo range y (µm)", 0.0, 80.0, 2), ], # tomo_fovx_offset/tomo_fovy_offset: alignment values (LamNIAlignmentMixin), # not part of _TOMO_SCAN_PARAM_NAMES -- shown/editable here (mirroring diff --git a/tests/tests_bec_ipython_client/test_lamni_get_val_range.py b/tests/tests_bec_ipython_client/test_lamni_get_val_range.py new file mode 100644 index 00000000..8e5774f1 --- /dev/null +++ b/tests/tests_bec_ipython_client/test_lamni_get_val_range.py @@ -0,0 +1,46 @@ +"""Tests for LamNI._get_val()'s min_/max_ range validation. + +Mirrors test_flomni_get_val_range.py -- LamNI's tomo_parameters() CLI wizard +previously had no range validation at all (unlike the GUI's spinbox +ranges); _get_val() now optionally re-prompts until the typed value is in +range, same mechanism as Flomni's. +""" + +import builtins + +from csaxs_bec.bec_ipython_client.plugins.LamNI.lamni import LamNI + + +def _fake_inputs(monkeypatch, values): + it = iter(values) + monkeypatch.setattr(builtins, "input", lambda prompt="": next(it)) + + +def test_get_val_without_bounds_accepts_anything(monkeypatch): + _fake_inputs(monkeypatch, ["12345"]) + assert LamNI._get_val("x", 0.0, float) == 12345.0 + + +def test_get_val_accepts_in_range_value_first_try(monkeypatch): + _fake_inputs(monkeypatch, ["10"]) + assert LamNI._get_val("shellstep", 0.0, float, min_=0.025, max_=20.0) == 10.0 + + +def test_get_val_reprompts_when_above_max(monkeypatch, capsys): + _fake_inputs(monkeypatch, ["999", "50"]) + result = LamNI._get_val("circfov", 0.0, float, min_=0.0, max_=500.0) + assert result == 50.0 + assert "must be <= 500.0" in capsys.readouterr().out + + +def test_get_val_reprompts_when_below_min(monkeypatch, capsys): + _fake_inputs(monkeypatch, ["-5", "10"]) + result = LamNI._get_val("piezo range x", 20.0, float, min_=0.0, max_=80.0) + assert result == 10.0 + assert "must be >= 0.0" in capsys.readouterr().out + + +def test_get_val_boundary_values_are_accepted(monkeypatch): + _fake_inputs(monkeypatch, ["0.025", "20.0"]) + assert LamNI._get_val("a", 0.0, float, min_=0.025, max_=20.0) == 0.025 + assert LamNI._get_val("b", 0.0, float, min_=0.025, max_=20.0) == 20.0