Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESS_GUI: Save/Load Corfunc and Invariant Perspectives #1675

Merged
merged 20 commits into from
Nov 24, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
754a079
Serialize/deserialize Invariant perspective. Update project and analy…
krzywon Aug 6, 2020
6a8e71a
Add data removal routine and tweak state generation in InvariantPersp…
krzywon Aug 7, 2020
931878a
Close all perspectives instead of relying on data deletion schemes an…
krzywon Aug 7, 2020
5bb10e3
Finalize invariant serialization and deserialization routines.
krzywon Aug 7, 2020
5687d81
Add data removal routine to the Corfunc perspective. refs #1655
krzywon Aug 7, 2020
46a1423
Serialize Corfunc perspective and allow main window to get serialized…
krzywon Aug 10, 2020
d7d738b
Bug fixes associated with data removal methods for Invariant and Corf…
krzywon Aug 10, 2020
03d152d
Bug fixes associated with serialiation methods for Corfunc perspectiv…
krzywon Aug 12, 2020
96caf67
Send corfunc parameters to perspective when loading projects and fix …
krzywon Aug 17, 2020
daf3cd2
Add Invariant serialization unit tests, bug fixing, and lint.
krzywon Aug 19, 2020
54e09e8
Add unit tests for removing data and loading params from dictionary t…
krzywon Aug 21, 2020
00afdc0
Fix issues with corfunc serialization and associated unit tests that …
krzywon Sep 10, 2020
ff3cbe4
Bug fixes and add general unit test to all serializable perspectives …
krzywon Sep 10, 2020
e69907f
Merge branch 'ESS_GUI' into ESS_GUI_serialize_corfunc_invariant
krzywon Sep 10, 2020
cfe995a
Remove time-heavy routine that doesn't add much when removing data fr…
krzywon Sep 11, 2020
dd79ee0
Fix failing unit tests
krzywon Sep 16, 2020
16eab7e
Fix issue storing numpy floats and reset data name dictionary when lo…
krzywon Sep 22, 2020
2c7269d
Add exception handling when closing/loading perspectives and set floa…
krzywon Sep 28, 2020
50444e8
Fix typo.
krzywon Sep 29, 2020
cb851b1
Plot any corfunc and invariant plots when loading projects. Enable co…
krzywon Nov 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/sas/qtgui/Calculators/DataOperationUtilityPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ def updateCombobox(self, filenames):
list_datafiles = []

for key_id in list(filenames.keys()):
if filenames[key_id].title:
if filenames[key_id].name:
# filenames with titles
new_title = filenames[key_id].title
new_title = filenames[key_id].name
list_datafiles.append(new_title)
self.list_data_items.append(new_title)

Expand Down Expand Up @@ -355,8 +355,8 @@ def _findId(self, name):

for key_id in list(self.filenames.keys()):
# data with title
if self.filenames[key_id].title:
input = self.filenames[key_id].title
if self.filenames[key_id].name:
input = self.filenames[key_id].name
# data without title
else:
input = str(key_id)
Expand Down
25 changes: 17 additions & 8 deletions src/sas/qtgui/MainWindow/DataExplorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
import sas.qtgui.Perspectives as Perspectives

DEFAULT_PERSPECTIVE = "Fitting"
ANALYSIS_TYPES = ['Fitting (*.fitv)', 'Inversion (*.pr)', 'All File (*.*)']
ANALYSIS_TYPES = ['Fitting (*.fitv)', 'Inversion (*.pr)', 'Invariant (*.inv)',
'Corfunc (*.crf)', 'All Files (*.*)']

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -270,14 +271,11 @@ def loadProject(self):
filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
if filename:
self.default_project_location = os.path.dirname(filename)
# Inversion perspective will remove all data with delete
# Delete all data and initialize all perspectives
self.deleteAllItems()
# Currently project load is available only for fitting
if self.cbFitting.currentText != DEFAULT_PERSPECTIVE:
self.cbFitting.setCurrentIndex(
self.cbFitting.findText(DEFAULT_PERSPECTIVE))
# delete all (including the default) tabs
self._perspective().deleteAllTabs()
self.cbFitting.disconnect()
self.parent.loadAllPerspectives()
self.initPerspectives()
self.readProject(filename)

