Skip to content

fix: apply channel z_offset to stage position z when there is no z_plan - #274

Merged
tlambert03 merged 2 commits into
pymmcore-plus:mainfrom
hinderling:fix/channel-z-offset-without-zplan
Oct 7, 2026
Merged

tlambert03 merged 2 commits into
pymmcore-plus:mainfrom
hinderling:fix/channel-z-offset-without-zplan

Conversation

@hinderling

Copy link
Copy Markdown
Contributor

Problem

Channel.z_offset is only applied in _xyzpos() when z comes from a z_plan. When z comes only from the stage position, the elif position: branch uses position.z and drops the offset. So the same sequence behaves differently depending on whether a z-stack is enabled. User-visible: pymmcore-widgets' channel table has a Z offset column, which has no effect unless a z-plan is also set.

import useq

seq = useq.MDASequence(
    stage_positions=[(0.0, 0.0, 0.0)],
    channels=[{"config": "a"}, {"config": "b", "z_offset": 5.0}],
)
[e.z_pos for e in seq]  # [0.0, 0.0] on main, expected [0.0, 5.0]

With z_plan={"range": 2.0, "step": 1.0} the offset is applied (channel b gets 4/5/6 vs a's -1/0/1). Channel.z_offset is documented as a relative Z offset in microns, with no mention of requiring a z-stack. pymmcore-plus doesn't read z_offset itself (the offset is meant to be baked into event.z_pos here), so nothing else applies it.

Fix

Add the channel z_offset in the elif position: branch too. The two branches are mutually exclusive, so the offset is never applied twice. If the position has no z (position.z is None), z_pos stays None: there is nothing to offset.

Test

Added to tests/test_sequence.py:

  • test_channel_z_offset_without_z_plan: fails on main ([10.0, 10.0] != [10.0, 15.0]), passes with the fix.
  • test_channel_z_offset_with_z_plan: offset applied once (unchanged behaviour).
  • test_channel_z_offset_without_position_z: z_pos stays None.

Behaviour change

Sequences with a nonzero z_offset and no z_plan will now move Z per channel, where before the offset was ignored. z_offset defaults to 0.0, so only sequences that set it explicitly are affected.

Not covered (possible follow-ups)

  • No absolute z to offset. With no stage_positions, or a position without z, z_pos stays None ("don't move Z") and the offset is still dropped. The docstring describes the offset as relative to the current position, which an event can't express without a notion of the current z (there is already a TODO about this in _xyzpos). Supporting it would likely mean resolving the offset in the engine.
  • Position sub-sequences. For stage_positions=[{"x": 0, "y": 0, "z": 3, "sequence": {"channels": [{"config": "b", "z_offset": 5.0}]}}], z_pos is still 3.0: _position_offsets forces z to the parent event's z whenever the sub-sequence has no z_plan, which overwrites the sub-sequence channel's offset. Separate code path, so I left it out of this PR.

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.63%. Comparing base (96299a2) to head (7e39049).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #274   +/-   ##
=======================================
  Coverage   93.62%   93.63%           
=======================================
  Files          33       33           
  Lines        2589     2591    +2     
=======================================
+ Hits         2424     2426    +2     
  Misses        165      165           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tlambert03

Copy link
Copy Markdown
Member

thanks!

@tlambert03
tlambert03 merged commit 8e44aa2 into pymmcore-plus:main Oct 7, 2026
22 checks passed
@hinderling
hinderling deleted the fix/channel-z-offset-without-zplan branch October 7, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants