Skip to content

refactor(cli)!: run apply-inv-tf through iohub's process_single_position, with --resume and per-reconstruction provenance keys - #583

Merged
ieivanov merged 13 commits into
mainfrom
feat/apply-inv-tf-iohub-resume
Oct 16, 2026
Merged

ieivanov merged 13 commits into
mainfrom
feat/apply-inv-tf-iohub-resume

Conversation

@ieivanov

@ieivanov ieivanov commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Merge order and dependencies

This is a stacked PR. Merge in this order:

  1. fix(ngff): sync a unit's shards before recording it as done czbiohub-sf/iohub#476 (fsync a unit's shards before recording it as done). Needed for --resume to be trustworthy. This PR pins iohub to tests: run example tests on ci and mock show #476's commit via [tool.uv.sources].
  2. perf(cli): share the transfer function across apply-inv-tf workers and read each volume once #582 (share the transfer function across workers, read each volume once). This PR is based on perf(cli): share the transfer function across apply-inv-tf workers and read each volume once #582's branch, so the diff shown here is only this PR's own change.
  3. This PR. After perf(cli): share the transfer function across apply-inv-tf workers and read each volume once #582 merges, retarget it to main.
  4. An iohub release containing tests: run example tests on ci and mock show #476. Then replace the [tool.uv.sources] pin with a PyPI floor (iohub>=<that release>).
  5. feat(reconstruct)!: resume interrupted apply-inv-tf positions; one provenance key per reconstruction czbiohub-sf/biahub#387: passes --resume from biahub apply-inv-tf, biahub reconstruct and the reconstruct Nextflow module, as deskew and flat-field do. It pins waveorder to this PR's commit until a release. It's stacked on build(deps): pin iohub to the fsync-before-marker fix czbiohub-sf/biahub#345, the biahub side of the iohub#476 pin.

The process_single_position API used here (num_workers, use_threads, resume, resume_token) is already in iohub 0.3.11, so iohub>=0.3.11 is the floor. #476 only changes what a finished unit's record guarantees.

⚠️ Breaking change: reconstruction provenance schema

Reconstruction settings move from one nested key to one top-level key per reconstruction. Stores written before this change keep their old key; nothing is migrated.

