Skip to content

Commit

Permalink
Enforce movement to excluded regions (#78)
Browse files Browse the repository at this point in the history
* add to benchtop_run.toml exclusion layers

* perform an excluded check before motioning the motion group

* add messages to the ValueErrors raised by DividerExclusion._validate_inputs()

* mark any point outside the motion space as excluded

* use the parent argument in __init__ instead of self.parent()

* use the currentIndex() method, and not the property

* create message box class MSpaceMessageBox

* supply log entry if probe drive target position is not executed since the probe drive is still moving

* fix log string

* add clarifying comments and annotations

* in MGWidget._validate_motion_group() handle case where motion group has yet to be defined

* incorporate MSpaceMessageBox indo DriveControlWidget and AxisControlWidget
  • Loading branch information
rocco8773 authored Jan 15, 2025
1 parent f894e38 commit fceb085
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 20 deletions.
9 changes: 9 additions & 0 deletions bapsf_motion/actors/motion_group_.py
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,15 @@ def move_to(self, pos, axis: Optional[int] = None):
if isinstance(pos, u.Quantity):
pos = pos.value

if isinstance(self.mb, MotionBuilder):
if self.mb.is_excluded(pos):
self.logger.error(
f"The requested position {pos} for motion group "
f"'{self.name}' is in an excluded region of the "
f"motion space. NOT MOVEMENT PREFORMED!!"
)
return

dr_pos = self.transform(pos, to_coords="drive").squeeze()
return self.drive.move_to(pos=dr_pos, axis=axis)

Expand Down
12 changes: 12 additions & 0 deletions bapsf_motion/examples/benchtop_run.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ space.1.num = 51
layer.0.type = "grid"
layer.0.limits = [[-3, 3], [-3, 3]]
layer.0.steps = [7, 7]
exclusion.0.type = "divider"
exclusion.0.mb = ["inf", -4.0]
exclusion.0.exclude = "-e0"
exclusion.1.type = "divider"
exclusion.1.mb = ["inf", 4.0]
exclusion.1.exclude = "+e0"
exclusion.2.type = "divider"
exclusion.2.mb = [0.0, -4.0]
exclusion.2.exclude = "-e1"
exclusion.3.type = "divider"
exclusion.3.mb = [0.0, 4.0]
exclusion.3.exclude = "+e1"

[run.mg.transform]
type = "identity"
12 changes: 6 additions & 6 deletions bapsf_motion/gui/configure/drive_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,15 +389,15 @@ def __init__(self, mg: MotionGroup, parent: "mgw.MGWidget" = None):
if isinstance(self.mg, MotionGroup) and isinstance(self.mg.drive, Drive):
self.mg.drive.terminate(delay_loop_stop=True)
_drive_config = _deepcopy_dict(self.mg.drive.config)
elif not isinstance(self.parent(), mgw.MGWidget):
elif not isinstance(parent, mgw.MGWidget):
pass
elif self.parent().drive_dropdown.currentText != "Custom Drive":
index = self.parent().drive_dropdown.currentIndex
elif parent.drive_dropdown.currentText != "Custom Drive":
index = parent.drive_dropdown.currentIndex()
_drive_config = _deepcopy_dict(
self.parent().drive_defaults[index][1]
parent.drive_defaults[index][1]
)
elif "drive" in self.parent()._initial_mg_config:
_drive_config = _deepcopy_dict(self.parent()._initial_mg_config["drive"])
elif "drive" in parent._initial_mg_config:
_drive_config = _deepcopy_dict(parent._initial_mg_config["drive"])

self.drive_config = _drive_config

Expand Down
126 changes: 115 additions & 11 deletions bapsf_motion/gui/configure/motion_group_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from PySide6.QtCore import Qt, Signal, Slot, QSize
from PySide6.QtGui import QDoubleValidator
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QSizePolicy,
QTextEdit,
QVBoxLayout,
Expand All @@ -37,6 +39,71 @@
from bapsf_motion.utils import units as u


class MSpaceMessageBox(QMessageBox):
"""
Modal warning dialog box to warn the user the motion space has yet
to be defined. Thus, there are no restrictions on probe drive
movement, and it is up to the user to prevent any collisions.
"""
def __init__(self, parent: QWidget):
super().__init__(parent)

self._display_dialog = True

self.setWindowTitle("Motion Space NOT Defined")
self.setText(
"Motion Space is NOT defined, so there are no restrictions "
"on probe drive motion. It is up to the user to avoid "
"collisions.\n\n"
"Proceed with movement?"
)
self.setIcon(QMessageBox.Icon.Warning)
self.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Abort
)
self.setDefaultButton(QMessageBox.StandardButton.Abort)

