From 1aed392cf7314b71481fdafaef3fd1e7a4c7f464 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Tue, 18 Nov 2025 19:55:18 +0100 Subject: [PATCH 01/23] started work on alternative saxs impl --- .../GenericScatteringCalculator.py | 62 ++++++++++++--- .../Calculators/SAXSPluginModelGenerator.py | 60 +++++++++++++++ .../UI/GenericScatteringCalculator.ui | 5 ++ .../Perspectives/Fitting/FittingWidget.py | 1 + src/sas/sascalc/calculator/sas_gen.py | 76 ++++++++++++------- 5 files changed, 166 insertions(+), 38 deletions(-) create mode 100644 src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index f99c2c527d..3f15f05c11 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -20,6 +20,7 @@ import sas.qtgui.Utilities.GuiUtils as GuiUtils import sas.sascalc.calculator.gsc_model as gsc_model +from sas.sascalc.calculator.sas_gen import ComputationType from sas.qtgui.Plotting.Arrow3D import Arrow3D from sas.qtgui.Plotting.PlotterBase import PlotterBase from sas.qtgui.Plotting.PlotterData import Data1D, Data2D @@ -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): """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,24 @@ def change_is_avg(self): self.checkboxLogSpace.setEnabled(self.is_avg) self.checkboxPluginModel.setEnabled(self.is_avg) + # set the type of calculation + match self.cbOptionsCalc.currentIndex(): + case 0: + self.model.set_calculation_type(ComputationType.SANS_2D) + case 1: + self.model.set_calculation_type(ComputationType.SANS_1D) + case 2: + self.model.set_calculation_type(ComputationType.SANS_1D_BETA) + case 3: + self.model.set_calculation_type(ComputationType.SAXS) + self.checkboxPluginModel.setEnabled(False) + self.checkboxPluginModel.setChecked(True) + self.txtFileName.setText("saxs_fitting") + self.txtFileName.setEnabled(False) + self.cmdCompute.setText("Generate plugin model") + return + + self.cmdCompute.setText("Compute") if self.is_avg: self.txtMx.setText("0.0") self.txtMy.setText("0.0") @@ -704,13 +723,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 = """ + All supported files (*.CIF *.cif *.pdb *.PDB);; + CIF files (*.SLD *.sld);; + 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 +746,10 @@ def loadFile(self): All files (*.*) """ self.datafile = QtWidgets.QFileDialog.getOpenFileName(self, "Choose a file", "", f_type)[0] + + if self.model.type is ComputationType.SAXS: + return + # If a file has been sucessfully chosen if self.datafile: # set basic data about the file @@ -1412,6 +1442,16 @@ 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 write_plugin_model + write_plugin_model(self.datafile) + self.manager.communicator().customModelDirectoryChanged.emit() + # automatically select the new model & focus on the fitting panel? + return + try: # create the combined sld data and update from gui sld_data = self.create_full_sld_data() @@ -1594,7 +1634,7 @@ def getFileName(self): def onSaveFile(self): """Save data as .sld file""" - path = os.path.dirname(str(self.datafile)) + path = os.path.dirname(str(self.nuc_filename if self.nuc_filename else self.mag_filename)) default_name = os.path.join(path, 'sld_file') parent = self directory = default_name diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py new file mode 100644 index 0000000000..c223d7e389 --- /dev/null +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -0,0 +1,60 @@ +from pathlib import Path + +from sas.system.user import find_plugins_dir + +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. + """ + + return ( + +f'''\ +r""" +This file is auto-generated, and any changes will be overwritten. + +This plugin model uses the AUSAXS library (doi: 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 = "SAXS fitting" +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'] +] + +### +from pyausaxs.wrapper.IterativeFit import * + +structure_path = "{structure_path}" +mol = ausaxs.create_molecule(structure_path) +mol.hydrate() +fit = ausaxs.start_iterative_fit(mol) + +def Iq(q, c, d): + return fit.evaluate([c, d]) +Iq.vectorized = True +''') \ No newline at end of file diff --git a/src/sas/qtgui/Calculators/UI/GenericScatteringCalculator.ui b/src/sas/qtgui/Calculators/UI/GenericScatteringCalculator.ui index 8e759393c4..ac6c1a3451 100644 --- a/src/sas/qtgui/Calculators/UI/GenericScatteringCalculator.ui +++ b/src/sas/qtgui/Calculators/UI/GenericScatteringCalculator.ui @@ -2194,6 +2194,11 @@ NOTE: Currently not impacted by Solvent SLD Debye full avg. w/ β(Q) + + + SAXS fitting + + diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 90fb03f3bd..6df8a836b8 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -38,6 +38,7 @@ from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller from sas.sascalc.fit import models from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit +from sas.system.user import find_plugins_dir from sas.system import HELP_SYSTEM TAB_MAGNETISM = 4 diff --git a/src/sas/sascalc/calculator/sas_gen.py b/src/sas/sascalc/calculator/sas_gen.py index ae2c6bb548..e2a17c5130 100644 --- a/src/sas/sascalc/calculator/sas_gen.py +++ b/src/sas/sascalc/calculator/sas_gen.py @@ -9,6 +9,7 @@ import logging import os import sys +from enum import Enum import numpy as np from periodictable import formula, nsf @@ -53,6 +54,12 @@ def transform_center(pos_x, pos_y, pos_z): posz = pos_z - (min(pos_z) + max(pos_z)) / 2.0 return posx, posy, posz +class ComputationType(Enum): + SANS_2D = 0, + SANS_1D = 1, + SANS_1D_BETA = 2, + SAXS = 3 + class GenSAS: """ Generic SAS computation Model based on sld (n & m) arrays @@ -75,6 +82,7 @@ def __init__(self): self.data_vol = None # [A^3] self.is_avg = False self.is_elements = False + self.type = ComputationType.SANS_2D ## Name of the model self.name = "GenSAS" ## Define parameters @@ -106,6 +114,12 @@ def __init__(self): # fixed parameters self.fixed = [] + def set_calculation_type(self, computation_type : ComputationType): + """ + Set the computation type. This will determine which calculation is performed. + """ + self.type = computation_type + def set_pixel_volumes(self, volume): """ Set the volume of a pixel in (A^3) unit @@ -180,33 +194,41 @@ def calculate_Iq(self, qx, qy=None): x, y, z = self.transform_positions() sld = self.data_sldn - self.params['solvent_SLD'] vol = self.data_vol - if qy is not None and len(qy) > 0: - # 2-D calculation - qx, qy = _vec(qx), _vec(qy) - # MagSLD can have sld_m = None, although in practice usually a zero array - # if all are None can continue as normal, otherwise set None to array of zeroes to allow rotations - mx, my, mz = self.transform_magnetic_slds() - in_spin = self.params['Up_frac_in'] - out_spin = self.params['Up_frac_out'] - # transform angles from environment to beamline coords - s_theta, s_phi = self.transform_angles() - - if self.is_elements: - I_out = Iqxy( - qx, qy, x, y, z, sld, vol, mx, my, mz, - in_spin, out_spin, s_theta, s_phi, - self.data_elements, self.is_elements) - else: - I_out = Iqxy( - qx, qy, x, y, z, sld, vol, mx, my, mz, - in_spin, out_spin, s_theta, s_phi, - ) - else: - # 1-D calculation - q = _vec(qx) - if self.is_avg: - x, y, z = transform_center(x, y, z) - I_out = Iq(q, x, y, z, sld, vol, is_avg=self.is_avg) + match self.type: + case ComputationType.SANS_2D: + if qy is not None and len(qy) > 0: + raise ValueError("For SANS_2D computation qy must be None or empty") + + # 2-D calculation + qx, qy = _vec(qx), _vec(qy) + # MagSLD can have sld_m = None, although in practice usually a zero array + # if all are None can continue as normal, otherwise set None to array of zeroes to allow rotations + mx, my, mz = self.transform_magnetic_slds() + in_spin = self.params['Up_frac_in'] + out_spin = self.params['Up_frac_out'] + # transform angles from environment to beamline coords + s_theta, s_phi = self.transform_angles() + + if self.is_elements: + I_out = Iqxy( + qx, qy, x, y, z, sld, vol, mx, my, mz, + in_spin, out_spin, s_theta, s_phi, + self.data_elements, self.is_elements) + else: + I_out = Iqxy( + qx, qy, x, y, z, sld, vol, mx, my, mz, + in_spin, out_spin, s_theta, s_phi, + ) + + case ComputationType.SANS_1D | ComputationType.SANS_1D_BETA: + # 1-D calculation + q = _vec(qx) + if self.is_avg: + x, y, z = transform_center(x, y, z) + I_out = Iq(q, x, y, z, sld, vol, is_avg=self.is_avg) + + case ComputationType.SAXS: + raise RuntimeError("SAXS calculations can only be performed through a plugin model! Please click the \"plugin model\" button instead.") vol_correction = self.data_total_volume / self.params['total_volume'] result = ((self.params['scale'] * vol_correction) * I_out From 0aaaa6677464947e4988f55e46230f4a4cf4139e Mon Sep 17 00:00:00 2001 From: krellemeister Date: Wed, 19 Nov 2025 23:09:26 +0100 Subject: [PATCH 02/23] updated plugin generator --- src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py index c223d7e389..200030da6c 100644 --- a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -43,18 +43,18 @@ def get_model_text(structure_path: str) -> str: parameters = [ # name, units, default, [min, max], type, description ['c', '', 1, [0, 100], '', 'Solvent density'], - ['d', '', 1, [0, 2], '', 'Excluded volume parameter'] + #['d', '', 1, [0, 2], '', 'Excluded volume parameter'] ] ### -from pyausaxs.wrapper.IterativeFit import * +import pyausaxs as ausaxs structure_path = "{structure_path}" mol = ausaxs.create_molecule(structure_path) mol.hydrate() -fit = ausaxs.start_iterative_fit(mol) +fit = ausaxs.manual_fit(mol) -def Iq(q, c, d): - return fit.evaluate([c, d]) +def Iq(q, c): + return fit.evaluate([c], q) Iq.vectorized = True ''') \ No newline at end of file From 4f1d777133d9f42985ea4d868957062671c19025 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 22 Nov 2025 10:54:01 +0100 Subject: [PATCH 03/23] seems to work --- .../GenericScatteringCalculator.py | 37 ++++++++++++++++++- .../Calculators/SAXSPluginModelGenerator.py | 16 +++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index 3f15f05c11..6cc781ef68 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -748,6 +748,7 @@ def loadFile(self): 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 @@ -1448,8 +1449,40 @@ def onCompute(self): raise RuntimeError("No structure file is loaded! SAXS calculations require a structure file.") from sas.qtgui.Calculators.SAXSPluginModelGenerator import write_plugin_model write_plugin_model(self.datafile) - self.manager.communicator().customModelDirectoryChanged.emit() - # automatically select the new model & focus on the fitting panel? + 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 = "SAXS fit" + 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 + return try: diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py index 200030da6c..202d936e08 100644 --- a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -1,3 +1,4 @@ +import os from pathlib import Path from sas.system.user import find_plugins_dir @@ -29,14 +30,14 @@ def get_model_text(structure_path: str) -> str: r""" This file is auto-generated, and any changes will be overwritten. -This plugin model uses the AUSAXS library (doi: https://doi.org/10.1107/S160057672500562X) to fit the provided SAXS data to the file: +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 = "SAXS fitting" +name = "SAXS fit ({os.path.basename(structure_path).split('.')[0]})" title = "AUSAXS" description = "Structural validation using AUSAXS" category = "plugin" @@ -50,11 +51,14 @@ def get_model_text(structure_path: str) -> str: import pyausaxs as ausaxs structure_path = "{structure_path}" -mol = ausaxs.create_molecule(structure_path) -mol.hydrate() -fit = ausaxs.manual_fit(mol) def Iq(q, c): - return fit.evaluate([c], q) + # 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 ''') \ No newline at end of file From 5ff63338745c2d23ad5f97eb92999979f5dc2095 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 10:17:01 +0000 Subject: [PATCH 04/23] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 2 +- src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py | 3 ++- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index 6cc781ef68..ad657f5907 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -20,7 +20,6 @@ import sas.qtgui.Utilities.GuiUtils as GuiUtils import sas.sascalc.calculator.gsc_model as gsc_model -from sas.sascalc.calculator.sas_gen import ComputationType from sas.qtgui.Plotting.Arrow3D import Arrow3D from sas.qtgui.Plotting.PlotterBase import PlotterBase from sas.qtgui.Plotting.PlotterData import Data1D, Data2D @@ -28,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 diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py index 202d936e08..3324afb2eb 100644 --- a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -3,6 +3,7 @@ from sas.system.user import find_plugins_dir + def write_plugin_model(structure_path: str): """ Write the AUSAXS SAXS plugin model to the plugins directory. @@ -61,4 +62,4 @@ def Iq(q, c): Iq._initialized = True return Iq._fitobj.evaluate([c], q) Iq.vectorized = True -''') \ No newline at end of file +''') diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 6df8a836b8..104e7e2716 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -38,8 +38,8 @@ from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller from sas.sascalc.fit import models from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit -from sas.system.user import find_plugins_dir from sas.system import HELP_SYSTEM +from sas.system.user import find_plugins_dir TAB_MAGNETISM = 4 TAB_POLY = 3 From 6b3a085297a3f25bbd997aa0bc3f54f57adfb96e Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 22 Nov 2025 11:41:19 +0100 Subject: [PATCH 05/23] reverted unnecessary change --- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index ad657f5907..8f955bde24 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -1667,7 +1667,7 @@ def getFileName(self): def onSaveFile(self): """Save data as .sld file""" - path = os.path.dirname(str(self.nuc_filename if self.nuc_filename else self.mag_filename)) + path = os.path.dirname(str(self.datafile)) default_name = os.path.join(path, 'sld_file') parent = self directory = default_name From 7e6e29d34fc0fe1c2a69cecec4e3838c1a605e70 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 22 Nov 2025 11:42:48 +0100 Subject: [PATCH 06/23] change_calculation_type --> change_computation_type --- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 8 ++++---- src/sas/sascalc/calculator/sas_gen.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index 8f955bde24..bcb7a0eaed 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -662,13 +662,13 @@ def change_computation_type(self): # set the type of calculation match self.cbOptionsCalc.currentIndex(): case 0: - self.model.set_calculation_type(ComputationType.SANS_2D) + self.model.set_computation_type(ComputationType.SANS_2D) case 1: - self.model.set_calculation_type(ComputationType.SANS_1D) + self.model.set_computation_type(ComputationType.SANS_1D) case 2: - self.model.set_calculation_type(ComputationType.SANS_1D_BETA) + self.model.set_computation_type(ComputationType.SANS_1D_BETA) case 3: - self.model.set_calculation_type(ComputationType.SAXS) + self.model.set_computation_type(ComputationType.SAXS) self.checkboxPluginModel.setEnabled(False) self.checkboxPluginModel.setChecked(True) self.txtFileName.setText("saxs_fitting") diff --git a/src/sas/sascalc/calculator/sas_gen.py b/src/sas/sascalc/calculator/sas_gen.py index e2a17c5130..6d4b504bad 100644 --- a/src/sas/sascalc/calculator/sas_gen.py +++ b/src/sas/sascalc/calculator/sas_gen.py @@ -114,7 +114,7 @@ def __init__(self): # fixed parameters self.fixed = [] - def set_calculation_type(self, computation_type : ComputationType): + def set_computation_type(self, computation_type : ComputationType): """ Set the computation type. This will determine which calculation is performed. """ From 11d14ce23f00d44c6f06d2a29526f625eee4cf9a Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 22 Nov 2025 11:45:58 +0100 Subject: [PATCH 07/23] modified 2D guard logic --- src/sas/sascalc/calculator/sas_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/sascalc/calculator/sas_gen.py b/src/sas/sascalc/calculator/sas_gen.py index 6d4b504bad..f0be2ade50 100644 --- a/src/sas/sascalc/calculator/sas_gen.py +++ b/src/sas/sascalc/calculator/sas_gen.py @@ -196,7 +196,7 @@ def calculate_Iq(self, qx, qy=None): vol = self.data_vol match self.type: case ComputationType.SANS_2D: - if qy is not None and len(qy) > 0: + if not (qy is not None and len(qy) > 0): raise ValueError("For SANS_2D computation qy must be None or empty") # 2-D calculation From 7302bd8f86c8ef40a0d7be89aa6c5464741c6542 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 22 Nov 2025 11:55:16 +0100 Subject: [PATCH 08/23] updated pyausaxs to v1.0.9 --- build_tools/requirements.txt | 2 +- src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index b5b0fea925..3bce958104 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -16,7 +16,7 @@ numpy packaging periodictable platformdirs -pyausaxs==1.0.4 +pyausaxs==1.0.9 pybind11 pylint pyopencl diff --git a/src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py b/src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py index e7dd409023..5b2138c7c6 100644 --- a/src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py +++ b/src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py @@ -1,6 +1,6 @@ import logging -from pyausaxs import AUSAXS +import pyausaxs as ausaxs from sas.sascalc.calculator.ausaxs.sasview_sans_debye import sasview_sans_debye @@ -14,9 +14,8 @@ def evaluate_sans_debye(q, coords, w): *w* is the weight associated with each point. """ try: - ausaxs = AUSAXS() - Iq = ausaxs.debye(q, coords[0,:], coords[1,:], coords[2,:], w) + Iq = ausaxs.sasview.debye_no_ff(q, coords[0,:], coords[1,:], coords[2,:], w) return Iq except Exception as e: logging.warning("AUSAXS Debye calculation failed: %s. Falling back to default implementation.", e) - return sasview_sans_debye(q, coords, w) + return sasview_sans_debye(q, coords, w) \ No newline at end of file From bd3ddc09250205bc97645ef6fb203896ba4ca800 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 22 Nov 2025 14:01:37 +0000 Subject: [PATCH 09/23] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py b/src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py index 5b2138c7c6..8bdf515108 100644 --- a/src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py +++ b/src/sas/sascalc/calculator/ausaxs/ausaxs_sans_debye.py @@ -18,4 +18,4 @@ def evaluate_sans_debye(q, coords, w): return Iq except Exception as e: logging.warning("AUSAXS Debye calculation failed: %s. Falling back to default implementation.", e) - return sasview_sans_debye(q, coords, w) \ No newline at end of file + return sasview_sans_debye(q, coords, w) From 8f53f2b0c01007b22ab6fd078c4e0de9d85bbb05 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 22 Nov 2025 15:22:38 +0100 Subject: [PATCH 10/23] fixed ausaxs check for readiness --- test/sascalculator/utest_sas_gen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/sascalculator/utest_sas_gen.py b/test/sascalculator/utest_sas_gen.py index bd10005e0d..d0aff7bc50 100644 --- a/test/sascalculator/utest_sas_gen.py +++ b/test/sascalculator/utest_sas_gen.py @@ -225,15 +225,15 @@ def test_debye_impl(self): """ Test that the Debye algorithm supplied by the external AUSAXS library agrees with the default implementation. """ - from pyausaxs import AUSAXS + import pyausaxs as ausaxs from sas.sascalc.calculator.ausaxs import ausaxs_sans_debye, sasview_sans_debye rng = np.random.default_rng(1984) - ausaxs = AUSAXS() # ensure the library is available and ready to run on all CI systems - assert ausaxs.ready(), "AUSAXS library not available, test cannot be run." + # this awkward syntax will be improved in a future version of pyausaxs ... + assert ausaxs.wrapper.AUSAXS.AUSAXS().ready(), "AUSAXS library not available, test cannot be run." # get all pdb files in the data folder import glob From bfbcf44d9d1822ca02a4c22de7b6f369651b43b4 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 22 Nov 2025 15:24:53 +0100 Subject: [PATCH 11/23] maybe tests passing now? --- src/sas/sascalc/calculator/sas_gen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sas/sascalc/calculator/sas_gen.py b/src/sas/sascalc/calculator/sas_gen.py index f0be2ade50..769fefa91c 100644 --- a/src/sas/sascalc/calculator/sas_gen.py +++ b/src/sas/sascalc/calculator/sas_gen.py @@ -288,6 +288,7 @@ def run(self, x=0.0): if len(x[1]) > 0: raise ValueError("Not a 1D vector.") # 1D I is found at y=0 in the 2D pattern + self.set_computation_type(ComputationType.SANS_1D) out = self.calculate_Iq(x[0]) return out else: From 642cf67f6c4c5c4065f88ebbe198ae5fd16a2b81 Mon Sep 17 00:00:00 2001 From: Krelle Date: Sat, 6 Dec 2025 16:52:41 +0100 Subject: [PATCH 12/23] added default ComputationType case --- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index bcb7a0eaed..8e818237ec 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -675,6 +675,8 @@ def change_computation_type(self): 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: From 7ea6e6b59fb19d4f2c234dbfb6cd1c2c84178557 Mon Sep 17 00:00:00 2001 From: Krelle Date: Sat, 6 Dec 2025 16:56:59 +0100 Subject: [PATCH 13/23] fixed file name mixup --- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index 8e818237ec..8baf40c214 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -728,7 +728,7 @@ def loadFile(self): if self.model.type is ComputationType.SAXS: f_type = """ All supported files (*.CIF *.cif *.pdb *.PDB);; - CIF files (*.SLD *.sld);; + CIF files (*.CIF *.cif);; PDB files (*.pdb *.PDB);; """ else: From f9b2eaa9450c83346e878edab42b923abb60cba0 Mon Sep 17 00:00:00 2001 From: Krelle Date: Sat, 6 Dec 2025 17:01:01 +0100 Subject: [PATCH 14/23] os-independent file names --- src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py index 3324afb2eb..5c6d27fdb3 100644 --- a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -51,7 +51,7 @@ def get_model_text(structure_path: str) -> str: ### import pyausaxs as ausaxs -structure_path = "{structure_path}" +structure_path = "{str(Path(structure_path).resolve())}" def Iq(q, c): # Initialize on first call to keep objects alive for function lifetime From 77be83657ec4477cb95577d0db10faebd66473a8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:01:31 +0000 Subject: [PATCH 15/23] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index 8baf40c214..1b87059d15 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -675,7 +675,7 @@ def change_computation_type(self): self.txtFileName.setEnabled(False) self.cmdCompute.setText("Generate plugin model") return - case _: + case _: raise RuntimeError(f"Unknown computation type selected: {self.cbOptionsCalc.currentIndex()}") self.cmdCompute.setText("Compute") From edc4d559b71a6a0dce63c84c75b1c26554a2f5b3 Mon Sep 17 00:00:00 2001 From: Kristian Date: Wed, 14 Jan 2026 20:02:29 +0100 Subject: [PATCH 16/23] simplified logic --- .../qtgui/Calculators/GenericScatteringCalculator.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index 1b87059d15..8a28c48622 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -660,15 +660,11 @@ def change_computation_type(self): self.checkboxPluginModel.setEnabled(self.is_avg) # set the type of calculation + self.model.set_computation_type(self.cbOptionsCalc.currentIndex()) match self.cbOptionsCalc.currentIndex(): - case 0: - self.model.set_computation_type(ComputationType.SANS_2D) - case 1: - self.model.set_computation_type(ComputationType.SANS_1D) - case 2: - self.model.set_computation_type(ComputationType.SANS_1D_BETA) + case 0 | 1 | 2: + pass case 3: - self.model.set_computation_type(ComputationType.SAXS) self.checkboxPluginModel.setEnabled(False) self.checkboxPluginModel.setChecked(True) self.txtFileName.setText("saxs_fitting") From c4e8ae0997db7ace510fddb8a8a58601e26ba5b0 Mon Sep 17 00:00:00 2001 From: Kristian Date: Wed, 14 Jan 2026 20:05:10 +0100 Subject: [PATCH 17/23] created common access point for base plugin name --- .../qtgui/Calculators/GenericScatteringCalculator.py | 4 ++-- src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index 8a28c48622..0a1ddc9769 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -1445,7 +1445,7 @@ def onCompute(self): 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 write_plugin_model + from sas.qtgui.Calculators.SAXSPluginModelGenerator import write_plugin_model, get_base_plugin_name write_plugin_model(self.datafile) self.manager.communicator().customModelDirectoryChanged.emit() # notify that a new plugin model is available @@ -1466,7 +1466,7 @@ def onCompute(self): # plugin name base is 'SAXS fit' # the actual model name includes a structure tag, e.g. 'SAXS fit (2epe)' - model_name = "SAXS fit" + model_name = get_base_plugin_name() midx = fw.cbModel.findText(model_name, QtCore.Qt.MatchStartsWith) if midx == -1: return diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py index 5c6d27fdb3..866ba1a79c 100644 --- a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -3,6 +3,14 @@ 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): """ @@ -38,7 +46,7 @@ def get_model_text(structure_path: str) -> str: ''' f'''\ -name = "SAXS fit ({os.path.basename(structure_path).split('.')[0]})" +name = "{get_base_plugin_name()} ({os.path.basename(structure_path).split('.')[0]})" title = "AUSAXS" description = "Structural validation using AUSAXS" category = "plugin" From 606de0ca6ef30e4b005c2835c56fd0f615017ee2 Mon Sep 17 00:00:00 2001 From: Kristian Date: Wed, 14 Jan 2026 20:10:45 +0100 Subject: [PATCH 18/23] fixed exception message --- src/sas/sascalc/calculator/sas_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/sascalc/calculator/sas_gen.py b/src/sas/sascalc/calculator/sas_gen.py index 769fefa91c..7a804ab02b 100644 --- a/src/sas/sascalc/calculator/sas_gen.py +++ b/src/sas/sascalc/calculator/sas_gen.py @@ -197,7 +197,7 @@ def calculate_Iq(self, qx, qy=None): match self.type: case ComputationType.SANS_2D: if not (qy is not None and len(qy) > 0): - raise ValueError("For SANS_2D computation qy must be None or empty") + raise ValueError("For a SANS_2D computation, qy cannot be None or empty") # 2-D calculation qx, qy = _vec(qx), _vec(qy) From afa9f532cb70b761f931b9755a9dfbd29e48e2b9 Mon Sep 17 00:00:00 2001 From: Kristian Date: Wed, 14 Jan 2026 20:12:53 +0100 Subject: [PATCH 19/23] removed os import --- src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py index 866ba1a79c..9f953d3a05 100644 --- a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -1,4 +1,3 @@ -import os from pathlib import Path from sas.system.user import find_plugins_dir @@ -46,7 +45,7 @@ def get_model_text(structure_path: str) -> str: ''' f'''\ -name = "{get_base_plugin_name()} ({os.path.basename(structure_path).split('.')[0]})" +name = "{get_base_plugin_name()} ({Path(structure_path).name.split('.')[0]})" title = "AUSAXS" description = "Structural validation using AUSAXS" category = "plugin" From b118e063d1319e8aa1ca7465ada420b140e99369 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:13:24 +0000 Subject: [PATCH 20/23] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 2 +- src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index 0a1ddc9769..b16126ff84 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -1445,7 +1445,7 @@ def onCompute(self): 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 write_plugin_model, get_base_plugin_name + 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 diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py index 9f953d3a05..1d1d172b04 100644 --- a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -2,6 +2,7 @@ from sas.system.user import find_plugins_dir + def get_base_plugin_name() -> str: """ Get the base name for the AUSAXS SAXS plugin model. From 02363ba1639ddaf4005099e24472947858f4c99d Mon Sep 17 00:00:00 2001 From: Kristian Date: Thu, 15 Jan 2026 08:58:28 +0100 Subject: [PATCH 21/23] fix ComputationType enum --- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 2 +- src/sas/sascalc/calculator/sas_gen.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index b16126ff84..6a6bf2cc77 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -660,7 +660,7 @@ def change_computation_type(self): self.checkboxPluginModel.setEnabled(self.is_avg) # set the type of calculation - self.model.set_computation_type(self.cbOptionsCalc.currentIndex()) + self.model.set_computation_type(ComputationType(self.cbOptionsCalc.currentIndex())) match self.cbOptionsCalc.currentIndex(): case 0 | 1 | 2: pass diff --git a/src/sas/sascalc/calculator/sas_gen.py b/src/sas/sascalc/calculator/sas_gen.py index 7a804ab02b..300543a622 100644 --- a/src/sas/sascalc/calculator/sas_gen.py +++ b/src/sas/sascalc/calculator/sas_gen.py @@ -55,9 +55,9 @@ def transform_center(pos_x, pos_y, pos_z): return posx, posy, posz class ComputationType(Enum): - SANS_2D = 0, - SANS_1D = 1, - SANS_1D_BETA = 2, + SANS_2D = 0 + SANS_1D = 1 + SANS_1D_BETA = 2 SAXS = 3 class GenSAS: From b0aa9bf3d99b650ab7ee6d1f766cd634a65800d1 Mon Sep 17 00:00:00 2001 From: Kristian Date: Thu, 15 Jan 2026 15:16:07 +0100 Subject: [PATCH 22/23] use posix paths --- src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py index 1d1d172b04..383f901938 100644 --- a/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py +++ b/src/sas/qtgui/Calculators/SAXSPluginModelGenerator.py @@ -59,7 +59,7 @@ def get_model_text(structure_path: str) -> str: ### import pyausaxs as ausaxs -structure_path = "{str(Path(structure_path).resolve())}" +structure_path = "{str(Path(structure_path).as_posix())}" def Iq(q, c): # Initialize on first call to keep objects alive for function lifetime From d683f88159c75e0c218c40e0ec55e07055afae9c Mon Sep 17 00:00:00 2001 From: Kristian Date: Tue, 20 Jan 2026 09:47:47 +0100 Subject: [PATCH 23/23] unpinned pyausaxs --- build_tools/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index 3bce958104..76210f56dc 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -16,7 +16,7 @@ numpy packaging periodictable platformdirs -pyausaxs==1.0.9 +pyausaxs pybind11 pylint pyopencl