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 &quot;Vary&quot; and &quot;Delta&quot; (if &quot;Use delta for boundaries&quot; 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 &quot;Vary&quot; and &quot;Delta&quot; 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>&quot;None&quot; means no relative constraints.</p><p><br/></p><p>&quot;System&quot; 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>&quot;None&quot; means no relative constraints.</p><p><br/></p><p>&quot;System&quot; 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()