Skip to content

Commit

Permalink
feat: remove save logic and use pymmcore-plus writer from MDAWidget
Browse files Browse the repository at this point in the history
  • Loading branch information
fdrgsp committed Feb 18, 2024
1 parent fd788a4 commit 671e40e
Show file tree
Hide file tree
Showing 7 changed files with 14 additions and 198 deletions.
9 changes: 0 additions & 9 deletions src/napari_micromanager/_gui_objects/_mda_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,10 @@ def value(self) -> MDASequence:
sequence = super().value()
widget_meta = sequence.metadata.get(MMCORE_WIDGETS_META, {})
split = self.checkBox_split_channels.isChecked() and len(sequence.channels) > 1

sequence.metadata[SEQUENCE_META_KEY] = SequenceMeta(
mode="mda",
split_channels=bool(split),
save_dir=widget_meta.get("save_dir", ""),
file_name=widget_meta.get("save_name", ""),
should_save=bool("save_dir" in widget_meta),
)
return sequence # type: ignore[no-any-return]

Expand All @@ -60,12 +57,6 @@ def setValue(self, value: MDASequence) -> None:
nmm_meta = SequenceMeta(**nmm_meta)
if not isinstance(nmm_meta, SequenceMeta): # pragma: no cover
raise TypeError(f"Expected {SequenceMeta}, got {type(nmm_meta)}")

# update pymmcore_widgets metadata if SequenceMeta are provided
widgets_meta = value.metadata.setdefault(MMCORE_WIDGETS_META, {})
widgets_meta.setdefault("save_dir", nmm_meta.save_dir)
widgets_meta.setdefault("save_name", nmm_meta.file_name)

# set split_channels checkbox
self.checkBox_split_channels.setChecked(bool(nmm_meta.split_channels))
super().setValue(value)
30 changes: 8 additions & 22 deletions src/napari_micromanager/_mda_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from superqt.utils import create_worker, ensure_main_thread

from ._mda_meta import SEQUENCE_META_KEY, SequenceMeta
from ._saving import save_sequence

if TYPE_CHECKING:
from uuid import UUID
Expand Down Expand Up @@ -53,6 +52,9 @@ class LayerMeta(TypedDict):
translate: NotRequired[bool]


EXP = "Exp"


# NOTE: import from pymmcore-plus when new version will be released:
# from pymmcore_plus.mda.handlers._util import get_full_sequence_axes
def get_full_sequence_axes(sequence: MDASequence) -> tuple[str, ...]:
Expand Down Expand Up @@ -144,7 +146,7 @@ def _on_mda_started(self, sequence: MDASequence) -> None:
dtype=dtype,
chunks=tuple([1] * len(shape) + yx_shape), # VERY IMPORTANT FOR SPEED!
)
fname = meta.file_name if meta.should_save else "Exp"
fname = meta.file_name or EXP
self._create_empty_image_layer(z, f"{fname}_{id_}", sequence, **kwargs)

# store the zarr array and temporary directory for later cleanup
Expand All @@ -162,8 +164,6 @@ def _on_mda_started(self, sequence: MDASequence) -> None:
self._watch_mda,
_start_thread=True,
_connect={"yielded": self._update_viewer_dims},
# NOTE: once we have a proper writer, we can add here:
# "finished": self._process_remaining_frames
)

# Set the viewer slider on the first layer frame
Expand Down Expand Up @@ -237,28 +237,14 @@ def _reset_viewer_dims(self) -> None:

def _on_mda_finished(self, sequence: MDASequence) -> None:
self._mda_running = False
self._process_remaining_frames()

# NOTE: this will be REMOVED when using proper WRITER (e.g.
# https://github.com/pymmcore-plus/pymmcore-MDA-writers or
# https://github.com/fdrgsp/pymmcore-MDA-writers/tree/update_writer). See the
# comment in _process_remaining_frames for more details.
self._process_remaining_frames(sequence)

def _process_remaining_frames(self, sequence: MDASequence) -> None:
def _process_remaining_frames(self) -> None:
"""Process any remaining frames after the MDA has finished."""
# NOTE: when switching to a proper wtiter to save files, this method will not
# have the sequence argument, it will not be called by `_on_mda_finished` but we
# can link it to the self._io_t.finished signal ("finished": self._process_
# remaining_frames) and the saving code below will be removed.
self._reset_viewer_dims()
while self._deck:
self._process_frame(*self._deck.pop())

# to remove when using proper writer
if (meta := sequence.metadata.get(SEQUENCE_META_KEY)) is not None:
sequence = cast("ActiveMDASequence", sequence)
save_sequence(sequence, self.viewer.layers, meta)