def loadAnalysis(self):
Expand Down Expand Up @@ -512,9 +510,11 @@ def readProject(self, filename):
grid_page.append(grid_name)
self.parent.showBatchOutput(grid_page)
continue
# Store constraint pages until all individual fits are open
if 'cs_tab' in key:
cs_keys.append(key)
continue
# Load last visible perspective as stored in project file
if 'visible_perspective' in key:
visible_perspective = value
# send newly created items to the perspective
Expand Down Expand Up @@ -598,6 +598,14 @@ def updatePerspectiveWithProperties(self, key, value):
params = value['pr_params']
self.sendItemToPerspective(items[0])
self._perspective().updateFromParameters(params)
if 'invar_params' in value:
self.cbFitting.setCurrentIndex(self.cbFitting.findText('Invariant'))
self.sendItemToPerspective(items[0])
self._perspective().updateFromParameters(value['invar_params'])
if 'corfunc_params' in value:
self.cbFitting.setCurrentIndex(self.cbFitting.findText('Corfunc'))
self.sendItemToPerspective(items[0])
self._perspective().updateFromParameters(value['corfunc_params'])
if 'cs_tab' in key and 'is_constraint' in value:
# Create a C&S page
self._perspective().addConstraintTab()
Expand Down Expand Up @@ -1679,6 +1687,7 @@ def deleteAllItems(self):
self.communicator.dataDeletedSignal.emit(deleted_items)
# update stored_data
self.manager.update_stored_data(deleted_names)
self.manager.delete_data(data_id=[], theory_id=[], delete_all=True)

# Clear the model
self.model.clear()
Expand Down
35 changes: 29 additions & 6 deletions src/sas/qtgui/MainWindow/GuiManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def __init__(self, parent=None):

# Currently displayed perspective
self._current_perspective = None
self.loadedPerspectives = {}

# Populate the main window with stuff
self.addWidgets()
Expand Down Expand Up @@ -126,11 +127,7 @@ def addWidgets(self):
Populate the main window with widgets
"""
# Preload all perspectives
loaded_dict = {}
for name, perspective in Perspectives.PERSPECTIVES.items():
loaded_perspective = perspective(parent=self)
loaded_dict[name] = loaded_perspective
self.loadedPerspectives = loaded_dict
self.loadAllPerspectives()

# Add FileDialog widget as docked
self.filesWidget = DataExplorerWindow(self._parent, self, manager=self._data_manager)
Expand Down Expand Up @@ -187,6 +184,32 @@ def addWidgets(self):
self.DataOperation = DataOperationUtilityPanel(self)
self.FileConverter = FileConverterWidget(self)

def loadAllPerspectives(self):
# Close any existing perspectives to prevent multiple open instances
self.closeAllPerspectives()
# Load all perspectives
loaded_dict = {}
for name, perspective in Perspectives.PERSPECTIVES.items():
try:
loaded_perspective = perspective(parent=self)
loaded_dict[name] = loaded_perspective
except Exception as e:
logger.log(f"Unable to load {name} perspective.\n{e}")
self.loadedPerspectives = loaded_dict

def closeAllPerspectives(self):
# Close all perspectives if they are open
if isinstance(self.loadedPerspectives, dict):
for name, perspective in self.loadedPerspectives.items():
try:
perspective.setClosable(True)
self._workspace.workspace.removeSubWindow(self.subwindow)
perspective.close()
except Exception as e:
logger.log(f"Unable to close {name} perspective\n{e}")
self.loadedPerspectives = {}
self._current_perspective = None

def addCategories(self):
"""
Make sure categories.json exists and if not compile it and install in ~/.sasview
Expand Down Expand Up @@ -661,7 +684,7 @@ def actionSave_Project(self):
# Save from all serializable perspectives
# Analysis should return {data-id: serialized-state}
for name, per in self.loadedPerspectives.items():
if hasattr(per, 'isSerializable') and per.isSerializable:
if hasattr(per, 'isSerializable') and per.isSerializable():
analysis = per.serializeAll()
for key, value in analysis.items():
if key in final_data:
Expand Down
152 changes: 146 additions & 6 deletions src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ def draw_real_space(self):
class CorfuncWindow(QtWidgets.QDialog, Ui_CorfuncDialog):
"""Displays the correlation function analysis of sas data."""
name = "Corfunc" # For displaying in the combo box
ext = " crf" # File extension used for saving analysis files

