Skip to content

Commit

Permalink
Merge pull request #80 from kmnhan/manager-threadedio
Browse files Browse the repository at this point in the history
  • Loading branch information
kmnhan authored Jan 4, 2025
2 parents d9524c1 + 510f473 commit 8845851
Show file tree
Hide file tree
Showing 18 changed files with 2,728 additions and 2,322 deletions.
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 @@ class ImageTool(BaseImageTool):

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 @@ def __init__(self, data=None, **kwargs) -> None:
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
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
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
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
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 @@ class ColorMapState(TypedDict):
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 @@ class ImageSlicerState(TypedDict):
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 @@ def state(self) -> ImageSlicerState:
"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 @@ def state(self, state: ImageSlicerState) -> None:
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 @@ def write_state(self) -> None:
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 @@ def flush_history(self) -> None:
@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 @@ def undo(self) -> None:
@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 @@ def initialize_actions(self) -> None:
)

@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 disconnect_axes_signals(self) -> None:

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 @@ def lock_levels(self, lock: bool) -> None:
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 @@ def add_tool_window(self, widget: QtWidgets.QWidget) -> None:
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 @@ def _update_aspect_lock_state() -> None:

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)

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

0 comments on commit 8845851

Please sign in to comment.