Skip to content

Commit a0b799d

Browse files
authored
Merge pull request #1742 from HEXRD/relative-constraints
Add options to use relative constraints
2 parents 0ba5015 + 5b636c1 commit a0b799d

File tree

5 files changed

+192
-32
lines changed

5 files changed

+192
-32
lines changed

hexrdgui/calibration/calibration_dialog.py

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
normalize_euler_convention,
1111
param_names_euler_convention,
1212
)
13+
from hexrd.fitting.calibration.relative_constraints import (
14+
RelativeConstraintsType,
15+
)
1316

1417
from hexrdgui import resource_loader
1518
from hexrdgui.calibration.tree_item_models import (
@@ -38,6 +41,7 @@ class CalibrationDialog(QObject):
3841
edit_picks_clicked = Signal()
3942
save_picks_clicked = Signal()
4043
load_picks_clicked = Signal()
44+
relative_constraints_changed = Signal(RelativeConstraintsType)
4145
engineering_constraints_changed = Signal(str)
4246

4347
pinhole_correction_settings_modified = Signal()
@@ -47,8 +51,10 @@ class CalibrationDialog(QObject):
4751
finished = Signal()
4852

4953
def __init__(self, instr, params_dict, format_extra_params_func=None,
50-
parent=None, engineering_constraints=None,
51-
window_title='Calibration Dialog', help_url='calibration/'):
54+
parent=None, relative_constraints=None,
55+
engineering_constraints=None,
56+
window_title='Calibration Dialog',
57+
help_url='calibration/'):
5258
super().__init__(parent)
5359

