diff --git a/hexrdgui/calibration/calibration_dialog.py b/hexrdgui/calibration/calibration_dialog.py
index 3c7a3a082..432d0e600 100644
--- a/hexrdgui/calibration/calibration_dialog.py
+++ b/hexrdgui/calibration/calibration_dialog.py
@@ -3,7 +3,6 @@
import yaml
from PySide6.QtCore import QObject, Qt, Signal
-from PySide6.QtGui import QColor
from PySide6.QtWidgets import QComboBox, QDoubleSpinBox, QMessageBox, QSpinBox
from hexrd.fitting.calibration.lmfit_param_handling import (
@@ -12,11 +11,15 @@
)
from hexrdgui import resource_loader
+from hexrdgui.calibration.tree_item_models import (
+ DefaultCalibrationTreeItemModel,
+ DeltaCalibrationTreeItemModel,
+)
from hexrdgui.constants import ViewType
from hexrdgui.hexrd_config import HexrdConfig
from hexrdgui.pinhole_correction_editor import PinholeCorrectionEditor
from hexrdgui.tree_views.multi_column_dict_tree_view import (
- MultiColumnDictTreeItemModel, MultiColumnDictTreeView
+ MultiColumnDictTreeView
)
from hexrdgui.ui_loader import UiLoader
from hexrdgui.utils.dialog import add_help_url
@@ -557,97 +560,9 @@ def tree_view_columns(self):
@property
def tree_view_model_class(self):
if self.delta_boundaries:
- return DeltaTreeItemModel
+ return DeltaCalibrationTreeItemModel
else:
- return DefaultTreeItemModel
-
-
-def _tree_columns_to_indices(columns):
- return {
- 'Key': 0,
- **{
- k: list(columns).index(k) + 1 for k in columns
- }
- }
-
-
-class TreeItemModel(MultiColumnDictTreeItemModel):
- """Subclass the tree item model so we can customize some behavior"""
-
- def set_config_val(self, path, value):
- super().set_config_val(path, value)
- # Now set the parameter too
- param_path = path[:-1] + ['_param']
- try:
- param = self.config_val(param_path)
- except KeyError:
- raise Exception('Failed to set parameter!', param_path)
-
- # Now set the attribute on the param
- attribute = path[-1].removeprefix('_')
-
- setattr(param, attribute, value)
-
-
-class DefaultTreeItemModel(TreeItemModel):
- """This model uses minimum/maximum for the boundary constraints"""
- COLUMNS = {
- 'Value': '_value',
- 'Vary': '_vary',
- 'Minimum': '_min',
- 'Maximum': '_max',
- }
- COLUMN_INDICES = _tree_columns_to_indices(COLUMNS)
-
- VALUE_IDX = COLUMN_INDICES['Value']
- MAX_IDX = COLUMN_INDICES['Maximum']
- MIN_IDX = COLUMN_INDICES['Minimum']
- BOUND_INDICES = (VALUE_IDX, MAX_IDX, MIN_IDX)
-
- def data(self, index, role):
- if role == Qt.ForegroundRole and index.column() in self.BOUND_INDICES:
- # If a value hit the boundary, color both the boundary and the
- # value red.
- item = self.get_item(index)
- if not item.child_items:
- atol = 1e-3
- pairs = [
- (self.VALUE_IDX, self.MAX_IDX),
- (self.VALUE_IDX, self.MIN_IDX),
- ]
- for pair in pairs:
- if index.column() not in pair:
- continue
-
- if abs(item.data(pair[0]) - item.data(pair[1])) < atol:
- return QColor('red')
-
- return super().data(index, role)
-
-
-class DeltaTreeItemModel(TreeItemModel):
- """This model uses the delta for the parameters"""
- COLUMNS = {
- 'Value': '_value',
- 'Vary': '_vary',
- 'Delta': '_delta',
- }
- COLUMN_INDICES = _tree_columns_to_indices(COLUMNS)
-
- VALUE_IDX = COLUMN_INDICES['Value']
- DELTA_IDX = COLUMN_INDICES['Delta']
- BOUND_INDICES = (VALUE_IDX, DELTA_IDX)
-
- def data(self, index, role):
- if role == Qt.ForegroundRole and index.column() in self.BOUND_INDICES:
- # If a delta is zero, color both the delta and the value red.
- item = self.get_item(index)
- if not item.child_items:
- atol = 1e-3
- if abs(item.data(self.DELTA_IDX)) < atol:
- return QColor('red')
-
- return super().data(index, role)
+ return DefaultCalibrationTreeItemModel
TILT_LABELS_EULER = {
diff --git a/hexrdgui/calibration/tree_item_models.py b/hexrdgui/calibration/tree_item_models.py
new file mode 100644
index 000000000..a84ee9382
--- /dev/null
+++ b/hexrdgui/calibration/tree_item_models.py
@@ -0,0 +1,94 @@
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QColor
+
+from hexrdgui.tree_views.multi_column_dict_tree_view import (
+ MultiColumnDictTreeItemModel
+)
+
+
+def _tree_columns_to_indices(columns):
+ return {
+ 'Key': 0,
+ **{
+ k: list(columns).index(k) + 1 for k in columns
+ }
+ }
+
+
+class CalibrationTreeItemModel(MultiColumnDictTreeItemModel):
+ """Subclass the tree item model so we can customize some behavior"""
+
+ def set_config_val(self, path, value):
+ super().set_config_val(path, value)
+ # Now set the parameter too
+ param_path = path[:-1] + ['_param']
+ try:
+ param = self.config_val(param_path)
+ except KeyError:
+ raise Exception('Failed to set parameter!', param_path)
+
+ # Now set the attribute on the param
+ attribute = path[-1].removeprefix('_')
+
+ setattr(param, attribute, value)
+
+
+class DefaultCalibrationTreeItemModel(CalibrationTreeItemModel):
+ """This model uses minimum/maximum for the boundary constraints"""
+ COLUMNS = {
+ 'Value': '_value',
+ 'Vary': '_vary',
+ 'Minimum': '_min',
+ 'Maximum': '_max',
+ }
+ COLUMN_INDICES = _tree_columns_to_indices(COLUMNS)
+
+ VALUE_IDX = COLUMN_INDICES['Value']
+ MAX_IDX = COLUMN_INDICES['Maximum']
+ MIN_IDX = COLUMN_INDICES['Minimum']
+ BOUND_INDICES = (VALUE_IDX, MAX_IDX, MIN_IDX)
+
+ def data(self, index, role):
+ if role == Qt.ForegroundRole and index.column() in self.BOUND_INDICES:
+ # If a value hit the boundary, color both the boundary and the
+ # value red.
+ item = self.get_item(index)
+ if not item.child_items:
+ atol = 1e-3
+ pairs = [
+ (self.VALUE_IDX, self.MAX_IDX),
+ (self.VALUE_IDX, self.MIN_IDX),
+ ]
+ for pair in pairs:
+ if index.column() not in pair:
+ continue
+
+ if abs(item.data(pair[0]) - item.data(pair[1])) < atol:
+ return QColor('red')
+
+ return super().data(index, role)
+
+
+class DeltaCalibrationTreeItemModel(CalibrationTreeItemModel):
+ """This model uses the delta for the parameters"""
+ COLUMNS = {
+ 'Value': '_value',
+ 'Vary': '_vary',
+ 'Delta': '_delta',
+ }
+ COLUMN_INDICES = _tree_columns_to_indices(COLUMNS)
+
+ VALUE_IDX = COLUMN_INDICES['Value']
+ DELTA_IDX = COLUMN_INDICES['Delta']
+ BOUND_INDICES = (VALUE_IDX, DELTA_IDX)
+
+ def data(self, index, role):
+ if role == Qt.ForegroundRole and index.column() in self.BOUND_INDICES:
+ # If a delta is zero, color both the delta and the value red.
+ item = self.get_item(index)
+ if not item.child_items:
+ atol = 1e-3
+ if abs(item.data(self.DELTA_IDX)) < atol:
+ return QColor('red')
+
+ return super().data(index, role)
diff --git a/hexrdgui/calibration/wppf_options_dialog.py b/hexrdgui/calibration/wppf_options_dialog.py
index 03757f75e..625748a7b 100644
--- a/hexrdgui/calibration/wppf_options_dialog.py
+++ b/hexrdgui/calibration/wppf_options_dialog.py
@@ -1,15 +1,16 @@
+import copy
from pathlib import Path
import h5py
import matplotlib.pyplot as plt
import numpy as np
+import re
+import yaml
-from PySide6.QtCore import Qt, QObject, QTimer, Signal
-from PySide6.QtWidgets import (
- QCheckBox, QFileDialog, QHBoxLayout, QMessageBox, QSizePolicy,
- QTableWidgetItem, QWidget
-)
+from PySide6.QtCore import QObject, Signal
+from PySide6.QtWidgets import QFileDialog, QHBoxLayout, QMessageBox
+from hexrdgui import resource_loader
from hexrd.instrument import unwrap_dict_to_h5, unwrap_h5_to_dict
from hexrd.material import _angstroms
from hexrd.wppf import LeBail, Rietveld
@@ -20,33 +21,32 @@
_generate_default_parameters_Rietveld,
)
+from hexrdgui.calibration.tree_item_models import (
+ DefaultCalibrationTreeItemModel,
+ DeltaCalibrationTreeItemModel,
+)
from hexrdgui.dynamic_widget import DynamicWidget
from hexrdgui.hexrd_config import HexrdConfig
-from hexrdgui.scientificspinbox import ScientificDoubleSpinBox
+from hexrdgui.point_picker_dialog import PointPickerDialog
from hexrdgui.select_items_dialog import SelectItemsDialog
+from hexrdgui.tree_views.multi_column_dict_tree_view import (
+ MultiColumnDictTreeView
+)
from hexrdgui.ui_loader import UiLoader
from hexrdgui.utils import block_signals, clear_layout, has_nan
from hexrdgui.wppf_style_picker import WppfStylePicker
+import hexrdgui.resources.wppf.tree_views as tree_view_resources
inverted_peakshape_dict = {v: k for k, v in peakshape_dict.items()}
DEFAULT_PEAK_SHAPE = 'pvtch'
-COLUMNS = {
- 'name': 0,
- 'value': 1,
- 'minimum': 2,
- 'maximum': 3,
- 'vary': 4
-}
-
-LENGTH_SUFFIXES = ['_a', '_b', '_c']
-
class WppfOptionsDialog(QObject):
run = Signal()
+ undo_clicked = Signal()
finished = Signal()
def __init__(self, parent=None):
@@ -59,19 +59,21 @@ def __init__(self, parent=None):
self.populate_background_methods()
self.populate_peakshape_methods()
- self.value_spinboxes = []
- self.minimum_spinboxes = []
- self.maximum_spinboxes = []
- self.vary_checkboxes = []
-
self.dynamic_background_widgets = []
+ self.spline_points = []
self._wppf_object = None
self._prev_background_method = None
+ self._undo_stack = []
+
+ self.params = self.generate_params()
+ self.initialize_tree_view()
- self.reset_params()
self.load_settings()
+ # Default setting for delta boundaries
+ self.delta_boundaries = False
+
self.update_gui()
self.setup_connections()
@@ -81,15 +83,19 @@ def setup_connections(self):
self.ui.peak_shape.currentIndexChanged.connect(self.update_params)
self.ui.background_method.currentIndexChanged.connect(
self.update_background_parameters)
+ self.ui.delta_boundaries.toggled.connect(
+ self.on_delta_boundaries_toggled)
self.ui.select_experiment_file_button.pressed.connect(
self.select_experiment_file)
self.ui.display_wppf_plot.toggled.connect(
self.display_wppf_plot_toggled)
self.ui.edit_plot_style.pressed.connect(self.edit_plot_style)
+ self.ui.pick_spline_points.clicked.connect(self.pick_spline_points)
- self.ui.export_table.clicked.connect(self.export_table)
- self.ui.import_table.clicked.connect(self.import_table)
- self.ui.reset_table_to_defaults.clicked.connect(self.reset_params)
+ self.ui.export_params.clicked.connect(self.export_params)
+ self.ui.import_params.clicked.connect(self.import_params)
+ self.ui.reset_params_to_defaults.clicked.connect(self.reset_params)
+ self.ui.undo_last_run.clicked.connect(self.pop_undo_stack)
self.ui.save_plot.pressed.connect(self.save_plot)
self.ui.reset_object.pressed.connect(self.reset_object)
@@ -138,6 +144,9 @@ def update_enable_states(self):
enable_refinement_steps = self.method != 'Rietveld'
self.ui.refinement_steps.setEnabled(enable_refinement_steps)
+ if not enable_refinement_steps:
+ # Also set the value to 1
+ self.ui.refinement_steps.setValue(1)
def populate_background_methods(self):
self.ui.background_method.addItems(list(background_methods.keys()))
@@ -225,13 +234,25 @@ def preview_spectrum(self):
self.reset_object()
def begin_run(self):
+ if self.background_method == 'spline':
+ points = self.background_method_dict['spline']
+ if not points:
+ # Force points to be chosen now
+ self.pick_spline_points()
+
try:
self.validate()
except Exception as e:
QMessageBox.critical(self.ui, 'HEXRD', str(e))
return
+ if self.delta_boundaries:
+ # If delta boundaries are being used, set the min/max according to
+ # the delta boundaries. Lmfit requires min/max to run.
+ self.apply_delta_boundaries()
+
self.save_settings()
+ self.push_undo_stack()
self.run.emit()
def finish(self):
@@ -247,6 +268,11 @@ def validate(self):
msg = 'All parameters are fixed. Need to vary at least one'
raise Exception(msg)
+ if self.background_method == 'spline':
+ points = self.background_method_dict['spline']
+ if not points:
+ raise Exception('Points must be chosen to use "spline" method')
+
def generate_params(self):
kwargs = {
'method': self.method,
@@ -257,8 +283,8 @@ def generate_params(self):
return generate_params(**kwargs)
def reset_params(self):
- self.params = self.generate_params()
- self.update_table()
+ self.params.param_dict = self.generate_params()
+ self.update_tree_view()
def update_params(self):
if not hasattr(self, 'params'):
@@ -276,7 +302,7 @@ def update_params(self):
param_dict[key] = param
self.params.param_dict = param_dict
- self.update_table()
+ self.update_tree_view()
def show(self):
self.ui.show()
@@ -285,7 +311,7 @@ def select_materials(self):
materials = self.powder_overlay_materials
selected = self.selected_materials
items = [(name, name in selected) for name in materials]
- dialog = SelectItemsDialog(items, self.ui)
+ dialog = SelectItemsDialog(items, 'Select Materials', self.ui)
if dialog.exec() and self.selected_materials != dialog.selected_items:
self.selected_materials = dialog.selected_items
self.update_params()
@@ -340,6 +366,21 @@ def peak_shape(self, v):
label = peakshape_dict[v]
self.ui.peak_shape.setCurrentText(label)
+ @property
+ def peak_shape_tree_dict(self):
+ filename = f'peak_{self.peak_shape}.yml'
+ return load_yaml_dict(tree_view_resources, filename)
+
+ @property
+ def background_tree_dict(self):
+ filename = f'background_{self.background_method}.yml'
+ return load_yaml_dict(tree_view_resources, filename)
+
+ @property
+ def method_tree_dict(self):
+ filename = f'{self.method}.yml'
+ return load_yaml_dict(tree_view_resources, filename)
+
@property
def peak_shape_index(self):
return self.ui.peak_shape.currentIndex()
@@ -376,6 +417,10 @@ def background_method_dict(self):
if len(value) == 1:
value = value[0]
+ if method == 'spline':
+ # For spline, the value is stored on self
+ value = self.spline_points
+
return {method: value}
@background_method_dict.setter
@@ -387,7 +432,11 @@ def background_method_dict(self, v):
# Make sure these get updated (it may have already been called, but
# calling it twice is not a problem)
self.update_background_parameters()
- if v[method]:
+
+ if method == 'spline':
+ # Store the spline points on self
+ self.spline_points = v[method]
+ elif v[method]:
widgets = self.dynamic_background_widgets
if len(widgets) == 1:
widgets[0].set_value(v[method])
@@ -399,6 +448,33 @@ def background_method_dict(self, v):
# We probably need to update the parameters as well
self.update_params()
+ def pick_spline_points(self):
+ if self.background_method != 'spline':
+ # Should not be using this method
+ return
+
+ # Make a canvas with the spectrum plotted.
+ expt_spectrum = self.wppf_object_kwargs['expt_spectrum']
+ fig, ax = plt.subplots()
+ ax.plot(*expt_spectrum.T, '-k')
+
+ ax.set_xlabel(r'2$\theta$')
+ ax.set_ylabel(r'intensity (a.u.)')
+
+ dialog = PointPickerDialog(fig.canvas, 'Pick Background Points',
+ parent=self.ui)
+ if not dialog.exec():
+ # User canceled.
+ return
+
+ # Make sure these are native types for saving
+ self.spline_points = (
+ np.asarray([dialog.points]).tolist() if dialog.points else []
+ )
+
+ # We must reset the WPPF object to reflect these changes
+ self.reset_object()
+
@property
def limit_tth(self):
return self.ui.limit_tth.isChecked()
@@ -533,58 +609,11 @@ def edit_plot_style(self):
dialog = WppfStylePicker(self.ui)
dialog.ui.exec()
- def create_label(self, v):
- w = QTableWidgetItem(v)
- w.setTextAlignment(Qt.AlignCenter)
- return w
-
- def create_spinbox(self, v):
- sb = ScientificDoubleSpinBox(self.ui.table)
- sb.setKeyboardTracking(False)
- sb.setValue(float(v))
- sb.valueChanged.connect(self.update_config)
-
- size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- sb.setSizePolicy(size_policy)
- return sb
-
- def create_value_spinbox(self, v):
- sb = self.create_spinbox(v)
- self.value_spinboxes.append(sb)
- return sb
-
- def create_minimum_spinbox(self, v):
- sb = self.create_spinbox(v)
- self.minimum_spinboxes.append(sb)
- return sb
-
- def create_maximum_spinbox(self, v):
- sb = self.create_spinbox(v)
- self.maximum_spinboxes.append(sb)
- return sb
-
- def create_vary_checkbox(self, b):
- cb = QCheckBox(self.ui.table)
- cb.setChecked(b)
- cb.toggled.connect(self.on_checkbox_toggled)
-
- self.vary_checkboxes.append(cb)
- return self.create_table_widget(cb)
-
- def create_table_widget(self, w):
- # These are required to center the widget...
- tw = QWidget(self.ui.table)
- layout = QHBoxLayout(tw)
- layout.addWidget(w)
- layout.setAlignment(Qt.AlignCenter)
- layout.setContentsMargins(0, 0, 0, 0)
- return tw
-
def update_gui(self):
with block_signals(self.ui.display_wppf_plot):
self.display_wppf_plot = HexrdConfig().display_wppf_plot
self.update_background_parameters()
- self.update_table()
+ self.update_tree_view()
def update_background_parameters(self):
if self.background_method == self._prev_background_method:
@@ -593,6 +622,10 @@ def update_background_parameters(self):
self._prev_background_method = self.background_method
+ # Update the visibility of this button
+ self.ui.pick_spline_points.setVisible(
+ self.background_method == 'spline')
+
main_layout = self.ui.background_method_parameters_layout
clear_layout(main_layout)
self.dynamic_background_widgets.clear()
@@ -625,74 +658,251 @@ def update_background_parameters(self):
# methods have parameters.
self.update_params()
- def clear_table(self):
- self.value_spinboxes.clear()
- self.minimum_spinboxes.clear()
- self.maximum_spinboxes.clear()
- self.vary_checkboxes.clear()
- self.ui.table.clearContents()
+ def initialize_tree_view(self):
+ if hasattr(self, 'tree_view'):
+ # It has already been initialized
+ return
+
+ tree_dict = self.tree_view_dict_of_params
+ self.tree_view = MultiColumnDictTreeView(
+ tree_dict,
+ self.tree_view_columns,
+ parent=self.parent(),
+ model_class=self.tree_view_model_class,
+ )
+ self.tree_view.check_selection_index = 2
+ self.ui.tree_view_layout.addWidget(self.tree_view)
- def update_table(self):
- table = self.ui.table
+ # Make the key section a little larger
+ self.tree_view.header().resizeSection(0, 300)
+ def reinitialize_tree_view(self):
# Keep the same scroll position
- scrollbar = table.verticalScrollBar()
+ scrollbar = self.tree_view.verticalScrollBar()
scroll_value = scrollbar.value()
- with block_signals(table):
- self.clear_table()
- self.ui.table.setRowCount(len(self.params.param_dict))
- for i, (key, param) in enumerate(self.params.param_dict.items()):
- name = param.name
- w = self.create_label(name)
- table.setItem(i, COLUMNS['name'], w)
-
- w = self.create_value_spinbox(self.convert(name, param.value))
- table.setCellWidget(i, COLUMNS['value'], w)
-
- w = self.create_minimum_spinbox(self.convert(name, param.lb))
- w.setEnabled(param.vary)
- table.setCellWidget(i, COLUMNS['minimum'], w)
-
- w = self.create_maximum_spinbox(self.convert(name, param.ub))
- w.setEnabled(param.vary)
- table.setCellWidget(i, COLUMNS['maximum'], w)
-
- w = self.create_vary_checkbox(param.vary)
- table.setCellWidget(i, COLUMNS['vary'], w)
-
- # During event processing, it looks like the scrollbar gets resized
- # so its maximum is one less than one it actually is. Thus, if we
- # set the value to the maximum right now, it will end up being one
- # less than the actual maximum.
- # Thus, we need to post an event to the event loop to set the
- # scroll value after the other event processing. This works, but
- # the UI still scrolls back one and then to the maximum. So it
- # doesn't look that great. FIXME: figure out how to fix this.
- QTimer.singleShot(0, lambda: scrollbar.setValue(scroll_value))
-
- def on_checkbox_toggled(self):
- self.update_min_max_enable_states()
- self.update_config()
-
- def update_min_max_enable_states(self):
- for i in range(len(self.params.param_dict)):
- enable = self.vary_checkboxes[i].isChecked()
- self.minimum_spinboxes[i].setEnabled(enable)
- self.maximum_spinboxes[i].setEnabled(enable)
-
- def update_config(self):
- for i, (name, param) in enumerate(self.params.param_dict.items()):
- if any(name.endswith(x) for x in LENGTH_SUFFIXES):
- # Convert from angstrom to nm for WPPF
- multiplier = 0.1
+ self.ui.tree_view_layout.removeWidget(self.tree_view)
+ self.tree_view.deleteLater()
+ del self.tree_view
+ self.initialize_tree_view()
+
+ # Restore scroll bar position
+ self.tree_view.verticalScrollBar().setValue(scroll_value)
+
+ def update_tree_view(self):
+ tree_dict = self.tree_view_dict_of_params
+ self.tree_view.model().config = tree_dict
+ self.tree_view.reset_gui()
+
+ @property
+ def tree_view_dict_of_params(self):
+ params_dict = self.params.param_dict
+
+ tree_dict = {}
+ template_dict = self.tree_view_mapping
+
+ # Keep track of which params have been used.
+ used_params = []
+
+ def create_param_item(param):
+ used_params.append(param.name)
+ d = {
+ '_param': param,
+ '_value': param.value,
+ '_vary': bool(param.vary),
+ }
+ if self.delta_boundaries:
+ if not hasattr(param, 'delta'):
+ # We store the delta on the param object
+ # Default the delta to the minimum of the differences
+ diffs = [
+ abs(param.min - param.value),
+ abs(param.max - param.value),
+ ]
+ param.delta = min(diffs)
+
+ d['_delta'] = param.delta
else:
- multiplier = 1
+ d.update(**{
+ '_min': param.min,
+ '_max': param.max,
+ })
+ return d
+
+ # Treat these root keys specially
+ special_cases = [
+ 'Materials',
+ ]
+
+ def recursively_set_items(this_config, this_template):
+ param_set = False
+ for k, v in this_template.items():
+ if k in special_cases:
+ # Skip over it
+ continue
+
+ if isinstance(v, dict):
+ this_config.setdefault(k, {})
+ if recursively_set_items(this_config[k], v):
+ param_set = True
+ else:
+ # Pop this key if no param was set
+ this_config.pop(k)
+ else:
+ # Assume it is a string. Grab it if in the params dict.
+ if v in params_dict:
+ this_config[k] = create_param_item(params_dict[v])
+ param_set = True
+
+ return param_set
+
+ # First, recursively set items (except special cases)
+ recursively_set_items(tree_dict, template_dict)
+
+ # If the background method is chebyshev, fill those in
+ if self.background_method == 'chebyshev':
+ # Put the background first
+ tree_dict = {'Background': {}, **tree_dict}
+ background = tree_dict['Background']
+ i = 0
+ while f'bkg_{i}' in params_dict:
+ background[i] = create_param_item(params_dict[f'bkg_{i}'])
+ i += 1
+
+ # Now generate the materials
+ materials_template = template_dict['Materials'].pop('{mat}')
+
+ def recursively_format_site_id(mat, site_id, this_config,
+ this_template):
+ for k, v in this_template.items():
+ if isinstance(v, dict):
+ this_config.setdefault(k, {})
+ recursively_format_site_id(mat, site_id, this_config[k], v)
+ else:
+ # Should be a string. Replace {mat} and {site_id} if needed
+ kwargs = {}
+ if '{mat}' in v:
+ kwargs['mat'] = mat
+
+ if '{site_id}' in v:
+ kwargs['site_id'] = site_id
+
+ if kwargs:
+ v = v.format(**kwargs)
+
+ if v in params_dict:
+ this_config[k] = create_param_item(params_dict[v])
+
+ def recursively_format_mat(mat, this_config, this_template):
+ for k, v in this_template.items():
+ if k == 'Atomic Site: {site_id}':
+ # Identify all site IDs by regular expression
+ expr = re.compile(f'^{mat}_(.*)_x$')
+ site_ids = []
+ for name in params_dict:
+ m = expr.match(name)
+ if m:
+ site_id = m.group(1)
+ if site_id not in site_ids:
+ site_ids.append(site_id)
+
+ for site_id in site_ids:
+ new_k = k.format(site_id=site_id)
+ this_config.setdefault(new_k, {})
+ recursively_format_site_id(
+ mat,
+ site_id,
+ this_config[new_k],
+ v,
+ )
+ elif isinstance(v, dict):
+ this_config.setdefault(k, {})
+ recursively_format_mat(mat, this_config[k], v)
+ else:
+ # Should be a string. Replace {mat} if needed
+ if '{mat}' in v:
+ v = v.format(mat=mat)
+
+ if v in params_dict:
+ this_config[k] = create_param_item(params_dict[v])
+
+ mat_dict = tree_dict.setdefault('Materials', {})
+ for mat in self.selected_materials:
+ this_config = mat_dict.setdefault(mat, {})
+ this_template = copy.deepcopy(materials_template)
+
+ # For the parameters, we need to convert dashes to underscores
+ mat = mat.replace('-', '_')
+ recursively_format_mat(mat, this_config, this_template)
+
+ # Now all keys should have been used. Verify this is true.
+ if sorted(used_params) != sorted(list(params_dict)):
+ used = ', '.join(sorted(used_params))
+ params = ', '.join(sorted(params_dict))
+ msg = (
+ f'Internal error: used_params ({used})\n\ndid not match '
+ f'params_dict! ({params})'
+ )
+ raise Exception(msg)
+
+ return tree_dict
+
+ @property
+ def tree_view_mapping(self):
+ # This will always be a deep copy, so we can modify.
+ method_dict = self.method_tree_dict
+
+ # Insert the background and peak shape dicts too
+ method_dict['Background'] = self.background_tree_dict
+ method_dict['Instrumental Parameters']['Peak Parameters'] = (
+ self.peak_shape_tree_dict
+ )
+
+ return method_dict
+
+ @property
+ def tree_view_columns(self):
+ return self.tree_view_model_class.COLUMNS
- param.value = self.value_spinboxes[i].value() * multiplier
- param.lb = self.minimum_spinboxes[i].value() * multiplier
- param.ub = self.maximum_spinboxes[i].value() * multiplier
- param.vary = self.vary_checkboxes[i].isChecked()
+ @property
+ def tree_view_model_class(self):
+ if self.delta_boundaries:
+ return DeltaCalibrationTreeItemModel
+ else:
+ return DefaultCalibrationTreeItemModel
+
+ @property
+ def delta_boundaries(self):
+ return self.ui.delta_boundaries.isChecked()
+
+ @delta_boundaries.setter
+ def delta_boundaries(self, b):
+ self.ui.delta_boundaries.setChecked(b)
+
+ def on_delta_boundaries_toggled(self, b):
+ # The columns have changed, so we need to reinitialize the tree view
+ self.reinitialize_tree_view()
+
+ def apply_delta_boundaries(self):
+ # lmfit only uses min/max, not delta
+ # So if we used a delta, apply that to the min/max
+
+ if not self.delta_boundaries:
+ # We don't actually need to apply delta boundaries...
+ return
+
+ def recurse(cur):
+ for k, v in cur.items():
+ if '_param' in v:
+ param = v['_param']
+ # There should be a delta.
+ # We want an exception if it is missing.
+ param.min = param.value - param.delta
+ param.max = param.value + param.delta
+ elif isinstance(v, dict):
+ recurse(v)
+
+ recurse(self.tree_view.model().config)
@property
def all_widgets(self):
@@ -702,18 +912,10 @@ def all_widgets(self):
'peak_shape',
'background_method',
'experiment_file',
- 'table',
'display_wppf_plot',
]
return [getattr(self.ui, x) for x in names]
- def convert(self, name, val):
- # Check if we need to convert this data to other units
- if any(name.endswith(x) for x in LENGTH_SUFFIXES):
- # Convert from nm to Angstroms
- return val * 10.0
- return val
-
@property
def wppf_object(self):
if self._wppf_object is None:
@@ -748,8 +950,8 @@ def wppf_object_kwargs(self):
else:
x, y = HexrdConfig().last_unscaled_azimuthal_integral_data
if isinstance(y, np.ma.MaskedArray):
- # Fill any masked values with zero
- y = y.filled(0)
+ # Fill any masked values with nan
+ y = y.filled(np.nan)
# Re-format it to match the expected input format
expt_spectrum = np.array(list(zip(x, y)))
@@ -778,7 +980,9 @@ def wppf_object_kwargs(self):
return {
'expt_spectrum': expt_spectrum,
'params': self.params,
- 'phases': self.materials,
+ # Make a deep copy of the materials so that WPPF
+ # won't modify any arrays in place shared by our materials.
+ 'phases': copy.deepcopy(self.materials),
'wavelength': wavelength,
'bkgmethod': self.background_method_dict,
'peakshape': self.peak_shape,
@@ -799,16 +1003,16 @@ def update_wppf_object(self):
setattr(obj, key, val)
- def export_table(self):
+ def export_params(self):
selected_file, selected_filter = QFileDialog.getSaveFileName(
- self.ui, 'Export Table', HexrdConfig().working_dir,
+ self.ui, 'Export Parameters', HexrdConfig().working_dir,
'HDF5 files (*.h5 *.hdf5)')
if selected_file:
HexrdConfig().working_dir = str(Path(selected_file).parent)
- return self.export_params(selected_file)
+ return self.save_params(selected_file)
- def export_params(self, filename):
+ def save_params(self, filename):
filename = Path(filename)
if filename.exists():
filename.unlink()
@@ -819,16 +1023,16 @@ def export_params(self, filename):
with h5py.File(filename, 'w') as wf:
unwrap_dict_to_h5(wf, export_data)
- def import_table(self):
+ def import_params(self):
selected_file, selected_filter = QFileDialog.getOpenFileName(
- self.ui, 'Import Table', HexrdConfig().working_dir,
+ self.ui, 'Import Parameters', HexrdConfig().working_dir,
'HDF5 files (*.h5 *.hdf5)')
if selected_file:
HexrdConfig().working_dir = str(Path(selected_file).parent)
- return self.import_params(selected_file)
+ return self.load_params(selected_file)
- def import_params(self, filename):
+ def load_params(self, filename):
filename = Path(filename)
if not filename.exists():
raise FileNotFoundError(filename)
@@ -856,7 +1060,7 @@ def to_native_bools(d):
for key in self.params.param_dict.keys():
self.params[key] = dict_to_param(import_params[key])
- self.update_table()
+ self.update_tree_view()
def validate_import_params(self, import_params, filename):
here = self.params.param_dict.keys()
@@ -894,6 +1098,42 @@ def validate_import_params(self, import_params, filename):
QMessageBox.critical(self.ui, 'HEXRD', msg)
raise Exception(msg)
+ def push_undo_stack(self):
+ settings = HexrdConfig().config['calibration'].get('wppf', {})
+
+ stack_item = {
+ 'settings': settings,
+ 'spline_points': self.spline_points,
+ 'method': self.method,
+ 'refinement_steps': self.refinement_steps,
+ 'peak_shape': self.peak_shape,
+ 'background_method': self.background_method,
+ 'selected_materials': self.selected_materials,
+ '_wppf_object': self._wppf_object,
+ 'params': self.params,
+ }
+
+ stack_item = {k: copy.deepcopy(v) for k, v in stack_item.items()}
+
+ self._undo_stack.append(stack_item)
+ self.update_undo_enable_state()
+
+ def pop_undo_stack(self):
+ stack_item = self._undo_stack.pop(-1)
+
+ for k, v in stack_item.items():
+ setattr(self, k, v)
+
+ self.save_settings()
+ self.update_undo_enable_state()
+ self.update_enable_states()
+ self.update_tree_view()
+
+ self.undo_clicked.emit()
+
+ def update_undo_enable_state(self):
+ self.ui.undo_last_run.setEnabled(len(self._undo_stack) > 0)
+
def generate_params(method, materials, peak_shape, bkgmethod):
func_dict = {
@@ -920,6 +1160,18 @@ def dict_to_param(d):
return Parameter(**d)
+LOADED_YAML_DICTS = {}
+
+
+def load_yaml_dict(module, filename):
+ key = (module.__name__, filename)
+ if key not in LOADED_YAML_DICTS:
+ text = resource_loader.load_resource(module, filename)
+ LOADED_YAML_DICTS[key] = yaml.safe_load(text)
+
+ return copy.deepcopy(LOADED_YAML_DICTS[key])
+
+
if __name__ == '__main__':
from PySide6.QtWidgets import QApplication
diff --git a/hexrdgui/calibration/wppf_runner.py b/hexrdgui/calibration/wppf_runner.py
index 5f4df0168..37611c23d 100644
--- a/hexrdgui/calibration/wppf_runner.py
+++ b/hexrdgui/calibration/wppf_runner.py
@@ -12,9 +12,11 @@ class WppfRunner:
def __init__(self, parent=None):
self.parent = parent
+ self.undo_stack = []
def clear(self):
self.wppf_options_dialog = None
+ self.undo_stack.clear()
def run(self):
self.validate()
@@ -35,6 +37,7 @@ def visible_powder_overlays(self):
def select_options(self):
dialog = WppfOptionsDialog(self.parent)
dialog.run.connect(self.run_wppf)
+ dialog.undo_clicked.connect(self.pop_undo_stack)
dialog.finished.connect(self.clear)
dialog.show()
self.wppf_options_dialog = dialog
@@ -53,7 +56,8 @@ def run_wppf(self):
refine_func()
self.rerender_wppf()
- self.write_lattice_params_to_materials()
+ self.push_undo_stack()
+ self.write_params_to_materials()
self.update_param_values()
def rerender_wppf(self):
@@ -65,15 +69,15 @@ def rerender_wppf(self):
# calls to the event loop in the future instead.
QCoreApplication.processEvents()
- def write_lattice_params_to_materials(self):
+ def write_params_to_materials(self):
for name, wppf_mat in self.wppf_object.phases.phase_dict.items():
mat = HexrdConfig().material(name)
# Work around differences in WPPF objects
if isinstance(self.wppf_object, Rietveld):
- lparms = wppf_mat['synchrotron'].lparms
- else:
- lparms = wppf_mat.lparms
+ wppf_mat = wppf_mat['synchrotron']
+
+ lparms = wppf_mat.lparms
# Convert units from nm to angstroms
lparms = copy.deepcopy(lparms)
@@ -81,6 +85,38 @@ def write_lattice_params_to_materials(self):
lparms[i] *= 10.0
mat.latticeParameters = lparms
+ mat.atominfo[:] = wppf_mat.atom_pos
+ mat.U[:] = wppf_mat.U
+
+ HexrdConfig().flag_overlay_updates_for_material(name)
+ HexrdConfig().material_modified.emit(name)
+
+ HexrdConfig().overlay_config_changed.emit()
+
+ def push_undo_stack(self):
+ # Save the previous material parameters
+ mat_params = {}
+ for name in self.wppf_object.phases.phase_dict:
+ mat = HexrdConfig().material(name)
+ mat_params[name] = {
+ 'lparms': mat.lparms,
+ 'atominfo': mat.atominfo,
+ 'U': mat.U,
+ }
+
+ # Make a deep copy of all parameters
+ self.undo_stack.append(copy.deepcopy(mat_params))
+
+ def pop_undo_stack(self):
+ entry = self.undo_stack.pop()
+
+ for name, mat_params in entry.items():
+ mat = HexrdConfig().material(name)
+
+ mat.lparms = mat_params['lparms']
+ mat.atominfo[:] = mat_params['atominfo']
+ mat.U[:] = mat_params['U']
+
HexrdConfig().flag_overlay_updates_for_material(name)
HexrdConfig().material_modified.emit(name)
diff --git a/hexrdgui/image_canvas.py b/hexrdgui/image_canvas.py
index 774552737..b1174b332 100644
--- a/hexrdgui/image_canvas.py
+++ b/hexrdgui/image_canvas.py
@@ -1460,6 +1460,12 @@ def update_wppf_plot(self):
self.wppf_plot = axis.scatter(*wppf_data, **style)
+ # Rescale.
+ # This actually ignores the scatter plot data when rescaling,
+ # which is fine. We will stay zoomed in on the line.
+ axis.relim()
+ axis.autoscale_view(scalex=False)
+
def detector_axis(self, detector_name):
if self.mode == ViewType.raw:
if HexrdConfig().stitch_raw_roi_images:
diff --git a/hexrdgui/point_picker_dialog.py b/hexrdgui/point_picker_dialog.py
new file mode 100644
index 000000000..eb16e13eb
--- /dev/null
+++ b/hexrdgui/point_picker_dialog.py
@@ -0,0 +1,138 @@
+from matplotlib.backends.backend_qtagg import NavigationToolbar2QT
+import matplotlib.pyplot as plt
+
+from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QDialog, QDialogButtonBox, QLabel, QVBoxLayout
+
+
+class PointPickerDialog(QDialog):
+ def __init__(self, canvas, window_title='Pick Points', parent=None):
+ super().__init__(parent)
+
+ if len(canvas.figure.get_axes()) != 1:
+ raise NotImplementedError('Only one axis is currently supported')
+
+ self.setWindowTitle(window_title)
+
+ layout = QVBoxLayout(self)
+ self.setLayout(layout)
+
+ label = QLabel(
+ 'Left-click to add points, right-click to remove points'
+ )
+ layout.addWidget(label)
+ layout.setAlignment(label, Qt.AlignHCenter)
+
+ self.canvas = canvas
+ layout.addWidget(canvas)
+
+ self.toolbar = NavigationToolbar2QT(canvas, self)
+ layout.addWidget(self.toolbar)
+ layout.setAlignment(self.toolbar, Qt.AlignHCenter)
+
+ # Add a button box for accept/cancel
+ buttons = QDialogButtonBox.Ok | QDialogButtonBox.Cancel
+ self.button_box = QDialogButtonBox(buttons, self)
+ layout.addWidget(self.button_box)
+
+ self.points = []
+ self.scatter_artist = self.axis.scatter([], [], c='r', marker='x')
+
+ # Default size
+ self.resize(800, 600)
+
+ self.setup_connections()
+
+ def setup_connections(self):
+ self.pick_event_id = self.canvas.mpl_connect(
+ 'button_press_event', self.point_picked)
+
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+
+ self.finished.connect(self.on_finished)
+
+ def on_finished(self):
+ # Perform any needed cleanup
+ self.disconnect()
+
+ if self.scatter_artist is not None:
+ self.scatter_artist.remove()
+ self.scatter_artist = None
+
+ def disconnect(self):
+ self.canvas.mpl_disconnect(self.pick_event_id)
+
+ @property
+ def figure(self):
+ return self.canvas.figure
+
+ @property
+ def axis(self):
+ # We currently assume only one axis
+ return self.figure.get_axes()[0]
+
+ def point_picked(self, event):
+ if event.button == 3:
+ # Right-click removes points
+ self.undo_point()
+ return
+
+ if event.button != 1:
+ # Ignore anything other than left-click at this point.
+ return
+
+ if event.inaxes is None:
+ # The axis was not clicked. Ignore.
+ return
+
+ if self.axis.get_navigate_mode() is not None:
+ # Zooming or panning is active. Ignore this point.
+ return
+
+ self.points.append((event.xdata, event.ydata))
+ self.update_scatter_plot()
+
+ def undo_point(self):
+ if not self.points:
+ return
+
+ self.points.pop()
+ self.update_scatter_plot()
+
+ def update_scatter_plot(self):
+ # We unfortunately cannot set an empty list. So do nans instead.
+ points = self.points if self.points else [np.nan, np.nan]
+ self.scatter_artist.set_offsets(points)
+ self.canvas.draw_idle()
+
+
+if __name__ == '__main__':
+ import sys
+
+ import numpy as np
+
+ from PySide6.QtWidgets import QApplication
+
+ app = QApplication(sys.argv)
+
+ # spectrum = np.load('spectrum.npy')
+
+ # Generate 4 sine waves for a test
+ length = np.pi * 2 * 4
+ num_points = 1000
+ spectrum = np.vstack((
+ np.arange(num_points),
+ np.sin(np.arange(0, length, length / num_points)) * 50 + 50,
+ )).T
+
+ fig, ax = plt.subplots()
+ ax.plot(*spectrum.T, '-k')
+
+ ax.set_xlabel(r'2$\theta$')
+ ax.set_ylabel(r'intensity (a.u.)')
+
+ dialog = PointPickerDialog(fig.canvas)
+ dialog.show()
+ dialog.accepted.connect(lambda: print('Accepted:', dialog.points))
+ app.exec()
diff --git a/hexrdgui/resources/ui/wppf_options_dialog.ui b/hexrdgui/resources/ui/wppf_options_dialog.ui
index ec0ca4e9d..7c1f20a4b 100644
--- a/hexrdgui/resources/ui/wppf_options_dialog.ui
+++ b/hexrdgui/resources/ui/wppf_options_dialog.ui
@@ -6,14 +6,154 @@
0
0
- 635
- 703
+ 1000
+ 1000
WPPF Options Dialog
+ -
+
+
+ Pick Spline Points
+
+
+
+ -
+
+
+ Parameter Settings
+
+
+
-
+
+
+ Export
+
+
+
+ -
+
+
+ Import
+
+
+
+ -
+
+
+ Reset to Defaults
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ WPPF Method:
+
+
+
+ -
+
+
+ false
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Display WPPF plot in polar view?
+
+
+
+ -
+
+
+ Edit Plot Style
+
+
+
+
+
+ -
+
+
-
+
+
+ Limit 2θ?
+
+
+
+ -
+
+
+ false
+
+
+ false
+
+
+ °
+
+
+ 8
+
+
+ 100000.000000000000000
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+ -
+
+
+
+ -
+
+
+ false
+
+
+ false
+
+
+ °
+
+
+ 8
+
+
+ 100000.000000000000000
+
+
+
+
+
-
-
@@ -28,65 +168,30 @@
- -
-
-
- <html><head/><body><p>Only materials with powder overlays can be selected</p></body></html>
-
+
-
+
+
+ -
+
- Select Materials
+ Use Experiment File
- -
-
+
-
+
- Refinement Steps:
+ Background Method:
- -
-
-
- QAbstractItemView::NoEditTriggers
+
-
+
+
+ <html><head/><body><p>Only materials with powder overlays can be selected</p></body></html>
-
- true
-
-
-
- Name
-
-
-
-
- Value
-
-
-
-
- Minimum
-
-
-
-
- Maximum
-
-
-
-
- Vary
-
-
-
-
- -
-
-
- -
-
- WPPF Method:
+ Select Materials
@@ -103,35 +208,14 @@
- -
-
-
-
-
-
- Display WPPF plot in polar view?
-
-
-
- -
-
-
- Edit Plot Style
-
-
-
-
-
- -
-
+
-
+
- Peak shape:
+ Refinement Steps:
- -
-
-
- -
+
-
-
@@ -185,6 +269,16 @@
+ -
+
+
+ false
+
+
+ Undo Run
+
+
+
-
@@ -204,137 +298,35 @@
- -
-
-
- false
-
+
-
+
- Select File
+ Peak shape:
- -
-
-
- Table Settings
-
-
-
-
-
-
- Export
-
-
-
- -
-
-
- Import
-
-
-
- -
-
-
- Reset to Defaults
-
-
-
-
-
-
- -
-
-
-
-
-
- Limit 2θ?
-
-
-
- -
-
-
- false
-
-
- false
-
-
- °
-
-
- 8
-
-
- 100000.000000000000000
-
-
-
- -
-
-
- false
-
-
-
- 0
- 0
-
-
-
- -
-
-
-
- -
-
-
- false
-
-
- false
-
-
- °
-
-
- 8
-
-
- 100000.000000000000000
-
-
-
-
-
- -
-
-
- -
-
+
-
+
- Background Method:
+ Use delta boundaries
- -
-
+
-
+
false
-
+ Select File
- -
-
-
- Use Experiment File
-
-
+
-
+
+
+ -
+
@@ -351,6 +343,7 @@
refinement_steps
peak_shape
background_method
+ pick_spline_points
use_experiment_file
experiment_file
select_experiment_file_button
@@ -359,7 +352,10 @@
max_tth
display_wppf_plot
edit_plot_style
- table
+ delta_boundaries
+ export_params
+ import_params
+ reset_params_to_defaults
save_plot
reset_object
preview_spectrum
diff --git a/hexrdgui/resources/wppf/tree_views/LeBail.yml b/hexrdgui/resources/wppf/tree_views/LeBail.yml
new file mode 100644
index 000000000..2feb4ee0d
--- /dev/null
+++ b/hexrdgui/resources/wppf/tree_views/LeBail.yml
@@ -0,0 +1,43 @@
+Background: # Filled in automatically
+Instrumental Parameters:
+ Zero Point Error (Bragg-Brentano): 'zero_error'
+ Sample Displacement (Bragg-Brentano): 'shft'
+ Transparency Coefficient (Bragg-Brentano): 'trns'
+ Peak Broadening:
+ U: 'U'
+ V: 'V'
+ W: 'W'
+ Peak Parameters: # Filled in automatically
+Materials:
+ '{mat}':
+ Stacking Fault:
+ α: '{mat}_sf_alpha'
+ β: '{mat}_twin_beta'
+ Peak Broadening:
+ Lorentzian Scherrer Broadening: '{mat}_X'
+ Gaussian Scherrer Broadening: '{mat}_P'
+ Microstrain: '{mat}_Y'
+ Lattice Constants:
+ a: '{mat}_a'
+ b: '{mat}_b'
+ c: '{mat}_c'
+ α: '{mat}_alpha'
+ β: '{mat}_beta'
+ γ: '{mat}_gamma'
+ Anisotropic Broadening:
+ Mixing Coefficient: '{mat}_eta_fwhm'
+ S₄₀₀: '{mat}_s400'
+ S₀₄₀: '{mat}_s040'
+ S₀₀₄: '{mat}_s004'
+ S₂₂₀: '{mat}_s220'
+ S₂₀₂: '{mat}_s202'
+ S₀₂₂: '{mat}_s022'
+ S₃₁₀: '{mat}_s310'
+ S₁₀₃: '{mat}_s103'
+ S₀₃₁: '{mat}_s031'
+ S₁₃₀: '{mat}_s130'
+ S₃₀₁: '{mat}_s301'
+ S₀₁₃: '{mat}_s013'
+ S₂₁₁: '{mat}_s211'
+ S₁₂₁: '{mat}_s121'
+ S₁₁₂: '{mat}_s112'
diff --git a/hexrdgui/resources/wppf/tree_views/Rietveld.yml b/hexrdgui/resources/wppf/tree_views/Rietveld.yml
new file mode 100644
index 000000000..a2200e9e5
--- /dev/null
+++ b/hexrdgui/resources/wppf/tree_views/Rietveld.yml
@@ -0,0 +1,54 @@
+Background: # Filled in automatically
+Scale Factor: 'scale'
+X-Ray Source:
+ Horizontal Polarization: 'Ph'
+Instrumental Parameters:
+ Zero Point Error (Bragg-Brentano): 'zero_error'
+ Sample Displacement (Bragg-Brentano): 'shft'
+ Transparency Coefficient (Bragg-Brentano): 'trns'
+ Peak Broadening:
+ U: 'U'
+ V: 'V'
+ W: 'W'
+ Peak Parameters: # Filled in automatically
+Materials:
+ '{mat}':
+ Stacking Fault:
+ α: '{mat}_sf_alpha'
+ β: '{mat}_twin_beta'
+ Phase Fraction: '{mat}_phase_fraction'
+ Peak Broadening:
+ Lorentzian Scherrer Broadening: '{mat}_X'
+ Gaussian Scherrer Broadening: '{mat}_P'
+ Microstrain: '{mat}_Y'
+ Lattice Constants:
+ a: '{mat}_a'
+ b: '{mat}_b'
+ c: '{mat}_c'
+ α: '{mat}_alpha'
+ β: '{mat}_beta'
+ γ: '{mat}_gamma'
+ 'Atomic Site: {site_id}':
+ Fractional Atom Positions:
+ X: '{mat}_{site_id}_x'
+ Y: '{mat}_{site_id}_y'
+ Z: '{mat}_{site_id}_z'
+ Site Occupation: '{mat}_{site_id}_occ'
+ Debye-Waller Factor: '{mat}_{site_id}_dw'
+ Anisotropic Broadening:
+ Mixing Coefficient: '{mat}_eta_fwhm'
+ S₄₀₀: '{mat}_s400'
+ S₀₄₀: '{mat}_s040'
+ S₀₀₄: '{mat}_s004'
+ S₂₂₀: '{mat}_s220'
+ S₂₀₂: '{mat}_s202'
+ S₀₂₂: '{mat}_s022'
+ S₃₁₀: '{mat}_s310'
+ S₁₀₃: '{mat}_s103'
+ S₀₃₁: '{mat}_s031'
+ S₁₃₀: '{mat}_s130'
+ S₃₀₁: '{mat}_s301'
+ S₀₁₃: '{mat}_s013'
+ S₂₁₁: '{mat}_s211'
+ S₁₂₁: '{mat}_s121'
+ S₁₁₂: '{mat}_s112'
diff --git a/hexrdgui/resources/wppf/tree_views/background_chebyshev.yml b/hexrdgui/resources/wppf/tree_views/background_chebyshev.yml
new file mode 100644
index 000000000..c1c467062
--- /dev/null
+++ b/hexrdgui/resources/wppf/tree_views/background_chebyshev.yml
@@ -0,0 +1 @@
+i: 'bkg_{i}'
diff --git a/hexrdgui/resources/wppf/tree_views/background_snip1d.yml b/hexrdgui/resources/wppf/tree_views/background_snip1d.yml
new file mode 100644
index 000000000..e69de29bb
diff --git a/hexrdgui/resources/wppf/tree_views/background_spline.yml b/hexrdgui/resources/wppf/tree_views/background_spline.yml
new file mode 100644
index 000000000..e69de29bb
diff --git a/hexrdgui/resources/wppf/tree_views/peak_pvfcj.yml b/hexrdgui/resources/wppf/tree_views/peak_pvfcj.yml
new file mode 100644
index 000000000..bb4711469
--- /dev/null
+++ b/hexrdgui/resources/wppf/tree_views/peak_pvfcj.yml
@@ -0,0 +1,3 @@
+Axial Divergence:
+ H/L: 'HL'
+ S/L: 'SL'
diff --git a/hexrdgui/resources/wppf/tree_views/peak_pvpink.yml b/hexrdgui/resources/wppf/tree_views/peak_pvpink.yml
new file mode 100644
index 000000000..0362e2fe5
--- /dev/null
+++ b/hexrdgui/resources/wppf/tree_views/peak_pvpink.yml
@@ -0,0 +1,5 @@
+peak asymmetry:
+ α₀: 'alpha0'
+ α₁: 'alpha1'
+ β₀: 'beta0'
+ β₁: 'beta1'
diff --git a/hexrdgui/resources/wppf/tree_views/peak_pvtch.yml b/hexrdgui/resources/wppf/tree_views/peak_pvtch.yml
new file mode 100644
index 000000000..e69de29bb
diff --git a/hexrdgui/select_items_dialog.py b/hexrdgui/select_items_dialog.py
index bbeb96d11..98cd9b111 100644
--- a/hexrdgui/select_items_dialog.py
+++ b/hexrdgui/select_items_dialog.py
@@ -5,9 +5,10 @@
class SelectItemsDialog(QDialog):
- def __init__(self, items, parent=None):
+ def __init__(self, items, window_title='Select Items', parent=None):
super().__init__(parent)
+ self.setWindowTitle(window_title)
self.setLayout(QVBoxLayout(self))
self.select_items_widget = SelectItemsWidget(items, parent)