diff --git a/LICENSE.TXT b/LICENSE.TXT index 9d9d600e50..2d6e2ed6c8 100644 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -1,4 +1,5 @@ -Copyright (c) 2009-2020, SasView Developers +Copyright (c) 2009-2021, SasView Developers + All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/build_tools/conda_qt5_win_commercial.yml b/build_tools/conda_qt5_win_commercial.yml index 460e51d7a6..f62bcc9b1f 100644 --- a/build_tools/conda_qt5_win_commercial.yml +++ b/build_tools/conda_qt5_win_commercial.yml @@ -33,5 +33,4 @@ dependencies: - ../../../PyQt5_commercial-5.12.2-5.12.3-cp35.cp36.cp37.cp38-none-win_amd64.whl - qt5reactor==0.5 - tinycc==1.1 - - pillow==8.1.2 - + - pillow==8.1.2 \ No newline at end of file diff --git a/build_tools/release_automation.py b/build_tools/release_automation.py index b8b35678a6..12e80dff6b 100644 --- a/build_tools/release_automation.py +++ b/build_tools/release_automation.py @@ -31,7 +31,7 @@ {'name': 'McKerns, Mike', 'affiliation': 'California Institute of Technology', 'type':'Researcher'}, {'name': 'Mothander, Karolina', 'affiliation': 'Lund University', 'type':'Researcher'}, {'name': 'Narayanan, Theyencheri', 'affiliation': 'European Synchrotron Radiation Facility', 'type':'Researcher'}, - {'name': 'Parsons, Drew', 'affiliation': 'University of New South Wales', 'type':'DataManager'}, + {'name': 'Parsons, Drew', 'affiliation': 'University of Cagliari and the Debian Project', 'type':'DataManager'}, {'name': 'Porcar, Lionel', 'affiliation': 'Institut Laue-Langevin', 'type':'Researcher'}, {'name': 'Pozzo, Lilo', 'affiliation': 'University of Washington', 'type':'Researcher'}, {'name': 'Rakitin, Maksim', 'affiliation': 'Brookhaven National Laboratory','type':'DataManager'}, @@ -65,7 +65,7 @@ {'name': 'Nielsen, Torben','affiliation': 'Data Management and Software Centre, European Spallation Source'}, {'name': "O'Driscoll, Lewis",'affiliation': 'STFC - Rutherford Appleton Laboratory'}, {'name': 'Potrzebowski, Wojciech','affiliation': 'Data Management and Software Centre, European Spallation Source ERIC', 'orcid': '0000-0002-7789-6779'}, - {'name': "Prescott, Stewart",'affiliation': 'University of New South Wales'}, + {'name': "Prescott, Stuart",'affiliation': 'University of New South Wales and the Debian Project', 'orcid': '0000-0001-5639-9688'}, {'name': 'Ferraz Leal, Ricardo','affiliation': 'Oak Ridge National Laboratory'}, {'name': 'Rozyczko, Piotr','affiliation': 'Data Management and Software Centre, European Spallation Source ERIC', 'orcid' : '0000-0002-2359-1013' }, {'name': 'Snow, Tim','affiliation': 'Diamond Light Source','orcid': '0000-0001-7146-6885'}, diff --git a/docs/sphinx-docs/source/conf.py b/docs/sphinx-docs/source/conf.py index eef3c240ad..80818a9ea6 100644 --- a/docs/sphinx-docs/source/conf.py +++ b/docs/sphinx-docs/source/conf.py @@ -70,7 +70,7 @@ # General information about the project. project = u'SasView' -copyright = u'2019, The SasView Project' +copyright = u'2021, The SasView Project' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -79,7 +79,7 @@ # The short X.Y version. version = '5.0' # The full version, including alpha/beta/rc tags. -release = '5.0.3' +release = '5.0.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/sphinx-docs/source/user/RELEASE.rst b/docs/sphinx-docs/source/user/RELEASE.rst index e0cadb2d4d..e08c690177 100644 --- a/docs/sphinx-docs/source/user/RELEASE.rst +++ b/docs/sphinx-docs/source/user/RELEASE.rst @@ -11,6 +11,162 @@ Release Notes Features ======== +New in Version 5.0.4 +-------------------- +This is a point release which fixes some issues reported in earlier versions +of 5.0.x: + +* A bug that had been around since 4.2.2 and which prevented Batch Fitting from + using any dI values in the data file has finally been fixed. The consequence of + this bug was that Single Fits and Batch Fits on the same datasets could return + different parameters. Now, where present, dI values will always be used by default + in both cases. +* The bug introduced in 5.0.3 which prevented the plotting of Batch Fit results has also + been fixed. +* An issue with the behaviour of the 1D pinhole resolution smearing routine in cases of + large divergence has been addressed. +* A number of improvements have been made to plotting and plot management. +* Several usability issues in the P(r) Inversion and Invariant Analysis perspectives + have been addressed. +* Improvements have also been made to the functioning of Project Save/Load. + +There are also some new features in this version: + +* Though not strictly a new feature, the functionality and operation of parameter + constraints has been significantly overhauled for this version. +* The slicer functionality has been significantly overhauled and made to work properly. +* The slider bars on plots for selecting the q-range for fitting that featured in earlier + versions of SasView have been re-introduced by popular request, although they do not + yet work on linearized plots. +* It is now possible to swap the dataset used to create an existing FitPage for a different + dataset. This removes the need to re-generate a complex model (eg, featuring many parameters + and/or constraints) many times over to use it to fit several datasets. +* It is now also possible to assign custom names to loaded datasets, rather than just + identifying the data by its filename. Right-click on a dataset in the Data Explorer + to activate. + +New features/improvements +^^^^^^^^^^^^^^^^^^^^^^^^^ +* sasview #1725: Horizontal line at y=0 needed in P(r) plots +* sasview #1702: Allow for a choice of how data is named in the Data Explorer +* sasview #1699: Allow check/uncheck of sub-selected data in Data Explorer +* sasview #1676: Checkbox of highlighted row is checked also when clicking another checkbox +* sasview #1303: CanSAS XML Reader refactor + +* sasmodels #443: Update to polydispersity.rst +* sasmodels #429: Create model for superparamagnetic relaxing particles +* sasmodels #390: Re-describe Source intensity in model parameter tables +* sasmodels #253: use new orientation for magnetic models (Trac #910) + + +Bug fixes +^^^^^^^^^ +* Fixes sasview #1796: Batch Fitting does not respect Q-range for fitting +* Fixes sasview #1795: Display of batch fitting results is broken in 5.0.4 +* Fixes sasview #1794: Batch fitting in 5.x returns different parameters to single fits in 5.x +* Fixes sasview #1782: RgQmax and RgQmin are inverted in the Gunier linear fit +* Fixes sasview #1776: Slicers Using Masked Data Points in Calculation +* Fixes sasview #1754: Delete Data does not remove data or plots from Fitting, P(r) or Inversion +* Fixes sasview #1738: Conflicting definition of displayData() +* Fixes sasview #1711: sasview 5, Q resolution smearing issues with broad_peak model +* Fixes sasview #1710: sasview fails to open .h5 files using h5py 3.1 +* Fixes sasview #1701: Issue with slashes in data titles in CanSAS1D (and probably NXcanSAS) +* Fixes sasview #1698: Provide P(Q) separately when fitting +* Fixes sasview #1696: Failure in getSymbolDict on selecting parameters for constraints +* Fixes sasview #1681: Generic Scattering Calculator produces empty 2D map on sld file +* Fixes sasview #1674: Reloading a project in the same session duplicates the model/residuals in the Data Explorer +* Fixes sasview #1671: pdh data loader bug in 4.x and ESS_GUI 5.x +* Fixes sasview #1657: Loading project without fit_params entry causes empty fit window (v5.0.3) +* Fixes sasview #1655: Corfunc and Invariant Perspectives not able to remove/swap data (5.x) +* Fixes sasview #1654: 5.0.4 disable rather than remove constraints if do a fit on a single FitPage +* Fixes sasview #1653: 5.0.4 new constraints checks over zealous on load project +* Fixes sasview #1649: 4.x/5.x: Slicer Parameters control only appears in context menu once you have sliced +* Fixes sasview #1648: 5.0.3 not updating radius_effective in GUI +* Fixes sasview #1647: 4.x/5.x: Sector slicer tool Q plot could do with better resolution +* Fixes sasview #1646: 4.x/5.x: Annulus slicer tool phi plot could do with better resolution +* Fixes sasview #1640: Linux: SasView >5.0.1 binary cannot copy default custom_config.py +* Fixes sasview #1616: ESS_GUI: Model label on plot keeps being reset +* Fixes sasview #1611: Inconsistent behaviour of extrapolation Fit/Fix radio buttons in Invariant Perspective +* Fixes sasview #1610: Chart in Invariant Perspective Status Dialog not displaying the low-Q contribution to Q* +* Fixes sasview #1609: Changing Q-range limits in Invariant Perspective has no effect on extrapolations +* Fixes sasview #1608: No Q-limit bars in Invariant Perspective +* Fixes sasview #1607: Once extrapolation is turned on in the invariant it cannot be turned off +* Fixes sasview #1606: Invariant does not report the total invariant +* Fixes sasview #1605: Problem loading canSAS data into Invariant +* Fixes sasview #1604: The Invariant low Q extrapolation choice is not honored +* Fixes sasview #1600: v5 constrained value within single FitPage not being returned to gui +* Fixes sasview #1589: 5.0 turn off or remove constraint ? +* Fixes sasview #1583: calc.py throws erros after building SasView +* Fixes sasview #1574: Invariant perspective fixes need to be ported to 5.x +* Fixes sasview #1566: Default Checkboxes in data manager need changing +* Fixes sasview #1557: GUI losing track of fitpage and plot associations +* Fixes sasview #1547: Resolution is incorrectly handled in 5.x +* Fixes sasview #1544: Need to examine 2D data pixel sizes +* Fixes sasview #1542: Crosstalk between Corfunc and Invariant perspectives +* Fixes sasview #1541: Invariant and the infinite multiplication of plots +* Fixes sasview #1539: Corfunc requires two shots to populate the data name box +* Fixes sasview #1537: Allow for replacing data in a Fit Page +* Fixes sasview #1535: ESS_GUI: Existing common parameters not preserved between models in 5.x +* Fixes sasview #1534: ESS_GUI: Something strange with 5.x and the .sasview folder +* Fixes sasview #1532: Add a constraint checking mechanism +* Fixes sasview #1526: Project Save/Load functionality of 4.x needs to be restored +* Fixes sasview #1478: v5 & v4 TEst that P(Q)S(Q) plugin works +* Fixes sasview #1472: Sort out the Invariant Perspective & Documentation (#1434 & #1461) +* Fixes sasview #1469: 2D tools +* Fixes sasview #1453: 5.1 gui initialisation issue for Onion model +* Fixes sasview #1446: 5.0 dI uncertainty unavailable in batch mode +* Fixes sasview #1408: Magnetic model documentation is inconsistent with code +* Fixes sasview #1381: Slicer in 5.0 doesn't contain the batch, fitting, log/linear etc features +* Fixes sasview #1340: 5.0 invariant mac not plotting +* Fixes sasview #1243: Display title rather than filename in data browser (Trac #1213) +* Fixes sasview #1137: Verify and document up_frac_i and up_frac_f calculations for magnetic models (Trac #1086) +* Fixes sasview #863: Make it easier to use the same fit set-up with different data sets (Trac #747) + +* Fixes sasmodels #367: Correlation length model documentation is wrong +* Fixes sasmodels #210: Show all failing tests rather than stopping at the first + +New Models +^^^^^^^^^^ +The following models have been added to the [Model Marketplace](http://marketplace.sasview.org/) since v5.0.0 was released: + +* Magnetic vortex in a disc +* Field-dependent magnetic SANS of misaligned magnetic moments in bulk ferromagnets +* SANS of bulk ferromagnets +* Core_shell_ellipsoid_tied and core_shell_ellipsoid_repar +* Lamellar_Slab_APL_nW +* 5 Layer Core Shell Disc +* Superparamagnetic Core-Shell Spheres with 3D field orientation +* Superparamagnetic Core-Shell Spheres +* Octahedron +* Magnetically oriented, rotating and precessing anisometric particle (MORP) +* Cumulants +* Cumulants DLS +* Peak Voigt +* Long Cylinder +* Sphere Concentration A +* Binary Blend +* Exponential +* 2 Layer General Guinier Porod +* Core double shell sphere filled with many cylinders in the core +* Fractal S(q) +* Mass Fractal S(q) +* Core shell cuboid +* Core shell sphere filled with a cylinder in the core +* Correlated_spheres + +Known Issues +^^^^^^^^^^^^ +At this time, the reinstated slider bars on plots for selecting the +q-range for fitting do not work on linearized plots. + +All the known bugs/feature requests can be found in the issues on github. +Note the sasmodels issues are now separate from the sasview issues (i.e. different repositories) + +[sasview](https://github.com/SasView/sasview/milestones) + +[sasmodels](https://github.com/SasView/sasmodels/milestones) + + New in Version 5.0.3 -------------------- This is a point release which fixes several issues, but in particular: @@ -87,6 +243,11 @@ Known Issues ^^^^^^^^^^^^ At this time, and unlike version 4.x, only fitting and P(r) inversion sessions can be saved as project files. +There is also a bug which is stopping Batch Fitting from using the intensity uncertainty (dI) data if this +is present in the files being processed. As the default behaviour of normal Single Fitting is to automatically +use the dI data in the file if it is present, this means that the results of Single Fitting and Batch Fitting +the same data will differ. + All the known bugs/feature requests can be found in the issues on github. Note the sasmodels issues are now separate from the sasview issues (i.e. different repositories) @@ -1020,23 +1181,16 @@ users may wish to be aware of can be viewed at the following links: [sasmodels](https://github.com/SasView/sasmodels/milestones) -All versions upto and including 5.0.2 - All systems ---------------------------------------------------- -A very long-standing error has been identified in the Invariant Analysis -perspective. The value of the specific surface $Sv$ that is being returned -is in fact *twice* the value that it should be. +All 5.0.x versions / 4.2.2 - All systems +---------------------------------------- +A problem has been identified in Version 4.2.2 which also affects all 5.0.x +versions. The Easy Add/Multiply Editor dialog should not be used to combine +a plugin model with a built-in model, or to combine two plugin models. In +5.0.x the operation will fail, generating an error message in the Log Explorer +similar to -5.0.0 / 5.0.1 / 5.0.2 - All systems ------------------------------------ -In these versions, and unlike version 4.x, only fitting sessions can be saved as project files. + ModuleNotFoundError: No module named 'plugin_module_name' -4.2.2 / 5.0.0 / 5.0.1 - All systems ------------------------------------ -A problem has been identified in Version 4.2.2 which also affects versions -5.0.0 and 5.0.1. The Easy Add/Multiply Editor dialog should not be used to -combine a plugin model with a built-in model, or to combine two plugin models. -In 5.0.0 the operation will fail (generating an error message in the Log Explorer). -Whilst in 5.0.1 the operation has been blocked until the problem can be fixed. If it is necessary to generate a plugin model from more than two built-in models, please edit the plugin model .py file directly and specify the combination of built-in models directly. For example:: @@ -1047,6 +1201,24 @@ built-in models directly. For example:: model_info.name = 'MyBigPluginModel' model_info.description = 'For fitting pores in crystalline framework' Model = make_model_from_info(model_info) + +5.0.0 / 5.0.1 / 5.0.2 / 5.0.3 - All systems +------------------------------------------- +There is a bug which is stopping Batch Fitting from using the intensity +uncertainty (dI) data if this is present in the files being processed. +As the default behaviour of normal Single Fitting is to automatically +use the dI data in the file if it is present, this means that the results +of Single Fitting and Batch Fitting the same data will differ. + +All versions upto and including 5.0.2 - All systems +--------------------------------------------------- +A very long-standing error has been identified in the Invariant Analysis +perspective. The value of the specific surface $Sv$ that is being returned +is in fact *twice* the value that it should be. + +5.0.0 / 5.0.1 / 5.0.2 - All systems +----------------------------------- +In these versions, and unlike version 4.x, only fitting sessions can be saved as project files. 4.2.0 - All systems ------------------- diff --git a/installers/installer.iss b/installers/installer.iss index 0d96b4fee7..b7ac4c797f 100644 --- a/installers/installer.iss +++ b/installers/installer.iss @@ -2,8 +2,8 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "SasView" -#define MyAppVersion "5.0.3" -#define MyAppPublisher "(c) 2009 - 2020, UTK, UMD, NIST, ORNL, ISIS, ESS, ILL, ANSTO, TU Delft and DLS" +#define MyAppVersion "5.0.4" +#define MyAppPublisher "(c) 2009 - 2021, UTK, UMD, NIST, ORNL, ISIS, ESS, ILL, ANSTO, TU Delft and DLS" #define MyAppURL "http://www.sasview.org" #define MyAppExeName "sasview.exe" @@ -21,6 +21,7 @@ AppUpdatesURL={#MyAppURL} DefaultDirName=c:\{#MyAppName}-{#MyAppVersion} DefaultGroupName={#MyAppName}-{#MyAppVersion} DisableProgramGroupPage=yes +DisableDirPage=no UsedUserAreasWarning=no LicenseFile=license.txt ArchitecturesInstallIn64BitMode=x64 diff --git a/installers/installer_generator64.py b/installers/installer_generator64.py index 70d917f418..aae6b20647 100644 --- a/installers/installer_generator64.py +++ b/installers/installer_generator64.py @@ -32,6 +32,7 @@ SetupIconFile = local_config.SetupIconFile_win LicenseFile = 'license.txt' DisableProgramGroupPage = 'yes' +DisableDirPage = 'no' Compression = 'lzma' SolidCompression = 'yes' PrivilegesRequired = 'none' @@ -345,6 +346,7 @@ def generate_installer(): TEMPLATE += "DefaultDirName=%s\n" % str(DefaultDirName) TEMPLATE += "DefaultGroupName=%s\n" % str(DefaultGroupName) TEMPLATE += "DisableProgramGroupPage=%s\n" % str(DisableProgramGroupPage) + TEMPLATE += "DisableDirPage=%s\n" % str(DisableDirPage) TEMPLATE += "LicenseFile=%s\n" % str(LicenseFile) TEMPLATE += "OutputBaseFilename=%s\n" % str(OutputBaseFilename) TEMPLATE += "SetupIconFile=%s\n" % str(SetupIconFile) diff --git a/installers/license.txt b/installers/license.txt index 3c263e9a1d..0d1c6b73e6 100644 --- a/installers/license.txt +++ b/installers/license.txt @@ -18,4 +18,4 @@ sentence: *"This work benefited from the use of the SasView application, originally developed under NSF award DMR-0520547."* -Copyright (c) 2009-2020 UTK, UMD, ESS, NIST, ORNL, ISIS, ILL, DLS, TUD, BAM, ANSTO +Copyright (c) 2009-2021 UTK, UMD, ESS, NIST, ORNL, ISIS, ILL, DLS, DUT, BAM, ANSTO diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index 390d09da2d..c93cce3c23 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -398,6 +398,17 @@ def allDataForModel(self, model): if data is None: continue # Now, all plots under this item name = data.name + ###################################################### + # Reset all slider values in data so save/load does not choke on them + # Remove once slider definition moved out of PlotterData + data.slider_low_q_setter = None + data.slider_high_q_setter = None + data.slider_low_q_input = None + data.slider_high_q_input = None + data.slider_update_on_move = False + data.slider_low_q_getter = None + data.slider_high_q_getter = None + ###################################################### is_checked = item.checkState() properties['checked'] = is_checked other_datas = [] @@ -405,6 +416,18 @@ def allDataForModel(self, model): other_datas = GuiUtils.plotsFromDisplayName(name, model) # skip the main plot other_datas = list(other_datas.values())[1:] + for datas in other_datas: + ###################################################### + # Reset all slider values in data so save/load does not choke on them + # Remove once slider definition moved out of PlotterData + datas.slider_low_q_setter = None + datas.slider_high_q_setter = None + datas.slider_low_q_input = None + datas.slider_high_q_input = None + datas.slider_update_on_move = False + datas.slider_low_q_getter = None + datas.slider_high_q_getter = None + ###################################################### all_data[data.id] = [data, properties, other_datas] return all_data @@ -737,6 +760,7 @@ def deleteTheory(self, event): # Delete these rows from the model deleted_names.append(str(self.theory_model.item(ind).text())) deleted_items.append(item) + self.closePlotsForItem(item) self.theory_model.removeRow(ind) # Decrement index since we just deleted it @@ -1346,7 +1370,7 @@ def readData(self, path): data_error = True except Exception as ex: - logging.error(str(ex) + sys.exc_info()[1]) + logging.error(str(ex) + str(sys.exc_info()[1])) any_error = True if any_error or data_error or error_message != "": @@ -1678,7 +1702,7 @@ def showEditDataMask(self, data=None): msg.exec_() return except Exception as ex: - logging.error(str(ex) + sys.exc_info()[1]) + logging.error(str(ex) + str(sys.exc_info()[1])) msg.exec_() return @@ -1750,6 +1774,8 @@ def deleteAllItems(self): deleted_items += deleted_theory_items deleted_names = [item.text() for item in deleted_items] deleted_names += deleted_theory_items + # Close all active plots + self.closeAllPlots() # Let others know we deleted data self.communicator.dataDeletedSignal.emit(deleted_items) # update stored_data diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index 7e2d684aba..0a02fd57c8 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -196,7 +196,8 @@ def closeAllPerspectives(self): for name, perspective in self.loadedPerspectives.items(): try: perspective.setClosable(True) - self._workspace.workspace.removeSubWindow(self.subwindow) + if self.subwindow in self._workspace.workspace.subWindowList(): + self._workspace.workspace.removeSubWindow(self.subwindow) perspective.close() except Exception as e: logger.warning(f"Unable to close {name} perspective\n{e}") @@ -667,6 +668,8 @@ def actionSave_Project(self): Menu Save Project """ filename = self.filesWidget.saveProject() + if not filename: + return # datasets all_data = self.filesWidget.getSerializedData() diff --git a/src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py b/src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py index 6288ac08f3..02be5814a6 100644 --- a/src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py +++ b/src/sas/qtgui/MainWindow/UnitTesting/DataExplorerTest.py @@ -246,12 +246,12 @@ def testDeleteTheory(self): QMessageBox.question = MagicMock(return_value=QMessageBox.No) # Populate the model - item1 = QStandardItem(True) + item1 = HashableStandardItem(True) item1.setCheckable(True) item1.setCheckState(Qt.Checked) item1.setText("item 1") self.form.theory_model.appendRow(item1) - item2 = QStandardItem(True) + item2 = HashableStandardItem(True) item2.setCheckable(True) item2.setCheckState(Qt.Unchecked) item2.setText("item 2") diff --git a/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py b/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py index 4ea0a5f62e..c90e38fe93 100644 --- a/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py +++ b/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py @@ -296,8 +296,10 @@ def removeData(self, data_list=None): # Clear data plots self._canvas.data = None self._canvas.extrap = None + self._canvas.draw_q_space() self._realplot.data = None self._realplot.extrap = None + self._realplot.draw_real_space() # Clear calculator, model, and data path self._calculator = CorfuncCalculator() self._model_item = None @@ -305,12 +307,6 @@ def removeData(self, data_list=None): 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""" @@ -700,9 +696,9 @@ def updateFromParameters(self, params): self.cmdCalculateBg.setEnabled(params.get('background', '0') != '0') self.cmdSave.setEnabled(params.get('guinier_a', '0.0') != '0.0') self.cmdExtrapolate.setEnabled(params.get('guinier_a', '0.0') != '0.0') - self.cmdTransform.setEnabled(params.get('long_period', '0') != '0.0') - self.cmdExtract.setEnabled(params.get('long_period', '0') != '0.0') + self.cmdTransform.setEnabled(params.get('long_period', '0') != '0') + self.cmdExtract.setEnabled(params.get('long_period', '0') != '0') if params.get('guinier_a', '0.0') != '0.0': self.extrapolate() - if params.get('long_period', '0') != '0.0': + if params.get('long_period', '0') != '0': self.transform() diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py b/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py index 7badb1d122..f7336f74fd 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py @@ -247,7 +247,7 @@ def addFit(self, data, is_batch=False, tab_index=None): self.tabs.append(tab) if data: self.updateFitDict(data, tab_name) - self.maxIndex = tab_index + 1 + self.maxIndex = max([tab.tab_id for tab in self.tabs], default=0) + 1 icon = QtGui.QIcon() if is_batch: @@ -409,9 +409,10 @@ def setData(self, data_item=None, is_batch=False, tab_index=None): # Find the first unassigned tab. # If none, open a new tab. available_tabs = [tab.acceptsData() for tab in self.tabs] + tab_ids = [tab.tab_id for tab in self.tabs] if tab_index is not None: - if tab_index >= self.maxIndex: + if tab_index not in tab_ids: self.addFit(data, is_batch=is_batch, tab_index=tab_index) else: self.setCurrentIndex(tab_index-1) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 60ab93b6de..8ea6267849 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -210,6 +210,8 @@ def data(self, value): for data_item in value: logic = FittingLogic(data=GuiUtils.dataFromItem(data_item)) self._logic.append(logic) + # Option widget logic was destroyed - reestablish + self.options_widget.logic = self._logic[0] # update the ordering tab self.order_widget.updateData(self.all_data) @@ -1758,9 +1760,6 @@ def batchFitComplete(self, result): # Switch indexes self.data_index = res_index - # Recompute Q ranges - if self.data_is_loaded: - self.q_range_min, self.q_range_max, self.npts = self.logic.computeDataRange() # Recalculate theories method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D diff --git a/src/sas/qtgui/Perspectives/Fitting/OrderWidget.py b/src/sas/qtgui/Perspectives/Fitting/OrderWidget.py index 65fb8d3605..6177037b2b 100644 --- a/src/sas/qtgui/Perspectives/Fitting/OrderWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/OrderWidget.py @@ -35,7 +35,7 @@ def setupTable(self): if not hasattr(item, 'data'): continue dataset = GuiUtils.dataFromItem(item) if dataset is None: continue - dataset_name = dataset.filename + dataset_name = dataset.name self.order[dataset_name] = item self.lstOrder.addItem(dataset_name) diff --git a/src/sas/qtgui/Perspectives/Inversion/InversionPerspective.py b/src/sas/qtgui/Perspectives/Inversion/InversionPerspective.py index 2c3361622d..061e5eedd2 100644 --- a/src/sas/qtgui/Perspectives/Inversion/InversionPerspective.py +++ b/src/sas/qtgui/Perspectives/Inversion/InversionPerspective.py @@ -204,8 +204,8 @@ def setupLinks(self): lambda: self._calculator.set_alpha(is_float(self.regularizationConstantInput.text()))) self.maxDistanceInput.textChanged.connect( lambda: self._calculator.set_dmax(is_float(self.maxDistanceInput.text()))) - self.maxQInput.textChanged.connect(self.check_q_high) - self.minQInput.textChanged.connect(self.check_q_low) + self.maxQInput.editingFinished.connect(self.check_q_high) + self.minQInput.editingFinished.connect(self.check_q_low) self.slitHeightInput.textChanged.connect( lambda: self._calculator.set_slit_height(is_float(self.slitHeightInput.text()))) self.slitWidthInput.textChanged.connect( @@ -454,11 +454,15 @@ def stopCalculation(self): self.isCalculating = False self.updateGuiValues() - def check_q_low(self, q_value): + def check_q_low(self, q_value=None): """ Validate the low q value """ - q_min = min(self._calculator.x) - q_max = self._calculator.get_qmax() - q_value = float(q_value) + if not q_value: + q_value = float(self.minQInput.text()) if self.minQInput.text() else '' + if q_value == '': + self.model.setItem(WIDGETS.W_QMIN, QtGui.QStandardItem(q_value)) + return + q_min = min(self._calculator.x) if any(self._calculator.x) else -1 * np.inf + q_max = self._calculator.get_qmax() if self._calculator.get_qmax() is not None else np.inf if q_value > q_max: # Value too high - coerce to max q self.model.setItem(WIDGETS.W_QMIN, QtGui.QStandardItem("{:.4g}".format(q_max))) @@ -470,11 +474,15 @@ def check_q_low(self, q_value): self.model.setItem(WIDGETS.W_QMIN, QtGui.QStandardItem("{:.4g}".format(q_value))) self._calculator.set_qmin(q_value) - def check_q_high(self, q_value): + def check_q_high(self, q_value=None): """ Validate the value of high q sent by the slider """ - q_max = max(self._calculator.x) - q_min = self._calculator.get_qmin() - q_value = float(q_value) + if not q_value: + q_value = float(self.maxQInput.text()) if self.maxQInput.text() else '' + if q_value == '': + self.model.setItem(WIDGETS.W_QMAX, QtGui.QStandardItem(q_value)) + return + q_max = max(self._calculator.x) if any(self._calculator.x) else np.inf + q_min = self._calculator.get_qmin() if self._calculator.get_qmin() is not None else -1 * np.inf if q_value > q_max: # Value too high - coerce to max q self.model.setItem(WIDGETS.W_QMAX, QtGui.QStandardItem("{:.4g}".format(q_max))) @@ -1042,7 +1050,9 @@ def _calculateUpdate(self, output_tuple): self.dataPlot.show_q_range_sliders = True self.dataPlot.slider_low_q_input = self.minQInput + self.dataPlot.slider_low_q_setter = self.check_q_low self.dataPlot.slider_high_q_input = self.maxQInput + self.dataPlot.slider_high_q_setter = self.check_q_high # Udpate internals and GUI self.updateDataList(self._data) diff --git a/src/sas/qtgui/Plotting/LinearFit.py b/src/sas/qtgui/Plotting/LinearFit.py index c0256df167..771739073d 100644 --- a/src/sas/qtgui/Plotting/LinearFit.py +++ b/src/sas/qtgui/Plotting/LinearFit.py @@ -39,6 +39,9 @@ def __init__(self, parent=None, self.parent = parent self.max_range = max_range + # Set fit minimum to 0.0 if below zero + if fit_range[0] < 0.0: + fit_range = (0.0, fit_range[1]) self.fit_range = fit_range self.xLabel = xlabel self.yLabel = ylabel @@ -48,14 +51,14 @@ def __init__(self, parent=None, self.bg_on = False # Scale dependent content - self.guiner_box.setVisible(False) + self.guinier_box.setVisible(False) if (self.yLabel == "ln(y)" or self.yLabel == "ln(y*x)") and \ (self.xLabel == "x^(2)"): if self.yLabel == "ln(y*x)": self.label_12.setText('