Before (#547): output position zattrs

"waveorder": {
  "Retardance,Orientation,Transmittance,Depolarization": { "...birefringence config..." },
  "GFP_Density3D": { "...fluorescence config..." }
}

After

"waveorder-Birefringence": { "...birefringence config..." },
"waveorder-GFP_Density3D": { "...fluorescence config..." }
reconstruction old: zattrs["waveorder"][...] new: top-level key
phase 3D / 2D "Phase3D" / "Phase2D" "waveorder-Phase3D" / "waveorder-Phase2D"
fluorescence "GFP_Density3D" "waveorder-GFP_Density3D"
birefringence "Retardance,Orientation,Transmittance,Depolarization" "waveorder-Birefringence" (shortened)
birefringence + phase 3D "Retardance,…,Phase3D,Retardance_Joint_Decon,Orientation_Joint_Decon,Phase_Joint_Decon" "waveorder-Birefringence,Phase3D" (joint-decon channels implied)

The key comes from ReconstructionSettings.provenance_key. get_reconstruction_provenance(settings) returns {key: settings} for create_empty_plate(extra_metadata=...), which biahub uses as well.

Who's affected: anyone reading zattrs["waveorder"][<channel names>], the access #547 documented. There are no readers in waveorder, biahub or imaging-qc-pipeline; biahub only copies the key forward, and still copies the legacy nested waveorder key for older stores.

Why one key per reconstruction

  • It matches biahub's layout. biahub records each processing step's config as its own top-level key, at the same level: biahub-flat_field, biahub-deskew, biahub-concatenate, cytoland. The reconstruction step becomes one more sibling (waveorder-Phase3D) instead of the only step nested under a package key. An assembled v6 position after this change:
    { "ome": {...}, "biahub-flat_field": {...}, "biahub-deskew": {...},
      "waveorder-Phase3D": {...}, "cytoland": {...}, "biahub-concatenate": {...} }
  • It fixes lost provenance in biahub. The nested dict had two writers with different semantics. biahub recorded it at plate creation through create_empty_plate(extra_metadata=...), which overwrites the top-level key; waveorder merged into it after the timepoints. On the biahub path, a second config appending channels therefore wiped the first config's settings (reproduced: after a fluorescence then a phase config, only Phase3D remained). With one key per reconstruction, iohub's overwrite is exactly right and no merge is needed anywhere.
  • It's written once, at plate creation, before any timepoint runs, like every other step. wo apply-inv-tf records it in create_empty_plate, and apply_inverse_transfer_function_single_position no longer writes metadata per position. Verified: with the per-position step forced to fail, the output already holds waveorder-Phase3D while its data is still all zeros. It also suits --resume: the settings are on disk even if the run is interrupted.

Alternative considered: keep the nested dict, make both writers merge (option A)

"waveorder": { "GFP_Density3D": {...}, "Phase3D": {...} }
  • Pros: no schema change, and Namespace reconstruction settings by output channel names #547's documented access keeps working.
  • Cons:
    • iohub's extra_metadata can only overwrite. biahub's init would have to read and merge the existing dict before create_empty_plate, or iohub would need a deep-merge option.
    • waveorder would keep its per-position read-merge-write after reconstruction, so the settings only land once a position finishes.
    • The reconstruction step would stay the one step nested under a package key rather than sitting beside the others.

If the schema change is a blocker, option A is the fallback. It fixes the lost-provenance bug without breaking readers, at the cost of the merge code on both sides.

Summary

Closes #574, following the path proposed in #574 (comment).

apply_inverse_transfer_function_single_position drove its own per-timepoint loop (apply_inverse_to_zyx_and_save). A reconstruct killed part-way through a position (Slurm preemption, walltime) therefore recomputed every timepoint on retry, unlike flat-field, deskew and assemble. It now runs through iohub's process_single_position, and gains --resume. The reconstruction's provenance moves to one top-level key per reconstruction, recorded at plate creation (breaking, see above).

Changes

  • cli/utils.py:
    • apply_inverse_czyx, a module-level numpy adapter. iohub reads each CZYX volume once and hands it over as numpy. The adapter labels it with the input's coordinates (taken once from Position.to_xarray(), so they come from the same store as the data), calls the waveorder.api function unchanged, and returns .values. It's module-level so it pickles into iohub's spawn pool.
    • reconstruction_fingerprint, the resume token (see below).
    • apply_inverse_to_zyx_and_save and waveorder's _check_nan_n_zeros are deleted.
  • cli/apply_inverse_transfer_function.py:
    • Channels are resolved by name, in config order, as one group, against the input and the output store. So several configs writing different channel groups into one plate (Namespace reconstruction settings by output channel names #547) still write their own channels.
    • Timepoints are addressed by index, passed as both input and output indices, since the output plate has the input's full T.
    • The partial and executor code is replaced by process_single_position(apply_inverse_czyx, ..., num_workers=num_processes, resume=..., resume_token=..., model_function=..., czyx_coords=..., **apply_inverse_args).
    • The per-position zattrs["waveorder"] merge-write after the timepoints is removed. Provenance is recorded at plate creation instead (see the breaking-change section).
    • get_reconstruction_provenance(settings): {settings.provenance_key: settings.model_dump()}. apply_inverse_transfer_function_cli passes it to create_empty_plate(extra_metadata=...).
  • cli/settings.py: ReconstructionSettings.provenance_key, e.g. waveorder-Phase3D, waveorder-Birefringence, waveorder-Birefringence,Phase3D, waveorder-GFP_Density3D.
  • --resume on wo apply-inv-tf and wo reconstruct, and resume=False on apply_inverse_transfer_function_cli / ..._single_position (default off, so biahub's call is unaffected).
    • The token hashes three things: the reconstruction settings (excluding time_indices, which selects timepoints rather than changing them), the settings stored in the transfer-function store, and the transfer function's shapes. Resuming after a settings change or a recomputed transfer function recomputes instead of silently reusing stale timepoints.
    • Resume needs an OME-Zarr v0.5 output. For v0.4 the CLI says so, since it silences iohub's UserWarning, and recomputes everything.
  • pyproject.toml: iohub>=0.3.11, plus the [tool.uv.sources] pin to iohub#476 (see merge order).

What #582 bought is preserved:

  • The transfer function is still passed with every task as torch tensors. import torch registers torch's pickling reductions on the stdlib ForkingPickler, so they cross iohub's stdlib spawn pool as a shared-memory handle. I checked this against iohub's exact pool pattern: 0.7 MB of pipe traffic for 32 tasks carrying a 1 GB tensor.
  • Each volume is read once. iohub reads it with oindex into numpy, checks it, then calls the adapter, so there's no dask array to recompute.

Behaviour changes

  • Skip rule (deliberate): a timepoint is now skipped when any input channel is all zeros or NaNs (iohub's rule), not only when all channels are. This only affects multi-channel (birefringence) inputs. A missing polarization state makes that reconstruction meaningless anyway. Decided in review of the proposal; worth a changelog note.
  • Bug fix: the old write mapped the input's t coordinate back to an index through the output's transform. For an input with a T translation, every timepoint was written to a shifted (or out-of-bounds) index. Addressing by index fixes this by construction.

Testing

  • tests/api_tests + tests/cli_tests: 197 passed, 1 skipped, 2 xfailed, against iohub#476.
  • test_append_channel_reconstruction (birefringence, then fluorescence into one plate) now checks waveorder-Birefringence and waveorder-GFP_Density3D against each config, and that no legacy waveorder key is written. New test_provenance_key[...] covers each reconstruction mode.
  • New tests. All but "channels by name" fail on perf(cli): share the transfer function across apply-inv-tf workers and read each volume once #582's code, each for the reason it targets. "Channels by name" also passes there, since it guards against a trap rather than an existing bug.
    • test_apply_inv_tf_addresses_timepoints_by_index: a time_indices subset on an input with a T translation lands on the right indices (the old code shifted them).
    • test_apply_inv_tf_writes_its_channels_by_name: writing Phase3D into a plate whose channel 0 belongs to another config leaves channel 0 untouched.
    • test_apply_inv_tf_skips_a_timepoint_with_a_blank_channel: the new skip rule, on a birefringence input.
    • test_apply_inv_tf_resume: resume skips finished timepoints. Without --resume, everything is recomputed. A settings change, or a transfer function recomputed with other settings, recomputes despite --resume.
    • test_apply_inv_tf_resume_on_v04_output_recomputes_and_says_so.
    • test_apply_inverse_czyx_labels_the_volume_for_the_model.
  • test_apply_inv_tf_process_pool_matches_serial is kept: 2 spawned workers give output identical to the serial path.

Benchmark

wo apply-inv-tf -j 16 on one v6 position (C/2/000002), time_indices 1–16, TF with the correct pixel sizes. All runs are in one SLURM job on one node (cpu-g-2, 16 CPUs), alternating #582's head and this PR, both with iohub#476. The measurement method is the same as in #582: whole-process-tree I/O and CPU deltas; storage reads = rchar − pipe traffic; PSS for memory.

#582 head (097ff97) this PR (0c21a27)
wall time 42.9 / 43.6 s 44.3 / 43.7 s
CPU (process tree, user+sys) 513 / 517 s 510 / 504 s
pipe traffic 0 0 (the TF still crosses as a shared-memory handle)
storage reads 11.2 GB 11.2 GB (each volume still read once)
storage writes 9.6 GB 9.6 GB
peak PSS, process tree 106–117 GB 113–117 GB
output bit-identical (16/16 time points, both repeats)

Moving to iohub's loop, with its per-unit completion markers and #476's fsync, costs nothing measurable (≤ 1 s of wall time, within noise).

Resume, rerun with --resume into a finished output: all 16 units skipped ("already written"), in 13.9 s, 65 s of CPU, 3.6 GB read (mostly the TF load and the completion probes) and 0 bytes written. A full run takes ~44 s, 510 s of CPU and 9.6 GB written.

🤖 Generated with Claude Code

ieivanov and others added 6 commits September 24, 2026 11:35
apply_inverse_transfer_function_single_position bound the loaded
transfer function into the per-time-point partial. ProcessPoolExecutor
pickles every task's arguments, so the full transfer function was
serialised by the parent and deserialised by a worker once per time
point, and converted to tensors (with a copy) on every call. For a 3D
phase transfer function of (181, 768, 794) complex64 pairs (~1.8 GB),
768 time points meant ~1.36 TB of pipe traffic per position and ~30 GB
of queued pickles in the parent.

Each pool worker now loads the transfer function once in an initializer,
converts it to torch tensors, and passes them to every time point it
runs. The serial path loads it once in-process. _to_tensor returns
already-converted tensors as-is; xr.Dataset callers are unchanged.

The executor type, spawn context, and thread settings are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Loading the transfer function in every worker removed the per-time-point
pickling but read the store once per worker and kept one copy per worker
in RAM (16 x 3 GB for a v6 3D phase TF).

The parent now loads it once, moves the tensors into shared memory with
share_memory_(), and passes them as the pool initializer's argument.
torch.multiprocessing's pickling reductions send a shared tensor as a
handle to its memory, so every worker maps the same copy: one storage
read, one copy in RAM, and nothing large through the worker pipes. A
missing transfer-function key again fails in the parent, before the pool
starts.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…nction

The explicit share_memory_() was redundant: importing torch.multiprocessing
registers pickling reductions that move a pickled CPU tensor into shared
memory and send a handle to it, which already happens when the tensors are
passed as the pool initializer's argument. Drop the call and document the
mechanism, including why it only applies once the arrays are torch tensors.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
torch.multiprocessing's pickling reductions share CPU tensors for any
ForkingPickler route, including the executor's per-task call queue, so a
tensor transfer function bound into each time point's partial already
crosses as a small shared-memory handle. The pool initializer, the
module-level worker global, and the task wrapper are no longer needed; the
executor code is back to main's.

_load_transfer_function_dataset becomes _load_transfer_function and
returns the tensors directly instead of an xr.Dataset of numpy arrays that
was converted afterwards. The public apply_inverse_transfer_function type
hints and docstrings now say they accept a mapping of tensors.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
apply_inverse_to_zyx_and_save called `.values` on the dask-backed input
slice twice, once for the NaN/zero check and again inside the model, so
every time point was read and decompressed from the store twice (0.48 GB
extra per time point on a v6 A549 position). Load the slice once.

_to_tensor copied every xr.Dataset variable before wrapping it. The
apply-inverse models never modify the transfer function in place, so wrap
it with torch.from_numpy directly and share its memory.

Tests: the input is read once per time point (fails with the old code),
and apply leaves an xr.Dataset transfer function unchanged while sharing
its memory, for phase and fluorescence.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…on, with --resume

apply_inverse_transfer_function_single_position drove its own per-timepoint
loop through apply_inverse_to_zyx_and_save, so a reconstruct killed part-way
through a position (preemption, walltime) recomputed every timepoint on
retry, unlike flat-field, deskew and assemble. Run it through iohub's
process_single_position instead, as proposed in #574:

- apply_inverse_czyx is a module-level numpy adapter: iohub reads each CZYX
  volume once and hands it over as numpy; the adapter labels it with the
  input's coordinates (taken once from Position.to_xarray()), calls the
  waveorder.api function unchanged, and returns its values.
- Channels are resolved by name, in config order, as one group, against the
  input and the output stores, so configs writing different channel groups
  into one plate still land on their own channels.
- Timepoints are addressed by index. The old write mapped the input's t
  coordinate back through the output's transform, which shifted every
  timepoint of an input with a T translation.
- --resume (wo apply-inv-tf, wo reconstruct; resume= on the Python functions,
  default False) skips timepoints a previous interrupted run finished. The
  resume token fingerprints the reconstruction settings, the settings the
  transfer function was computed with, and its shapes, so a settings change
  or a recomputed transfer function recomputes instead of reusing stale data.
  An OME-Zarr v0.4 output cannot track progress; the CLI says so.

The transfer function is still passed as torch tensors with every task, so
it crosses iohub's spawn pool as a shared-memory handle, and each volume is
still read once.

Behaviour change: a timepoint is now skipped when any input channel is all
zeros or NaNs (iohub's rule), not only when all channels are. This only
matters for multi-channel (birefringence) inputs; a missing polarization
state makes that reconstruction meaningless anyway.

Requires iohub>=0.3.11 for the API; pinned via tool.uv.sources to
czbiohub-sf/iohub#476, which syncs a unit's shards before recording it as
done, until iohub releases it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…at plate creation

BREAKING CHANGE: reconstruction settings move from one nested key,
zattrs["waveorder"][<output channel names>], to one top-level key per
reconstruction, zattrs["waveorder-<group>"], e.g. waveorder-Phase3D,
waveorder-GFP_Density3D, waveorder-Birefringence (previously keyed
"Retardance,Orientation,Transmittance,Depolarization") and
waveorder-Birefringence,Phase3D. Existing stores keep the old key.

Why: the nested dict had two writers with different semantics. biahub
records it at plate creation through create_empty_plate(extra_metadata=...),
which overwrites the whole top-level key, and waveorder merged into it after
the timepoints. A second config appending channels to a plate therefore
dropped the first config's settings on the biahub path (#547's multi-group
provenance). With one key per reconstruction, iohub's overwrite is exactly
right, no merge is needed anywhere, and each reconstruction sits beside the
biahub-<step> keys of the other processing steps.

- ReconstructionSettings.provenance_key names the key;
  get_reconstruction_provenance(settings) returns {key: settings} for
  create_empty_plate(extra_metadata=...), which biahub uses too.
- apply_inverse_transfer_function_cli records it when creating the plate;
  apply_inverse_transfer_function_single_position writes no metadata.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ieivanov added a commit to czbiohub-sf/biahub that referenced this pull request Sep 26, 2026
…r reconstruction

BREAKING CHANGE: reconstruct output positions record their settings under
top-level zattrs["waveorder-<group>"] keys (waveorder-Phase3D,
waveorder-Birefringence, waveorder-GFP_Density3D, ...) instead of
zattrs["waveorder"][<output channel names>], following
mehta-lab/waveorder#583.

_init_output_plate recorded {"waveorder": {group: settings}} through
create_empty_plate(extra_metadata=...), which overwrites the whole top-level
key, so a second config appending channels to a plate dropped the first
config's settings. It now records waveorder's own
get_reconstruction_provenance(settings): one key per reconstruction, beside
the biahub-<step> keys, so each config overwrites only its own entry.

PROVENANCE_METADATA_KEYS carries waveorder-* forward, and keeps the nested
waveorder key so older stores still carry theirs. The waveorder pin moves to
the #583 commit with this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@ieivanov ieivanov changed the title refactor(cli): run apply-inv-tf through iohub's process_single_position, with --resume refactor(cli)!: run apply-inv-tf through iohub's process_single_position, with --resume and per-reconstruction provenance keys Sep 26, 2026
@ieivanov

Copy link
Copy Markdown
Contributor Author

@talonchandler @srivarra let me know if the breaking change in the metadata schema is acceptable or if we should consider a different option.

Before:

"waveorder": {
  "Retardance,Orientation,Transmittance,Depolarization": { "...birefringence config..." },
  "GFP_Density3D": { "...fluorescence config..." }
}

After:

"waveorder-Birefringence": { "...birefringence config..." },
"waveorder-GFP_Density3D": { "...fluorescence config..." }

This aligns the metadata schema with biahub, writing one field per reconstruction steps (in this example Birefringence and GFP_Density3D fluor deconvolution are considered separate reconstruction steps, even if they are passed through the same waveorder yaml file) and allow us to reuse some code. We could also keep the current schema if you prefer.

Once we agree on that part, we can review the rest of the PR.

@srivarra

Copy link
Copy Markdown
Contributor

@ieivanov

I don't see a problem with the change in change in metadata schema. Does this help WaveOrder align closer to the DCA specs (do they have anything any best practices w.r.t this)?

@ieivanov

Copy link
Copy Markdown
Contributor Author

Does this help WaveOrder align closer to the DCA specs (do they have anything any best practices w.r.t this)

Not really - DCA spec so far is about what kind of sample the data is for (what cells, what markers, what perturbations, etc.) rather than what kind of processing was performed, so not relevant here.

The change in the schema is largely motivated by aligning to the biahub schema and reusing some of our existing code. This would be a breaking change for anyone reading that metadata programatically. Do you know of any such use cases?

@srivarra

Copy link
Copy Markdown
Contributor

@ieivanov

anyone reading that metadata programatically. Do you know of any such use cases?

Checked the AirTable to see if we ever used that metadata, and it doesn't look like it. So this seems safe to me.

@talonchandler

Copy link
Copy Markdown
Collaborator

Looks good to me. I couldn't find any cases of code snippets using these metadata, so please go ahead.

@ieivanov

Copy link
Copy Markdown
Contributor Author

Sounds good, thanks guys. I'll look more closely thru the rest of the PR and I'll make it when it's ready for review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The committed lockfile does not contain the required iohub revision, leaving frozen installations without the resume durability fix.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Refactors reconstruction through iohub to support resumable timepoint processing and per-reconstruction provenance.

Changes:

  • Adds --resume and iohub-based processing.
  • Introduces reconstruction fingerprints and top-level provenance keys.
  • Expands reconstruction and provenance tests.
File Description
waveorder/​cli/​utils.py Adds NumPy adapter and resume fingerprinting.
waveorder/​cli/​settings.py Defines reconstruction provenance keys.
waveorder/​cli/​reconstruct.py Adds resume support.
waveorder/​cli/​parsing.py Defines the --resume option.
waveorder/​cli/​apply_inverse_transfer_function.py Uses iohub processing and records provenance.
tests/​cli_tests/​test_settings.py Tests provenance key generation.
tests/​cli_tests/​test_reconstruct.py Tests processing, resume, indexing, and provenance.
pyproject.toml Updates and pins iohub.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pyproject.toml Outdated
@ieivanov
ieivanov requested a balanced review from Copilot October 7, 2026 00:03
@ieivanov
ieivanov marked this pull request as ready for review October 7, 2026 00:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The resume token incorrectly includes transfer-function time_indices, causing unnecessary recomputation when only the selected timepoints change.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (1)

Comment thread waveorder/cli/utils.py
… metadata

reconstruct rewrites the transfer function metadata from the full config on
every run, so time_indices leaked into apply-inv-tf's resume token and a new
time selection recomputed already-finished timepoints. Exclude time_indices
and input_channel_names, which select data rather than shape the transfer
function; both remain in the reconstruction settings part of the token.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@ieivanov

ieivanov commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

@talonchandler @srivarra I think this PR is ready (along with #582). I've tested it on a full mantis run as part of czbiohub-sf/biahub#387.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Resume correctness is not guaranteed for pip installations or transfer functions whose contents change without changing metadata or shape.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Pip installs unpatched iohub despite resume durability requirement

pyproject.toml:47

[tool.uv.sources] is only honored by uv, while this standard dependency is what pip uses (and pip is the documented installation path). A pip/VCS installation can therefore resolve iohub 0.3.11 without the shard-fsync fix that this PR says is required for trustworthy completion records, allowing --resume to skip data whose write was not durable. Require the patched commit/release in standard package metadata as well, or gate resume until an iohub release containing the fix is installed.

Comment thread waveorder/cli/utils.py
@codecov

codecov Bot commented Oct 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (perf/load-tf-once-per-worker@097ff97). Learn more about missing BASE report.

Additional details and impacted files
@@                       Coverage Diff                       @@
##             perf/load-tf-once-per-worker     #583   +/-   ##
===============================================================
  Coverage                                ?   56.02%           
===============================================================
  Files                                   ?       75           
  Lines                                   ?    12618           
  Branches                                ?        0           
===============================================================
  Hits                                    ?     7069           
  Misses                                  ?     5549           
  Partials                                ?        0           

☔ 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.

Base automatically changed from perf/load-tf-once-per-worker to main October 8, 2026 18:19
ieivanov and others added 3 commits October 8, 2026 11:21
Conflicts with #582 (this branch's earlier perf commits, squash-merged)
resolved in favor of this branch, which supersedes them. Drops main's
test for apply_inverse_to_zyx_and_save, which this branch removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Brings in #578 (auto_regularization). Test conflicts resolved by keeping
both sides' additions.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…sume-local

# Conflicts:
#	tests/cli_tests/test_reconstruct.py
#	waveorder/cli/apply_inverse_transfer_function.py

@talonchandler talonchandler left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No concerns from my side. @ieivanov I'm not sure if you need/want an iohub release. Feel free to merge.

@ieivanov

Copy link
Copy Markdown
Contributor Author

For now, let's keep iohub pinned to a commit hash on main. I'm working with @srivarra thru a couple more changes to iohub. When we are through we'll create a release and I'll open a waveorder PR to pin the new iohub version. Merging now.

@ieivanov
ieivanov merged commit 8d79650 into main Oct 16, 2026
7 checks passed
@ieivanov
ieivanov deleted the feat/apply-inv-tf-iohub-resume branch October 16, 2026 00:18
ieivanov added a commit to czbiohub-sf/biahub that referenced this pull request Oct 16, 2026
A reconstruct task preempted or killed at walltime recomputed every
timepoint of its position on retry; flat-field, deskew and assemble resume
per unit. waveorder now runs apply-inv-tf through iohub's
process_single_position and takes resume= (mehta-lab/waveorder#583), so:

- `biahub apply-inv-tf` and `biahub reconstruct` take @resume()
  (--resume/--no-resume, default off) and pass it to
  apply_inverse_transfer_function_single_position. waveorder computes the
  resume token itself, from the reconstruction settings and the transfer
  function, so a changed config or a recomputed transfer function
  recomputes instead of being skipped.
- The reconstruct Nextflow module passes --resume, as deskew, flat-field and
  assemble do.
- waveorder is pinned to #583 (which also carries #582's shared transfer
  function) until a release contains both.
- The CLI-consistency canon lists @resume() in the decorator stack and says
  which commands take it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ieivanov added a commit to czbiohub-sf/biahub that referenced this pull request Oct 16, 2026
…r reconstruction

BREAKING CHANGE: reconstruct output positions record their settings under
top-level zattrs["waveorder-<group>"] keys (waveorder-Phase3D,
waveorder-Birefringence, waveorder-GFP_Density3D, ...) instead of
zattrs["waveorder"][<output channel names>], following
mehta-lab/waveorder#583.

_init_output_plate recorded {"waveorder": {group: settings}} through
create_empty_plate(extra_metadata=...), which overwrites the whole top-level
key, so a second config appending channels to a plate dropped the first
config's settings. It now records waveorder's own
get_reconstruction_provenance(settings): one key per reconstruction, beside
the biahub-<step> keys, so each config overwrites only its own entry.

PROVENANCE_METADATA_KEYS carries waveorder-* forward, and keeps the nested
waveorder key so older stores still carry theirs. The waveorder pin moves to
the #583 commit with this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ieivanov added a commit to czbiohub-sf/biahub that referenced this pull request Oct 16, 2026
mehta-lab/waveorder#583 is on waveorder main as 8d79650, so move the pin
off the PR branch commit. waveorder's own uv sources pin iohub to b7bc648
(the #476 merge) while biahub needs d1f9e89 (adds #479); uv refuses two
git URLs for one package, so override iohub to biahub's rev for the whole
graph. Relock.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
ieivanov added a commit to czbiohub-sf/biahub that referenced this pull request Oct 16, 2026
…ovenance key per reconstruction (#387)

* feat(reconstruct): resume interrupted apply-inv-tf positions

A reconstruct task preempted or killed at walltime recomputed every
timepoint of its position on retry; flat-field, deskew and assemble resume
per unit. waveorder now runs apply-inv-tf through iohub's
process_single_position and takes resume= (mehta-lab/waveorder#583), so:

- `biahub apply-inv-tf` and `biahub reconstruct` take @resume()
  (--resume/--no-resume, default off) and pass it to
  apply_inverse_transfer_function_single_position. waveorder computes the
  resume token itself, from the reconstruction settings and the transfer
  function, so a changed config or a recomputed transfer function
  recomputes instead of being skipped.
- The reconstruct Nextflow module passes --resume, as deskew, flat-field and
  assemble do.
- waveorder is pinned to #583 (which also carries #582's shared transfer
  function) until a release contains both.
- The CLI-consistency canon lists @resume() in the decorator stack and says
  which commands take it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* feat(reconstruct)!: record reconstruction provenance under one key per reconstruction

BREAKING CHANGE: reconstruct output positions record their settings under
top-level zattrs["waveorder-<group>"] keys (waveorder-Phase3D,
waveorder-Birefringence, waveorder-GFP_Density3D, ...) instead of
zattrs["waveorder"][<output channel names>], following
mehta-lab/waveorder#583.

_init_output_plate recorded {"waveorder": {group: settings}} through
create_empty_plate(extra_metadata=...), which overwrites the whole top-level
key, so a second config appending channels to a plate dropped the first
config's settings. It now records waveorder's own
get_reconstruction_provenance(settings): one key per reconstruction, beside
the biahub-<step> keys, so each config overwrites only its own entry.

PROVENANCE_METADATA_KEYS carries waveorder-* forward, and keeps the nested
waveorder key so older stores still carry theirs. The waveorder pin moves to
the #583 commit with this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* build(deps): pin waveorder to the merged mehta-lab/waveorder#583

mehta-lab/waveorder#583 is on waveorder main as 8d79650, so move the pin
off the PR branch commit. waveorder's own uv sources pin iohub to b7bc648
(the #476 merge) while biahub needs d1f9e89 (adds #479); uv refuses two
git URLs for one package, so override iohub to biahub's rev for the whole
graph. Relock.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
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.

Reconstruct drives its own write loop, so it cannot resume interrupted work

4 participants