_cb = QCheckBox("Suppress future warnings for this motion group.")
self.setCheckBox(_cb)

self.checkBox().checkStateChanged.connect(self._update_display_dialog)

@property
def display_dialog(self) -> bool:
return self._display_dialog

@display_dialog.setter
def display_dialog(self, value: bool) -> None:
if not isinstance(value, bool):
return

# ensure the display boolean (display_dialog) is in sync
# with the dialog check box ... these two values are supposed
# to be NOTs of each other
check_state = self.checkBox().checkState()
if check_state is Qt.CheckState.Checked is value:
self.checkBox().setChecked(not value)

self._display_dialog = value

@Slot(Qt.CheckState)
def _update_display_dialog(self, state: Qt.CheckState) -> None:
self.display_dialog = not (state is Qt.CheckState.Checked)

def exec(self) -> bool:
if not self.display_dialog:
return True

button = super().exec()

if button == QMessageBox.StandardButton.Yes:
# Make sure the Abort button always remains the default choice
self.setDefaultButton(QMessageBox.StandardButton.Abort)
return True
elif button == QMessageBox.StandardButton.Abort:
return False


class AxisControlWidget(QWidget):
axisLinked = Signal()
axisUnlinked = Signal()
Expand Down Expand Up @@ -122,6 +189,10 @@ def __init__(self, parent=None):

# Define ADVANCED WIDGETS

self.mspace_warning_dialog = None
if isinstance(parent, DriveControlWidget):
self.mspace_warning_dialog = parent.mspace_warning_dialog

self.setLayout(self._define_layout())
self._connect_signals()

Expand Down Expand Up @@ -206,13 +277,22 @@ def _jog_backward(self):
self._move_to(pos)

def _move_to(self, target_ax_pos):
target_pos = self.mg.position.value
target_pos[self.axis_index] = target_ax_pos

if self.mg.drive.is_moving:
self.logger.info(
"Probe drive is currently moving. Did NOT perform move "
f"to {target_pos}."
)
return

position = self.mg.position.value
position[self.axis_index] = target_ax_pos
proceed = True
if not isinstance(self.mg.mb, MotionBuilder):
proceed = self.mspace_warning_dialog.exec()

self.mg.move_to(position)
if proceed:
self.mg.move_to(target_pos)

def _update_display_of_axis_status(self):
if self._mg.terminated:
Expand Down Expand Up @@ -382,6 +462,8 @@ def __init__(self, parent=None):
# Define TEXT WIDGETS
# Define ADVANCED WIDGETS

self.mspace_warning_dialog = MSpaceMessageBox(parent=self)

self.setLayout(self._define_layout())
self._connect_signals()

Expand Down Expand Up @@ -474,7 +556,20 @@ def _move_to(self):
for acw in self._axis_control_widgets
if not acw.isHidden()
]
self.mg.move_to(target_pos)

if self.mg.drive.is_moving:
self.logger.info(
"Probe drive is currently moving. Did NOT perform move "
f"to {target_pos}."
)
return

proceed = True
if not isinstance(self.mg.mb, MotionBuilder):
proceed = self.mspace_warning_dialog.exec()

if proceed:
self.mg.move_to(target_pos)

