Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-blocking IO for ImageTool manager #80

Merged
merged 17 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 13 additions & 17 deletions docs/source/user-guide/kconv.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@
"eplt.plot_array(cut)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Converting to momentum space\n",
"----------------------------"
]
},
{
"cell_type": "raw",
"metadata": {
Expand All @@ -212,18 +220,11 @@
}
},
"source": [
"Although the functions for momentum conversion are implemented in\n",
":mod:`erlab.analysis.kspace`\\ , the actual conversion is performed using an `xarray\n",
"accessor <https://docs.xarray.dev/en/stable/internals/extending-xarray.html>`_. Let's\n",
"see how it works."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Converting to momentum space\n",
"----------------------------"
"Momentum conversion is done by the :meth:`convert\n",
"<erlab.accessors.kspace.MomentumAccessor.convert>` method of the :meth:`DataArray.kspace\n",
"<erlab.accessors.kspace.MomentumAccessor>` accessor. The bounds and resolution are\n",
"automatically determined from the data if no input is provided. The method returns a new\n",
"DataArray in momentum space."
]
},
{
Expand All @@ -240,11 +241,6 @@
}
},
"source": [
"Momentum conversion is done by the :meth:`convert\n",
"<erlab.accessors.kspace.MomentumAccessor.convert>` method of the ``kspace`` accessor.\n",
"The bounds and resolution are automatically determined from the data if no input is\n",
"provided. The method returns a new DataArray in momentum space.\n",
"\n",
".. note ::\n",
"\n",
" For momentum conversion to work properly, the data must follow the conventions\n",
Expand Down
2 changes: 1 addition & 1 deletion src/erlab/analysis/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def shift(
arr = out[slices]
shifts: list[float] = [0.0] * arr.ndim
shift_val: float = float(shift.isel(dict(zip(shift.dims, idxs, strict=True))))
shifts[cast(int, arr.get_axis_num(along))] = shift_val
shifts[arr.get_axis_num(along)] = shift_val

# Apply shift
out[slices] = scipy.ndimage.shift(arr.values, shifts, **shift_kwargs)
Expand Down
1 change: 0 additions & 1 deletion src/erlab/interactive/fermiedge.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ def abort_fit(self) -> None:
self.parallel_obj._aborting = True
self.parallel_obj._exception = True

@erlab.interactive.utils._coverage_resolve_trace
def run(self) -> None:
self.sigIterated.emit(0)
with erlab.utils.parallel.joblib_progress_qt(self.sigIterated) as _:
Expand Down
28 changes: 26 additions & 2 deletions src/erlab/interactive/imagetool/_mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@

def __init__(self, data=None, **kwargs) -> None:
super().__init__(data, **kwargs)
self._recent_name_filter: str | None = None
self._recent_directory: str | None = None
self.__recent_name_filter: str | None = None
self.__recent_directory: str | None = None

self.initialize_actions()
self.setMenuBar(ItoolMenuBar(self))
Expand All @@ -295,6 +295,30 @@
self._update_title()
self.slicer_area.installEventFilter(self)

@property
def _recent_name_filter(self) -> str | None:
if self.slicer_area._manager_instance is not None:
return self.slicer_area._manager_instance._recent_name_filter

Check warning on line 301 in src/erlab/interactive/imagetool/_mainwindow.py

View check run for this annotation

Codecov / codecov/patch

src/erlab/interactive/imagetool/_mainwindow.py#L301

Added line #L301 was not covered by tests
return self.__recent_name_filter

@_recent_name_filter.setter
def _recent_name_filter(self, value: str | None) -> None:
if self.slicer_area._manager_instance is not None:
self.slicer_area._manager_instance._recent_name_filter = value

Check warning on line 307 in src/erlab/interactive/imagetool/_mainwindow.py

View check run for this annotation

Codecov / codecov/patch

src/erlab/interactive/imagetool/_mainwindow.py#L307

Added line #L307 was not covered by tests
self.__recent_name_filter = value

@property
def _recent_directory(self) -> str | None:
if self.slicer_area._manager_instance is not None:
return self.slicer_area._manager_instance._recent_directory

Check warning on line 313 in src/erlab/interactive/imagetool/_mainwindow.py

View check run for this annotation

Codecov / codecov/patch

src/erlab/interactive/imagetool/_mainwindow.py#L313

Added line #L313 was not covered by tests
return self.__recent_directory

@_recent_directory.setter
def _recent_directory(self, value: str | None) -> None:
if self.slicer_area._manager_instance is not None:
self.slicer_area._manager_instance._recent_directory = value

Check warning on line 319 in src/erlab/interactive/imagetool/_mainwindow.py

View check run for this annotation

Codecov / codecov/patch

src/erlab/interactive/imagetool/_mainwindow.py#L319

Added line #L319 was not covered by tests
self.__recent_directory = value

def initialize_actions(self) -> None:
self.open_act = QtWidgets.QAction("&Open...", self)
self.open_act.setShortcut(QtGui.QKeySequence.StandardKey.Open)
Expand Down
72 changes: 67 additions & 5 deletions src/erlab/interactive/imagetool/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
levels: NotRequired[tuple[float, float]]


class PlotItemState(TypedDict):
"""A dictionary containing the state of a `PlotItem` instance."""

vb_aspect_locked: bool | float
vb_x_inverted: bool
vb_y_inverted: bool


class ImageSlicerState(TypedDict):
"""A dictionary containing the state of an `ImageSlicerArea` instance."""

Expand All @@ -63,6 +71,7 @@
cursor_colors: list[str]
file_path: NotRequired[str | None]
splitter_sizes: NotRequired[list[list[int]]]
plotitem_states: NotRequired[list[PlotItemState]]


suppressnanwarning = np.testing.suppress_warnings()
Expand Down Expand Up @@ -644,6 +653,7 @@
"splitter_sizes": self.splitter_sizes,
"file_path": str(self._file_path) if self._file_path is not None else None,
"cursor_colors": [c.name() for c in self.cursor_colors],
"plotitem_states": [p._serializable_state for p in self.axes],
}

@state.setter
Expand All @@ -668,6 +678,11 @@
self._file_path = pathlib.Path(file_path)
self.sigDataChanged.emit()

plotitem_states = state.get("plotitem_states", None)
if plotitem_states is not None:
for ax, plotitem_state in zip(self.axes, plotitem_states, strict=True):
ax._serializable_state = plotitem_state

# Restore colormap settings
try:
self.set_colormap(**state.get("color", {}), update=True)
Expand Down Expand Up @@ -822,13 +837,15 @@
curr_state.pop("splitter_sizes", None)

if last_state is None or last_state != curr_state:
# Only store state if it has changed
self._prev_states.append(curr_state)
self._next_states.clear()
self.sigHistoryChanged.emit()

@QtCore.Slot()
@suppress_history
def flush_history(self) -> None:
"""Clear the undo and redo history."""
self._prev_states.clear()
self._next_states.clear()
self.sigHistoryChanged.emit()
Expand All @@ -837,6 +854,7 @@
@link_slicer
@suppress_history
def undo(self) -> None:
"""Undo the most recent action."""
if not self.undoable:
return
self._next_states.append(self.state)
Expand All @@ -847,13 +865,15 @@
@link_slicer
@suppress_history
def redo(self) -> None:
"""Redo the most recently undone action."""
if not self.redoable:
return
self._prev_states.append(self.state)
self.state = self._next_states.pop()
self.sigHistoryChanged.emit()

def initialize_actions(self) -> None:
"""Initialize :class:`QtWidgets.QAction` instances."""
self.view_all_act = QtWidgets.QAction("View &All", self)
self.view_all_act.setShortcut("Ctrl+A")
self.view_all_act.triggered.connect(self.view_all)
Expand Down Expand Up @@ -925,17 +945,30 @@
)

