Skip to content

Commit

Permalink
Ensure GovernExclusion and ConfigureGUI work together (#80)
Browse files Browse the repository at this point in the history
* use the motor signal movement_finished() to update the Axis control displays

* disable configuration controls while the probe drive is moving

* if the status of one axis changes make the other AxisControlWidgets update

* make the drive widget update its display when the configuration is changed

* fix over indent

* move all connected functions to MGWidget.configChanged into the _config_changed_handler() method

* when _update_drive_control_widget() execute _refresh_drive_control() if the drive control widget does not have the valid MG

* change default colors to be more color-blind friendly

* Make sure MGWidget is seeded with a mg name if none is given, and the tooltip is accurately produced

* add tooltips for the MotionBuilder gear btn

* do not false invalidate drive if we are editing an existing motion group

* only compare dictionaries if mg_config is not None

* move all connected functionality to signal configChanged into method _config_changed_handler()

* GovernExclusions must regenerate their mask during a global mask update

* remove unused import

* make sure we do not index self.exclusions if no exclusion have been added to the list yet

* make MBItem._determine_name() an abstract method to children can better handle their naming

* simplify plotting of global mask

* update Shadow2DExclusion._paint_mask to handle triangles where all point are on a line

* remove govern exclusion from dropdown if one is already defined
  • Loading branch information
rocco8773 authored Jan 15, 2025
1 parent 24faecd commit f894e38
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 44 deletions.
33 changes: 22 additions & 11 deletions bapsf_motion/gui/configure/motion_builder_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
)
from bapsf_motion.motion_builder import MotionBuilder
from bapsf_motion.motion_builder.layers import layer_registry
from bapsf_motion.motion_builder.exclusions import exclusion_registry
from bapsf_motion.motion_builder.exclusions import exclusion_registry, GovernExclusion
from bapsf_motion.utils import _deepcopy_dict
from bapsf_motion.utils import units as u

Expand Down Expand Up @@ -124,10 +124,7 @@ def __init__(self, mg: MotionGroup, parent: "mgw.MGWidget" = None):
def _connect_signals(self):
super()._connect_signals()

self.configChanged.connect(self.update_canvas)
self.configChanged.connect(self.update_exclusion_list_box)
self.configChanged.connect(self.update_layer_list_box)
self.configChanged.connect(self._validate_mb)
self.configChanged.connect(self._config_changed_handler)

self.add_ex_btn.clicked.connect(self._exclusion_configure_new)
self.remove_ex_btn.clicked.connect(self._exclusion_remove_from_mb)
Expand Down Expand Up @@ -612,6 +609,16 @@ def _define_params_field_widget(self, ex_or_ly, _type):

# -- WIDGET INTERACTION FUNCTIONALITY --

def _config_changed_handler(self):
# Note: none of the methods executed here should cause a
# configChanged event
self._validate_mb()

# now update displays
self.update_exclusion_list_box()
self.update_layer_list_box()
self.update_canvas()

def _exclusion_configure_new(self):
if not self._params_widget.isHidden():
self._hide_and_clear_params_widget()
Expand All @@ -621,6 +628,13 @@ def _exclusion_configure_new(self):
_available = self.exclusion_registry.get_names_by_dimensionality(
self.dimensionality
)
if self.mb.exclusions and isinstance(self.mb.exclusions[0], GovernExclusion):
# remove govern exclusion since we can only have one defined
for name in tuple(_available):
ex = self.exclusion_registry.get_exclusion(name)
if issubclass(ex, GovernExclusion):
_available.remove(name)

self._refresh_params_combo_box(_available)
self.params_combo_box.setObjectName("exclusion")

Expand Down Expand Up @@ -969,12 +983,9 @@ def update_canvas(self):
self.logger.info(f"MB config = {self.mb.config}")

