feat/skip lfzp_in/losa_in/losa_out moves when already in position

Ports flomni's ffzp_in()/fosa_in() "skip the move (and, for the FZP,
the expensive feedback-disable + reset-enable cycle) if already there"
optimization to lamni's lfzp_in()/losa_in(). Also adds the same
skip-if-already-there check to losa_out(), which has no flomni
equivalent to mirror (fosa_out() always moves unconditionally there)
-- added as a lamni-side enhancement per discussion.

Checked every existing caller of lfzp_in()/loptics_in() before making
this change, since a skip changes behavior for callers that used to
get an unconditional reset. Found one real risk:
x_ray_eye_align.py's "Step 0: FZP centre" (the start of a fresh
alignment run) had its manual feedback disable/enable calls already
commented out, meaning it relied entirely on lfzp_in()'s old
unconditional reset to establish a correct interferometer zero.
flomni's equivalent call site doesn't have this problem because
flomni's alignment flow does its own explicit
feedback_enable_with_reset() independently beforehand -- lamni's
doesn't. Fixed by passing force_feedback_reset=True at that one call
site so it keeps its current always-reset behavior; no other lamni
caller needed this.

Item 4 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:
x01dc
2026-07-21 12:40:45 +02:00
co-authored by Claude Sonnet 5
parent b4c262d41b
commit 7863b8fd37
3 changed files with 244 additions and 10 deletions
@@ -0,0 +1,143 @@
"""Tests for lamni's "skip if already in position" optics optimization,
ported from flomni's ffzp_in()/fosa_in() (see csaxs_bec/bec_ipython_client/
plugins/LamNI/AI_docs/FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md, item 4).
Methods under test (lamni_optics_mixin.py) reference module-level `dev`/
`umv` globals (resolved from this module's own namespace, not an instance
attribute), so tests monkeypatch those module globals directly.
"""
import types
import pytest
import csaxs_bec.bec_ipython_client.plugins.LamNI.lamni_optics_mixin as optics_mixin_module
from csaxs_bec.bec_ipython_client.plugins.LamNI.lamni_optics_mixin import LamNIOpticsMixin
class FakeAxis:
def __init__(self, value, in_pos=None, out_pos=None):
self.value = value
self.readback = types.SimpleNamespace(get=lambda: self.value)
self.user_parameter = {}
if in_pos is not None:
self.user_parameter["in"] = in_pos
if out_pos is not None:
self.user_parameter["out"] = out_pos
class FakeController:
def __init__(self):
self.calls = []
def feedback_disable(self):
self.calls.append("disable")
def feedback_enable_with_reset(self):
self.calls.append("enable_with_reset")
class FakeDev:
def __init__(self, rtx_enabled=True):
self.loptx = FakeAxis(-0.5, in_pos=-0.5, out_pos=-1.0)
self.lopty = FakeAxis(3.3, in_pos=3.3, out_pos=3.0)
self.losax = FakeAxis(-1.0, in_pos=-1.0, out_pos=0.0)
self.losay = FakeAxis(-0.2, in_pos=-0.2, out_pos=1.0)
self.losaz = FakeAxis(1.0, in_pos=1.0, out_pos=-1.0)
self.rtx = types.SimpleNamespace(enabled=rtx_enabled, controller=FakeController())
def __contains__(self, name):
return hasattr(self, name)
def __getitem__(self, name):
return getattr(self, name)
@pytest.fixture
def fake_dev(monkeypatch):
dev = FakeDev()
monkeypatch.setattr(optics_mixin_module, "dev", dev)
return dev
@pytest.fixture
def umv_calls(monkeypatch):
calls = []
monkeypatch.setattr(optics_mixin_module, "umv", lambda *args: calls.append(args))
return calls
def make_optics():
return LamNIOpticsMixin()
def test_lfzp_in_skips_move_when_already_in_position(fake_dev, umv_calls, capsys):
optics = make_optics()
optics.lfzp_in()
assert umv_calls == []
assert "already at the in position" in capsys.readouterr().out
assert fake_dev.rtx.controller.calls == []
def test_lfzp_in_moves_and_resets_feedback_when_out_of_position(fake_dev, umv_calls):
fake_dev.loptx.value = -0.9 # not at "in" (-0.5)
optics = make_optics()
optics.lfzp_in()
assert umv_calls == [(fake_dev.loptx, -0.5, fake_dev.lopty, 3.3)]
assert fake_dev.rtx.controller.calls == ["disable", "enable_with_reset"]
def test_lfzp_in_force_feedback_reset_runs_cycle_even_if_already_in(fake_dev, umv_calls):
optics = make_optics()
optics.lfzp_in(force_feedback_reset=True)
assert umv_calls == [] # still no physical move needed
assert fake_dev.rtx.controller.calls == ["disable", "enable_with_reset"]
def test_losa_in_skips_move_when_already_in_position(fake_dev, umv_calls, capsys):
optics = make_optics()
optics.losa_in()
assert umv_calls == []
assert "already at the IN position" in capsys.readouterr().out
def test_losa_in_moves_when_out_of_position(fake_dev, umv_calls):
fake_dev.losaz.value = 5.0 # not at "in" (1.0)
optics = make_optics()
optics.losa_in()
assert umv_calls == [
(fake_dev.losax, -1.0, fake_dev.losay, -0.2),
(fake_dev.losaz, 1.0),
]
def test_losa_out_skips_move_when_already_out(fake_dev, umv_calls, capsys):
fake_dev.losay.value = 1.0 # matches "out"
fake_dev.losaz.value = -1.0 # matches "out"
optics = make_optics()
optics.losa_out()
assert umv_calls == []
assert "already at the OUT position" in capsys.readouterr().out
def test_losa_out_moves_when_not_out(fake_dev, umv_calls):
optics = make_optics() # currently at "in" positions, not "out"
optics.losa_out()
assert umv_calls == [
(fake_dev.losaz, -1.0),
(fake_dev.losay, 1.0),
]