Skip to content

Commit 57f8e31

Browse files
authored
Merge pull request #1706 from HEXRD/lmfit-boundaries-delta
Add option to set lmfit boundaries as a delta
2 parents 73ff4e4 + c1c6f99 commit 57f8e31

File tree

3 files changed

+179
-35
lines changed

3 files changed

+179
-35
lines changed

hexrdgui/calibration/calibration_dialog.py

Lines changed: 163 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
)
2121
from hexrdgui.ui_loader import UiLoader
2222
from hexrdgui.utils.dialog import add_help_url
23+
from hexrdgui.utils.guess_instrument_type import guess_instrument_type
2324

2425
import hexrdgui.resources.calibration
2526

@@ -65,6 +66,12 @@ def __init__(self, instr, params_dict, format_extra_params_func=None,
6566
self.format_extra_params_func = format_extra_params_func
6667
self.engineering_constraints = engineering_constraints
6768

69+
instr_type = guess_instrument_type(instr.detectors)
70+
# Use delta boundaries by default for anything other than TARDIS
71+
# and PXRDIP. We might want to change this to a whitelist later.
72+
use_delta_boundaries = instr_type not in ('TARDIS', 'PXRDIP')
73+
self.delta_boundaries = use_delta_boundaries
74+
6875
self.initialize_advanced_options()
6976

7077
self.load_tree_view_mapping()
@@ -79,6 +86,8 @@ def setup_connections(self):
7986
self.ui.draw_picks.toggled.connect(self.on_draw_picks_toggled)
8087
self.ui.engineering_constraints.currentIndexChanged.connect(
8188
self.on_engineering_constraints_changed)
89+
self.ui.delta_boundaries.toggled.connect(
90+
self.on_delta_boundaries_toggled)
8291
self.ui.edit_picks_button.clicked.connect(self.on_edit_picks_clicked)
8392
self.ui.save_picks_button.clicked.connect(self.on_save_picks_clicked)
8493
self.ui.load_picks_button.clicked.connect(self.on_load_picks_clicked)
@@ -166,6 +175,11 @@ def on_draw_picks_toggled(self, b):
166175
self.draw_picks_toggled.emit(b)
167176

168177
def on_run_button_clicked(self):
178+
if self.delta_boundaries:
179+
# If delta boundaries are being used, set the min/max according to
180+
# the delta boundaries. Lmfit requires min/max to run.
181+
self.apply_delta_boundaries()
182+
169183
try:
170184
self.validate_parameters()
171185
except Exception as e:
@@ -182,6 +196,27 @@ def on_undo_run_button_clicked(self):
182196
def finish(self):
183197
self.finished.emit()
184198

199+
def apply_delta_boundaries(self):
200+
# lmfit only uses min/max, not delta
201+
# So if we used a delta, apply that to the min/max
202+
203+
if not self.delta_boundaries:
204+
# We don't actually need to apply delta boundaries...
205+
return
206+
207+
def recurse(cur):
208+
for k, v in cur.items():
209+
if '_param' in v:
210+
param = v['_param']
211+
# There should be a delta.
212+
# We want an exception if it is missing.
213+
param.min = param.value - param.delta
214+
param.max = param.value + param.delta
215+
elif isinstance(v, dict):
216+
recurse(v)
217+
218+
recurse(self.tree_view.model().config)
219+
185220
def validate_parameters(self):
186221
# Recursively look through the tree dict, and add on errors
187222
config = self.tree_view.model().config
@@ -197,6 +232,11 @@ def recurse(cur):
197232
full_path = '->'.join(path)
198233
msg = f'{full_path}: min is greater than max'
199234
errors.append(msg)
235+
elif param.min == param.max:
236+
# Slightly modify these to prevent lmfit
237+
# from raising an exception.
238+
param.min -= 1e-8
239+
param.max += 1e-8
200240
elif isinstance(v, dict):
201241
recurse(v)
202242
path.pop(-1)
@@ -237,6 +277,14 @@ def engineering_constraints(self, v):
237277

238278
w.setCurrentText(v)
239279

280+
@property
281+
def delta_boundaries(self):
282+
return self.ui.delta_boundaries.isChecked()
283+
284+
@delta_boundaries.setter
285+
def delta_boundaries(self, b):
286+
self.ui.delta_boundaries.setChecked(b)
287+
240288
def on_edit_picks_clicked(self):
241289
self.edit_picks_clicked.emit()
242290

@@ -263,6 +311,10 @@ def tth_distortion(self, v):
263311
def on_engineering_constraints_changed(self):
264312
self.engineering_constraints_changed.emit(self.engineering_constraints)
265313

314+
def on_delta_boundaries_toggled(self, b):
315+
# The columns have changed, so we need to reinitialize the tree view
316+
self.reinitialize_tree_view()
317+
266318
def update_from_calibrator(self, calibrator):
267319
self.engineering_constraints = calibrator.engineering_constraints
268320
self.tth_distortion = calibrator.tth_distortion
@@ -286,13 +338,28 @@ def tree_view_dict_of_params(self):
286338

287339
def create_param_item(param):
288340
used_params.append(param.name)
289-
return {
341+
d = {
290342
'_param': param,
291343
'_value': param.value,
292344
'_vary': bool(param.vary),
293-
'_min': param.min,
294-
'_max': param.max,
295345
}
346+
if self.delta_boundaries:
347+
if not hasattr(param, 'delta'):
348+
# We store the delta on the param object
349+
# Default the delta to the minimum of the differences
350+
diffs = [
351+
abs(param.min - param.value),
352+
abs(param.max - param.value),
353+
]
354+
param.delta = min(diffs)
355+
356+
d['_delta'] = param.delta
357+
else:
358+
d.update(**{
359+
'_min': param.min,
360+
'_max': param.max,
361+
})
362+
return d
296363

297364
# Treat these root keys specially
298365
special_cases = [
@@ -395,15 +462,31 @@ def initialize_tree_view(self):
395462
return
396463

397464
tree_dict = self.tree_view_dict_of_params
398-
self.tree_view = MultiColumnDictTreeView(tree_dict, TREE_VIEW_COLUMNS,
399-
parent=self.parent(),
400-
model_class=TreeItemModel)
465+
self.tree_view = MultiColumnDictTreeView(
466+
tree_dict,
467+
self.tree_view_columns,
468+
parent=self.parent(),
469+
model_class=self.tree_view_model_class,
470+
)
401471
self.tree_view.check_selection_index = 2
402472
self.ui.tree_view_layout.addWidget(self.tree_view)
403473

404474
# Make the key section a little larger
405475
self.tree_view.header().resizeSection(0, 300)
406476

477+
def reinitialize_tree_view(self):
478+
# Keep the same scroll position
479+
scrollbar = self.tree_view.verticalScrollBar()
480+
scroll_value = scrollbar.value()
481+
482+
self.ui.tree_view_layout.removeWidget(self.tree_view)
483+
self.tree_view.deleteLater()
484+
del self.tree_view
485+
self.initialize_tree_view()
486+
487+
# Restore scroll bar position
488+
self.tree_view.verticalScrollBar().setValue(scroll_value)
489+
407490
def update_tree_view(self):
408491
tree_dict = self.tree_view_dict_of_params
409492
self.tree_view.model().config = tree_dict
@@ -422,37 +505,70 @@ def clear_polar_view_tth_correction(self, show_warning=True):
422505
QMessageBox.information(self.parent(), 'HEXRD', msg)
423506
editor.apply_to_polar_view = False
424507

508+
@property
509+
def tree_view_columns(self):
510+
return self.tree_view_model_class.COLUMNS
425511

426-
TREE_VIEW_COLUMNS = {
427-
'Value': '_value',
428-
'Vary': '_vary',
429-
'Minimum': '_min',
430-
'Maximum': '_max',
431-
}
432-
TREE_VIEW_COLUMN_INDICES = {
433-
'Key': 0,
434-
**{
435-
k: list(TREE_VIEW_COLUMNS).index(k) + 1 for k in TREE_VIEW_COLUMNS
512+
@property
513+
def tree_view_model_class(self):
514+
if self.delta_boundaries:
515+
return DeltaTreeItemModel
516+
else:
517+
return DefaultTreeItemModel
518+
519+
520+
def _tree_columns_to_indices(columns):
521+
return {
522+
'Key': 0,
523+
**{
524+
k: list(columns).index(k) + 1 for k in columns
525+
}
436526
}
437-
}
438-
VALUE_IDX = TREE_VIEW_COLUMN_INDICES['Value']
439-
MAX_IDX = TREE_VIEW_COLUMN_INDICES['Maximum']
440-
MIN_IDX = TREE_VIEW_COLUMN_INDICES['Minimum']
441-
BOUND_INDICES = (VALUE_IDX, MAX_IDX, MIN_IDX)
442527

443528

444529
class TreeItemModel(MultiColumnDictTreeItemModel):
445530
"""Subclass the tree item model so we can customize some behavior"""
531+
532+
def set_config_val(self, path, value):
533+
super().set_config_val(path, value)
534+
# Now set the parameter too
535+
param_path = path[:-1] + ['_param']
536+
try:
537+
param = self.config_val(param_path)
538+
except KeyError:
539+
raise Exception('Failed to set parameter!', param_path)
540+
541+
# Now set the attribute on the param
542+
attribute = path[-1].removeprefix('_')
543+
544+
setattr(param, attribute, value)
545+
546+
547+
class DefaultTreeItemModel(TreeItemModel):
548+
"""This model uses minimum/maximum for the boundary constraints"""
549+
COLUMNS = {
550+
'Value': '_value',
551+
'Vary': '_vary',
552+
'Minimum': '_min',
553+
'Maximum': '_max',
554+
}
555+
COLUMN_INDICES = _tree_columns_to_indices(COLUMNS)
556+
557+
VALUE_IDX = COLUMN_INDICES['Value']
558+
MAX_IDX = COLUMN_INDICES['Maximum']
559+
MIN_IDX = COLUMN_INDICES['Minimum']
560+
BOUND_INDICES = (VALUE_IDX, MAX_IDX, MIN_IDX)
561+
446562
def data(self, index, role):
447-
if role == Qt.ForegroundRole and index.column() in BOUND_INDICES:
563+
if role == Qt.ForegroundRole and index.column() in self.BOUND_INDICES:
448564
# If a value hit the boundary, color both the boundary and the
449565
# value red.
450566
item = self.get_item(index)
451567
if not item.child_items:
452568
atol = 1e-3
453569
pairs = [
454-
(VALUE_IDX, MAX_IDX),
455-
(VALUE_IDX, MIN_IDX),
570+
(self.VALUE_IDX, self.MAX_IDX),
571+
(self.VALUE_IDX, self.MIN_IDX),
456572
]
457573
for pair in pairs:
458574
if index.column() not in pair:
@@ -463,18 +579,30 @@ def data(self, index, role):
463579

464580
return super().data(index, role)
465581

466-
def set_config_val(self, path, value):
467-
super().set_config_val(path, value)
468-
# Now set the parameter too
469-
param_path = path[:-1] + ['_param']
470-
try:
471-
param = self.config_val(param_path)
472-
except KeyError:
473-
raise Exception('Failed to set parameter!', param_path)
474582

475-
# Now set the attribute on the param
476-
attribute = path[-1].removeprefix('_')
477-
setattr(param, attribute, value)
583+
class DeltaTreeItemModel(TreeItemModel):
584+
"""This model uses the delta for the parameters"""
585+
COLUMNS = {
586+
'Value': '_value',
587+
'Vary': '_vary',
588+
'Delta': '_delta',
589+
}
590+
COLUMN_INDICES = _tree_columns_to_indices(COLUMNS)
591+
592+
VALUE_IDX = COLUMN_INDICES['Value']
593+
DELTA_IDX = COLUMN_INDICES['Delta']
594+
BOUND_INDICES = (VALUE_IDX, DELTA_IDX)
595+
596+
def data(self, index, role):
597+
if role == Qt.ForegroundRole and index.column() in self.BOUND_INDICES:
598+
# If a delta is zero, color both the delta and the value red.
599+
item = self.get_item(index)
600+
if not item.child_items:
601+
atol = 1e-3
602+
if abs(item.data(self.DELTA_IDX)) < atol:
603+
return QColor('red')
604+
605+
return super().data(index, role)
478606

479607

480608
TILT_LABELS_EULER = {

hexrdgui/constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ class LLNLTransform:
107107
'IMAGE-PLATE-3',
108108
'IMAGE-PLATE-4',
109109
],
110+
'PXRDIP': [
111+
'IMAGE-PLATE-B',
112+
'IMAGE-PLATE-D',
113+
'IMAGE-PLATE-L',
114+
'IMAGE-PLATE-R',
115+
'IMAGE-PLATE-U',
116+
],
110117
}
111118

112119
KEY_ROTATE_ANGLE_FINE = 0.00175

hexrdgui/resources/ui/calibration_dialog.ui

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@
4747
</property>
4848
</widget>
4949
</item>
50+
<item row="1" column="0">
51+
<widget class="QCheckBox" name="delta_boundaries">
52+
<property name="text">
53+
<string>Use delta for boundaries</string>
54+
</property>
55+
</widget>
56+
</item>
5057
</layout>
5158
</widget>
5259
</item>
@@ -431,6 +438,7 @@ See scipy.optimize.least_squares for more details.</string>
431438
<tabstops>
432439
<tabstop>draw_picks</tabstop>
433440
<tabstop>engineering_constraints</tabstop>
441+
<tabstop>delta_boundaries</tabstop>
434442
<tabstop>edit_picks_button</tabstop>
435443
<tabstop>save_picks_button</tabstop>
436444
<tabstop>load_picks_button</tabstop>
@@ -442,6 +450,7 @@ See scipy.optimize.least_squares for more details.</string>
442450
<tabstop>max_nfev</tabstop>
443451
<tabstop>jac</tabstop>
444452
<tabstop>method</tabstop>
453+
<tabstop>undo_run_button</tabstop>
445454
<tabstop>run_button</tabstop>
446455
</tabstops>
447456
<resources/>

0 commit comments

Comments
 (0)