feat/tomo queue reacquire from an earlier projection
Add tomo_queue_reacquire(job_index, projection_number) to the shared TomoQueueMixin: reopens any queue job (even an already-"done" one) at an earlier projection than its own recorded progress, and resets every job queued after it -- tomo and command jobs alike -- to "pending", so a following tomo_queue_resume() (a new alias for tomo_queue_execute(), added for naming symmetry with tomo_scan_resume()) re-runs everything from there forward in order. Reuses the existing tomo_scan_resume() machinery by writing the requested resume point into the shared progress global var, rather than adding new parameters to tomo_scan()/tomo_queue_execute(). Guards against clobbering another job's progress if one is already "incomplete"/"running" elsewhere in the queue. A flat projection number is used as the resume-point unit for all three tomo_types, including type 1 (8 equally-spaced sub-tomograms), via a new per-setup _resolve_type1_projection() that maps it to (subtomo_start, start_angle). flomni's version reuses its existing _subtomo_angle_plan(); lamni's inline angle math in sub_tomo_scan() was extracted into an analogous _subtomo_angle_plan() static method first (behavior-preserving -- verified against the existing angle-math test suite) so both setups share the same pattern. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
"""Tests for TomoQueueMixin.tomo_queue_reacquire()/tomo_queue_resume() and
|
||||
the per-setup _resolve_type1_projection() resolvers it depends on (see
|
||||
csaxs_bec/bec_ipython_client/plugins/OMNY_shared/tomo_queue_mixin.py).
|
||||
|
||||
Round-trip tests build a bare LamNI/Flomni instance (bypassing __init__'s
|
||||
heavy side effects, same pattern as test_lamni_tomo_queue.py) and confirm
|
||||
_resolve_type1_projection() agrees with what sub_tomo_scan() itself would
|
||||
actually produce at a given flat projection number -- not a re-derivation
|
||||
of the same formula, but a comparison against the real scan-execution path.
|
||||
"""
|
||||
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
import csaxs_bec.bec_ipython_client.plugins.LamNI.lamni as lamni_module
|
||||
from csaxs_bec.bec_ipython_client.plugins.flomni.flomni import Flomni
|
||||
from csaxs_bec.bec_ipython_client.plugins.flomni.flomni import _ProgressProxy as FlomniProgressProxy
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI.lamni import LamNI, _ProgressProxy
|
||||
from csaxs_bec.bec_ipython_client.plugins.OMNY_shared.tomo_queue_mixin import TomoQueueError
|
||||
|
||||
STEPSIZES = [10.0, 7.0, 25.0, 12.5]
|
||||
|
||||
|
||||
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 yesno(self, *args, **kwargs):
|
||||
return True
|
||||
|
||||
|
||||
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():
|
||||
lamni_module.dev = _FakeDev()
|
||||
obj = object.__new__(LamNI)
|
||||
obj.client = FakeClient()
|
||||
obj.OMNYTools = FakeOMNYTools()
|
||||
obj.reconstructor = types.SimpleNamespace(folder_name=None)
|
||||
obj.tomo_id = -1
|
||||
obj._progress_proxy = _ProgressProxy(obj.client)
|
||||
obj._init_tomo_queue()
|
||||
return obj
|
||||
|
||||
|
||||
def make_flomni():
|
||||
obj = object.__new__(Flomni)
|
||||
obj.client = FakeClient()
|
||||
obj.OMNYTools = FakeOMNYTools()
|
||||
obj._progress_proxy = FlomniProgressProxy(obj.client)
|
||||
obj._init_tomo_queue()
|
||||
return obj
|
||||
|
||||
|
||||
# ── _resolve_type1_projection round-trip: lamni ─────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stepsize", STEPSIZES)
|
||||
def test_lamni_resolve_type1_projection_matches_sub_tomo_scan(stepsize):
|
||||
lamni = make_lamni()
|
||||
lamni.tomo_angle_stepsize = stepsize
|
||||
lamni._write_subtomo_to_scilog = lambda subtomo_number: None
|
||||
recorded = {}
|
||||
lamni._tomo_scan_at_angle = lambda angle, subtomo_number: recorded.__setitem__(
|
||||
lamni.progress["projection"], (subtomo_number, angle)
|
||||
)
|
||||
for subtomo in range(1, 9):
|
||||
lamni.sub_tomo_scan(subtomo)
|
||||
|
||||
assert len(recorded) > 0
|
||||
for projection_number, (expected_subtomo, expected_angle) in recorded.items():
|
||||
subtomo_start, start_angle = lamni._resolve_type1_projection(projection_number)
|
||||
assert subtomo_start == expected_subtomo
|
||||
assert start_angle == pytest.approx(expected_angle)
|
||||
|
||||
|
||||
def test_lamni_resolve_type1_projection_out_of_range():
|
||||
lamni = make_lamni()
|
||||
lamni.tomo_angle_stepsize = 10.0
|
||||
with pytest.raises(ValueError):
|
||||
lamni._resolve_type1_projection(-1)
|
||||
with pytest.raises(ValueError):
|
||||
lamni._resolve_type1_projection(10_000)
|
||||
|
||||
|
||||
# ── _resolve_type1_projection round-trip: flomni ────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stepsize", STEPSIZES)
|
||||
@pytest.mark.parametrize("angle_range", [180, 360])
|
||||
def test_flomni_resolve_type1_projection_matches_sub_tomo_scan(stepsize, angle_range):
|
||||
flomni = make_flomni()
|
||||
flomni.tomo_angle_range = angle_range
|
||||
flomni.tomo_angle_stepsize = stepsize
|
||||
recorded = {}
|
||||
flomni._tomo_scan_at_angle = lambda angle, subtomo_number: recorded.__setitem__(
|
||||
flomni.progress["projection"], (subtomo_number, angle)
|
||||
)
|
||||
for subtomo in range(1, 9):
|
||||
flomni.sub_tomo_scan(subtomo)
|
||||
|
||||
assert len(recorded) > 0
|
||||
for projection_number, (expected_subtomo, expected_angle) in recorded.items():
|
||||
subtomo_start, start_angle = flomni._resolve_type1_projection(projection_number)
|
||||
assert subtomo_start == expected_subtomo
|
||||
assert start_angle == pytest.approx(expected_angle)
|
||||
|
||||
|
||||
def test_flomni_resolve_type1_projection_out_of_range():
|
||||
flomni = make_flomni()
|
||||
flomni.tomo_angle_range = 180
|
||||
flomni.tomo_angle_stepsize = 10.0
|
||||
with pytest.raises(ValueError):
|
||||
flomni._resolve_type1_projection(-1)
|
||||
with pytest.raises(ValueError):
|
||||
flomni._resolve_type1_projection(10_000)
|
||||
|
||||
|
||||
# ── tomo_queue_reacquire() / tomo_queue_resume() mechanics (lamni) ──────────
|
||||
|
||||
|
||||
def test_tomo_queue_resume_is_an_alias_for_execute():
|
||||
lamni = make_lamni()
|
||||
calls = []
|
||||
lamni.tomo_queue_execute = lambda start_index=0: calls.append(start_index)
|
||||
|
||||
lamni.tomo_queue_resume()
|
||||
lamni.tomo_queue_resume(start_index=2)
|
||||
|
||||
assert calls == [0, 2]
|
||||
|
||||
|
||||
def test_tomo_queue_reacquire_type1_sets_progress_and_status():
|
||||
lamni = make_lamni()
|
||||
lamni.tomo_type = 1
|
||||
lamni.tomo_angle_stepsize = 10.0
|
||||
index = lamni.tomo_queue_add(label="job0")
|
||||
lamni._tomo_queue_proxy.update(index, status="done")
|
||||
|
||||
lamni.tomo_queue_reacquire(index, projection_number=5)
|
||||
|
||||
job = lamni._tomo_queue_proxy.as_list()[index]
|
||||
assert job["status"] == "incomplete"
|
||||
subtomo_start, start_angle = lamni._resolve_type1_projection(5)
|
||||
assert lamni.progress["subtomo"] == subtomo_start
|
||||
assert lamni.progress["angle"] == pytest.approx(start_angle)
|
||||
assert lamni.progress["tomo_start_time"] is not None
|
||||
assert lamni.progress["accumulated_idle_time"] == 0.0
|
||||
|
||||
|
||||
def test_tomo_queue_reacquire_type23_sets_projection_directly():
|
||||
lamni = make_lamni()
|
||||
lamni.tomo_type = 2
|
||||
lamni.golden_max_number_of_projections = 0
|
||||
index = lamni.tomo_queue_add(label="job0")
|
||||
lamni._tomo_queue_proxy.update(index, status="done")
|
||||
|
||||
lamni.tomo_queue_reacquire(index, projection_number=17)
|
||||
|
||||
assert lamni.progress["projection"] == 17
|
||||
job = lamni._tomo_queue_proxy.as_list()[index]
|
||||
assert job["status"] == "incomplete"
|
||||
|
||||
|
||||
def test_tomo_queue_reacquire_resets_later_jobs_to_pending_tomo_and_command():
|
||||
lamni = make_lamni()
|
||||
lamni.tomo_type = 1
|
||||
lamni.tomo_angle_stepsize = 10.0
|
||||
|
||||
i0 = lamni.tomo_queue_add(label="job0")
|
||||
i1 = lamni.tomo_queue_add(label="job1")
|
||||
lamni.tomo_queue_add_command({"action": "move", "kwargs": {"positions": {"mokev": 6.2}}})
|
||||
i3 = lamni.tomo_queue_add(label="job3")
|
||||
|
||||
for i in (i0, i1, i3):
|
||||
lamni._tomo_queue_proxy.update(i, status="done")
|
||||
|
||||
lamni.tomo_queue_reacquire(i0, projection_number=0)
|
||||
|
||||
jobs = lamni._tomo_queue_proxy.as_list()
|
||||
assert jobs[i0]["status"] == "incomplete"
|
||||
for later in jobs[i0 + 1 :]:
|
||||
assert later["status"] == "pending"
|
||||
|
||||
|
||||
def test_tomo_queue_reacquire_rejects_out_of_range_job_index():
|
||||
lamni = make_lamni()
|
||||
with pytest.raises(TomoQueueError):
|
||||
lamni.tomo_queue_reacquire(0, projection_number=0)
|
||||
|
||||
|
||||
def test_tomo_queue_reacquire_rejects_command_job():
|
||||
lamni = make_lamni()
|
||||
lamni.tomo_queue_add_command({"action": "move", "kwargs": {"positions": {"mokev": 6.2}}})
|
||||
with pytest.raises(TomoQueueError):
|
||||
lamni.tomo_queue_reacquire(0, projection_number=0)
|
||||
|
||||
|
||||
def test_tomo_queue_reacquire_rejects_when_another_job_is_incomplete():
|
||||
lamni = make_lamni()
|
||||
lamni.tomo_type = 1
|
||||
lamni.tomo_angle_stepsize = 10.0
|
||||
i0 = lamni.tomo_queue_add(label="job0")
|
||||
i1 = lamni.tomo_queue_add(label="job1")
|
||||
lamni._tomo_queue_proxy.update(i1, status="incomplete")
|
||||
|
||||
with pytest.raises(TomoQueueError):
|
||||
lamni.tomo_queue_reacquire(i0, projection_number=0)
|
||||
|
||||
|
||||
def test_tomo_queue_reacquire_allows_targeting_the_incomplete_job_itself():
|
||||
lamni = make_lamni()
|
||||
lamni.tomo_type = 1
|
||||
lamni.tomo_angle_stepsize = 10.0
|
||||
index = lamni.tomo_queue_add(label="job0")
|
||||
lamni._tomo_queue_proxy.update(index, status="incomplete")
|
||||
|
||||
lamni.tomo_queue_reacquire(index, projection_number=0)
|
||||
|
||||
job = lamni._tomo_queue_proxy.as_list()[index]
|
||||
assert job["status"] == "incomplete"
|
||||
Reference in New Issue
Block a user