diff --git a/hexrdgui/calibration/calibration_dialog.py b/hexrdgui/calibration/calibration_dialog.py
index 99ff19c0d..a53f80578 100644
--- a/hexrdgui/calibration/calibration_dialog.py
+++ b/hexrdgui/calibration/calibration_dialog.py
@@ -20,6 +20,7 @@
)
from hexrdgui.ui_loader import UiLoader
from hexrdgui.utils.dialog import add_help_url
+from hexrdgui.utils.guess_instrument_type import guess_instrument_type
import hexrdgui.resources.calibration
@@ -65,6 +66,12 @@ def __init__(self, instr, params_dict, format_extra_params_func=None,
self.format_extra_params_func = format_extra_params_func
self.engineering_constraints = engineering_constraints
+ instr_type = guess_instrument_type(instr.detectors)
+ # Use delta boundaries by default for anything other than TARDIS
+ # and PXRDIP. We might want to change this to a whitelist later.
+ use_delta_boundaries = instr_type not in ('TARDIS', 'PXRDIP')
+ self.delta_boundaries = use_delta_boundaries
+
self.initialize_advanced_options()
self.load_tree_view_mapping()
@@ -79,6 +86,8 @@ def setup_connections(self):
self.ui.draw_picks.toggled.connect(self.on_draw_picks_toggled)
self.ui.engineering_constraints.currentIndexChanged.connect(
self.on_engineering_constraints_changed)
+ self.ui.delta_boundaries.toggled.connect(
+ self.on_delta_boundaries_toggled)
self.ui.edit_picks_button.clicked.connect(self.on_edit_picks_clicked)
self.ui.save_picks_button.clicked.connect(self.on_save_picks_clicked)
self.ui.load_picks_button.clicked.connect(self.on_load_picks_clicked)
@@ -166,6 +175,11 @@ def on_draw_picks_toggled(self, b):
self.draw_picks_toggled.emit(b)
def on_run_button_clicked(self):
+ if self.delta_boundaries:
+ # If delta boundaries are being used, set the min/max according to
+ # the delta boundaries. Lmfit requires min/max to run.
+ self.apply_delta_boundaries()
+
try:
self.validate_parameters()
except Exception as e:
@@ -182,6 +196,27 @@ def on_undo_run_button_clicked(self):
def finish(self):
self.finished.emit()
+ def apply_delta_boundaries(self):
+ # lmfit only uses min/max, not delta
+ # So if we used a delta, apply that to the min/max
+
+ if not self.delta_boundaries:
+ # We don't actually need to apply delta boundaries...
+ return
+
+ def recurse(cur):
+ for k, v in cur.items():
+ if '_param' in v:
+ param = v['_param']
+ # There should be a delta.
+ # We want an exception if it is missing.
+ param.min = param.value - param.delta
+ param.max = param.value + param.delta
+ elif isinstance(v, dict):
+ recurse(v)
+
+ recurse(self.tree_view.model().config)
+
def validate_parameters(self):
# Recursively look through the tree dict, and add on errors
config = self.tree_view.model().config
@@ -197,6 +232,11 @@ def recurse(cur):
full_path = '->'.join(path)
msg = f'{full_path}: min is greater than max'
errors.append(msg)
+ elif param.min == param.max:
+ # Slightly modify these to prevent lmfit
+ # from raising an exception.
+ param.min -= 1e-8
+ param.max += 1e-8
elif isinstance(v, dict):
recurse(v)
path.pop(-1)
@@ -237,6 +277,14 @@ def engineering_constraints(self, v):
w.setCurrentText(v)
+ @property
+ def delta_boundaries(self):
+ return self.ui.delta_boundaries.isChecked()
+
+ @delta_boundaries.setter
+ def delta_boundaries(self, b):
+ self.ui.delta_boundaries.setChecked(b)
+
def on_edit_picks_clicked(self):
self.edit_picks_clicked.emit()
@@ -263,6 +311,10 @@ def tth_distortion(self, v):
def on_engineering_constraints_changed(self):
self.engineering_constraints_changed.emit(self.engineering_constraints)
+ def on_delta_boundaries_toggled(self, b):
+ # The columns have changed, so we need to reinitialize the tree view
+ self.reinitialize_tree_view()
+
def update_from_calibrator(self, calibrator):
self.engineering_constraints = calibrator.engineering_constraints
self.tth_distortion = calibrator.tth_distortion
@@ -286,13 +338,28 @@ def tree_view_dict_of_params(self):
def create_param_item(param):
used_params.append(param.name)
- return {
+ d = {
'_param': param,
'_value': param.value,
'_vary': bool(param.vary),
- '_min': param.min,
- '_max': param.max,
}
+ if self.delta_boundaries:
+ if not hasattr(param, 'delta'):
+ # We store the delta on the param object
+ # Default the delta to the minimum of the differences
+ diffs = [
+ abs(param.min - param.value),
+ abs(param.max - param.value),
+ ]
+ param.delta = min(diffs)
+
+ d['_delta'] = param.delta
+ else:
+ d.update(**{
+ '_min': param.min,
+ '_max': param.max,
+ })
+ return d
# Treat these root keys specially
special_cases = [
@@ -395,15 +462,31 @@ def initialize_tree_view(self):
return
tree_dict = self.tree_view_dict_of_params
- self.tree_view = MultiColumnDictTreeView(tree_dict, TREE_VIEW_COLUMNS,
- parent=self.parent(),
- model_class=TreeItemModel)
+ self.tree_view = MultiColumnDictTreeView(
+ tree_dict,
+ self.tree_view_columns,
+ parent=self.parent(),
+ model_class=self.tree_view_model_class,
+ )
self.tree_view.check_selection_index = 2
self.ui.tree_view_layout.addWidget(self.tree_view)
# Make the key section a little larger
self.tree_view.header().resizeSection(0, 300)
+ def reinitialize_tree_view(self):
+ # Keep the same scroll position
+ scrollbar = self.tree_view.verticalScrollBar()
+ scroll_value = scrollbar.value()
+
+ self.ui.tree_view_layout.removeWidget(self.tree_view)
+ self.tree_view.deleteLater()
+ del self.tree_view
+ self.initialize_tree_view()
+
+ # Restore scroll bar position
+ self.tree_view.verticalScrollBar().setValue(scroll_value)
+
def update_tree_view(self):
tree_dict = self.tree_view_dict_of_params
self.tree_view.model().config = tree_dict
@@ -422,37 +505,70 @@ def clear_polar_view_tth_correction(self, show_warning=True):
QMessageBox.information(self.parent(), 'HEXRD', msg)
editor.apply_to_polar_view = False
+ @property
+ def tree_view_columns(self):
+ return self.tree_view_model_class.COLUMNS
-TREE_VIEW_COLUMNS = {
- 'Value': '_value',
- 'Vary': '_vary',
- 'Minimum': '_min',
- 'Maximum': '_max',
-}
-TREE_VIEW_COLUMN_INDICES = {
- 'Key': 0,
- **{
- k: list(TREE_VIEW_COLUMNS).index(k) + 1 for k in TREE_VIEW_COLUMNS
+ @property
+ def tree_view_model_class(self):
+ if self.delta_boundaries:
+ return DeltaTreeItemModel
+ else:
+ return DefaultTreeItemModel
+
+
+def _tree_columns_to_indices(columns):
+ return {
+ 'Key': 0,
+ **{
+ k: list(columns).index(k) + 1 for k in columns
+ }
}
-}
-VALUE_IDX = TREE_VIEW_COLUMN_INDICES['Value']
-MAX_IDX = TREE_VIEW_COLUMN_INDICES['Maximum']
-MIN_IDX = TREE_VIEW_COLUMN_INDICES['Minimum']
-BOUND_INDICES = (VALUE_IDX, MAX_IDX, MIN_IDX)
class TreeItemModel(MultiColumnDictTreeItemModel):
"""Subclass the tree item model so we can customize some behavior"""
+
+ def set_config_val(self, path, value):
+ super().set_config_val(path, value)
+ # Now set the parameter too
+ param_path = path[:-1] + ['_param']
+ try:
+ param = self.config_val(param_path)
+ except KeyError:
+ raise Exception('Failed to set parameter!', param_path)
+
+ # Now set the attribute on the param
+ attribute = path[-1].removeprefix('_')
+
+ setattr(param, attribute, value)
+
+
+class DefaultTreeItemModel(TreeItemModel):
+ """This model uses minimum/maximum for the boundary constraints"""
+ COLUMNS = {
+ 'Value': '_value',
+ 'Vary': '_vary',
+ 'Minimum': '_min',
+ 'Maximum': '_max',
+ }
+ COLUMN_INDICES = _tree_columns_to_indices(COLUMNS)
+
+ VALUE_IDX = COLUMN_INDICES['Value']
+ MAX_IDX = COLUMN_INDICES['Maximum']
+ MIN_IDX = COLUMN_INDICES['Minimum']
+ BOUND_INDICES = (VALUE_IDX, MAX_IDX, MIN_IDX)
+
def data(self, index, role):
- if role == Qt.ForegroundRole and index.column() in BOUND_INDICES:
+ if role == Qt.ForegroundRole and index.column() in self.BOUND_INDICES:
# If a value hit the boundary, color both the boundary and the
# value red.
item = self.get_item(index)
if not item.child_items:
atol = 1e-3
pairs = [
- (VALUE_IDX, MAX_IDX),
- (VALUE_IDX, MIN_IDX),
+ (self.VALUE_IDX, self.MAX_IDX),
+ (self.VALUE_IDX, self.MIN_IDX),
]
for pair in pairs:
if index.column() not in pair:
@@ -463,18 +579,30 @@ def data(self, index, role):
return super().data(index, role)
- def set_config_val(self, path, value):
- super().set_config_val(path, value)
- # Now set the parameter too
- param_path = path[:-1] + ['_param']
- try:
- param = self.config_val(param_path)
- except KeyError:
- raise Exception('Failed to set parameter!', param_path)
- # Now set the attribute on the param
- attribute = path[-1].removeprefix('_')
- setattr(param, attribute, value)
+class DeltaTreeItemModel(TreeItemModel):
+ """This model uses the delta for the parameters"""
+ COLUMNS = {
+ 'Value': '_value',
+ 'Vary': '_vary',
+ 'Delta': '_delta',
+ }
+ COLUMN_INDICES = _tree_columns_to_indices(COLUMNS)
+
+ VALUE_IDX = COLUMN_INDICES['Value']
+ DELTA_IDX = COLUMN_INDICES['Delta']
+ BOUND_INDICES = (VALUE_IDX, DELTA_IDX)
+
+ def data(self, index, role):
+ if role == Qt.ForegroundRole and index.column() in self.BOUND_INDICES:
+ # If a delta is zero, color both the delta and the value red.
+ item = self.get_item(index)
+ if not item.child_items:
+ atol = 1e-3
+ if abs(item.data(self.DELTA_IDX)) < atol:
+ return QColor('red')
+
+ return super().data(index, role)
TILT_LABELS_EULER = {
diff --git a/hexrdgui/constants.py b/hexrdgui/constants.py
index d4d7cafd9..35f543c77 100644
--- a/hexrdgui/constants.py
+++ b/hexrdgui/constants.py
@@ -107,6 +107,13 @@ class LLNLTransform:
'IMAGE-PLATE-3',
'IMAGE-PLATE-4',
],
+ 'PXRDIP': [
+ 'IMAGE-PLATE-B',
+ 'IMAGE-PLATE-D',
+ 'IMAGE-PLATE-L',
+ 'IMAGE-PLATE-R',
+ 'IMAGE-PLATE-U',
+ ],
}
KEY_ROTATE_ANGLE_FINE = 0.00175
diff --git a/hexrdgui/resources/ui/calibration_dialog.ui b/hexrdgui/resources/ui/calibration_dialog.ui
index 88525e765..0e50ffbf4 100644
--- a/hexrdgui/resources/ui/calibration_dialog.ui
+++ b/hexrdgui/resources/ui/calibration_dialog.ui
@@ -47,6 +47,13 @@
+ -
+
+
+ Use delta for boundaries
+
+
+
@@ -431,6 +438,7 @@ See scipy.optimize.least_squares for more details.
draw_picks
engineering_constraints
+ delta_boundaries
edit_picks_button
save_picks_button
load_picks_button
@@ -442,6 +450,7 @@ See scipy.optimize.least_squares for more details.
max_nfev
jac
method
+ undo_run_button
run_button