fix(flomni): reuse the same phase table for 360deg tomo instead of two

Supersedes the complementary even/odd-eighths tables from 900c810 with
a single shared phase_eighths table used in both 180 and 360 mode, so
subtomo N carries the same phase-tier role regardless of
tomo_angle_range. The complementary property of the low/high phase
sets now falls out as a consequence of the original table's structure
instead of needing a separately derived table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
x01dc
2026-07-15 11:18:37 +02:00
co-authored by Claude Sonnet 5
parent aebfccc5f3
commit 1eb324aad3
2 changed files with 56 additions and 26 deletions
@@ -2317,25 +2317,35 @@ class Flomni(
the original scheme). In 360 mode each sub-tomogram instead
covers only a 180-degree span - never the full circle - split by
subtomo_number % 4 into a "low" half [0,180) (n%4 in (1,0)) and a
"high" half [180,360) (n%4 in (2,3)). Without this split, every
sub-tomogram's own 360-degree sweep would contain angle pairs
exactly 180 degrees apart - redundant tomographic information,
since a measurement 180 degrees from one already taken
contributes no new information.
"high" half [180,360) (n%4 in (2,3)), each sub-tomogram in the
high half literally its low-half counterpart's own 180-degree
scan, shifted by +180 (per subtomo_number, using the SAME
phase_eighths value as 180 mode - not a different phase table).
Without this split, every sub-tomogram's own 360-degree sweep
would contain angle pairs exactly 180 degrees apart - redundant
tomographic information, since a measurement 180 degrees from one
already taken contributes no new information.
Critically, the low half and high half use COMPLEMENTARY halves
of the original 8-way phase_eighths table (low: the even eighths
{0,2,4,6}; high: the odd eighths {1,3,5,7}), not the same phases
shifted by 180 - using the same phases would make every low-half
angle exactly 180 degrees from a high-half angle, reintroducing
the redundancy this scheme exists to remove. With complementary
phases, low and high each independently interlace their own half
at spacing step/4, but together - once the high half is folded
mod 180 - they reconstruct the full 8-way, step/8 grid with every
position hit EXACTLY once: the combined 360-degree angle set is
mod-180-identical to what an equivalent 180-degree scan would
produce, at the same total projection count, with zero redundant
measurements anywhere.
Reusing the same per-subtomo-number phase_eighths value in both
modes (rather than reassigning phases by acquisition order within
each half) is what makes subtomo N carry the same
interlacing/refinement role in both modes - e.g. subtomo 2 always
represents the phase_eighths[2] tier, whether that's within
[0,180) (180 mode) or shifted to [180,360) (360 mode, since
subtomo 2 % 4 == 2, a "high" sub-tomogram) - instead of an
unrelated phase suddenly appearing under the same subtomo number
depending on tomo_angle_range. Since the low half uses
subtomo_numbers {1,4,5,8} (phase_eighths values {0,6,1,7}) and the
high half uses {2,3,6,7} (phase_eighths values {4,2,5,3}), these
two phase sets are complementary (their union is all 8 eighths,
with no overlap) purely as a property of the original
phase_eighths table's structure - not something this branch has
to separately re-derive. Once the high half is folded mod 180,
low and high together reconstruct the full 8-way, step/8 grid
with every position hit EXACTLY once: the combined 360-degree
angle set is mod-180-identical to what an equivalent 180-degree
scan would produce, at the same total projection count, with zero
redundant measurements anywhere.
Returns:
angles (np.ndarray): the N angles (degrees) for this sub-tomogram.
@@ -2357,20 +2367,19 @@ class Flomni(
N = int(180.0 / tomo_angle_stepsize)
step = 180.0 / N
# Same phase_eighths table in both modes - see docstring above for
# why reusing it (rather than a separately-derived phase table for
# 360 mode) is what keeps subtomo N's role consistent between
# modes.
phase_eighths = {1: 0, 2: 4, 3: 2, 4: 6, 5: 1, 6: 5, 7: 3, 8: 7}
phase = step / 8.0 * phase_eighths[subtomo_number]
if tomo_angle_range == 180:
base = 0.0
phase_eighths = {1: 0, 2: 4, 3: 2, 4: 6, 5: 1, 6: 5, 7: 3, 8: 7}
phase = step / 8.0 * phase_eighths[subtomo_number]
forward = bool(subtomo_number % 2)
else:
# Low half (subtomos 1,4,5,8): even eighths. High half
# (subtomos 2,3,6,7): odd eighths. Complementary, not shared -
# see docstring above for why that's required to avoid
# redundant 180-degrees-apart measurements.
eighths = {1: 0, 4: 4, 5: 2, 8: 6, 2: 1, 3: 5, 6: 3, 7: 7}
mod4 = subtomo_number % 4
base = 0.0 if mod4 in (1, 0) else 180.0
phase = step / 8.0 * eighths[subtomo_number]
forward = mod4 in (1, 2)
if not explicit_start_angle:
@@ -94,6 +94,27 @@ def test_360_mode_mod_180_identical_to_180_mode(stepsize):
assert set_folded == set180
@pytest.mark.parametrize("stepsize", STEPSIZES)
@pytest.mark.parametrize("n", range(1, 9))
def test_360_mode_subtomo_keeps_same_phase_tier_as_180_mode(stepsize, n):
"""Subtomo N must carry the same phase_eighths tier in 360 mode as in
180 mode - e.g. subtomo 2 always represents the phase_eighths[2] tier,
whether that's within [0,180) (180 mode) or shifted +180 into
[180,360) (360 mode). A previous implementation reassigned phases by
acquisition-order-within-half instead of reusing the per-subtomo-number
value, so e.g. the angle physically corresponding to subtomo 2's tier
in 180 mode ended up under subtomo 4 in 360 mode - confusing, and
breaks any expectation that "subtomo N" means the same thing across
modes."""
angles180, _, N, step = plan(n, 180, stepsize)
angles360, _, _, _ = plan(n, 360, stepsize)
base = 0.0 if n % 4 in (1, 0) else 180.0
offset180 = set(np.round(np.mod(angles180, step), 6))
offset360 = set(np.round(np.mod(angles360 - base, step), 6))
assert offset360 == offset180
@pytest.mark.parametrize("tomo_angle_range", [180, 360])
@pytest.mark.parametrize("stepsize", STEPSIZES)
def test_resume_round_trip(tomo_angle_range, stepsize):