5460
loader = UiLoader()
@@ -68,11 +74,16 @@ def __init__(self, instr, params_dict, format_extra_params_func=None,
6874
HexrdConfig().physics_package_modified.connect(
6975
self.on_pinhole_correction_settings_modified)
7076

77+
self.populate_relative_constraint_options()
78+
7179
self.instr = instr
7280
self._params_dict = params_dict
7381
self.format_extra_params_func = format_extra_params_func
82+
self.relative_constraints = relative_constraints
7483
self.engineering_constraints = engineering_constraints
7584

85+
self._ignore_next_tree_view_update = False
86+
7687
instr_type = guess_instrument_type(instr.detectors)
7788
# Use delta boundaries by default for anything other than TARDIS
7889
# and PXRDIP. We might want to change this to a whitelist later.
@@ -98,6 +109,8 @@ def setup_connections(self):
98109
self.on_active_beam_changed)
99110
self.ui.show_picks_from_all_xray_sources.toggled.connect(
100111
self.show_picks_from_all_xray_sources_toggled)
112+
self.ui.relative_constraints.currentIndexChanged.connect(
113+
self.on_relative_constraints_changed)
101114
self.ui.engineering_constraints.currentIndexChanged.connect(
102115
self.on_engineering_constraints_changed)
103116
self.ui.delta_boundaries.toggled.connect(
@@ -128,6 +141,17 @@ def hide(self):
128141
def load_settings(self):
129142
pass
130143

144+
def populate_relative_constraint_options(self):
145+
# We are skipping group constraints until it is actually implemented
146+
options = [
147+
RelativeConstraintsType.none,
148+
RelativeConstraintsType.system,
149+
]
150+
w = self.ui.relative_constraints
151+
w.clear()
152+
for option in options:
153+
w.addItem(option.value, option)
154+
131155
def update_edit_picks_enable_state(self):
132156
is_polar = HexrdConfig().image_mode == ViewType.polar
133157

@@ -308,6 +332,21 @@ def undo_enabled(self):
308332
def undo_enabled(self, b):
309333
self.ui.undo_run_button.setEnabled(b)
310334

335+
@property
336+
def relative_constraints(self) -> RelativeConstraintsType:
337+
ret = self.ui.relative_constraints.currentData()
338+
return ret if ret is not None else RelativeConstraintsType.none
339+
340+
@relative_constraints.setter
341+
def relative_constraints(self, v: RelativeConstraintsType):
342+
v = v if v is not None else RelativeConstraintsType.none
343+
w = self.ui.relative_constraints
344+
options = [w.itemText(i) for i in range(w.count())]
345+
if v.value not in options:
346+
raise Exception(f'Invalid relative constraints: {v.value}')
347+
348+
w.setCurrentText(v.value)
349+
311350
@property
312351
def engineering_constraints(self):
313352
return self.ui.engineering_constraints.currentText()
@@ -353,6 +392,20 @@ def tth_distortion(self, v):
353392
first = next(iter(v.values()))
354393
self.pinhole_correction_editor.update_from_object(first)
355394

395+
def on_relative_constraints_changed(self):
396+
# If the relative constraints is not None, then the engineering
397+
# constraints must be set to None
398+
enable = self.relative_constraints == RelativeConstraintsType.none
399+
if not enable:
400+
self._ignore_next_tree_view_update = True
401+
self.engineering_constraints = None
402+
403+
self.ui.engineering_constraints.setEnabled(enable)
404+
self.ui.mirror_constraints_from_first_detector.setEnabled(enable)
405+
406+
self.relative_constraints_changed.emit(self.relative_constraints)
407+
self.reinitialize_tree_view()
408+
356409
def on_engineering_constraints_changed(self):
357410
self.engineering_constraints_changed.emit(self.engineering_constraints)
358411

@@ -404,6 +457,7 @@ def mirror_constraints_from_first_detector(self):
404457
self.tree_view.reset_gui()
405458

406459
def update_from_calibrator(self, calibrator):
460+
self.relative_constraints = calibrator.relative_constraints_type
407461
self.engineering_constraints = calibrator.engineering_constraints
408462
self.tth_distortion = calibrator.tth_distortion
409463
self.params_dict = calibrator.params
@@ -504,6 +558,9 @@ def recursively_set_items(this_config, this_template):
504558
# Now generate the detectors
505559
detector_template = template_dict['detectors'].pop('{det}')
506560

561+
euler_convention = HexrdConfig().euler_angle_convention
562+
euler_normalized = normalize_euler_convention(euler_convention)
563+
507564
def recursively_format_det(det, this_config, this_template):
508565
for k, v in this_template.items():
509566
if isinstance(v, dict):
@@ -524,10 +581,11 @@ def recursively_format_det(det, this_config, this_template):
524581
current = template.format(det=det, i=i)
525582
elif k == 'tilt':
526583
# Special case. Take into account euler angles.
527-
convention = HexrdConfig().euler_angle_convention
528-
normalized = normalize_euler_convention(convention)
529-
param_names = param_names_euler_convention(det, convention)
530-
labels = TILT_LABELS_EULER[normalized]
584+
param_names = param_names_euler_convention(
585+
det,
586+
euler_convention,
587+
)
588+
labels = TILT_LABELS_EULER[euler_normalized]
531589
this_dict = this_config.setdefault(k, {})
532590
for label, param_name in zip(labels, param_names):
533591
param = params_dict[param_name]
@@ -540,14 +598,39 @@ def recursively_format_det(det, this_config, this_template):
540598
if v in params_dict:
541599
this_config[k] = create_param_item(params_dict[v])
542600

543-
det_dict = tree_dict.setdefault('detectors', {})
544-
for det_key in self.instr.detectors:
545-
this_config = det_dict.setdefault(det_key, {})
546-
this_template = copy.deepcopy(detector_template)
547-
548-
# For the parameters, we need to convert dashes to underscores
549-
det = det_key.replace('-', '_')
550-
recursively_format_det(det, this_config, this_template)
601+
if self.relative_constraints == RelativeConstraintsType.none:
602+
det_dict = tree_dict.setdefault('detectors', {})
603+
for det_key in self.instr.detectors:
604+
this_config = det_dict.setdefault(det_key, {})
605+
this_template = copy.deepcopy(detector_template)
606+
607+
# For the parameters, we need to convert dashes to underscores
608+
det = det_key.replace('-', '_')
609+
recursively_format_det(det, this_config, this_template)
610+
elif self.relative_constraints == RelativeConstraintsType.group:
611+
raise NotImplementedError(self.relative_constraints)
612+
elif self.relative_constraints == RelativeConstraintsType.system:
613+
det_dict = tree_dict.setdefault('detector system', {})
614+
615+
tvec_names = [
616+
'system_tvec_x',
617+
'system_tvec_y',
618+
'system_tvec_z',
619+
]
620+
tilt_names = param_names_euler_convention(
621+
'system', euler_convention)
622+
623+
this_config = det_dict.setdefault('translation', {})
624+
tvec_keys = ['X', 'Y', 'Z']
625+
for key, name in zip(tvec_keys, tvec_names):
626+
this_config[key] = create_param_item(params_dict[name])
627+
628+
this_config = det_dict.setdefault('tilt', {})
629+
tilt_keys = TILT_LABELS_EULER[euler_normalized]
630+
for key, name in zip(tilt_keys, tilt_names):
631+
this_config[key] = create_param_item(params_dict[name])
632+
else:
633+
raise NotImplementedError(self.relative_constraints)
551634

552635
if self.format_extra_params_func is not None:
553636
self.format_extra_params_func(params_dict, tree_dict,
@@ -597,6 +680,12 @@ def reinitialize_tree_view(self):
597680
self.tree_view.verticalScrollBar().setValue(scroll_value)
598681

599682
def update_tree_view(self):
683+
if self._ignore_next_tree_view_update:
684+
# Sometimes this is necessary when updating multiple
685+
# parameters at once.
686+
self._ignore_next_tree_view_update = False
687+
return
688+
600689
tree_dict = self.tree_view_dict_of_params
601690
self.tree_view.model().config = tree_dict
602691
self.tree_view.reset_gui()

hexrdgui/calibration/calibration_dialog_callbacks.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
from PySide6.QtCore import Signal
55
from PySide6.QtWidgets import QFileDialog
66

7+
import lmfit
8+
79
from hexrd.fitting.calibration.lmfit_param_handling import (
10+
create_instr_params,
811
update_instrument_from_params,
912
)
13+
from hexrd.instrument import HEDMInstrument
1014

1115
from hexrdgui.hexrd_config import HexrdConfig
1216
from hexrdgui.utils import instr_to_internal_dict
@@ -50,6 +54,8 @@ def setup_connections(self):
5054
dialog.edit_picks_clicked.connect(self.on_edit_picks_clicked)
5155
dialog.save_picks_clicked.connect(self.on_save_picks_clicked)
5256
dialog.load_picks_clicked.connect(self.on_load_picks_clicked)
57+
dialog.relative_constraints_changed.connect(
58+
self.on_relative_constraints_changed)
5359
dialog.engineering_constraints_changed.connect(
5460
self.on_engineering_constraints_changed)
5561
dialog.run.connect(self.on_run_clicked)
@@ -112,9 +118,12 @@ def update_dialog_from_calibrator(self):
112118

113119
def push_undo_stack(self):
114120
stack_item = {
121+
'relative_constraints': self.calibrator.relative_constraints,
115122
'engineering_constraints': self.calibrator.engineering_constraints,
116123
'tth_distortion': self.calibrator.tth_distortion,
117124
'params': self.calibrator.params,
125+
# Create a custom instrument parameters list to use for undo
126+
'instr_params': _create_instr_params(self.instr),
118127
'advanced_options': self.dialog.advanced_options,
119128
}
120129
# Make deep copies to ensure originals will not be edited
@@ -127,9 +136,10 @@ def pop_undo_stack(self):
127136
stack_item = self.undo_stack.pop(-1)
128137

129138
calibrator_items = [
139+
'relative_constraints',
130140
'engineering_constraints',
131141
'tth_distortion',
132-
# Put this last so it will get set last
142+
# Put this later in the list so it will get set later
133143
'params',
134144
]
135145

@@ -140,8 +150,7 @@ def pop_undo_stack(self):
140150

141151
update_instrument_from_params(
142152
self.instr,
143-
self.calibrator.params,
144-
self.euler_convention,
153+
stack_item['instr_params'],
145154
)
146155
self.update_config_from_instrument()
147156
self.update_dialog_from_calibrator()
@@ -152,13 +161,18 @@ def pop_undo_stack(self):
152161
def update_undo_enable_state(self):
153162
self.dialog.undo_enabled = bool(self.undo_stack)
154163

164+
def on_relative_constraints_changed(self, new_constraint):
165+
self.calibrator.relative_constraints_type = new_constraint
166+
self.on_constraints_changed()
167+
155168
def on_engineering_constraints_changed(self, new_constraint):
156169
self.calibrator.engineering_constraints = new_constraint
170+
self.on_constraints_changed()
157171

172+
def on_constraints_changed(self):
158173
# Keep old settings in the dialog if they are present in the new params
159174
# Remember everything except the name (should be the same) and
160-
# the expression (which might be modified from the engineering
161-
# constraints).
175+
# the expression (which might be modified from the constraints).
162176
to_remember = [
163177
'value',
164178
'vary',
@@ -239,6 +253,7 @@ def run_calibration(self, **extra_kwargs):
239253

240254
def on_calibration_finished(self):
241255
self.update_config_from_instrument()
256+
self.dialog.params_dict = self.calibrator.params
242257

243258
def update_config_from_instrument(self):
244259
output_dict = instr_to_internal_dict(self.instr)
@@ -261,15 +276,9 @@ def update_config_from_instrument(self):
261276

262277
self.instrument_updated.emit()
263278

264-
# Update the tree_view in the GUI with the new refinements
265-
self.update_refinements_tree_view()
266-
267279
# Update the drawn picks with their new locations
268280
self.redraw_picks()
269281

270-
def update_refinements_tree_view(self):
271-
self.dialog.params_dict = self.calibrator.params
272-
273282
def update_tth_distortion_from_dialog(self):
274283
self.calibrator.tth_distortion = self.dialog.tth_distortion
275284

@@ -321,3 +330,10 @@ def on_finished(self):
321330
self.draw_picks(False)
322331
# Ensure focus mode is off (even if it wasn't set)
323332
self.set_focus_mode(False)
333+
334+
335+
def _create_instr_params(instr: HEDMInstrument) -> lmfit.Parameters:
336+
params = create_instr_params(instr)
337+
params_dict = lmfit.Parameters()
338+
params_dict.add_many(*params)
339+
return params_dict

hexrdgui/calibration/tree_item_models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,26 @@ def set_config_val(self, path, value):
3030
# Now set the attribute on the param
3131
attribute = path[-1].removeprefix('_')
3232

33+
if attribute == 'value':
34+
# Make sure the min/max are shifted to accomodate this value
35+
if value < param.min or value > param.max:
36+
# Shift the min/max to accomodate, because lmfit won't
37+
# let us set the value otherwise.
38+
param.min = value - (param.value - param.min)
39+
param.max = value + (param.max - param.value)
40+
super().set_config_val(path[:-1] + ['_min'], param.min)
41+
super().set_config_val(path[:-1] + ['_max'], param.max)
42+
self.dict_modified.emit()
43+
44+
if '_min' in self.COLUMNS.values():
45+
# Get the GUI to update
46+
for name in ('_min', '_max'):
47+
col = list(self.COLUMNS.values()).index(name) + 1
48+
index = self.create_index(path[:-1], col)
49+
item = self.get_item(index)
50+
item.set_data(index.column(), getattr(param, name[1:]))
51+
self.dataChanged.emit(index, index)
52+
3353
setattr(param, attribute, value)
3454

3555

0 commit comments

Comments
 (0)