self.mpl_canvas.figure.clear()
ax = self.mpl_canvas.figure.add_subplot(111)
self.mb.mask.plot(
x=self.mb.mask.dims[0],
y=self.mb.mask.dims[1],
ax=ax,
)
ax = self.mpl_canvas.figure.gca()
xdim, ydim = self.mb.mspace_dims
self.mb.mask.plot(x=xdim, y=ydim, ax=ax)

pts = self.mb.motion_list
if pts is not None:
Expand Down
2 changes: 1 addition & 1 deletion bapsf_motion/gui/configure/motion_group_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ def __init__(
deployed_ips = []
if isinstance(self._parent.rm, RunManager):
for mg in self._parent.rm.mgs.values():
if dict_equal(mg_config, mg.config):
if mg_config is not None and dict_equal(mg_config, mg.config):
# assume we are editing an existing motion group
continue

Expand Down
8 changes: 7 additions & 1 deletion bapsf_motion/motion_builder/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ def _build_initial_ds(self):

return ds

def _determine_name(self):
return self.base_name

def add_layer(self, ly_type: str, **settings):
"""
Add a "point" layer to the motion builder.
Expand Down Expand Up @@ -279,7 +282,10 @@ def add_exclusion(self, ex_type: str, **settings):

if not isinstance(exclusion, GovernExclusion):
self._exclusions.append(exclusion)
elif not isinstance(self.exclusions[0], GovernExclusion):
elif (
len(self.exclusions) == 0
or not isinstance(self.exclusions[0], GovernExclusion)
):
self._exclusions.insert(0, exclusion)
else:
warnings.warn(
Expand Down
41 changes: 27 additions & 14 deletions bapsf_motion/motion_builder/exclusions/base.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
"""Module that defines the `BaseExclusion` abstract class."""
__all__ = ["BaseExclusion", "GovernExclusion"]

import ast
import numpy as np
import re
import xarray as xr

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Union
from typing import Any, Dict, Union

from bapsf_motion.motion_builder.item import MBItem


class BaseExclusion(ABC, MBItem):
class BaseExclusion(MBItem):
"""
Abstract base class for :term:`motion exclusion` classes.
Expand Down Expand Up @@ -133,18 +134,6 @@ def inputs(self) -> Dict[str, Any]:
"""
return self._inputs

@MBItem.name.setter
def name(self, name: str):
if not self.skip_ds_add:
# The exclusion name is a part of the Dataset management,
# so we can NOT/ should NOT rename it
return
elif not isinstance(name, str):
return

self._name = name
self._name_pattern = re.compile(rf"{name}(?P<number>[0-9]+)")

@abstractmethod
def _generate_exclusion(self) -> Union[np.ndarray, xr.DataArray]:
"""
Expand All @@ -161,6 +150,27 @@ def _validate_inputs(self) -> None:
"""
...

def _determine_name(self):
try:
return self.name
except AttributeError:
# self._name has not been defined yet
pass

names = set(self._ds.data_vars.keys())
ids = []
for name in names:
_match = self.name_pattern.fullmatch(name)
if _match is not None:
ids.append(
ast.literal_eval(_match.group("number"))
)

ids = list(set(ids))
_id = 0 if not ids else ids[-1] + 1

return f"{self.base_name}{_id:d}"

def is_excluded(self, point):
"""
Check if ``point`` resides in an excluded region defined by
Expand Down Expand Up @@ -230,4 +240,7 @@ def update_global_mask(self):
f"the exclusion can not be merged into the global maks."
)

# Since GovernExclusion use the existing mask to generate its own
# mask, the exclusion must be regenerated during every global update
self.regenerate_exclusion()
self.mask[...] = self.exclusion[...]
10 changes: 10 additions & 0 deletions bapsf_motion/motion_builder/exclusions/shadow.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,16 @@ def _paint_mask(self, rays: np.ndarray) -> xr.DataArray:
triangles[:, 2, :] - triangles[:, 0, :],
triangles[:, 1, :] - triangles[:, 0, :]
)