def _stop_move(self):
self.mg.stop()
Expand Down Expand Up @@ -794,7 +889,7 @@ def __init__(

if "name" not in self._mg_config or self._mg_config["name"] == "":
self._mg_config["name"] = "A New MG"
self.logger.info(f"starting _mg_config: {self._mg_config}")
self.logger.info(f"starting mg_config:\n {self._mg_config}")
self._update_mg_name_widget()

self._spawn_motion_group()
Expand Down Expand Up @@ -922,8 +1017,10 @@ def _define_central_builder_layout(self):
def _define_mspace_display_layout(self):
...

def _build_drive_defaults(self):

def _build_drive_defaults(self) -> List[Tuple[str, Dict[str, Any]]]:
# Returned _drive_defaults is a List of Tuple pairs
# - 1st Tuple element is the dropdown name
# - 2nd Tuple element is the dictionary configuration
if self._defaults is None or "drive" not in self._defaults:
self._drive_defaults = [("Custom Drive", {})]
return self._drive_defaults
Expand Down Expand Up @@ -1503,16 +1600,23 @@ def _spawn_motion_group(self):

def _validate_motion_group(self):
self.logger.info("Validating motion group")
vmg_name = self._validate_motion_group_name()

vmg_name = self._validate_motion_group_name()
vdrive = self._validate_drive()

if not isinstance(self.mg.mb, MotionBuilder):
if not isinstance(self.mg, MotionGroup):
mb = None
transform = None
else:
mb = self.mg.mb
transform = self.mg.transform

if not isinstance(mb, MotionBuilder):
self.mb_btn.set_invalid()
self.mb_btn.setToolTip("Motion space needs to be defined.")
self.done_btn.setEnabled(False)
else:
if "layer" not in self.mg.mb.config:
if "layer" not in mb.config:
self.mb_btn.set_invalid()
self.mb_btn.setToolTip(
"A point layer needs to be defined to generate a motion list."
Expand All @@ -1521,7 +1625,7 @@ def _validate_motion_group(self):
self.mb_btn.set_valid()
self.mb_btn.setToolTip("")

if not isinstance(self.mg.transform, BaseTransform):
if not isinstance(transform, BaseTransform):
self.transform_btn.set_invalid()
self.done_btn.setEnabled(False)

Expand Down
13 changes: 12 additions & 1 deletion bapsf_motion/motion_builder/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,18 @@ def is_excluded(self, point) -> bool:

select = {}
for ii, dim_name in enumerate(self.mspace_dims):
select[dim_name] = point[ii]
_res = self.mask_resolution[ii]
_coord = self.mspace_coords[dim_name]
_point = point[ii]

if (
_point < (np.min(_coord) - 0.5 * _res)
or _point > (np.max(_coord) + 0.5 * _res)
):
# point is outside the motion space
return True

select[dim_name] = _point

return not bool(self.mask.sel(method="nearest", **select).data)

Expand Down
35 changes: 33 additions & 2 deletions bapsf_motion/motion_builder/exclusions/divider.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import numbers
import numpy as np
import re
import warnings
import xarray as xr

from typing import Tuple

from bapsf_motion.motion_builder.exclusions.base import BaseExclusion
from bapsf_motion.motion_builder.exclusions.helpers import register_exclusion
from bapsf_motion.utils.exceptions import ConfigurationWarning


@register_exclusion
Expand Down Expand Up @@ -155,9 +157,38 @@ def _validate_inputs(self):
sign, axis = self._exclude_sign_and_axis()

if np.isinf(self.mb[0]) and axis == 1:
raise ValueError
warnings.warn(
f"Received an infinite slope and an excluded reference "
f"axis 1, assuming an excluded reference axis 0 was intended.",
category=ConfigurationWarning,
)
self.inputs["exclude"] = f"{sign}e0"

# double check correction was made
_s, _a = self._exclude_sign_and_axis()
if _s != sign and _a != 0:
raise ValueError(
"The excluded reference axis string is malformed. "
"Received an infinite slope and an excluded reference "
"axis 1, expected reference axis 0."
)

elif self.mb[0] == 0 and axis == 0:
raise ValueError
warnings.warn(
f"Received a zero slope and an excluded reference "
f"axis 0, assuming an excluded reference axis 1 was intended.",
category=ConfigurationWarning,
)
self.inputs["exclude"] = f"{sign}e1"

# double check correction was made
_s, _a = self._exclude_sign_and_axis()
if _s != sign and _a != 1:
raise ValueError(
"The excluded reference axis string is malformed. "
"Received a zero slope and an excluded reference "
"axis 0, expected reference axis 1."
)

def _generate_exclusion(self):
"""
Expand Down

0 comments on commit fceb085

Please sign in to comment.