def _create_empty_image_layer(
self,
arr: zarr.Array,
Expand Down Expand Up @@ -344,7 +330,7 @@ def _determine_sequence_layers(
`[('3670fc63-c570-4920-949f-16601143f2e3', [4, 2, 4], {})]`
"""
# if we got to this point, sequence.metadata[SEQUENCE_META_KEY] should exist
meta = sequence.metadata["napari_mm_sequence_meta"]
meta = sequence.metadata[SEQUENCE_META_KEY] # type: ignore

# these are all the layers we're going to create
# each item is a tuple of (id, shape, layer_metadata)
Expand Down Expand Up @@ -407,7 +393,7 @@ def _id_idx_layer(event: ActiveMDAEvent) -> tuple[str, tuple[int, ...], str]:
axis_order = list(get_full_sequence_axes(event.sequence))

suffix = ""
prefix = meta.file_name if meta.should_save else "Exp"
prefix = meta.file_name or EXP

if meta.split_channels and event.channel:
suffix = f"_{event.channel.config}_{event.index['c']:03d}"
Expand Down
3 changes: 0 additions & 3 deletions src/napari_micromanager/_mda_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ class SequenceMeta:
mode: str = ""
split_channels: bool = False
file_name: str = ""
save_dir: str = ""
should_save: bool = False # to remove when using pymmcore-plus writers
save_pos: bool = False # to remove when using pymmcore-plus writers

def replace(self, **kwargs: Any) -> SequenceMeta:
"""Return a new SequenceMeta with the given kwargs replaced."""
Expand Down
102 changes: 0 additions & 102 deletions src/napari_micromanager/_saving.py

This file was deleted.

2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def main_window(core: CMMCorePlus, make_napari_viewer):

@pytest.fixture(params=MDAS, ids=MDA_IDS)
def mda_sequence(request: pytest.FixtureRequest) -> useq.MDASequence:
seq_meta = SequenceMeta(mode="mda", file_name="test_mda", should_save=True)
seq_meta = SequenceMeta(mode="mda", file_name="test_mda")
return useq.MDASequence(**request.param, metadata={SEQUENCE_META_KEY: seq_meta})


Expand Down
6 changes: 5 additions & 1 deletion tests/test_layer_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ def test_layer_scale(

sequence = mda_sequence_splits.replace(
axis_order=axis_order,
metadata={SEQUENCE_META_KEY: SequenceMeta(should_save=False)},
)
z_step = sequence.z_plan and sequence.z_plan.step

# the MDASequence 'replace' switchesp the metadata to a dict, here we switch it back
meta = sequence.metadata[SEQUENCE_META_KEY]
meta = SequenceMeta(**meta)
sequence = sequence.replace(metadata={SEQUENCE_META_KEY: meta})

# create zarr layer
handler._on_mda_started(sequence)

Expand Down
60 changes: 0 additions & 60 deletions tests/test_multid_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@

from typing import TYPE_CHECKING

from napari_micromanager._gui_objects._mda_widget import MultiDWidget
from napari_micromanager._mda_meta import SEQUENCE_META_KEY, SequenceMeta
from pymmcore_plus.mda import MDAEngine
from useq import MDASequence

if TYPE_CHECKING:
from pathlib import Path

from napari_micromanager.main_window import MainWindow
from pytestqt.qtbot import QtBot
Expand All @@ -29,63 +26,6 @@ def test_main_window_mda(main_window: MainWindow):
assert main_window.viewer.layers[-1].data.nchunks_initialized == 32


def test_saving_mda(
qtbot: QtBot,
main_window: MainWindow,
mda_sequence_splits: MDASequence,
tmp_path: Path,
) -> None:
mda = mda_sequence_splits
main_window._show_dock_widget("MDA")
mda_widget = main_window._dock_widgets["MDA"].widget()
assert isinstance(mda_widget, MultiDWidget)

# FIXME:
# we have a bit of a battle here now for file-saving metadata between
# pymmcore_widgets and napari_micromanager's SequenceMetadata
# should standardize, possibly by adding to useq-schema
# this test uses the pymmcore-widgets metadata for now
widget_meta = mda.metadata.setdefault("pymmcore_widgets", {})
widget_meta["save_dir"] = str(tmp_path)
widget_meta["should_save"] = True

mda_widget.setValue(mda)
assert mda_widget.save_info.isChecked()
meta = mda_widget.value().metadata[SEQUENCE_META_KEY]
assert meta.save_dir == str(tmp_path)
# using `metadata=mda.metadata` here to keep the SequenceMeta object
# rather than a serialized dict... this is a hack to get around a broader issue
# with the .replace method
mda = mda.replace(axis_order=mda_widget.value().axis_order, metadata=mda.metadata)
mmc = main_window._mmc

# re-register twice to fully exercise the logic of the update
# functions - the initial connections are made in init
# then after that they are fully handled by the _update_mda_engine
# callbacks
mmc.register_mda_engine(MDAEngine(mmc))
mmc.register_mda_engine(MDAEngine(mmc))

# make the images non-square
mmc.setProperty("Camera", "OnCameraCCDYSize", 500)
with qtbot.waitSignal(mmc.mda.events.sequenceFinished, timeout=8000):
mda_widget.control_btns.run_btn.click()

data_shape = [x for x in main_window.viewer.layers[-1].data.shape if x > 1]
expected_shape = [x for x in (*mda.shape, 500, 512) if x > 1]

multiC = len(mda.channels) > 1
splitC = mda.metadata[SEQUENCE_META_KEY].split_channels
if multiC and splitC:
expected_shape.pop(mda.used_axes.find("c"))
nfiles = len(list((tmp_path / f"{meta.file_name}_000").iterdir()))
assert nfiles == 2 if multiC else 1
# splitC with one channel is the same as not split
else:
assert [p.name for p in tmp_path.iterdir()] == [f"{meta.file_name}_000.tif"]
assert data_shape == expected_shape


def test_script_initiated_mda(main_window: MainWindow, qtbot: QtBot):
# we should show the mda even if it came from outside
mmc = main_window._mmc
Expand Down

0 comments on commit 671e40e

Please sign in to comment.