feat(omny): port zero_deg_reference_at_each_subtomo to types 1/4/5
CI for csaxs_bec / test (push) Successful in 2m35s
CI for csaxs_bec / test (push) Successful in 2m35s
flomni already has this (type-1-only: a dedicated projection at exactly 0 degrees before every odd/forward sub-tomogram, plus once more at the end, for tracking radiation damage over a tomogram). Deliberately excluded from OMNY during the Phase 1 port as flomni-specific; Mirko asked for it to be extended to OMNY's generalized equally-spaced types (1/4/5, 2/4/8 sub-tomograms), and confirmed the pre-shot should fire even for sub-tomogram 1 (whose own first projection already lands on 0) to get two consecutive 0-degree shots as an immediate baseline pair. Also fixes a second hardcoded `tomo_type != 1` bug in the widget's _irrelevant_params_for_type() (same class as the earlier _validate() fix) that would have hidden this setting from OMNY type-4/5 job tooltips even when enabled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
This commit is contained in:
@@ -636,6 +636,75 @@ used it here too (there's existing precedent for this exact pattern in GUI code,
|
||||
records its `cached` kwarg -- confirms both that the getter only touches the signal and that it
|
||||
passes `cached=True`).
|
||||
|
||||
## zero_deg_reference_at_each_subtomo ported to OMNY's equally spaced types (1/4/5)
|
||||
|
||||
Mirko: "one thing flomni has and we could implement here as well is the repeated projection when
|
||||
the stage is close to 0 anyways... for the tomo types 1,4 and 5 it could also be useful. in tomo
|
||||
type 1 it would of course only mean that at the end of the measurement one additional projection
|
||||
will be recorded." This flag was deliberately excluded from OMNY's `_TOMO_SCAN_PARAM_NAMES` during
|
||||
the Phase 1 port as a "flomni-only concept" (flomni's version is type-1-only, tied to its fixed
|
||||
8-sub-tomogram/180-vs-360-mode scheme) -- this reverses that call for OMNY's generalized 1/4/5
|
||||
equally-spaced types specifically, it's not a blind re-add.
|
||||
|
||||
Verified against `_equally_spaced_subtomo_angles()`'s bit-reversal interleaving: odd (forward)
|
||||
sub-tomograms always start near angle 0 for any `n_subtomos` (2/4/8) -- exactly flomni's 180-mode
|
||||
rule (`subtomo_number % 2 == 1`), no 360-mode branch needed since OMNY only ever scans 180deg.
|
||||
Sub-tomogram 1 specifically always starts at *exactly* 0.0 (phase offset 0 by construction,
|
||||
regardless of tomo_type); the other odd sub-tomograms (3/5/7, types 4/5 only) start at a small but
|
||||
*nonzero* phase offset -- genuinely new angles near 0, not exact duplicates.
|
||||
|
||||
One design question flomni's own code doesn't answer for OMNY: firing the pre-shot before
|
||||
sub-tomogram 1 too would duplicate its own first projection (both exactly angle 0, back to back).
|
||||
Asked Mirko directly -- answer: "it will be very helpful to start with two projections at zero
|
||||
degrees" -- so the pre-shot fires before every odd sub-tomogram including 1, deliberately
|
||||
producing two consecutive 0-degree shots at the very start (useful as an immediate damage/noise-
|
||||
floor baseline pair), matching flomni's own ported logic exactly rather than diverging from it.
|
||||
|
||||
**Implementation** (`omny.py`): new `zero_deg_reference_at_each_subtomo` property (mirrors
|
||||
flomni's of the same name), added to `_TOMO_SCAN_PARAM_NAMES` (comment above the tuple updated --
|
||||
this is now the one flomni-adjacent concept OMNY *does* carry, unlike `tomo_angle_range`/
|
||||
`single_point_*`/`frames_per_trigger`, which still don't apply). `tomo_scan()`'s type-1-like loop:
|
||||
before each odd sub-tomogram (`ii % 2 == 1`), fires `self._tomo_scan_at_angle(0, ii)` -- guarded by
|
||||
`start_angle is None` so a resume mid-sub-tomogram doesn't force a detour through 0 it isn't
|
||||
actually passing through -- and once more after the whole loop finishes, for the final before/after
|
||||
bookend shot. `tomo_parameters()` gained a matching print (current-settings summary) and a
|
||||
`self.OMNYTools.yesno(...)` prompt (edit mode), gated by `self.tomo_type in self._TOMO_TYPE_1_LIKE`.
|
||||
|
||||
**Widget** (`tomo_params.py`): `SETUP_PROFILES["omny"]["has_zero_deg_reference"]` flipped
|
||||
`False` -> `True` -- the checkbox itself (`_build_type1_section()`'s `if self._profile[
|
||||
"has_zero_deg_reference"]: self._add_bool(...)`) was already fully generic, needing no new UI
|
||||
code. Added `zero_deg_reference_at_each_subtomo` to `OMNY_QUEUE_PARAM_NAMES`/`OMNY_DEFAULTS`
|
||||
(default `False`). **Second hardcoded-`!= 1` bug caught while wiring this up** (same class of bug
|
||||
as `_validate()`'s Phase 2 fix): `_irrelevant_params_for_type()` (used by `_job_tooltip()` to hide
|
||||
inapplicable fields) had `if tomo_type != 1: irrelevant |= _TYPE1_ONLY_PARAMS` -- would have hidden
|
||||
this setting from every OMNY type-4/5 job's tooltip even when genuinely enabled. Fixed to
|
||||
`tomo_type not in (1, 4, 5)` (safe without profile context: 4/5 are OMNY-only tomo_type values,
|
||||
flomni/lamni jobs never have them).
|
||||
|
||||
Regression tests: `test_tomo_scan_zero_deg_reference_disabled_by_default_no_extra_shots`,
|
||||
`..._type1_fires_before_subtomo1_and_after_subtomo2`, `..._type5_fires_before_every_odd_subtomo`,
|
||||
`..._skipped_only_for_the_resumed_subtomo` (`test_omny_tomo_scan.py`, spy on `_tomo_scan_at_angle`
|
||||
with `sub_tomo_scan` stubbed out); `test_irrelevant_params_for_type_keeps_zero_deg_reference_for_omny_types_4_and_5`
|
||||
(`test_omny_tomo_params_widget_math.py`); updated the pre-existing "no flomni-only concepts" test
|
||||
(`test_omny_tomo_queue.py`) and the pre-existing `has_zero_deg_reference is False` test (now
|
||||
`True`), both of which would otherwise have failed against this deliberate reversal. Full suite:
|
||||
770 passed.
|
||||
|
||||
**Deferred, needs Mirko's hardware knowledge, not implemented**: Mirko also asked for flomni's
|
||||
`collect_empty_frames()` (acquires 10 flat-field/empty frames at angle 0 with the sample shifted
|
||||
out of the beam by fovx/2, called once at the start of every new tomo_scan()) to be ported to
|
||||
OMNY. Checked flomni's implementation (`flomni.py:2570`) -- it's built entirely on flomni's fine-
|
||||
piezo/laser-tracker stack (`dev.rtx`/`rty`/`rtz`, `laser_tracker_check_signalstrength()`,
|
||||
`move_samx_to_scan_region()`), none of which OMNY has. OMNY's own alignment offsets
|
||||
(`omny_alignment_mixin.py`'s `get_alignment_offset()`) are a pure sinusoidal-fit computation
|
||||
folded directly into `cenx`/`ceny` passed to `scans.omny_fermat_scan()` -- there's no separate
|
||||
fine-stage motor to move at all, and no `scans.acquire()` (or equivalent stationary-multi-frame-
|
||||
burst call) used anywhere in OMNY's plugin code to crib from. A literal port isn't possible without
|
||||
knowing: (1) what OMNY-side call performs a stationary multi-frame acquisition (does one exist?),
|
||||
and (2) whether "shift out of the beam" for OMNY means moving `osamx` directly (it's a real,
|
||||
movable coarse stage, confirmed) or just passing a deliberately offset `cenx` into
|
||||
`omny_fermat_scan()`. Not guessed at -- next step is asking Mirko for these two specifics.
|
||||
|
||||
## Environment note (not code, but will bite again if forgotten)
|
||||
|
||||
Both `csaxs_bec` and `bec_widgets` were pip-installed editable pointing at pre-repo-
|
||||
|
||||
@@ -45,6 +45,17 @@ the broader OMNY live-testing effort this is part of.
|
||||
setting parameters via `tomo_parameters()`.
|
||||
- [ ] A real full scan for type 4 and type 5 (4/8 sub-tomograms).
|
||||
|
||||
## zero_deg_reference_at_each_subtomo (new, never tested live)
|
||||
|
||||
- [ ] `tomo_parameters()`: enable it for tomo_type 1 -- confirm the prompt/summary text and that a
|
||||
real `tomo_scan()` records two projections at angle 0 right at the start (subtomo 1's own
|
||||
first projection plus the deliberate duplicate) and one more at the very end.
|
||||
- [ ] Same for tomo_type 4 or 5 -- confirm extra 0-degree shots also appear before sub-tomograms
|
||||
3/5/7 (not just 1), each at a distinct (nonzero) angle from 0, not a duplicate.
|
||||
- [ ] Widget: the "0° reference each sub-tomo" checkbox now appears for OMNY (previously
|
||||
flomni/lamni only) -- toggle it for a type-4/5 job specifically and check the queued job's
|
||||
tooltip shows it (regression guard for a hardcoded `!= 1` bug caught while wiring this up).
|
||||
|
||||
## Smaller fixes
|
||||
|
||||
- [ ] `tomography_scannumbers.txt` and the PDF report land under `~/data/raw/...` (not the old
|
||||
|
||||
@@ -654,8 +654,10 @@ class OMNY(
|
||||
# Property names a tomo_queue_add() job snapshots (TomoQueueMixin
|
||||
# contract). Mirrors flomni's own tuple exactly, since OMNY's properties
|
||||
# already mirror flomni's names 1:1 -- no tomo_angle_range/single_point_*/
|
||||
# zero_deg_reference_at_each_subtomo/frames_per_trigger, since OMNY has
|
||||
# no such properties.
|
||||
# frames_per_trigger, since OMNY has no such properties.
|
||||
# zero_deg_reference_at_each_subtomo *is* included (unlike the above) --
|
||||
# it applies to any equally spaced tomo type, not just type 1, see
|
||||
# tomo_scan()'s type-1-like loop and its own property docstring below.
|
||||
_TOMO_SCAN_PARAM_NAMES = (
|
||||
"tomo_countingtime",
|
||||
"tomo_shellstep",
|
||||
@@ -671,6 +673,7 @@ class OMNY(
|
||||
"golden_ratio_bunch_size",
|
||||
"golden_max_number_of_projections",
|
||||
"golden_projections_at_0_deg_for_damage_estimation",
|
||||
"zero_deg_reference_at_each_subtomo",
|
||||
"corridor_size",
|
||||
"at_each_angle_hook",
|
||||
)
|
||||
@@ -1051,6 +1054,30 @@ class OMNY(
|
||||
def golden_projections_at_0_deg_for_damage_estimation(self, val: float):
|
||||
self.client.set_global_var("golden_projections_at_0_deg_for_damage_estimation", val)
|
||||
|
||||
@property
|
||||
def zero_deg_reference_at_each_subtomo(self):
|
||||
"""If True (tomo_type in _TOMO_TYPE_1_LIKE, i.e. 1/4/5 -- the equally
|
||||
spaced sub-tomogram modes), an additional projection at exactly 0
|
||||
degrees is acquired at the start of every odd (forward) sub-tomogram
|
||||
-- i.e. every time the rotation passes back through 0 degrees,
|
||||
including sub-tomogram 1 itself, which already lands on 0 degrees
|
||||
naturally -- and once more after the final sub-tomogram completes.
|
||||
Together with sub-tomogram 1's own first projection, this gives two
|
||||
consecutive 0-degree shots right at the start (deliberate: useful as
|
||||
an immediate damage/noise-floor baseline pair) and one more at every
|
||||
later natural pass through 0, useful for tracking radiation damage
|
||||
over time. Mirrors flomni's property of the same name, and
|
||||
golden_projections_at_0_deg_for_damage_estimation, which provides
|
||||
similar functionality for tomo_type 2/3."""
|
||||
val = self.client.get_global_var("zero_deg_reference_at_each_subtomo")
|
||||
if val is None:
|
||||
return False
|
||||
return val
|
||||
|
||||
@zero_deg_reference_at_each_subtomo.setter
|
||||
def zero_deg_reference_at_each_subtomo(self, val: bool):
|
||||
self.client.set_global_var("zero_deg_reference_at_each_subtomo", val)
|
||||
|
||||
@property
|
||||
def golden_ratio_bunch_size(self):
|
||||
val = self.client.get_global_var("golden_ratio_bunch_size")
|
||||
@@ -1354,9 +1381,34 @@ class OMNY(
|
||||
n_subtomos = {1: 2, 4: 4, 5: 8}[self.tomo_type]
|
||||
self.progress["tomo_type"] = f"Equally spaced sub-tomograms ({n_subtomos})"
|
||||
for ii in range(subtomo_start, n_subtomos + 1):
|
||||
if (
|
||||
start_angle is None
|
||||
and ii % 2 == 1
|
||||
and self.zero_deg_reference_at_each_subtomo
|
||||
):
|
||||
# Dedicated reference shot at exactly 0 degrees, taken
|
||||
# every time the rotation passes back through 0
|
||||
# (every odd/forward sub-tomogram -- see
|
||||
# _equally_spaced_subtomo_angles()), for tracking
|
||||
# radiation damage over the full tomogram. Fires
|
||||
# for sub-tomogram 1 too even though its own first
|
||||
# projection already lands on 0 -- two consecutive
|
||||
# 0-degree shots right at the start is deliberate
|
||||
# (immediate baseline pair), not skipped as
|
||||
# redundant. Skipped when resuming mid-sub-tomogram
|
||||
# (start_angle given explicitly) since we're not
|
||||
# actually passing through 0 deg at that moment.
|
||||
self._tomo_scan_at_angle(0, ii)
|
||||
self.sub_tomo_scan(ii, n_subtomos=n_subtomos, start_angle=start_angle)
|
||||
start_angle = None
|
||||
|
||||
if self.zero_deg_reference_at_each_subtomo:
|
||||
# Final reference shot at exactly 0 degrees once the whole
|
||||
# tomogram is complete, giving a clean "before vs after"
|
||||
# pair for radiation-damage comparison across the full
|
||||
# acquisition.
|
||||
self._tomo_scan_at_angle(0, n_subtomos)
|
||||
|
||||
elif self.tomo_type == 2:
|
||||
# Golden ratio tomography
|
||||
previous_subtomo_number = -1
|
||||
@@ -1889,6 +1941,11 @@ class OMNY(
|
||||
)
|
||||
print(f"Total number of projections: {180/self.tomo_angle_stepsize*n_subtomos}")
|
||||
print(f"Angular step within sub-tomogram: {self.tomo_angle_stepsize} degrees")
|
||||
if self.tomo_type in self._TOMO_TYPE_1_LIKE and self.zero_deg_reference_at_each_subtomo:
|
||||
print(
|
||||
"Repeating a reference projection at 0 degrees at the start of every forward"
|
||||
" sub-tomogram, plus once more at the end."
|
||||
)
|
||||
print(f"\nSample name: {self.sample_name}\n")
|
||||
|
||||
if self.OMNYTools.yesno("Are these parameters correctly set for your scan?", "y"):
|
||||
@@ -1994,6 +2051,13 @@ class OMNY(
|
||||
self.tomo_angle_stepsize = 180 / tomo_numberofprojections * n_subtomos
|
||||
print(f"The angular step in a subtomogram it will be {self.tomo_angle_stepsize}")
|
||||
|
||||
if self.tomo_type in self._TOMO_TYPE_1_LIKE:
|
||||
self.zero_deg_reference_at_each_subtomo = self.OMNYTools.yesno(
|
||||
"Repeat a reference projection at 0 degrees at the start of every forward"
|
||||
" sub-tomogram (plus once more at the end)?",
|
||||
"y" if self.zero_deg_reference_at_each_subtomo else "n",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_val(msg: str, default_value, data_type):
|
||||
return data_type(input(f"{msg} ({default_value}): ") or default_value)
|
||||
|
||||
@@ -80,11 +80,13 @@ logger = bec_logger.logger
|
||||
# ── constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
# Mirrors _update_type_visibility()'s section/field gating (_sec_type1 shown
|
||||
# only for type 1, golden_ratio_bunch_size shown only for type 2 within
|
||||
# _sec_type23, the rest of _sec_type23 shown for type 2/3) -- reused by
|
||||
# _job_tooltip() so a queued job's hover details don't show fields that are
|
||||
# meaningless for its own tomo_type (e.g. golden-ratio settings on a type-1
|
||||
# job), on both flomni and lamni.
|
||||
# for every equally spaced sub-tomogram type -- 1 for flomni/lamni, 1/4/5 for
|
||||
# OMNY --, golden_ratio_bunch_size shown only for type 2 within _sec_type23,
|
||||
# the rest of _sec_type23 shown for type 2/3) -- reused by _job_tooltip() so
|
||||
# a queued job's hover details don't show fields that are meaningless for
|
||||
# its own tomo_type (e.g. golden-ratio settings on a type-1 job).
|
||||
# tomo_angle_range is flomni-only regardless (OMNY has no such property, so
|
||||
# it's simply never present in an OMNY job's params to begin with).
|
||||
_TYPE1_ONLY_PARAMS = {"tomo_angle_range", "zero_deg_reference_at_each_subtomo"}
|
||||
_TYPE2_ONLY_PARAMS = {"golden_ratio_bunch_size"}
|
||||
_TYPE23_PARAMS = {
|
||||
@@ -97,7 +99,11 @@ def _irrelevant_params_for_type(tomo_type: Any) -> set[str]:
|
||||
"""Param names that don't apply to a job's own tomo_type and should be
|
||||
hidden from its tooltip."""
|
||||
irrelevant: set[str] = set()
|
||||
if tomo_type != 1:
|
||||
# tomo_type 1 for flomni/lamni; 1/4/5 (all equally spaced sub-tomogram
|
||||
# flavors) for OMNY -- 4/5 are OMNY-only tomo_type values (flomni/lamni
|
||||
# jobs never have them), so this is safe without knowing which setup a
|
||||
# given job's params belong to.
|
||||
if tomo_type not in (1, 4, 5):
|
||||
irrelevant |= _TYPE1_ONLY_PARAMS
|
||||
if tomo_type not in (2, 3):
|
||||
irrelevant |= _TYPE23_PARAMS
|
||||
@@ -240,6 +246,7 @@ OMNY_QUEUE_PARAM_NAMES = (
|
||||
"golden_ratio_bunch_size",
|
||||
"golden_max_number_of_projections",
|
||||
"golden_projections_at_0_deg_for_damage_estimation",
|
||||
"zero_deg_reference_at_each_subtomo",
|
||||
"corridor_size",
|
||||
"at_each_angle_hook",
|
||||
)
|
||||
@@ -260,6 +267,7 @@ OMNY_DEFAULTS: dict[str, Any] = {
|
||||
"golden_ratio_bunch_size": 20,
|
||||
"golden_max_number_of_projections": 1000.0,
|
||||
"golden_projections_at_0_deg_for_damage_estimation": 0,
|
||||
"zero_deg_reference_at_each_subtomo": False,
|
||||
"corridor_size": -1,
|
||||
"at_each_angle_hook": None,
|
||||
}
|
||||
@@ -2525,7 +2533,10 @@ SETUP_PROFILES: dict[str, dict[str, Any]] = {
|
||||
"defaults": OMNY_DEFAULTS,
|
||||
"has_180_mode": False,
|
||||
"has_single_point": False,
|
||||
"has_zero_deg_reference": False,
|
||||
# Applies to any equally spaced tomo type here (1/4/5), not just
|
||||
# type 1 -- see omny.py's zero_deg_reference_at_each_subtomo
|
||||
# property and tomo_scan()'s type-1-like loop.
|
||||
"has_zero_deg_reference": True,
|
||||
"fov_fields": [
|
||||
("fovx", "FOV x (µm)", 0.1, 200.0, 2),
|
||||
("fovy", "FOV y (µm)", 0.1, 100.0, 2),
|
||||
|
||||
@@ -20,6 +20,7 @@ from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params import (
|
||||
_compute_type1,
|
||||
_compute_type1_omny,
|
||||
_format_projections,
|
||||
_irrelevant_params_for_type,
|
||||
_lamni_compute_type1,
|
||||
_requested_to_stepsize,
|
||||
_requested_to_stepsize_omny,
|
||||
@@ -119,11 +120,15 @@ def test_omny_field_order_matches_flomni_minus_omny_only_gaps():
|
||||
]
|
||||
|
||||
|
||||
def test_omny_has_no_180_mode_single_point_or_zero_deg_reference():
|
||||
def test_omny_has_no_180_mode_or_single_point():
|
||||
"""has_zero_deg_reference IS True for OMNY (unlike 180-mode/single-point,
|
||||
which genuinely don't exist here): zero_deg_reference_at_each_subtomo
|
||||
applies to any equally spaced tomo type, not just type 1 -- see
|
||||
omny.py's own property docstring and tomo_scan()'s type-1-like loop."""
|
||||
profile = SETUP_PROFILES["omny"]
|
||||
assert profile["has_180_mode"] is False
|
||||
assert profile["has_single_point"] is False
|
||||
assert profile["has_zero_deg_reference"] is False
|
||||
assert profile["has_zero_deg_reference"] is True
|
||||
|
||||
|
||||
def test_compute_fermat_positions_omny_matches_scan_class_below_threshold():
|
||||
@@ -182,6 +187,20 @@ def test_format_projections_still_distinguishes_flomni_and_lamni_jobs():
|
||||
assert _format_projections(lamni_params) == str(lamni_N * 8)
|
||||
|
||||
|
||||
# --- _irrelevant_params_for_type(): regression guard for the type-4/5 gap --
|
||||
|
||||
|
||||
def test_irrelevant_params_for_type_keeps_zero_deg_reference_for_omny_types_4_and_5():
|
||||
"""_irrelevant_params_for_type() used to hardcode `tomo_type != 1`, which
|
||||
would hide zero_deg_reference_at_each_subtomo from an OMNY type-4/5 job's
|
||||
tooltip even though it's a real, applicable setting for those types too
|
||||
(only flomni/lamni are limited to type 1) -- fixed to `not in (1, 4, 5)`."""
|
||||
for tomo_type in (1, 4, 5):
|
||||
assert "zero_deg_reference_at_each_subtomo" not in _irrelevant_params_for_type(tomo_type)
|
||||
for tomo_type in (2, 3):
|
||||
assert "zero_deg_reference_at_each_subtomo" in _irrelevant_params_for_type(tomo_type)
|
||||
|
||||
|
||||
# --- _validate()'s tomo_type check: regression guard for the critical fix --
|
||||
|
||||
|
||||
|
||||
@@ -58,14 +58,17 @@ def make_omny(fake_client=None):
|
||||
|
||||
def test_tomo_scan_param_names_have_no_flomni_only_concepts():
|
||||
"""OMNY mirrors flomni's names for shared concepts, but has no
|
||||
180/360 toggle, no single-point acquisition, no zero-deg reference,
|
||||
and no frames_per_trigger -- none of these exist as OMNY properties."""
|
||||
180/360 toggle, no single-point acquisition, and no frames_per_trigger --
|
||||
none of these exist as OMNY properties. zero_deg_reference_at_each_subtomo
|
||||
is a deliberate exception: it applies to any equally spaced tomo type
|
||||
(1/4/5 for OMNY, not just type 1), so unlike the others it *is* present --
|
||||
see tomo_scan()'s type-1-like loop and the property's own docstring."""
|
||||
names = OMNY._TOMO_SCAN_PARAM_NAMES
|
||||
assert "tomo_angle_range" not in names
|
||||
assert "single_point_instead_of_fermat_scan" not in names
|
||||
assert "single_point_random_shift_max" not in names
|
||||
assert "zero_deg_reference_at_each_subtomo" not in names
|
||||
assert "frames_per_trigger" not in names
|
||||
assert "zero_deg_reference_at_each_subtomo" in names
|
||||
# shared-with-flomni names are present, using OMNY's own property names
|
||||
assert "fovx" in names
|
||||
assert "stitch_x" in names
|
||||
|
||||
@@ -147,6 +147,67 @@ def test_tomo_scan_accepts_interactive_kwarg(monkeypatch):
|
||||
assert omny.progress["tomo_start_time"] is not None
|
||||
|
||||
|
||||
def test_tomo_scan_zero_deg_reference_disabled_by_default_no_extra_shots(monkeypatch):
|
||||
"""zero_deg_reference_at_each_subtomo defaults to False -- no extra
|
||||
_tomo_scan_at_angle(0, ...) calls beyond whatever sub_tomo_scan() does
|
||||
on its own (stubbed to a no-op here, so none at all)."""
|
||||
omny = make_omny_for_tomo_scan(monkeypatch, 45.0, active_account="")
|
||||
calls = []
|
||||
omny._tomo_scan_at_angle = lambda angle, subtomo_number: calls.append((angle, subtomo_number))
|
||||
|
||||
omny.tomo_scan()
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_tomo_scan_zero_deg_reference_type1_fires_before_subtomo1_and_after_subtomo2(monkeypatch):
|
||||
"""Type 1 (n_subtomos=2): a pre-shot before sub-tomogram 1 (even though
|
||||
its own first projection already lands on 0 -- two consecutive 0-degree
|
||||
shots right at the start is deliberate) and a final shot after
|
||||
sub-tomogram 2, giving exactly one extra projection at the end plus one
|
||||
at the very start for this type."""
|
||||
omny = make_omny_for_tomo_scan(monkeypatch, 45.0, active_account="")
|
||||
omny.tomo_type = 1
|
||||
omny.zero_deg_reference_at_each_subtomo = True
|
||||
calls = []
|
||||
omny._tomo_scan_at_angle = lambda angle, subtomo_number: calls.append((angle, subtomo_number))
|
||||
|
||||
omny.tomo_scan()
|
||||
|
||||
assert calls == [(0, 1), (0, 2)]
|
||||
|
||||
|
||||
def test_tomo_scan_zero_deg_reference_type5_fires_before_every_odd_subtomo(monkeypatch):
|
||||
"""Type 5 (n_subtomos=8): a pre-shot before every odd/forward
|
||||
sub-tomogram (1, 3, 5, 7), plus the final shot after sub-tomogram 8."""
|
||||
omny = make_omny_for_tomo_scan(monkeypatch, 45.0, active_account="")
|
||||
omny.tomo_type = 5
|
||||
omny.zero_deg_reference_at_each_subtomo = True
|
||||
calls = []
|
||||
omny._tomo_scan_at_angle = lambda angle, subtomo_number: calls.append((angle, subtomo_number))
|
||||
|
||||
omny.tomo_scan()
|
||||
|
||||
assert calls == [(0, 1), (0, 3), (0, 5), (0, 7), (0, 8)]
|
||||
|
||||
|
||||
def test_tomo_scan_zero_deg_reference_skipped_only_for_the_resumed_subtomo(monkeypatch):
|
||||
"""Resuming mid-sub-tomogram (start_angle given explicitly) must skip the
|
||||
pre-shot only for that specific sub-tomogram -- we aren't actually
|
||||
passing through 0 deg at that moment -- but later odd sub-tomograms in
|
||||
the same tomo_scan() call still get theirs, and the final shot is
|
||||
unaffected either way."""
|
||||
omny = make_omny_for_tomo_scan(monkeypatch, 45.0, active_account="")
|
||||
omny.tomo_type = 5
|
||||
omny.zero_deg_reference_at_each_subtomo = True
|
||||
calls = []
|
||||
omny._tomo_scan_at_angle = lambda angle, subtomo_number: calls.append((angle, subtomo_number))
|
||||
|
||||
omny.tomo_scan(subtomo_start=3, start_angle=12.5)
|
||||
|
||||
assert calls == [(0, 5), (0, 7), (0, 8)]
|
||||
|
||||
|
||||
def test_tomo_scan_clears_heartbeat_on_normal_completion(monkeypatch):
|
||||
"""The busy-detector heartbeat must be cleared as soon as the scan
|
||||
finishes, not left for the next poll to time out before the GUI stops
|
||||
|
||||
Reference in New Issue
Block a user