fix(flomni): enforce GUI parameter ranges in tomo_parameters() CLI too
CI for csaxs_bec / test (push) Successful in 2m15s
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:
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
|
||||
Reference in new issue
Block a user