trigger = QtCore.pyqtSignal(tuple)

Expand All @@ -203,10 +204,12 @@ def __init__(self, parent=None):
self.mapper = None
self._path = ""
self.model = QtGui.QStandardItemModel(self)
self.communicate = GuiUtils.Communicate()
self.communicate = self.parent.communicator()
self.communicate.dataDeletedSignal.connect(self.removeData)
self._calculator = CorfuncCalculator()
self._allow_close = False
self._model_item = None
self.has_data = False
self.txtLowerQMin.setText("0.0")
self.txtLowerQMin.setEnabled(False)

Expand All @@ -230,6 +233,12 @@ def __init__(self, parent=None):
# Set up the mapper
self.setup_mapper()

def isSerializable(self):
"""
Tell the caller that this perspective writes its state
"""
return True

def setup_slots(self):
"""Connect the buttons to their appropriate slots."""
self.cmdExtrapolate.clicked.connect(self.extrapolate)
Expand Down Expand Up @@ -280,6 +289,29 @@ def setup_model(self):
self.model.setItem(W.W_POLY, QtGui.QStandardItem(str(0)))
self.model.setItem(W.W_PERIOD, QtGui.QStandardItem(str(0)))

def removeData(self, data_list=None):
"""Remove the existing data reference from the Invariant Persepective"""
if not data_list or self._model_item not in data_list:
return
# Clear data plots
self._canvas.data = None
self._canvas.extrap = None
self._realplot.data = None
self._realplot.extrap = None
# Clear calculator, model, and data path
self._calculator = CorfuncCalculator()
self._model_item = None
self.has_data = False
self._path = ""
# Pass an empty dictionary to set all inputs to their default values
self.updateFromParameters({})
# Disable buttons to return to base state
self.cmdExtrapolate.setEnabled(False)
self.cmdTransform.setEnabled(False)
self.cmdExtract.setEnabled(False)
self.cmdSave.setEnabled(False)
self.cmdCalculateBg.setEnabled(False)

def model_changed(self, _):
"""Actions to perform when the data is updated"""
if not self.mapper:
Expand Down Expand Up @@ -475,9 +507,17 @@ def setData(self, data_item, is_batch=False):
msg = "Incorrect type passed to the Corfunc Perspective"
raise AttributeError(msg)

if self.has_data:
msg = "Data is already loaded into the Corfunc perspective. Sending a new data set "
msg += f"will remove the Corfunc analysis for {self._path}. Continue?"
dialog = QtWidgets.QMessageBox(self, text=msg)
dialog.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
retval = dialog.exec_()
if retval == QtWidgets.QMessageBox.Cancel:
return

model_item = data_item[0]
data = GuiUtils.dataFromItem(model_item)
self._path = data.name
self._model_item = model_item
self._calculator.set_data(data)
self.cmdCalculateBg.setEnabled(True)
Expand All @@ -494,16 +534,17 @@ def setData(self, data_item, is_batch=False):
self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(""))
self.model.setItem(W.W_POLY, QtGui.QStandardItem(""))
self.model.setItem(W.W_PERIOD, QtGui.QStandardItem(""))
self.model.setItem(W.W_FILENAME, QtGui.QStandardItem(self._path))
self.model.itemChanged.connect(self.model_changed)

self._canvas.data = data
self._canvas.extrap = None
self.model_changed(None)
self.cmdTransform.setEnabled(False)
self._path = data.name
self.model.setItem(W.W_FILENAME, QtGui.QStandardItem(self._path))
self._realplot.data = None
self._realplot.draw_real_space()

self.has_data = True

