feat/offer tomo-param reset on lamni experiment-account change
Ports flomni's _maybe_reset_params_on_account_change()/ _set_default_tomo_params() to lamni: at LamNI.__init__, if the active BEC account differs from the one defaults were last applied for (persisted via defaults_applied_for_account), interactively offer to reset tomo params to defaults -- preventing a new experiment silently inheriting the previous one's tuned FOV/stitch/etc. Same account is a silent no-op, so restarting a client mid-experiment never disturbs tuned parameters. _set_default_tomo_params() is a direct port adapted to lamni's own param names (tomo_circfov/lamni_stitch_x/y/lamni_piezo_range_x/y instead of fovx/fovy/stitch_x/y, no tomo_angle_range/ single_point_random_shift_max -- lamni has neither concept). Per- sample alignment state is deliberately not touched, matching flomni. Item 2 of csaxs_bec/bec_ipython_client/plugins/LamNI/AI_docs/ FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user