zero_mask = denominator == 0
if np.any(zero_mask):
# denominator can be zero if all points on the triangle lie
# on a line
not_zero_mask = np.logical_not(zero_mask)
triangles = triangles[not_zero_mask, ...]
numerator = numerator[..., not_zero_mask]
denominator = denominator[not_zero_mask]

lambda_3 = numerator / denominator[None, None, ...]

# calculate lambda_2
Expand Down
20 changes: 5 additions & 15 deletions bapsf_motion/motion_builder/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import xarray as xr

from abc import ABC, abstractmethod
from typing import Hashable, Tuple

try:
Expand All @@ -16,7 +17,7 @@
ErrorOptions = str


class MBItem:
class MBItem(ABC):
r"""
A base class for any :term:`motion builder` class that will interact
with the `xarray` `~xarray.Dataset` containing the
Expand Down Expand Up @@ -155,7 +156,8 @@ def _validate_ds(ds: xr.Dataset) -> xr.Dataset:

return ds

def _determine_name(self):
@abstractmethod
def _determine_name(self) -> str:
"""
Determine the name for the motion builder item that will be used
in the `~xarray.Dataset`. This is generally the name of the
Expand All @@ -165,19 +167,7 @@ def _determine_name(self):
:attr:`name_pattern` and generate a unique :attr:`name` for
the motion builder item.
"""
try:
return self.name
except AttributeError:
# self._name has not been defined yet
pass

names = set(self._ds.data_vars.keys())
n_existing = 0
for name in names:
if self.name_pattern.fullmatch(name) is not None:
n_existing += 1

return f"{self.base_name}{n_existing + 1:d}"
...

def drop_vars(self, names: str, *, errors: ErrorOptions = "raise"):
new_ds = self._ds.drop_vars(names, errors=errors)
Expand Down
26 changes: 24 additions & 2 deletions bapsf_motion/motion_builder/layers/base.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
"""Module that defines the `BaseLayer` abstract class."""
__all__ = ["BaseLayer"]

import ast
import re
import numpy as np
import xarray as xr

from abc import ABC, abstractmethod
from abc import abstractmethod
from typing import Any, Dict, List, Union

from bapsf_motion.motion_builder.item import MBItem


class BaseLayer(ABC, MBItem):
class BaseLayer(MBItem):
"""
Abstract base class for :term:`motion layer` classes.
Expand Down Expand Up @@ -142,6 +143,27 @@ def _validate_inputs(self) -> None:
"""
...

def _determine_name(self):
try:
return self.name
except AttributeError:
# self._name has not been defined yet
pass

names = set(self._ds.data_vars.keys())
ids = []
for name in names:
_match = self.name_pattern.fullmatch(name)
if _match is not None:
ids.append(
ast.literal_eval(_match.group("number"))
)

ids = list(set(ids))
_id = 0 if not ids else ids[-1] + 1

return f"{self.base_name}{_id:d}"

def _generate_point_matrix_da(self):
"""
Generate the :term:`motion layer` array/matrix and add it to
Expand Down
10 changes: 10 additions & 0 deletions docs/notebooks/motion_list/Shadow2DExclusion.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,16 @@
" triangles[:, 2, :] - triangles[:, 0, :],\n",
" triangles[:, 1, :] - triangles[:, 0, :]\n",
" )\n",
" \n",
" zero_mask = denominator == 0\n",
" if np.any(zero_mask):\n",
" # denominator can be zero if all points on the triangle lie\n",
" # on a line\n",
" not_zero_mask = np.logical_not(zero_mask)\n",
" triangles = triangles[not_zero_mask, ...]\n",
" numerator = numerator[..., not_zero_mask]\n",
" denominator = denominator[not_zero_mask]\n",
"\n",
" lambda_3 = numerator / denominator[None, None, ...]\n",
"\n",
" # calculate lambda_2\n",
Expand Down

0 comments on commit f894e38

Please sign in to comment.