def setClosable(self, value=True):
"""
Expand All @@ -520,8 +561,8 @@ def closeEvent(self, event):
if self._allow_close:
# reset the closability flag
self.setClosable(value=False)
# Tell the MdiArea to close the container
if self.parent:
# Tell the MdiArea to close the container if it is visible
if self.parentWidget():
self.parentWidget().close()
event.accept()
else:
Expand Down Expand Up @@ -556,3 +597,102 @@ def on_save(self):
np.savetxt(outfile,
np.vstack([(data1.x, data1.y, data3.y, data_idf.y)]).T)
# pylint: enable=invalid-name

def serializeAll(self):
"""
Serialize the corfunc state so data can be saved
Corfunc is not batch-ready so this will only effect a single page
:return: {data-id: {self.name: {corfunc-state}}}
"""
return self.serializeCurrentPage()

def serializeCurrentPage(self):
"""
Serialize and return a dictionary of {data_id: corfunc-state}
Return empty dictionary if no data
:return: {data-id: {self.name: {corfunc-state}}}
"""
state = {}
if self.has_data:
tab_data = self.getPage()
data_id = tab_data.pop('data_id', '')
state[data_id] = {'corfunc_params': tab_data}
return state

def getPage(self):
"""
Serializes full state of this corfunc page
Called by Save Analysis
:return: {corfunc-state}
"""
# Get all parameters from page
data = GuiUtils.dataFromItem(self._model_item)
param_dict = self.getState()
param_dict['data_name'] = str(data.name)
param_dict['data_id'] = str(data.id)
return param_dict

def getState(self):
"""
Collects all active params into a dictionary of {name: value}
:return: {name: value}
"""
return {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably query the model (self.model) rather than UI elements.

'guinier_a': self.txtGuinierA.text(),
'guinier_b': self.txtGuinierB.text(),
'porod_k': self.txtPorodK.text(),
'porod_sigma': self.txtPorodSigma.text(),
'avg_core_thick': self.txtAvgCoreThick.text(),
'avg_inter_thick': self.txtAvgIntThick.text(),
'avg_hard_block_thick': self.txtAvgHardBlock.text(),
'local_crystalinity': self.txtLocalCrystal.text(),
'polydispersity': self.txtPolydisp.text(),
'long_period': self.txtLongPeriod.text(),
'lower_q_max': self.txtLowerQMax.text(),
'upper_q_min': self.txtUpperQMin.text(),
'upper_q_max': self.txtUpperQMax.text(),
'background': self.txtBackground.text(),
}

def updateFromParameters(self, params):
"""
Called by Open Project, Open Analysis, and removeData
:param params: {param_name: value} -> Default values used if not valid
:return: None
"""
# Params should be a dictionary
if not isinstance(params, dict):
c_name = params.__class__.__name__
msg = "Corfunc.updateFromParameters expects a dictionary"
raise TypeError(f"{msg}: {c_name} received")
# Assign values to 'Invariant' tab inputs - use defaults if not found
self.model.setItem(
W.W_GUINIERA, QtGui.QStandardItem(params.get('guinier_a', '0.0')))
self.model.setItem(
W.W_GUINIERB, QtGui.QStandardItem(params.get('guinier_b', '0.0')))
self.model.setItem(
W.W_PORODK, QtGui.QStandardItem(params.get('porod_k', '0.0')))
self.model.setItem(W.W_PORODSIGMA, QtGui.QStandardItem(
params.get('porod_sigma', '0.0')))
self.model.setItem(W.W_CORETHICK, QtGui.QStandardItem(
params.get('avg_core_thick', '0')))
self.model.setItem(W.W_INTTHICK, QtGui.QStandardItem(
params.get('avg_inter_thick', '0')))
self.model.setItem(W.W_HARDBLOCK, QtGui.QStandardItem(
params.get('avg_hard_block_thick', '0')))
self.model.setItem(W.W_CRYSTAL, QtGui.QStandardItem(
params.get('local_crystalinity', '0')))
self.model.setItem(
W.W_POLY, QtGui.QStandardItem(params.get('polydispersity', '0')))
self.model.setItem(
W.W_PERIOD, QtGui.QStandardItem(params.get('long_period', '0')))
self.model.setItem(
W.W_FILENAME, QtGui.QStandardItem(params.get('data_name', '')))
self.model.setItem(
W.W_QMIN, QtGui.QStandardItem(params.get('lower_q_max', '0.01')))
self.model.setItem(
W.W_QMAX, QtGui.QStandardItem(params.get('upper_q_min', '0.20')))
self.model.setItem(
W.W_QCUTOFF, QtGui.QStandardItem(params.get('upper_q_max', '0.22')))
self.model.setItem(W.W_BACKGROUND, QtGui.QStandardItem(
params.get('background', '0')))
Loading