fix(flomni): enforce GUI parameter ranges in tomo_parameters() CLI too
CI for csaxs_bec / test (push) Successful in 2m15s

_get_val() (the wizard's input prompt) previously did no range validation
at all -- only the Tomo Parameters GUI's spinboxes enforced limits, so the
CLI silently accepted any value of the right type (e.g. manual_shift_y=500
despite the GUI's ±50 µm range). _get_val() now takes optional min_/max_
and re-prompts until the value is in range; wired up for every
tomo_parameters() field that already has a GUI-defined range (tomo_countingtime,
tomo_shellstep, fermat_asymmetry, fovx, fovy, stitch_x/y, manual_shift_y,
frames_per_trigger, single_point_random_shift_max, total projections,
golden_ratio_bunch_size, golden_max_number_of_projections, projections/sub-tomo).
fermat_asymmetry reuses FlomniFermatScan's own MIN/MAX constants rather than
duplicating them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
This commit is contained in:
x01dcandClaude Sonnet 5 committed 2026-09-19 20:21:20 +02:00
1 parent 583435ddd6
commit 158dc800f0
2 files changed
+109 -13

No files matched your search

@@ -3818,13 +3818,23 @@ class Flomni(
if self.OMNYTools.yesno("Are these parameters correctly set for your scan?", "y"):
print("... excellent!")
else:
self.tomo_countingtime = self._get_val("<ctime> s", self.tomo_countingtime, float)
self.tomo_shellstep = self._get_val("<step size> um", self.tomo_shellstep, float)
self.fermat_asymmetry = self._get_val(
"<fermat asymmetry x/y, 1=symmetric>", self.fermat_asymmetry, float
from csaxs_bec.scans.flomni_fermat_scan import FlomniFermatScan
self.tomo_countingtime = self._get_val(
"<ctime> s", self.tomo_countingtime, float, min_=0.001, max_=100.0
)
self.fovx = self._get_val("<FOV X (max 220)> um", self.fovx, float)
self.fovy = self._get_val("<FOV Y (max 100)> um", self.fovy, float)
self.tomo_shellstep = self._get_val(
"<step size> um", self.tomo_shellstep, float, min_=0.001, max_=1000.0
)
self.fermat_asymmetry = self._get_val(
"<fermat asymmetry x/y, 1=symmetric>",
self.fermat_asymmetry,
float,
min_=FlomniFermatScan.MIN_FERMAT_ASYMMETRY,
max_=FlomniFermatScan.MAX_FERMAT_ASYMMETRY,
)
self.fovx = self._get_val("<FOV X (max 220)> um", self.fovx, float, min_=0.1, max_=220.0)
self.fovy = self._get_val("<FOV Y (max 100)> um", self.fovy, float, min_=0.1, max_=100.0)
if self.single_point_instead_of_fermat_scan:
print(
"Stitching is disabled while single point instead of fermat scan is"
@@ -3833,14 +3843,16 @@ class Flomni(
self.stitch_x = 0
self.stitch_y = 0
else:
self.stitch_x = self._get_val("<stitch X>", self.stitch_x, int)
self.stitch_y = self._get_val("<stitch Y>", self.stitch_y, int)
self.stitch_x = self._get_val("<stitch X>", self.stitch_x, int, min_=0, max_=50)
self.stitch_y = self._get_val("<stitch Y>", self.stitch_y, int, min_=0, max_=50)
self.ptycho_reconstruct_foldername = self._get_val(
"Reconstruction queue ", self.ptycho_reconstruct_foldername, str
)
self.manual_shift_y = self._get_val("<manual_shift_y> um", self.manual_shift_y, float)
self.manual_shift_y = self._get_val(
"<manual_shift_y> um", self.manual_shift_y, float, min_=-50.0, max_=50.0
)
self.frames_per_trigger = self._get_val(
"Frames per trigger (burst)", self.frames_per_trigger, int
"Frames per trigger (burst)", self.frames_per_trigger, int, min_=1, max_=100
)
self.single_point_instead_of_fermat_scan = bool(
self._get_val(
@@ -3863,6 +3875,8 @@ class Flomni(
"<single point random shift max> um (0 = off)",
self.single_point_random_shift_max,
float,
min_=0.0,
max_=10.0,
)
print("Tomography type:")
@@ -3884,7 +3898,7 @@ class Flomni(
# never reflect a prior adjustment.
_, _, current_total = self._tomo_type1_actual_grid()
tomo_numberofprojections = self._get_val(
"Total number of projections", current_total, int
"Total number of projections", current_total, int, min_=8, max_=10000
)
# N/step (and therefore total projections) are always
# computed against a fixed 180 degrees, independent of
@@ -3933,11 +3947,15 @@ class Flomni(
"Number of projections sorted per bunch (default 20)",
self.golden_ratio_bunch_size,
int,
min_=1,
max_=10000,
)
self.golden_max_number_of_projections = self._get_val(
"Stop after number of projections (zero for endless)",
self.golden_max_number_of_projections,
int,
min_=0,
max_=1_000_000,
)
self.golden_projections_at_0_deg_for_damage_estimation = self._get_val(
"Repeat projections at 0 deg every second subtomo 1/0 ?",
@@ -3950,12 +3968,16 @@ class Flomni(
"Number of projections per sub-tomogram",
int(180 / self.tomo_angle_stepsize),
int,
min_=1,
max_=10000,
)
self.tomo_angle_stepsize = 180 / numprj
self.golden_max_number_of_projections = self._get_val(
"Stop after number of projections (zero for endless)",
self.golden_max_number_of_projections,
int,
min_=0,
max_=1_000_000,
)
self.golden_projections_at_0_deg_for_damage_estimation = self._get_val(
"Repeat projections at 0 deg every second subtomo",
@@ -3964,8 +3986,21 @@ class Flomni(
)
@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."""
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
# Ordered set of all global-var-backed tomo scan parameters: exactly the
# settings shown by tomo_parameters(), plus manual_shift_y/
@@ -0,0 +1,61 @@
"""Tests for Flomni._get_val()'s min_/max_ range validation.
tomo_parameters() (the CLI wizard) used to accept any value of the right
type with no range check at all, even though the Tomo Parameters GUI
enforces spinbox ranges for the same fields (tomo_params.py) -- this let the
CLI silently set e.g. manual_shift_y or fovx far outside their GUI-intended
bounds. _get_val() now optionally re-prompts until the typed value is in
range, so tomo_parameters() enforces the same limits as the GUI.
"""
import builtins
import pytest
from csaxs_bec.bec_ipython_client.plugins.flomni.flomni import Flomni
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 Flomni._get_val("x", 0.0, float) == 12345.0
def test_get_val_accepts_in_range_value_first_try(monkeypatch):
_fake_inputs(monkeypatch, ["25"])
assert Flomni._get_val("x", 0.0, float, min_=0.1, max_=220.0) == 25.0
def test_get_val_reprompts_when_above_max(monkeypatch, capsys):
_fake_inputs(monkeypatch, ["500", "50"])
result = Flomni._get_val("FOV X", 0.0, float, min_=0.1, max_=220.0)
assert result == 50.0
assert "must be <= 220.0" in capsys.readouterr().out
def test_get_val_reprompts_when_below_min(monkeypatch, capsys):
_fake_inputs(monkeypatch, ["-999", "-30"])
result = Flomni._get_val("manual_shift_y", 0.0, float, min_=-50.0, max_=50.0)
assert result == -30.0
assert "must be >= -50.0" in capsys.readouterr().out
def test_get_val_reprompts_until_valid_across_multiple_bad_attempts(monkeypatch):
_fake_inputs(monkeypatch, ["-5", "999", "30"])
assert Flomni._get_val("stitch X", 0, int, min_=0, max_=50) == 30
def test_get_val_empty_input_uses_default_and_still_checked(monkeypatch):
# empty input -> falls back to default_value, which is already in range
_fake_inputs(monkeypatch, [""])
assert Flomni._get_val("x", 25, int, min_=0, max_=50) == 25
def test_get_val_boundary_values_are_accepted(monkeypatch):
_fake_inputs(monkeypatch, ["0.1", "220.0"])
assert Flomni._get_val("a", 0.0, float, min_=0.1, max_=220.0) == 0.1
assert Flomni._get_val("b", 0.0, float, min_=0.1, max_=220.0) == 220.0