diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index 93a4e05..e495ec3 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -238,10 +238,77 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools ) self._webpage_gen.start() + self._maybe_reset_params_on_account_change() + def set_web_password(self, password: str) -> None: """Set the web password for the current BEC account.""" self._webpage_gen.set_web_password(password) + def _maybe_reset_params_on_account_change(self) -> None: + """Offer a tomo-parameter reset when the active account has changed. + + Called at BEC session start. The account for which defaults were last + applied is stored in the global var ``defaults_applied_for_account`` so + it survives client restarts. Only a genuine change of account triggers + the (interactive) reset; the same account is a silent no-op, so + restarting a client within the same experiment never disturbs tuned + parameters. Mirrors Flomni._maybe_reset_params_on_account_change() + exactly. + """ + bec = builtins.__dict__.get("bec") + try: + account = bec.active_account + except Exception as exc: + print(f"account-change check skipped: cannot read active_account: {exc}") + return + if not account: + return + + last_account = self.client.get_global_var("defaults_applied_for_account") + if account == last_account: + return + + if self.OMNYTools.yesno( + f"New account '{account}' detected (previous: '{last_account}').\n" + "Reset tomo parameters to defaults for the new experiment?", + "y", + ): + self._set_default_tomo_params() + print(f"Tomo parameters reset to defaults for account '{account}'.") + self.client.set_global_var("defaults_applied_for_account", account) + + def _set_default_tomo_params(self) -> None: + """Write all tomo scan parameters back to their default values. + + These are the same baseline values used as the getter fallbacks. Per- + sample alignment state (corrections, alignment offsets) is + deliberately not reset here, as it is overwritten by the next + alignment anyway. Mirrors Flomni._set_default_tomo_params(), adapted + to lamni's own param names -- no fovx/fovy/stitch_x/stitch_y/ + tomo_angle_range/single_point_random_shift_max; lamni has + tomo_circfov/lamni_stitch_x/y/lamni_piezo_range_x/y instead, is + always 360 degrees, and has no single-point acquisition mode. + """ + self.tomo_shellstep = 1 + self.tomo_countingtime = 0.1 + self.manual_shift_x = 0.0 + self.manual_shift_y = 0.0 + self.tomo_circfov = 0.0 + self.tomo_type = 1 + self.corridor_size = -1 + self.lamni_stitch_x = 0 + self.lamni_stitch_y = 0 + self.ptycho_reconstruct_foldername = "ptycho_reconstruct" + self.tomo_angle_stepsize = 10.0 + self.golden_max_number_of_projections = 1000.0 + self.tomo_stitch_overlap = 0.2 + self.golden_projections_at_0_deg_for_damage_estimation = 0 + self.zero_deg_reference_at_each_subtomo = False + self.golden_ratio_bunch_size = 20 + self.frames_per_trigger = 1 + self.lamni_piezo_range_x = 20 + self.lamni_piezo_range_y = 20 + # ------------------------------------------------------------------ # Special angles # ------------------------------------------------------------------ diff --git a/tests/tests_bec_ipython_client/test_lamni_account_change_reset.py b/tests/tests_bec_ipython_client/test_lamni_account_change_reset.py new file mode 100644 index 0000000..d879831 --- /dev/null +++ b/tests/tests_bec_ipython_client/test_lamni_account_change_reset.py @@ -0,0 +1,127 @@ +"""Tests for lamni's tomo-parameter reset offer on experiment-account +change, ported from flomni (see csaxs_bec/bec_ipython_client/plugins/LamNI/ +AI_docs/FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md, item 2). + +Uses a bare LamNI instance (bypassing __init__'s heavy side effects), same +pattern as test_lamni_tomo_queue.py/test_tomo_queue_reacquire.py. +""" + +import builtins +import types + +import csaxs_bec.bec_ipython_client.plugins.LamNI.lamni as lamni_module +from csaxs_bec.bec_ipython_client.plugins.LamNI.lamni import LamNI, _ProgressProxy + + +class FakeClient: + """Minimal in-memory stand-in for BEC's global-var store.""" + + def __init__(self): + self._vars = {} + + def get_global_var(self, key): + return self._vars.get(key) + + def set_global_var(self, key, value): + self._vars[key] = value + + +class FakeOMNYTools: + def __init__(self, answer=True): + self.answer = answer + self.prompts = [] + + def yesno(self, prompt, default): + self.prompts.append(prompt) + return self.answer + + +class _FakeRtx: + user_parameter = {"large_range_scan": True} + + +class _FakeDev: + rtx = _FakeRtx() + + def __contains__(self, name): + return hasattr(self, name) + + def __getitem__(self, name): + return getattr(self, name) + + +def make_lamni(answer=True): + lamni_module.dev = _FakeDev() + obj = object.__new__(LamNI) + obj.client = FakeClient() + obj._progress_proxy = _ProgressProxy(obj.client) + obj.OMNYTools = FakeOMNYTools(answer=answer) + obj.reconstructor = types.SimpleNamespace(folder_name=None) + return obj + + +def test_skips_silently_when_no_active_account(): + lamni = make_lamni() + builtins.__dict__["bec"] = type("Bec", (), {"active_account": ""})() + + lamni._maybe_reset_params_on_account_change() + + assert lamni.client.get_global_var("defaults_applied_for_account") is None + assert lamni.OMNYTools.prompts == [] + + +def test_skips_silently_on_same_account(): + lamni = make_lamni() + lamni.client.set_global_var("defaults_applied_for_account", "e12345") + builtins.__dict__["bec"] = type("Bec", (), {"active_account": "e12345"})() + lamni.tomo_shellstep = 99.0 + + lamni._maybe_reset_params_on_account_change() + + assert lamni.OMNYTools.prompts == [] + assert lamni.tomo_shellstep == 99.0 + + +def test_prompts_and_resets_on_new_account_when_confirmed(): + lamni = make_lamni(answer=True) + lamni.client.set_global_var("defaults_applied_for_account", "e11111") + lamni.tomo_shellstep = 99.0 + lamni.tomo_circfov = 123.0 + builtins.__dict__["bec"] = type("Bec", (), {"active_account": "e22222"})() + + lamni._maybe_reset_params_on_account_change() + + assert len(lamni.OMNYTools.prompts) == 1 + assert "e22222" in lamni.OMNYTools.prompts[0] + assert "e11111" in lamni.OMNYTools.prompts[0] + assert lamni.tomo_shellstep == 1 + assert lamni.tomo_circfov == 0.0 + assert lamni.client.get_global_var("defaults_applied_for_account") == "e22222" + + +def test_records_account_without_resetting_when_declined(): + lamni = make_lamni(answer=False) + lamni.client.set_global_var("defaults_applied_for_account", "e11111") + lamni.tomo_shellstep = 99.0 + builtins.__dict__["bec"] = type("Bec", (), {"active_account": "e22222"})() + + lamni._maybe_reset_params_on_account_change() + + assert lamni.tomo_shellstep == 99.0 + assert lamni.client.get_global_var("defaults_applied_for_account") == "e22222" + + +def test_set_default_tomo_params_matches_getter_fallbacks(): + """Every value _set_default_tomo_params() writes must match the property + getter's own None-fallback -- otherwise "reset to defaults" and "never + configured" would silently disagree.""" + lamni = make_lamni() + + lamni._set_default_tomo_params() + + for name in LamNI._TOMO_SCAN_PARAM_NAMES: + if name == "at_each_angle_hook": + continue # deliberately not touched by a reset, mirrors flomni + fresh = object.__new__(LamNI) + fresh.client = FakeClient() + assert getattr(lamni, name) == getattr(fresh, name), name