diff --git a/hexrdgui/calibration/calibration_dialog.py b/hexrdgui/calibration/calibration_dialog.py
index a8f787625..0e26ce4e3 100644
--- a/hexrdgui/calibration/calibration_dialog.py
+++ b/hexrdgui/calibration/calibration_dialog.py
@@ -10,6 +10,9 @@
normalize_euler_convention,
param_names_euler_convention,
)
+from hexrd.fitting.calibration.relative_constraints import (
+ RelativeConstraintsType,
+)
from hexrdgui import resource_loader
from hexrdgui.calibration.tree_item_models import (
@@ -38,6 +41,7 @@ class CalibrationDialog(QObject):
edit_picks_clicked = Signal()
save_picks_clicked = Signal()
load_picks_clicked = Signal()
+ relative_constraints_changed = Signal(RelativeConstraintsType)
engineering_constraints_changed = Signal(str)
pinhole_correction_settings_modified = Signal()
@@ -47,8 +51,10 @@ class CalibrationDialog(QObject):
finished = Signal()
def __init__(self, instr, params_dict, format_extra_params_func=None,
- parent=None, engineering_constraints=None,
- window_title='Calibration Dialog', help_url='calibration/'):
+ parent=None, relative_constraints=None,
+ engineering_constraints=None,
+ window_title='Calibration Dialog',
+ help_url='calibration/'):
super().__init__(parent)
loader = UiLoader()
@@ -68,11 +74,16 @@ def __init__(self, instr, params_dict, format_extra_params_func=None,
HexrdConfig().physics_package_modified.connect(
self.on_pinhole_correction_settings_modified)
+ self.populate_relative_constraint_options()
+
self.instr = instr
self._params_dict = params_dict
self.format_extra_params_func = format_extra_params_func
+ self.relative_constraints = relative_constraints
self.engineering_constraints = engineering_constraints
+ self._ignore_next_tree_view_update = False
+
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.
@@ -98,6 +109,8 @@ def setup_connections(self):
self.on_active_beam_changed)
self.ui.show_picks_from_all_xray_sources.toggled.connect(
self.show_picks_from_all_xray_sources_toggled)
+ self.ui.relative_constraints.currentIndexChanged.connect(
+ self.on_relative_constraints_changed)
self.ui.engineering_constraints.currentIndexChanged.connect(
self.on_engineering_constraints_changed)
self.ui.delta_boundaries.toggled.connect(
@@ -128,6 +141,17 @@ def hide(self):
def load_settings(self):
pass
+ def populate_relative_constraint_options(self):
+ # We are skipping group constraints until it is actually implemented
+ options = [
+ RelativeConstraintsType.none,
+ RelativeConstraintsType.system,
+ ]
+ w = self.ui.relative_constraints
+ w.clear()
+ for option in options:
+ w.addItem(option.value, option)
+
def update_edit_picks_enable_state(self):
is_polar = HexrdConfig().image_mode == ViewType.polar
@@ -308,6 +332,21 @@ def undo_enabled(self):
def undo_enabled(self, b):
self.ui.undo_run_button.setEnabled(b)
+ @property
+ def relative_constraints(self) -> RelativeConstraintsType:
+ ret = self.ui.relative_constraints.currentData()
+ return ret if ret is not None else RelativeConstraintsType.none
+
+ @relative_constraints.setter
+ def relative_constraints(self, v: RelativeConstraintsType):
+ v = v if v is not None else RelativeConstraintsType.none
+ w = self.ui.relative_constraints
+ options = [w.itemText(i) for i in range(w.count())]
+ if v.value not in options:
+ raise Exception(f'Invalid relative constraints: {v.value}')
+
+ w.setCurrentText(v.value)
+
@property
def engineering_constraints(self):
return self.ui.engineering_constraints.currentText()
@@ -353,6 +392,20 @@ def tth_distortion(self, v):
first = next(iter(v.values()))
self.pinhole_correction_editor.update_from_object(first)
+ def on_relative_constraints_changed(self):
+ # If the relative constraints is not None, then the engineering
+ # constraints must be set to None
+ enable = self.relative_constraints == RelativeConstraintsType.none
+ if not enable:
+ self._ignore_next_tree_view_update = True
+ self.engineering_constraints = None
+
+ self.ui.engineering_constraints.setEnabled(enable)
+ self.ui.mirror_constraints_from_first_detector.setEnabled(enable)
+
+ self.relative_constraints_changed.emit(self.relative_constraints)
+ self.reinitialize_tree_view()
+
def on_engineering_constraints_changed(self):
self.engineering_constraints_changed.emit(self.engineering_constraints)
@@ -404,6 +457,7 @@ def mirror_constraints_from_first_detector(self):
self.tree_view.reset_gui()
def update_from_calibrator(self, calibrator):
+ self.relative_constraints = calibrator.relative_constraints_type
self.engineering_constraints = calibrator.engineering_constraints
self.tth_distortion = calibrator.tth_distortion
self.params_dict = calibrator.params
@@ -504,6 +558,9 @@ def recursively_set_items(this_config, this_template):
# Now generate the detectors
detector_template = template_dict['detectors'].pop('{det}')
+ euler_convention = HexrdConfig().euler_angle_convention
+ euler_normalized = normalize_euler_convention(euler_convention)
+
def recursively_format_det(det, this_config, this_template):
for k, v in this_template.items():
if isinstance(v, dict):
@@ -524,10 +581,11 @@ def recursively_format_det(det, this_config, this_template):
current = template.format(det=det, i=i)
elif k == 'tilt':
# Special case. Take into account euler angles.
- convention = HexrdConfig().euler_angle_convention
- normalized = normalize_euler_convention(convention)
- param_names = param_names_euler_convention(det, convention)
- labels = TILT_LABELS_EULER[normalized]
+ param_names = param_names_euler_convention(
+ det,
+ euler_convention,
+ )
+ labels = TILT_LABELS_EULER[euler_normalized]
this_dict = this_config.setdefault(k, {})
for label, param_name in zip(labels, param_names):
param = params_dict[param_name]
@@ -540,14 +598,39 @@ def recursively_format_det(det, this_config, this_template):
if v in params_dict:
this_config[k] = create_param_item(params_dict[v])
- det_dict = tree_dict.setdefault('detectors', {})
- for det_key in self.instr.detectors:
- this_config = det_dict.setdefault(det_key, {})
- this_template = copy.deepcopy(detector_template)
-
- # For the parameters, we need to convert dashes to underscores
- det = det_key.replace('-', '_')
- recursively_format_det(det, this_config, this_template)
+ if self.relative_constraints == RelativeConstraintsType.none:
+ det_dict = tree_dict.setdefault('detectors', {})
+ for det_key in self.instr.detectors:
+ this_config = det_dict.setdefault(det_key, {})
+ this_template = copy.deepcopy(detector_template)
+
+ # For the parameters, we need to convert dashes to underscores
+ det = det_key.replace('-', '_')
+ recursively_format_det(det, this_config, this_template)
+ elif self.relative_constraints == RelativeConstraintsType.group:
+ raise NotImplementedError(self.relative_constraints)
+ elif self.relative_constraints == RelativeConstraintsType.system:
+ det_dict = tree_dict.setdefault('detector system', {})
+
+ tvec_names = [
+ 'system_tvec_x',
+ 'system_tvec_y',
+ 'system_tvec_z',
+ ]
+ tilt_names = param_names_euler_convention(
+ 'system', euler_convention)
+
+ this_config = det_dict.setdefault('translation', {})
+ tvec_keys = ['X', 'Y', 'Z']
+ for key, name in zip(tvec_keys, tvec_names):
+ this_config[key] = create_param_item(params_dict[name])
+
+ this_config = det_dict.setdefault('tilt', {})
+ tilt_keys = TILT_LABELS_EULER[euler_normalized]
+ for key, name in zip(tilt_keys, tilt_names):
+ this_config[key] = create_param_item(params_dict[name])
+ else:
+ raise NotImplementedError(self.relative_constraints)
if self.format_extra_params_func is not None:
self.format_extra_params_func(params_dict, tree_dict,
@@ -597,6 +680,12 @@ def reinitialize_tree_view(self):
self.tree_view.verticalScrollBar().setValue(scroll_value)
def update_tree_view(self):
+ if self._ignore_next_tree_view_update:
+ # Sometimes this is necessary when updating multiple
+ # parameters at once.
+ self._ignore_next_tree_view_update = False
+ return
+
tree_dict = self.tree_view_dict_of_params
self.tree_view.model().config = tree_dict
self.tree_view.reset_gui()
diff --git a/hexrdgui/calibration/calibration_dialog_callbacks.py b/hexrdgui/calibration/calibration_dialog_callbacks.py
index 7725270fc..09b15bd2f 100644
--- a/hexrdgui/calibration/calibration_dialog_callbacks.py
+++ b/hexrdgui/calibration/calibration_dialog_callbacks.py
@@ -4,9 +4,13 @@
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QFileDialog
+import lmfit
+
from hexrd.fitting.calibration.lmfit_param_handling import (
+ create_instr_params,
update_instrument_from_params,
)
+from hexrd.instrument import HEDMInstrument
from hexrdgui.hexrd_config import HexrdConfig
from hexrdgui.utils import instr_to_internal_dict
@@ -50,6 +54,8 @@ def setup_connections(self):
dialog.edit_picks_clicked.connect(self.on_edit_picks_clicked)
dialog.save_picks_clicked.connect(self.on_save_picks_clicked)
dialog.load_picks_clicked.connect(self.on_load_picks_clicked)
+ dialog.relative_constraints_changed.connect(
+ self.on_relative_constraints_changed)
dialog.engineering_constraints_changed.connect(
self.on_engineering_constraints_changed)
dialog.run.connect(self.on_run_clicked)
@@ -112,9 +118,12 @@ def update_dialog_from_calibrator(self):
def push_undo_stack(self):
stack_item = {
+ 'relative_constraints': self.calibrator.relative_constraints,
'engineering_constraints': self.calibrator.engineering_constraints,
'tth_distortion': self.calibrator.tth_distortion,
'params': self.calibrator.params,
+ # Create a custom instrument parameters list to use for undo
+ 'instr_params': _create_instr_params(self.instr),
'advanced_options': self.dialog.advanced_options,
}
# Make deep copies to ensure originals will not be edited
@@ -127,9 +136,10 @@ def pop_undo_stack(self):
stack_item = self.undo_stack.pop(-1)
calibrator_items = [
+ 'relative_constraints',
'engineering_constraints',
'tth_distortion',
- # Put this last so it will get set last
+ # Put this later in the list so it will get set later
'params',
]
@@ -140,8 +150,7 @@ def pop_undo_stack(self):
update_instrument_from_params(
self.instr,
- self.calibrator.params,
- self.euler_convention,
+ stack_item['instr_params'],
)
self.update_config_from_instrument()
self.update_dialog_from_calibrator()
@@ -152,13 +161,18 @@ def pop_undo_stack(self):
def update_undo_enable_state(self):
self.dialog.undo_enabled = bool(self.undo_stack)
+ def on_relative_constraints_changed(self, new_constraint):
+ self.calibrator.relative_constraints_type = new_constraint
+ self.on_constraints_changed()
+
def on_engineering_constraints_changed(self, new_constraint):
self.calibrator.engineering_constraints = new_constraint
+ self.on_constraints_changed()
+ def on_constraints_changed(self):
# Keep old settings in the dialog if they are present in the new params
# Remember everything except the name (should be the same) and
- # the expression (which might be modified from the engineering
- # constraints).
+ # the expression (which might be modified from the constraints).
to_remember = [
'value',
'vary',
@@ -239,6 +253,7 @@ def run_calibration(self, **extra_kwargs):
def on_calibration_finished(self):
self.update_config_from_instrument()
+ self.dialog.params_dict = self.calibrator.params
def update_config_from_instrument(self):
output_dict = instr_to_internal_dict(self.instr)
@@ -261,15 +276,9 @@ def update_config_from_instrument(self):
self.instrument_updated.emit()
- # Update the tree_view in the GUI with the new refinements
- self.update_refinements_tree_view()
-
# Update the drawn picks with their new locations
self.redraw_picks()
- def update_refinements_tree_view(self):
- self.dialog.params_dict = self.calibrator.params
-
def update_tth_distortion_from_dialog(self):
self.calibrator.tth_distortion = self.dialog.tth_distortion
@@ -321,3 +330,10 @@ def on_finished(self):
self.draw_picks(False)
# Ensure focus mode is off (even if it wasn't set)
self.set_focus_mode(False)
+
+
+def _create_instr_params(instr: HEDMInstrument) -> lmfit.Parameters:
+ params = create_instr_params(instr)
+ params_dict = lmfit.Parameters()
+ params_dict.add_many(*params)
+ return params_dict
diff --git a/hexrdgui/calibration/tree_item_models.py b/hexrdgui/calibration/tree_item_models.py
index bbad7d8e6..f5d3be2b9 100644
--- a/hexrdgui/calibration/tree_item_models.py
+++ b/hexrdgui/calibration/tree_item_models.py
@@ -30,6 +30,26 @@ def set_config_val(self, path, value):
# Now set the attribute on the param
attribute = path[-1].removeprefix('_')
+ if attribute == 'value':
+ # Make sure the min/max are shifted to accomodate this value
+ if value < param.min or value > param.max:
+ # Shift the min/max to accomodate, because lmfit won't
+ # let us set the value otherwise.
+ param.min = value - (param.value - param.min)
+ param.max = value + (param.max - param.value)
+ super().set_config_val(path[:-1] + ['_min'], param.min)
+ super().set_config_val(path[:-1] + ['_max'], param.max)
+ self.dict_modified.emit()
+
+ if '_min' in self.COLUMNS.values():
+ # Get the GUI to update
+ for name in ('_min', '_max'):
+ col = list(self.COLUMNS.values()).index(name) + 1
+ index = self.create_index(path[:-1], col)
+ item = self.get_item(index)
+ item.set_data(index.column(), getattr(param, name[1:]))
+ self.dataChanged.emit(index, index)
+
setattr(param, attribute, value)
diff --git a/hexrdgui/resources/ui/calibration_dialog.ui b/hexrdgui/resources/ui/calibration_dialog.ui
index 7d704897b..e81794aba 100644
--- a/hexrdgui/resources/ui/calibration_dialog.ui
+++ b/hexrdgui/resources/ui/calibration_dialog.ui
@@ -7,7 +7,7 @@
0
0
1020
- 940
+ 923
@@ -20,7 +20,7 @@
Constraints
- -
+
-
<html><head/><body><p>Add engineering constraints for certain instrument types. This may add extra parameters to the table.</p><p><br/></p><p>For example, for TARDIS, the distance between IMAGE-PLATE-2 and IMAGE-PLATE-4 must be within a certain range. If TARDIS is selected, a new parameter is added with default values for this distance.</p><p><br/></p><p>If the instrument type can be guessed, it will be selected automatically when the dialog first appears. For example, TARDIS is automatically selected if any of the detector names are IMAGE-PLATE-2, IMAGE-PLATE-3, or IMAGE-PLATE-4.</p></body></html>
@@ -30,14 +30,14 @@
- -
+
-
Use delta for boundaries
- -
+
-
<html><head/><body><p>Add engineering constraints for certain instrument types. This may add extra parameters to the table.</p><p><br/></p><p>For example, for TARDIS, the distance between IMAGE-PLATE-2 and IMAGE-PLATE-4 must be within a certain range. If TARDIS is selected, a new parameter is added with default values for this distance.</p><p><br/></p><p>If the instrument type can be guessed, it will be selected automatically when the dialog first appears. For example, TARDIS is automatically selected if any of the detector names are IMAGE-PLATE-2, IMAGE-PLATE-3, or IMAGE-PLATE-4.</p></body></html>
@@ -54,7 +54,7 @@
- -
+
-
<html><head/><body><p>If clicked, the "Vary" and "Delta" (if "Use delta for boundaries" is checked) settings of the first detector's tilt/translation parameters will be copied to all other detectors' tilt/translation parameters.</p><p>This is helpful if you have many detectors and want to modify all of their "Vary" and "Delta" settings in a similar way.</p></body></html>
@@ -64,6 +64,23 @@
+ -
+
+
+ <html><head/><body><p>Options to set relative constraints between the detectors.</p><p><br/></p><p>"None" means no relative constraints.</p><p><br/></p><p>"System" means all detectors are relatively constrained to one another. In this case, the mean center of the detectors and a mean tilt may be refined.</p></body></html>
+
+
+ Relative constraints:
+
+
+
+ -
+
+
+ <html><head/><body><p>Options to set relative constraints between the detectors.</p><p><br/></p><p>"None" means no relative constraints.</p><p><br/></p><p>"System" means all detectors are relatively constrained to one another. In this case, the mean center of the detectors and a mean tilt may be refined.</p></body></html>
+
+
+
@@ -466,6 +483,7 @@ See scipy.optimize.least_squares for more details.
draw_picks
active_beam
show_picks_from_all_xray_sources
+ relative_constraints
engineering_constraints
delta_boundaries
mirror_constraints_from_first_detector
@@ -492,8 +510,8 @@ See scipy.optimize.least_squares for more details.
setVisible(bool)
- 258
- 618
+ 269
+ 592
509
diff --git a/hexrdgui/tree_views/base_dict_tree_item_model.py b/hexrdgui/tree_views/base_dict_tree_item_model.py
index 9f4a6b038..30036c922 100644
--- a/hexrdgui/tree_views/base_dict_tree_item_model.py
+++ b/hexrdgui/tree_views/base_dict_tree_item_model.py
@@ -124,6 +124,23 @@ def flags(self, index):
return flags
+ def create_index(self, path, column=0):
+ # Create an index, given a path
+
+ def recurse(item, cur_path):
+ for i, child_item in enumerate(item.child_items):
+ if child_item.data(KEY_COL) == cur_path[0]:
+ if len(cur_path) == 1:
+ return self.createIndex(
+ child_item.row(),
+ column,
+ child_item,
+ )
+ else:
+ return recurse(child_item, cur_path[1:])
+
+ return recurse(self.root_item, path)
+
def remove_items(self, items):
for item in items:
row = item.row()