@QtCore.Slot()
def history_changed(self) -> None:
def _history_changed(self) -> None:
"""Enable undo and redo actions based on the current history.

This slot is triggered when the history changes.
"""
self.undo_act.setEnabled(self.undoable)
self.redo_act.setEnabled(self.redoable)

@QtCore.Slot()
def cursor_count_changed(self) -> None:
def _cursor_count_changed(self) -> None:
"""Enable or disable the remove cursor action based on the number of cursors.

This slot is triggered when the number of cursors changes.
"""
self.rem_cursor_act.setDisabled(self.n_cursors == 1)
self.refresh_colormap()

@QtCore.Slot()
def refresh_actions_enabled(self) -> None:
"""Refresh the enabled state of miscellaneous actions.

This slot is triggered from the parent widget when the menubar containing the
actions is about to be shown.
"""
self.ktool_act.setEnabled(self.data.kspace._interactive_compatible)

def connect_axes_signals(self) -> None:
Expand All @@ -948,8 +981,8 @@

def connect_signals(self) -> None:
self.connect_axes_signals()
self.sigHistoryChanged.connect(self.history_changed)
self.sigCursorCountChanged.connect(self.cursor_count_changed)
self.sigHistoryChanged.connect(self._history_changed)
self.sigCursorCountChanged.connect(self._cursor_count_changed)
self.sigDataChanged.connect(self.refresh_all)
self.sigShapeChanged.connect(self.refresh_all)
self.sigWriteHistory.connect(self.write_state)
Expand Down Expand Up @@ -1433,6 +1466,12 @@
self._colorbar.setVisible(self.levels_locked)
self.sigViewOptionChanged.emit()

@property
def _manager_instance(
self,
) -> erlab.interactive.imagetool.manager.ImageToolManager | None:
return erlab.interactive.imagetool.manager._manager_instance

def add_tool_window(self, widget: QtWidgets.QWidget) -> None:
"""Save a reference to an additional window widget.

Expand All @@ -1459,7 +1498,7 @@
widget.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose)

if self._in_manager:
manager = erlab.interactive.imagetool.manager._manager_instance
manager = self._manager_instance
if manager:
manager.add_widget(widget)
return
Expand Down Expand Up @@ -1987,6 +2026,29 @@

self._rotate_action = QtWidgets.QAction("Apply Rotation")

@property
def _serializable_state(self) -> PlotItemState:
"""Subset of the state of the underlying viewbox that should be restorable."""
vb = self.getViewBox()
return {
"vb_aspect_locked": vb.state["aspectLocked"],
"vb_x_inverted": vb.state["xInverted"],
"vb_y_inverted": vb.state["yInverted"],
}

@_serializable_state.setter
def _serializable_state(self, state: PlotItemState) -> None:
vb = self.getViewBox()

locked = state["vb_aspect_locked"]
if isinstance(locked, bool):
vb.setAspectLocked(locked)
else:
vb.setAspectLocked(True, ratio=locked)

Check warning on line 2047 in src/erlab/interactive/imagetool/core.py

View check run for this annotation

Codecov / codecov/patch

src/erlab/interactive/imagetool/core.py#L2047

Added line #L2047 was not covered by tests

vb.invertX(state["vb_x_inverted"])
vb.invertY(state["vb_y_inverted"])

def _get_axis_dims(self, uniform: bool) -> tuple[str | None, ...]:
dim_list: list[str] = [
str(self.slicer_area.data.dims[ax]) for ax in self.display_axis
Expand Down
Loading
Loading