-
Notifications
You must be signed in to change notification settings - Fork 49
Support for SAXS fitting (v2) #3786
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
base: main
Are you sure you want to change the base?
Changes from all commits
1aed392
0aaaa66
4f1d777
5ff6333
6b3a085
7e6e29d
11d14ce
7302bd8
bd3ddc0
8f53f2b
bfbcf44
642cf67
7ea6e6b
f9b2eaa
77be836
edc4d55
c4e8ae0
606de0c
afa9f53
b118e06
02363ba
b0aa9bf
d683f88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ numpy | |
| packaging | ||
| periodictable | ||
| platformdirs | ||
| pyausaxs==1.0.4 | ||
| pyausaxs | ||
| pybind11 | ||
| pylint | ||
| pyopencl | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |
| from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor | ||
| from sas.sascalc.calculator import sas_gen | ||
| from sas.sascalc.calculator.geni import create_beta_plot, f_of_q, radius_of_gyration | ||
| from sas.sascalc.calculator.sas_gen import ComputationType | ||
| from sas.system.user import find_plugins_dir | ||
|
|
||
| # Local UI | ||
|
|
@@ -89,7 +90,7 @@ def __init__(self, parent=None): | |
| self.setup_display() | ||
|
|
||
| # combox box | ||
| self.cbOptionsCalc.currentIndexChanged.connect(self.change_is_avg) | ||
| self.cbOptionsCalc.currentIndexChanged.connect(self.change_computation_type) | ||
| # prevent layout shifting when widget hidden | ||
| # TODO: Is there a way to lcoate this policy in the ui file? | ||
| sizePolicy = self.cbOptionsCalc.sizePolicy() | ||
|
|
@@ -621,7 +622,7 @@ def update_cbOptionsCalc_visibility(self): | |
| self.cbOptionsCalc.setVisible(allow) | ||
| if (allow): | ||
| # A helper function to set up the averaging system | ||
| self.change_is_avg() | ||
| self.change_computation_type() | ||
| else: | ||
| # If magnetic data present then no averaging is allowed | ||
| self.is_avg = False | ||
|
|
@@ -633,7 +634,7 @@ def update_cbOptionsCalc_visibility(self): | |
| self.checkboxLogSpace.setEnabled(not self.is_mag) | ||
|
|
||
|
|
||
| def change_is_avg(self): | ||
| def change_computation_type(self): | ||
krzywon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """Adjusts the GUI for whether 1D averaging is enabled | ||
|
|
||
| If the user has chosen to carry out Debye full averaging then the magnetic sld | ||
|
|
@@ -658,6 +659,22 @@ def change_is_avg(self): | |
| self.checkboxLogSpace.setEnabled(self.is_avg) | ||
| self.checkboxPluginModel.setEnabled(self.is_avg) | ||
|
|
||
| # set the type of calculation | ||
| self.model.set_computation_type(ComputationType(self.cbOptionsCalc.currentIndex())) | ||
| match self.cbOptionsCalc.currentIndex(): | ||
krzywon marked this conversation as resolved.
Show resolved
Hide resolved
klytje marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| case 0 | 1 | 2: | ||
| pass | ||
| case 3: | ||
| self.checkboxPluginModel.setEnabled(False) | ||
| self.checkboxPluginModel.setChecked(True) | ||
| self.txtFileName.setText("saxs_fitting") | ||
| self.txtFileName.setEnabled(False) | ||
| self.cmdCompute.setText("Generate plugin model") | ||
| return | ||
| case _: | ||
| raise RuntimeError(f"Unknown computation type selected: {self.cbOptionsCalc.currentIndex()}") | ||
|
|
||
| self.cmdCompute.setText("Compute") | ||
| if self.is_avg: | ||
| self.txtMx.setText("0.0") | ||
| self.txtMy.setText("0.0") | ||
|
|
@@ -704,13 +721,20 @@ def loadFile(self): | |
| load_nuc = self.sender() == self.cmdNucLoad | ||
| # request a file from the user | ||
| if load_nuc: | ||
| f_type = """ | ||
| All supported files (*.SLD *.sld *.pdb *.PDB, *.vtk, *.VTK);; | ||
| SLD files (*.SLD *.sld);; | ||
| PDB files (*.pdb *.PDB);; | ||
| VTK files (*.vtk *.VTK);; | ||
| All files (*.*) | ||
| """ | ||
| if self.model.type is ComputationType.SAXS: | ||
| f_type = """ | ||
krzywon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| All supported files (*.CIF *.cif *.pdb *.PDB);; | ||
| CIF files (*.CIF *.cif);; | ||
| PDB files (*.pdb *.PDB);; | ||
| """ | ||
| else: | ||
| f_type = """ | ||
| All supported files (*.SLD *.sld *.pdb *.PDB, *.vtk, *.VTK);; | ||
| SLD files (*.SLD *.sld);; | ||
| PDB files (*.pdb *.PDB);; | ||
| VTK files (*.vtk *.VTK);; | ||
| All files (*.*) | ||
| """ | ||
| else: | ||
| f_type = """ | ||
| All supported files (*.OMF *.omf *.SLD *.sld, *.vtk, *.VTK);; | ||
|
|
@@ -720,6 +744,11 @@ def loadFile(self): | |
| All files (*.*) | ||
| """ | ||
| self.datafile = QtWidgets.QFileDialog.getOpenFileName(self, "Choose a file", "", f_type)[0] | ||
|
|
||
| if self.model.type is ComputationType.SAXS: | ||
| self.txtNucData.setText(os.path.basename(str(self.datafile))) | ||
| return | ||
|
|
||
| # If a file has been sucessfully chosen | ||
| if self.datafile: | ||
| # set basic data about the file | ||
|
|
@@ -1412,6 +1441,48 @@ def onCompute(self): | |
|
|
||
| Copied from previous version | ||
| """ | ||
|
|
||
| if self.model.type is ComputationType.SAXS: | ||
| if self.datafile is None: | ||
| raise RuntimeError("No structure file is loaded! SAXS calculations require a structure file.") | ||
| from sas.qtgui.Calculators.SAXSPluginModelGenerator import get_base_plugin_name, write_plugin_model | ||
| write_plugin_model(self.datafile) | ||
| self.manager.communicator().customModelDirectoryChanged.emit() # notify that a new plugin model is available | ||
|
|
||
| # try to bring the fit panel into focus and select the newly generated plugin | ||
| try: | ||
| self.manager.actionFitting() # switch to fitting window | ||
| per = self.manager.perspective() # internal access into the fitting window's state | ||
| # currentFittingWidget is provided by the Fitting perspective | ||
| fw = getattr(per, 'currentFittingWidget', None) | ||
| if fw is not None: | ||
| # select the plugin models category & our newly generated model | ||
| idx = fw.cbCategory.findText("Plugin Models") | ||
| if idx == -1: return | ||
|
|
||
| # force population of model combobox | ||
| fw.cbCategory.setCurrentIndex(idx) | ||
| fw.onSelectCategory() | ||
|
|
||
| # plugin name base is 'SAXS fit' | ||
| # the actual model name includes a structure tag, e.g. 'SAXS fit (2epe)' | ||
| model_name = get_base_plugin_name() | ||
| midx = fw.cbModel.findText(model_name, QtCore.Qt.MatchStartsWith) | ||
| if midx == -1: return | ||
|
|
||
| # load the model into the parameter table | ||
| fw.cbModel.setCurrentIndex(midx) | ||
| fw.onSelectModel() | ||
|
|
||
| # make sure the perspective window is visible and focused | ||
| self.close() # close the calculator window to highlight the changes to the fitting window | ||
| per.show() | ||
|
|
||
| except Exception: | ||
| pass | ||
|
Comment on lines
+1481
to
+1482
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How could a user arrive here and what should they do to correct it, if they do?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about something like this? It's not critical that this part succeeds, which is why I wrapped it in a try/catch clause. |
||
|
|
||
| return | ||
|
|
||
| try: | ||
| # create the combined sld data and update from gui | ||
| sld_data = self.create_full_sld_data() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| from pathlib import Path | ||
|
|
||
| from sas.system.user import find_plugins_dir | ||
|
|
||
|
|
||
| def get_base_plugin_name() -> str: | ||
| """ | ||
| Get the base name for the AUSAXS SAXS plugin model. | ||
|
|
||
| :return: The base name of the plugin model. | ||
| """ | ||
|
|
||
| return "SAXS fit" | ||
|
|
||
| def write_plugin_model(structure_path: str): | ||
| """ | ||
| Write the AUSAXS SAXS plugin model to the plugins directory. | ||
| The current version will be overwritten if it exists. | ||
|
|
||
| :param structure_path: Path to the structure file to be used by the plugin. | ||
| """ | ||
|
|
||
| path = Path(find_plugins_dir()) / "ausaxs_saxs_plugin.py" | ||
| text = get_model_text(structure_path) | ||
| with open(path, 'w') as f: | ||
| f.write(text) | ||
|
|
||
| def get_model_text(structure_path: str) -> str: | ||
| """ | ||
| Generate the text of the AUSAXS SAXS plugin model. | ||
|
|
||
| :param structure_path: Path to the structure file to be used by the plugin. | ||
| :return: The text of the plugin model. | ||
| """ | ||
|
|
||
klytje marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return ( | ||
|
|
||
| f'''\ | ||
| r""" | ||
| This file is auto-generated, and any changes will be overwritten. | ||
|
|
||
| This plugin model uses the AUSAXS library (https://doi.org/10.1107/S160057672500562X) to fit the provided SAXS data to the file: | ||
| * \"{structure_path}\" | ||
| If this is not the intended structure file, please regenerate the plugin model from the generic scattering calculator. | ||
| """ | ||
| ''' | ||
|
|
||
| f'''\ | ||
| name = "{get_base_plugin_name()} ({Path(structure_path).name.split('.')[0]})" | ||
| title = "AUSAXS" | ||
| description = "Structural validation using AUSAXS" | ||
| category = "plugin" | ||
| parameters = [ | ||
| # name, units, default, [min, max], type, description | ||
| ['c', '', 1, [0, 100], '', 'Solvent density'], | ||
| #['d', '', 1, [0, 2], '', 'Excluded volume parameter'] | ||
| ] | ||
|
|
||
| ### | ||
| import pyausaxs as ausaxs | ||
|
|
||
| structure_path = "{str(Path(structure_path).as_posix())}" | ||
|
|
||
| def Iq(q, c): | ||
| # Initialize on first call to keep objects alive for function lifetime | ||
| if not hasattr(Iq, '_initialized'): | ||
| Iq._mol = ausaxs.create_molecule(structure_path) | ||
| Iq._mol.hydrate() | ||
| Iq._fitobj = ausaxs.manual_fit(Iq._mol) | ||
| Iq._initialized = True | ||
| return Iq._fitobj.evaluate([c], q) | ||
| Iq.vectorized = True | ||
| ''') | ||
Uh oh!
There was an error while loading. Please reload this page.