Rod diameter [Å]

') self.rg_yx = True self.rg_on = True - self.guiner_box.setVisible(True) + self.guinier_box.setVisible(True) if (self.xLabel == "x^(4)") and (self.yLabel == "y*x^(4)"): self.bg_on = True @@ -110,7 +113,8 @@ def setRangeLabel(self, label=""): self.lblRange.setText(label) def range(self): - return (float(self.txtFitRangeMin.text()), float(self.txtFitRangeMax.text())) + return (float(self.txtFitRangeMin.text()) if float(self.txtFitRangeMin.text()) > 0 else 0.0, + float(self.txtFitRangeMax.text())) def fit(self, event): """ @@ -201,11 +205,11 @@ def fit(self, event): self.txtBerr.setText(formatNumber(self.ErrBvalue)) self.txtChi2.setText(formatNumber(self.Chivalue)) - # Possibly Guiner analysis + # Possibly Guinier analysis i0 = numpy.exp(cstB) - self.txtGuiner_1.setText(formatNumber(i0)) + self.txtGuinier_1.setText(formatNumber(i0)) err = numpy.abs(numpy.exp(cstB) * errB) - self.txtGuiner1_Err.setText(formatNumber(err)) + self.txtGuinier1_Err.setText(formatNumber(err)) if self.rg_yx: rg = numpy.sqrt(-2 * float(cstA)) @@ -224,13 +228,13 @@ def fit(self, event): else: err = '' - self.txtGuiner_2.setText(value) - self.txtGuiner2_Err.setText(err) + self.txtGuinier_2.setText(value) + self.txtGuinier2_Err.setText(err) value = formatNumber(rg * self.floatInvTransform(self.xminFit)) - self.txtGuiner_3.setText(value) + self.txtGuinier_4.setText(value) value = formatNumber(rg * self.floatInvTransform(self.xmaxFit)) - self.txtGuiner_4.setText(value) + self.txtGuinier_3.setText(value) tempx = numpy.array(tempx) tempy = numpy.array(tempy) diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index b5bc03054f..6e5a43776e 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -11,6 +11,7 @@ from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Plotting.PlotterBase import PlotterBase from sas.qtgui.Plotting.AddText import AddText +from sas.qtgui.Plotting.Binder import BindArtist from sas.qtgui.Plotting.SetGraphRange import SetGraphRange from sas.qtgui.Plotting.LinearFit import LinearFit from sas.qtgui.Plotting.QRangeSlider import QRangeSlider @@ -69,7 +70,7 @@ def __init__(self, parent=None, manager=None, quickplot=False): # Data container for the linear fit self.fit_result = Data1D(x=[], y=[], dy=None) - self.fit_result.symbol = 13 + self.fit_result.symbol = 17 self.fit_result.name = "Fit" parent.geometry() @@ -455,6 +456,7 @@ def onSetGraphRange(self): if self.setRange.exec_() == QtWidgets.QDialog.Accepted: x_range = self.setRange.xrange() y_range = self.setRange.yrange() + self.setRange.rangeModified = True if x_range is not None and y_range is not None: self.ax.set_xlim(x_range) self.ax.set_ylim(y_range) @@ -491,7 +493,7 @@ def onLinearFit(self, id): if fit_dialog.exec_() == QtWidgets.QDialog.Accepted: return - def replacePlot(self, id, new_plot): + def replacePlot(self, id, new_plot, retain_dimensions=False): """ Remove plot 'id' and add 'new_plot' to the chart. This effectlvely refreshes the chart with changes to one of its plots @@ -506,9 +508,18 @@ def replacePlot(self, id, new_plot): new_plot.custom_color = selected_plot.custom_color new_plot.markersize = selected_plot.markersize new_plot.symbol = selected_plot.symbol - + # Store user-defined plot range on replot + retain_dimensions = retain_dimensions or self.setRange.rangeModified + if retain_dimensions: + x_bounds = (self.ax.viewLim.xmin, self.ax.viewLim.xmax) + y_bounds = (self.ax.viewLim.ymin, self.ax.viewLim.ymax) self.removePlot(id) self.plot(data=new_plot) + # Apply user-defined plot range + if retain_dimensions: + self.ax.set_xbound(x_bounds[0], x_bounds[1]) + self.ax.set_ybound(y_bounds[0], y_bounds[1]) + self.setRange.rangeModified = True def onRemovePlot(self, id): """ @@ -529,6 +540,7 @@ def removePlot(self, id): # Remove the plot from the list of plots self.plot_dict.pop(id) + self.sliders.pop(id, None) # Labels might have been changed xl = self.ax.xaxis.label.get_text() @@ -537,6 +549,9 @@ def removePlot(self, id): mpl.pyplot.cla() self.ax.cla() + # Recreate Artist bindings after plot clear + self.connect = BindArtist(self.figure) + for ids in self.plot_dict: if ids != id: self.plot(data=self.plot_dict[ids], hide_error=self.plot_dict[ids].hide_error) @@ -647,9 +662,6 @@ def onFitDisplay(self, fit_data): self.fit_result.dx = None self.fit_result.dy = None - #Remove another Fit, if exists - self.removePlot("Fit") - self.fit_result.reset_view() #self.offset_graph() @@ -658,8 +670,12 @@ def onFitDisplay(self, fit_data): self.fit_result.title = 'Fit' self.fit_result.name = 'Fit' - # Plot the line - self.plot(data=self.fit_result, marker='-', hide_error=True) + if self.fit_result.name in self.plot_dict.keys(): + # Replace an existing Fit and ensure the plot range is not reset + self.replacePlot("Fit", new_plot=self.fit_result, retain_dimensions=True) + else: + # Otherwise, Plot a new line + self.plot(data=self.fit_result, marker='-', hide_error=True) def onToggleLegend(self): """ diff --git a/src/sas/qtgui/Plotting/PlotterBase.py b/src/sas/qtgui/Plotting/PlotterBase.py index 5cc357091b..11903bb71c 100644 --- a/src/sas/qtgui/Plotting/PlotterBase.py +++ b/src/sas/qtgui/Plotting/PlotterBase.py @@ -315,6 +315,7 @@ def closeEvent(self, event): """ Overwrite the close event adding helper notification """ + self.clearQRangeSliders() # Please remove me from your database. PlotHelper.deletePlot(PlotHelper.idOfPlot(self)) @@ -323,6 +324,13 @@ def closeEvent(self, event): event.accept() + def clearQRangeSliders(self): + # Destroy the Q-range sliders in 1D plots + if hasattr(self, 'sliders') and isinstance(self.sliders, dict): + for slider in self.sliders.values(): + slider.clear() + self.sliders = {} + def onImageSave(self): """ Use the internal MPL method for saving to file diff --git a/src/sas/qtgui/Plotting/QRangeSlider.py b/src/sas/qtgui/Plotting/QRangeSlider.py index 4434847edf..6eaaecab93 100644 --- a/src/sas/qtgui/Plotting/QRangeSlider.py +++ b/src/sas/qtgui/Plotting/QRangeSlider.py @@ -57,8 +57,6 @@ def clear(self): Clear this slicer and its markers """ self.clear_markers() - self.line_max.remove() - self.line_min.remove() def update(self, x=None, y=None): """ @@ -91,9 +89,10 @@ def move(self, x, y, ev): def clear_markers(self): """ - Should be no way to clear the markers + Clear each of the lines individually """ - pass + self.line_min.clear() + self.line_max.clear() def draw(self): """ @@ -139,6 +138,7 @@ def validate(self, param_name, param_value): return True def clear(self): + self.clear_markers() self.remove() def remove(self): @@ -172,9 +172,9 @@ def inputChanged(self): """ Track the input linked to the x value for this slider and update as needed """ self._get_q() self.y_marker = self.base.data.y[(np.abs(self.base.data.x - self.x)).argmin()] - self.update() + self.update(draw=True) - def update(self, x=None, y=None): + def update(self, x=None, y=None, draw=False): """ Update the line position on the graph. """ @@ -187,6 +187,8 @@ def update(self, x=None, y=None): self.inner_marker.set_xdata([self.x]) self.inner_marker.set_ydata([self.y_marker]) self.line.set_xdata([self.x]) + if draw: + self.base.draw() def save(self, ev): """ @@ -214,7 +216,7 @@ def move(self, x, y, ev): else: self.input.setText(f"{self.x:.3}") self.y_marker = self.base.data.y[(np.abs(self.base.data.x - self.x)).argmin()] - self.update() + self.update(draw=self.base.updateOnMove) def onRelease(self, ev): """ @@ -224,12 +226,16 @@ def onRelease(self, ev): self._set_q(self.x) else: self.input.setText(f"{self.x:.3}") - self.update() + self.update(draw=True) self.moveend(ev) return True def clear_markers(self): """ - Should be no way to clear the markers + Disconnect the input and clear the callbacks """ - pass + if self.input: + self.input.textChanged.disconnect(self.inputChanged) + self.setter = None + self.getter = None + self.input = None diff --git a/src/sas/qtgui/Plotting/SetGraphRange.py b/src/sas/qtgui/Plotting/SetGraphRange.py index 525d0e383e..b74b39f431 100644 --- a/src/sas/qtgui/Plotting/SetGraphRange.py +++ b/src/sas/qtgui/Plotting/SetGraphRange.py @@ -32,6 +32,8 @@ def __init__(self, parent=None, x_range=(0.0, 0.0), y_range=(0.0, 0.0)): self.txtYmin.setText(str(y_range[0])) self.txtYmax.setText(str(y_range[1])) + self.rangeModified = False + def xrange(self): """ Return a tuple with line edit content of (xmin, xmax) diff --git a/src/sas/qtgui/Plotting/SlicerParameters.py b/src/sas/qtgui/Plotting/SlicerParameters.py index 419a3662f7..df1d7c4753 100644 --- a/src/sas/qtgui/Plotting/SlicerParameters.py +++ b/src/sas/qtgui/Plotting/SlicerParameters.py @@ -340,7 +340,7 @@ def save1DPlotsForPlot(self, plots): items_for_fit = [] for plot in plots: for item in self.active_plots.keys(): - data = self.active_plots[item].data[0] + data = self.active_plots[item].data[-1] if not isinstance(data, Data1D): continue if plot not in data.name: diff --git a/src/sas/qtgui/Plotting/UI/LinearFitUI.ui b/src/sas/qtgui/Plotting/UI/LinearFitUI.ui index ae00c59360..c13c3478fa 100755 --- a/src/sas/qtgui/Plotting/UI/LinearFitUI.ui +++ b/src/sas/qtgui/Plotting/UI/LinearFitUI.ui @@ -257,12 +257,12 @@ - + true - Guiner analysis + Guinier analysis @@ -275,7 +275,7 @@ - + false @@ -292,7 +292,7 @@ - + false @@ -309,7 +309,7 @@ - + false @@ -326,7 +326,7 @@ - + false @@ -347,7 +347,7 @@ - + false @@ -370,7 +370,7 @@ - + false @@ -446,7 +446,7 @@ - guiner_box + guinier_box label groupBox_2 groupBox diff --git a/src/sas/qtgui/Utilities/GridPanel.py b/src/sas/qtgui/Utilities/GridPanel.py index 4893325aa8..af3efeb606 100644 --- a/src/sas/qtgui/Utilities/GridPanel.py +++ b/src/sas/qtgui/Utilities/GridPanel.py @@ -356,11 +356,11 @@ def setupTable(self, widget=None, data=None): for i_row, row in enumerate(data): # each row corresponds to a single fit chi2 = row[0].fitness - filename = "" + name = "" if hasattr(row[0].data, "sas_data"): - filename = row[0].data.sas_data.filename + name = row[0].data.sas_data.name widget.setItem(i_row, 0, QtWidgets.QTableWidgetItem(GuiUtils.formatNumber(chi2, high=True))) - widget.setItem(i_row, 1, QtWidgets.QTableWidgetItem(str(filename))) + widget.setItem(i_row, 1, QtWidgets.QTableWidgetItem(str(name))) # Now, all the parameters for i_col, param in enumerate(param_list[2:]): if param in row[0].param_list: diff --git a/src/sas/qtgui/Utilities/LocalConfig.py b/src/sas/qtgui/Utilities/LocalConfig.py index 49d21a9cb8..e4dda1025e 100644 --- a/src/sas/qtgui/Utilities/LocalConfig.py +++ b/src/sas/qtgui/Utilities/LocalConfig.py @@ -85,7 +85,7 @@ _diamond_url = "http://www.diamond.ac.uk" _corner_image = os.path.join(icon_path, "angles_flat.png") _welcome_image = os.path.join(icon_path, "SVwelcome.png") -_copyright = "Copyright (c) 2009-2020 UTK, UMD, ESS, NIST, ORNL, ISIS, ILL, DLS, TUD, BAM and ANSTO" +_copyright = "Copyright (c) 2009-2021 UTK, UMD, ESS, NIST, ORNL, ISIS, ILL, DLS, TUD, BAM and ANSTO" #edit the list of file state your plugin can read diff --git a/src/sas/sascalc/dataloader/manipulations.py b/src/sas/sascalc/dataloader/manipulations.py index 16b19f62e1..cea91c50c5 100644 --- a/src/sas/sascalc/dataloader/manipulations.py +++ b/src/sas/sascalc/dataloader/manipulations.py @@ -347,6 +347,7 @@ def _avg(self, data2D, maj): err_data = data2D.err_data[np.isfinite(data2D.data)] qx_data = data2D.qx_data[np.isfinite(data2D.data)] qy_data = data2D.qy_data[np.isfinite(data2D.data)] + mask_data = data2D.mask[np.isfinite(data2D.data)] # Build array of Q intervals if maj == 'x': @@ -371,6 +372,9 @@ def _avg(self, data2D, maj): # Average pixelsize in q space for npts in range(len(data)): + if not mask_data[npts]: + # ignore points that are masked + continue # default frac frac_x = 0 frac_y = 0 @@ -506,6 +510,7 @@ def _sum(self, data2D): err_data = data2D.err_data[np.isfinite(data2D.data)] qx_data = data2D.qx_data[np.isfinite(data2D.data)] qy_data = data2D.qy_data[np.isfinite(data2D.data)] + mask_data = data2D.mask[np.isfinite(data2D.data)] y = 0.0 err_y = 0.0 @@ -513,6 +518,9 @@ def _sum(self, data2D): # Average pixelsize in q space for npts in range(len(data)): + if not mask_data[npts]: + # ignore points that are masked + continue # default frac frac_x = 0 frac_y = 0 @@ -730,6 +738,7 @@ def __call__(self, data2D): err_data = data2D.err_data[np.isfinite(data2D.data)] qx_data = data2D.qx_data[np.isfinite(data2D.data)] qy_data = data2D.qy_data[np.isfinite(data2D.data)] + mask_data = data2D.mask[np.isfinite(data2D.data)] # Set space for 1d outputs phi_bins = np.zeros(self.nbins_phi) @@ -742,6 +751,9 @@ def __call__(self, data2D): phi_shift = Pi / self.nbins_phi for npt in range(len(data)): + if not mask_data[npt]: + # ignore points that are masked + continue frac = 0 # q-value at the point (npt) q_value = q_data[npt] @@ -831,6 +843,7 @@ def _agv(self, data2D, run='phi'): err_data = data2D.err_data[np.isfinite(data2D.data)] qx_data = data2D.qx_data[np.isfinite(data2D.data)] qy_data = data2D.qy_data[np.isfinite(data2D.data)] + mask_data = data2D.mask[np.isfinite(data2D.data)] dq_data = None if data2D.dqx_data is not None and data2D.dqy_data is not None: @@ -854,6 +867,9 @@ def _agv(self, data2D, run='phi'): binning = Binning(self.r_min, self.r_max, self.nbins, self.base) for n in range(len(data)): + if not mask_data[n]: + # ignore points that are masked + continue # q-value at the pixel (j,i) q_value = q_data[n] diff --git a/src/sas/sasview/__init__.py b/src/sas/sasview/__init__.py index fc9a06f6be..e56e0b4026 100644 --- a/src/sas/sasview/__init__.py +++ b/src/sas/sasview/__init__.py @@ -1,6 +1,6 @@ from distutils.version import StrictVersion -__version__ = "5.0.3" +__version__ = "5.0.4" StrictVersion(__version__) -__DOI__ = "Zenodo, DOI:10.5281/zenodo.3930098" -__release_date__ = "2020" +__DOI__ = "Zenodo, DOI:10.5281/zenodo.4467703" +__release_date__ = "2021" __build__ = "GIT_COMMIT"