2020)
2121from hexrdgui .ui_loader import UiLoader
2222from hexrdgui .utils .dialog import add_help_url
23+ from hexrdgui .utils .guess_instrument_type import guess_instrument_type
2324
2425import 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
444529class 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
480608TILT_LABELS_EULER = {
0 commit comments