diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml
index b56b99b0bd..8dcf35cd2b 100644
--- a/.github/workflows/nightly-build.yml
+++ b/.github/workflows/nightly-build.yml
@@ -56,7 +56,7 @@ jobs:
- name: Staple Release Build (OSX)
if: ${{ startsWith(matrix.os, 'macos') }}
- uses: devbotsxyz/xcode-staple@v1
+ uses: BoundfoxStudios/action-xcode-staple@v1
with:
product-path: "installers/dist/SasView-nightly-MacOSX.dmg"
diff --git a/LICENSE.TXT b/LICENSE.TXT
index a2f0710800..d5fb93139c 100644
--- a/LICENSE.TXT
+++ b/LICENSE.TXT
@@ -1,4 +1,4 @@
-Copyright (c) 2009-2022, SasView Developers
+Copyright (c) 2009-2023, SasView Developers
All rights reserved.
diff --git a/build_tools/release_automation.py b/build_tools/release_automation.py
index 06377e3a8e..07e0847cd0 100644
--- a/build_tools/release_automation.py
+++ b/build_tools/release_automation.py
@@ -14,9 +14,9 @@
#Should import release notes from git repo, for now will need to cut and paste
sasview_data = {
'metadata': {
- 'title': 'SasView version 5.0.5',
- 'description': '5.0.5 release',
- 'related_identifiers': [{'identifier': 'https://github.com/SasView/sasview/releases/tag/v5.0.5',
+ 'title': 'SasView version 5.0.6',
+ 'description': '5.0.6 release',
+ 'related_identifiers': [{'identifier': 'https://github.com/SasView/sasview/releases/tag/v5.0.6',
'relation': 'isAlternateIdentifier', 'scheme': 'url'}],
'contributors': [
{'name': 'Anuchitanukul, Atijit', 'affiliation': 'STFC - Rutherford Appleton Laboratory', 'type':'Researcher'},
@@ -49,6 +49,7 @@
{'name': 'Alina, Gervaise','affiliation': 'University of Tennessee Knoxville'},
{'name': 'Attala, Ziggy', 'affiliation': 'STFC - Rutherford Appleton Laboratory'},
{'name': 'Bakker, Jurrian','affiliation': 'Technical Unviersity Delft'},
+ {'name': 'Beaucage, Peter','affiliation': 'National Institute of Standards and Technology', 'orcid': '0000-0002-2147-0728'},
{'name': 'Bouwman, Wim','affiliation': 'Technical Univeristy Deflt' },
{'name': 'Bourne, Robert', 'affiliation': 'STFC - Rutherford Appleton Laboratory'},
{'name': 'Butler, Paul','affiliation': 'National Institute of Standards and Technology', 'orcid': '0000-0002-5978-4714'},
@@ -63,7 +64,7 @@
{'name': 'Jackson, Andrew','affiliation': 'European Spallation Source ERIC', 'orcid': '0000-0002-6296-0336'},
{'name': 'King, Stephen','affiliation': 'STFC - Rutherford Appleton Laboratory', 'orcid': '0000-0003-3386-9151'},
{'name': 'Kienzle, Paul','affiliation': 'National Institute of Standards and Technology'},
- {'name': 'Krzywon, Jeff','affiliation': 'National Institute of Standards and Technology'},
+ {'name': 'Krzywon, Jeff','affiliation': 'National Institute of Standards and Technology', 'orcid': '0000-0002-2380-4090'},
{'name': 'Maranville, Brian', 'affiliation': 'National Institute of Standards and Technology', 'orcid': '0000-0002-6105-8789'},
{'name': 'Martinez, Nicolas','affiliation': 'Institut Laue-Langevin'},
{'name': 'Murphy, Ryan', 'affiliation': 'National Institute of Standards and Technology', 'orcid': '0000-0002-4080-7525'},
@@ -76,7 +77,7 @@
{'name': 'Snow, Tim','affiliation': 'Diamond Light Source','orcid': '0000-0001-7146-6885'},
{'name': 'Washington, Adam','affiliation': 'STFC - Rutherford Appleton Laboratory'},
{'name': 'Wilkins, Lucas','affiliation': 'STFC - Rutherford Appleton Laboratory'},
- {'name': 'Wolf, Caitlyn','affiliation': 'National Institute of Standards and Technology'}
+ {'name': 'Wolf, Caitlyn','affiliation': 'National Institute of Standards and Technology', 'orcid': '0000-0002-2956-7049'}
],
'grants': [{'id': '10.13039/501100000780::654000'}],
'license': 'BSD-3-Clause',
diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt
index b785e11727..c00355eca1 100644
--- a/build_tools/requirements.txt
+++ b/build_tools/requirements.txt
@@ -1,5 +1,5 @@
-numpy
-scipy==1.7.3
+numpy<1.24
+scipy==1.10.0
docutils
pytest
pytest_qt
diff --git a/docs/sphinx-docs/source/conf.py b/docs/sphinx-docs/source/conf.py
index f695e6dfff..63e0ea8ae7 100644
--- a/docs/sphinx-docs/source/conf.py
+++ b/docs/sphinx-docs/source/conf.py
@@ -11,7 +11,7 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os, collections
+import sys, os, datetime
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@@ -69,8 +69,9 @@
master_doc = 'index'
# General information about the project.
-project = u'SasView'
-copyright = u'2022, The SasView Project'
+year = datetime.datetime.now().year
+project = 'SasView'
+copyright = f'{year}, 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,9 +80,9 @@
# The version number must follow StrictVersion rules as outlined
# in http://epydoc.sourceforge.net/stdlib/distutils.version.StrictVersion-class.html
# The short X.Y version.
-version = '5.0'
+version = '6.0'
# The full version, including e.g. alpha tags (a1).
-release = '5.0.5'
+release = '6.0.0a1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/src/sas/cli.py b/src/sas/cli.py
index e1ef60fa6e..4ecabcbf83 100644
--- a/src/sas/cli.py
+++ b/src/sas/cli.py
@@ -68,6 +68,8 @@ def parse_cli(argv):
help="Open console to display output (windows only)")
parser.add_argument("-q", "--quiet", action='store_true',
help="Don't print banner when entering interactive mode")
+ parser.add_argument("-l", "--loglevel", type=str,
+ help="Logging level (production or development for now)")
parser.add_argument("args", nargs="*",
help="script followed by args")
@@ -118,12 +120,18 @@ def main(logging="production"):
cli = parse_cli(sys.argv)
# Setup logger and sasmodels
- if logging == "production":
+ if cli.loglevel:
+ logging = cli.loglevel
+ logging = logging.upper()
+ if logging == "PRODUCTION":
log.production()
- elif logging == "development":
+ elif logging == "DEVELOPMENT":
log.development()
+ elif logging.upper() in {'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'}:
+ log.setup_logging(logging)
else:
raise ValueError(f"Unknown logging mode \"{logging}\"")
+
lib.setup_sasmodels()
lib.setup_qt_env() # Note: does not import any gui libraries
diff --git a/src/sas/qtgui/Calculators/DensityPanel.py b/src/sas/qtgui/Calculators/DensityPanel.py
index 635d4ba81e..d8f642774e 100644
--- a/src/sas/qtgui/Calculators/DensityPanel.py
+++ b/src/sas/qtgui/Calculators/DensityPanel.py
@@ -57,8 +57,7 @@ def setupUi(self):
self.ui = Ui_DensityPanel()
self.ui.setupUi(self)
- #self.setFixedSize(self.minimumSizeHint())
- self.resize(self.minimumSizeHint())
+ self.setFixedSize(self.minimumSizeHint())
# set validators
#self.ui.editMolecularFormula.setValidator(FormulaValidator(self.ui.editMolecularFormula))
diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py
index b22ba27761..e1a038f414 100644
--- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py
+++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py
@@ -57,6 +57,7 @@ def __init__(self, parent=None):
self.setupUi(self)
# disable the context help icon
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
+ self.setFixedSize(self.minimumSizeHint())
self.manager = parent
self.communicator = self.manager.communicator()
diff --git a/src/sas/qtgui/Calculators/ResolutionCalculatorPanel.py b/src/sas/qtgui/Calculators/ResolutionCalculatorPanel.py
index 34e9eaa5cd..3e2f2cf9c3 100644
--- a/src/sas/qtgui/Calculators/ResolutionCalculatorPanel.py
+++ b/src/sas/qtgui/Calculators/ResolutionCalculatorPanel.py
@@ -42,6 +42,7 @@ def __init__(self, parent=None):
self.setupUi(self)
# disable the context help icon
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
+ self.setFixedSize(self.minimumSizeHint())
self.manager = parent
diff --git a/src/sas/qtgui/Calculators/SldPanel.py b/src/sas/qtgui/Calculators/SldPanel.py
index 22400ca595..1b657e4c96 100644
--- a/src/sas/qtgui/Calculators/SldPanel.py
+++ b/src/sas/qtgui/Calculators/SldPanel.py
@@ -97,6 +97,7 @@ def __init__(self, parent=None):
self.setupUi()
# disable the context help icon
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
+ self.setFixedSize(self.minimumSizeHint())
self.setupModel()
self.setupMapper()
diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py
index d0ebdddf54..93148b6efb 100644
--- a/src/sas/qtgui/MainWindow/DataExplorer.py
+++ b/src/sas/qtgui/MainWindow/DataExplorer.py
@@ -22,8 +22,8 @@
from sas.qtgui.Plotting.PlotterData import Data1D
from sas.qtgui.Plotting.PlotterData import Data2D
from sas.qtgui.Plotting.PlotterData import DataRole
-from sas.qtgui.Plotting.Plotter import Plotter
-from sas.qtgui.Plotting.Plotter2D import Plotter2D
+from sas.qtgui.Plotting.Plotter import Plotter, PlotterWidget
+from sas.qtgui.Plotting.Plotter2D import Plotter2D, Plotter2DWidget
from sas.qtgui.Plotting.MaskEditor import MaskEditor
from sas.qtgui.MainWindow.DataManager import DataManager
@@ -232,7 +232,7 @@ def loadFolder(self, event=None):
caption = 'Choose a directory'
options = QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog
directory = self.default_load_location
- folder = QtWidgets.QFileDialog.getExistingDirectory(parent, caption, directory, "", options)
+ folder = QtWidgets.QFileDialog.getExistingDirectory(parent, caption, directory, options)
if folder is None:
return
@@ -1086,11 +1086,17 @@ def displayData(self, data_list, id=None):
plot_name = plot_to_show.name
role = plot_to_show.plot_role
- stand_alone_types = [DataRole.ROLE_RESIDUAL, DataRole.ROLE_STAND_ALONE]
+ stand_alone_types = [DataRole.ROLE_RESIDUAL, DataRole.ROLE_STAND_ALONE, DataRole.ROLE_POLYDISPERSITY]
if (role in stand_alone_types and shown) or role == DataRole.ROLE_DELETABLE:
# Nothing to do if stand-alone plot already shown or plot to be deleted
continue
+ elif role == DataRole.ROLE_RESIDUAL and config.DISABLE_RESIDUAL_PLOT:
+ # Nothing to do if residuals are not plotted
+ continue
+ elif role == DataRole.ROLE_POLYDISPERSITY and config.DISABLE_POLYDISPERSITY_PLOT:
+ # Nothing to do if polydispersity plot is not plotted
+ continue
elif role in stand_alone_types:
# Stand-alone plots should always be separate
self.plotData([(plot_item, plot_to_show)])
@@ -1132,7 +1138,7 @@ def addDataPlot2D(self, plot_set, item):
"""
Create a new 2D plot and add it to the workspace
"""
- plot2D = Plotter2D(self)
+ plot2D = Plotter2DWidget(parent=self, manager=self)
plot2D.item = item
plot2D.plot(plot_set)
self.addPlot(plot2D)
@@ -1160,7 +1166,7 @@ def plotData(self, plots, transform=True):
for item, plot_set in plots:
if isinstance(plot_set, Data1D):
if 'new_plot' not in locals():
- new_plot = Plotter(self)
+ new_plot = PlotterWidget(manager=self, parent=self)
new_plot.item = item
new_plot.plot(plot_set, transform=transform)
# active_plots may contain multiple charts
@@ -1245,7 +1251,7 @@ def appendPlot(self):
@staticmethod
def appendOrUpdatePlot(self, data, plot):
name = data.name
- if isinstance(plot, Plotter2D) or name in plot.plot_dict.keys():
+ if isinstance(plot, Plotter2DWidget) or name in plot.plot_dict.keys():
plot.replacePlot(name, data)
else:
plot.plot(data)
diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py
index 4e53db5816..2ebc711792 100644
--- a/src/sas/qtgui/MainWindow/GuiManager.py
+++ b/src/sas/qtgui/MainWindow/GuiManager.py
@@ -122,6 +122,9 @@ def __init__(self, parent=None):
logging.info(f" --- SasView session started, version {SASVIEW_VERSION}, {SASVIEW_RELEASE_DATE} ---")
# Log the python version
logging.info("Python: %s" % sys.version)
+ #logging.debug("Debug messages are shown.")
+ #logging.warn("Warnings are shown.")
+ #logging.error("Errors are shown.")
# Set up the status bar
self.statusBarSetup()
diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py b/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py
index 825c66a628..48330b8090 100644
--- a/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py
+++ b/src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py
@@ -643,7 +643,7 @@ def plotPolydispersities(model):
data1d.symbol = 'Line'
data1d.name = "{} polydispersity".format(name)
data1d.id = data1d.name # placeholder, has to be completed later
- data1d.plot_role = DataRole.ROLE_STAND_ALONE
+ data1d.plot_role = DataRole.ROLE_POLYDISPERSITY
plots.append(data1d)
return plots
diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
index 2d5088670d..fc89951667 100644
--- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
+++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
@@ -223,6 +223,8 @@ def data(self, value):
self.smearing_widget.resetSmearer()
# Enable/disable UI components
self.setEnablementOnDataLoad()
+ # Reinitialize model list for constrained/simult fitting
+ self.newModelSignal.emit()
def initializeGlobals(self):
"""
diff --git a/src/sas/qtgui/Plotting/Plotter2D.py b/src/sas/qtgui/Plotting/Plotter2D.py
index d939b0f199..1189cbec0a 100644
--- a/src/sas/qtgui/Plotting/Plotter2D.py
+++ b/src/sas/qtgui/Plotting/Plotter2D.py
@@ -19,9 +19,10 @@
from sas.qtgui.Plotting.BoxSum import BoxSum
from sas.qtgui.Plotting.SlicerParameters import SlicerParameters
-# TODO: move to sas.qtgui namespace
from sas.qtgui.Plotting.Slicers.BoxSlicer import BoxInteractorX
from sas.qtgui.Plotting.Slicers.BoxSlicer import BoxInteractorY
+from sas.qtgui.Plotting.Slicers.WedgeSlicer import WedgeInteractorQ
+from sas.qtgui.Plotting.Slicers.WedgeSlicer import WedgeInteractorPhi
from sas.qtgui.Plotting.Slicers.AnnulusSlicer import AnnulusInteractor
from sas.qtgui.Plotting.Slicers.SectorSlicer import SectorInteractor
from sas.qtgui.Plotting.Slicers.BoxSum import BoxSumCalculator
@@ -189,6 +190,10 @@ def createContextMenu(self):
self.actionBoxAveragingX.triggered.connect(self.onBoxAveragingX)
self.actionBoxAveragingY = self.contextMenu.addAction("&Box Averaging in Qy")
self.actionBoxAveragingY.triggered.connect(self.onBoxAveragingY)
+ self.actionWedgeAveragingQ = self.contextMenu.addAction("&Wedge Averaging in Q")
+ self.actionWedgeAveragingQ.triggered.connect(self.onWedgeAveragingQ)
+ self.actionWedgeAveragingPhi = self.contextMenu.addAction("&Wedge Averaging in Phi")
+ self.actionWedgeAveragingPhi.triggered.connect(self.onWedgeAveragingPhi)
# Additional items for slicer interaction
if self.slicer:
self.actionClearSlicer = self.contextMenu.addAction("&Clear Slicer")
@@ -456,6 +461,20 @@ def onBoxAveragingY(self):
"""
self.setSlicer(slicer=BoxInteractorY)
+ def onWedgeAveragingQ(self):
+ """
+ Perform 2D data averaging on Q
+ Create a new slicer .
+ """
+ self.setSlicer(slicer=WedgeInteractorQ)
+
+ def onWedgeAveragingPhi(self):
+ """
+ Perform 2D data averaging on Phi
+ Create a new slicer .
+ """
+ self.setSlicer(slicer=WedgeInteractorPhi)
+
def onColorMap(self):
"""
Display the color map dialog and modify the plot's map accordingly
diff --git a/src/sas/qtgui/Plotting/PlotterData.py b/src/sas/qtgui/Plotting/PlotterData.py
index 8fbaf7991e..e43539dcc0 100644
--- a/src/sas/qtgui/Plotting/PlotterData.py
+++ b/src/sas/qtgui/Plotting/PlotterData.py
@@ -27,6 +27,8 @@ class DataRole(Enum):
ROLE_RESIDUAL = 3
# Stand alone is for plots that should be plotted separately
ROLE_STAND_ALONE = 4
+ # Polydispersity is for stand-alone polydispersity plot
+ ROLE_POLYDISPERSITY = 5
class Data1D(PlottableData1D, LoadData1D):
diff --git a/src/sas/qtgui/Plotting/SlicerParameters.py b/src/sas/qtgui/Plotting/SlicerParameters.py
index 274f558f25..de33f8721d 100644
--- a/src/sas/qtgui/Plotting/SlicerParameters.py
+++ b/src/sas/qtgui/Plotting/SlicerParameters.py
@@ -14,6 +14,8 @@
from sas.qtgui.Plotting.PlotterData import Data1D
from sas.qtgui.Plotting.Slicers.BoxSlicer import BoxInteractorX
from sas.qtgui.Plotting.Slicers.BoxSlicer import BoxInteractorY
+from sas.qtgui.Plotting.Slicers.WedgeSlicer import WedgeInteractorQ
+from sas.qtgui.Plotting.Slicers.WedgeSlicer import WedgeInteractorPhi
from sas.qtgui.Plotting.Slicers.AnnulusSlicer import AnnulusInteractor
from sas.qtgui.Plotting.Slicers.SectorSlicer import SectorInteractor
@@ -56,7 +58,9 @@ def __init__(self, parent=None,
1: SectorInteractor,
2: AnnulusInteractor,
3: BoxInteractorX,
- 4: BoxInteractorY}
+ 4: BoxInteractorY,
+ 5: WedgeInteractorQ,
+ 6: WedgeInteractorPhi}
# Define a proxy model so cell enablement can be finegrained.
self.proxy = ProxyModel(self)
@@ -216,7 +220,7 @@ def onChooseFilesLocation(self):
caption = 'Save files to:'
options = QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog
directory = self.save_location
- folder = QtWidgets.QFileDialog.getExistingDirectory(parent, caption, directory, "", options)
+ folder = QtWidgets.QFileDialog.getExistingDirectory(parent, caption, directory, options)
if folder is None:
return
diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py
index af94748200..2c7e07c077 100644
--- a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py
+++ b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py
@@ -8,10 +8,13 @@
class AnnulusInteractor(BaseInteractor, SlicerModel):
"""
- Select an annulus through a 2D plot.
- This interactor is used to average 2D data with the region
- defined by 2 radius.
- this class is defined by 2 Ringinterators.
+ AnnulusInteractor plots a data1D average of an annulus area defined in a
+ Data2D object. The data1D averaging itself is performed in sasdata by
+ manipulations.py
+
+ This class uses the RingInteractor class to define two rings of radius
+ r1 and r2 (Q1 and Q2). All Q points at a constant angle phi from the x-axis
+ are averaged together to provide a 1D array in phi from 0 to 180 degrees.
"""
def __init__(self, base, axes, item=None, color='black', zorder=3):
@@ -48,6 +51,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3):
self.outer_circle.qmax = self.qmax * 1.2
self.update()
self._post_data()
+ self.draw()
self.setModelFromParams()
@@ -148,7 +152,6 @@ def _post_data(self, nbins=None):
if self.update_model:
self.setModelFromParams()
- self.draw()
def validate(self, param_name, param_value):
"""
@@ -188,6 +191,7 @@ def moveend(self, ev):
Redraw the plot with new parameters.
"""
self._post_data(self.nbins)
+ self.draw()
def restore(self, ev):
"""
@@ -232,6 +236,7 @@ def setParams(self, params):
self.outer_circle.set_cursor(outer, self.outer_circle._inner_mouse_y)
# Post the data given the nbins entered by the user
self._post_data(self.nbins)
+ self.draw()
def draw(self):
"""
@@ -241,13 +246,13 @@ def draw(self):
class RingInteractor(BaseInteractor):
"""
- Draw a ring Given a radius
+ Draw a ring on a data2D plot centered at (0,0) given a radius
"""
def __init__(self, base, axes, color='black', zorder=5, r=1.0, sign=1):
"""
:param: the color of the line that defined the ring
:param r: the radius of the ring
- :param sign: the direction of motion the the marker
+ :param sign: the direction of motion the marker
"""
BaseInteractor.__init__(self, base, axes, color=color)
@@ -353,7 +358,8 @@ def move(self, x, y, ev):
"""
self._inner_mouse_x = x
self._inner_mouse_y = y
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
def set_cursor(self, x, y):
"""
diff --git a/src/sas/qtgui/Plotting/Slicers/Arc.py b/src/sas/qtgui/Plotting/Slicers/Arc.py
deleted file mode 100644
index 4616479fb0..0000000000
--- a/src/sas/qtgui/Plotting/Slicers/Arc.py
+++ /dev/null
@@ -1,153 +0,0 @@
-"""
- Arc slicer for 2D data
-"""
-import numpy as np
-
-from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor
-
-class ArcInteractor(BaseInteractor):
- """
- Select an annulus through a 2D plot
- """
- def __init__(self, base, axes, color='black', zorder=5, r=1.0,
- theta1=np.pi / 8, theta2=np.pi / 4):
- BaseInteractor.__init__(self, base, axes, color=color)
- self.markers = []
- self.axes = axes
- self._mouse_x = r
- self._mouse_y = 0
- self._save_x = r
- self._save_y = 0
- self.scale = 10.0
- self.theta1 = theta1
- self.theta2 = theta2
- self.radius = r
- [self.arc] = self.axes.plot([], [], linestyle='-', marker='', color=self.color)
- self.npts = 20
- self.has_move = False
- self.connect_markers([self.arc])
- self.update()
-
- def set_layer(self, n):
- """
- Allow adding plot to the same panel
- :param n: the number of layer
- """
- self.layernum = n
- self.update()
-
- def clear(self):
- """
- Clear this slicer and its markers
- """
- self.clear_markers()
- try:
- for item in self.markers:
- item.remove()
- self.arc.remove()
- except:
- # Old version of matplotlib
- for item in range(len(self.axes.lines)):
- del self.axes.lines[0]
-
- def get_radius(self):
- """
- Return arc radius
- """
- radius = np.sqrt(np.power(self._mouse_x, 2) + \
- np.power(self._mouse_y, 2))
- return radius
-
- def update(self, theta1=None, theta2=None, nbins=None, r=None):
- """
- Update the plotted arc
- :param theta1: starting angle of the arc
- :param theta2: ending angle of the arc
- :param nbins: number of points along the arc
- :param r: radius of the arc
- """
- # Plot inner circle
- x = []
- y = []
- if theta1 is not None:
- self.theta1 = theta1
- if theta2 is not None:
- self.theta2 = theta2
- while self.theta2 < self.theta1:
- self.theta2 += (2 * np.pi)
- while self.theta2 >= (self.theta1 + 2 * np.pi):
- self.theta2 -= (2 * np.pi)
- self.npts = int((self.theta2 - self.theta1) / (np.pi / 120))
-
- if r is None:
- self.radius = np.sqrt(np.power(self._mouse_x, 2) + \
- np.power(self._mouse_y, 2))
- else:
- self.radius = r
- for i in range(self.npts):
- phi = (self.theta2 - self.theta1) / (self.npts - 1) * i + self.theta1
- xval = 1.0 * self.radius * np.cos(phi)
- yval = 1.0 * self.radius * np.sin(phi)
-
- x.append(xval)
- y.append(yval)
- self.arc.set_data(x, y)
-
- def save(self, ev):
- """
- Remember the roughness for this layer and the next so that we
- can restore on Esc.
- """
- self._save_x = self._mouse_x
- self._save_y = self._mouse_y
- self.base.freeze_axes()
-
- def moveend(self, ev):
- """
- After a dragging motion reset the flag self.has_move to False
- :param ev: event
- """
- self.has_move = False
-
- self.base.moveend(ev)
-
- def restore(self, ev):
- """
- Restore the roughness for this layer.
- """
- self._mouse_x = self._save_x
- self._mouse_y = self._save_y
-
- def move(self, x, y, ev):
- """
- Process move to a new position, making sure that the move is allowed.
- """
- self._mouse_x = x
- self._mouse_y = y
- self.has_move = True
- self.base.base.update()
-
- def set_cursor(self, radius, phi_min, phi_max, nbins):
- """
- """
- self.theta1 = phi_min
- self.theta2 = phi_max
- self.update(nbins=nbins, r=radius)
-
- def get_params(self):
- """
- """
- params = {}
- params["radius"] = self.radius
- params["theta1"] = self.theta1
- params["theta2"] = self.theta2
- return params
-
- def set_params(self, params):
- """
- """
- x = params["radius"]
- phi_max = self.theta2
- nbins = self.npts
- self.set_cursor(x, self._mouse_y, phi_max, nbins)
-
diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py
new file mode 100644
index 0000000000..0a44e25d42
--- /dev/null
+++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py
@@ -0,0 +1,124 @@
+import numpy as np
+
+from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor
+
+class ArcInteractor(BaseInteractor):
+ """
+ Draw an arc on a data2D plot with a variable radius (centered at [0,0]).
+ User interaction adjusts the parameter r
+
+ param r: radius from (0,0) of the arc on a data2D plot
+ param theta: angle from x-axis of the central point on the arc
+ param phi: angle from the centre point on the arc to each of its edges
+ """
+ def __init__(self, base, axes, color='black', zorder=5, r=1.0,
+ theta=np.pi / 3, phi=np.pi / 8):
+ BaseInteractor.__init__(self, base, axes, color=color)
+ self.markers = []
+ self.axes = axes
+ self.color = color
+ # Variables for the current mouse position
+ self._mouse_x = r
+ self._mouse_y = 0
+ # Last known mouse position, for when the cursor moves off the plot
+ self._save_x = r
+ self._save_y = 0
+ self.scale = 10.0
+ # Key variables for drawing the interactor element
+ self.theta = theta
+ self.phi = phi
+ self.radius = r
+ # Calculate the marker coordinates and define the marker
+ self.marker = self.axes.plot([], [], linestyle='',
+ marker='s', markersize=10,
+ color=self.color, alpha=0.6, pickradius=5,
+ label='pick', zorder=zorder,
+ visible=True)[0]
+ # Define the arc
+ self.arc = self.axes.plot([], [], linestyle='-', marker='', color=self.color)[0]
+ # The number of points that make the arc line
+ self.npts = 40
+ # Flag to keep track of motion
+ self.has_move = False
+ self.connect_markers([self.marker, self.arc])
+ self.update()
+
+ def set_layer(self, n):
+ """
+ Allow adding plot to the same panel
+ :param n: the number of layer
+ """
+ self.layernum = n
+ self.update()
+
+ def clear(self):
+ """
+ Clear this slicer and its markers
+ """
+ self.clear_markers()
+ self.marker.remove()
+ self.arc.remove()
+
+ def update(self, theta=None, phi=None, r=None):
+ """
+ Draw the new roughness on the graph.
+ :param theta: angle from x-axis of the central point on the arc
+ :param phi: angle from the centre point on the arc to each of its edges
+ :param r: radius from (0,0) of the arc on a data2D plot
+ """
+ if theta is not None:
+ self.theta = theta
+ if phi is not None:
+ self.phi = phi
+ if r is not None:
+ self.radius = r
+ # Calculate the points on the arc, and draw them
+ angle_offset = self.theta - self.phi
+ angle_factor = np.asarray([2 * self.phi / (self.npts - 1) * i + angle_offset for i in range(self.npts)])
+ x = self.radius * np.cos(angle_factor)
+ y = self.radius * np.sin(angle_factor)
+ self.arc.set_data(x.tolist(), y.tolist())
+
+ # Calculate the new marker location, and draw that too
+ marker_x = self.radius * np.cos(self.theta - 0.5 * self.phi)
+ marker_y = self.radius * np.sin(self.theta - 0.5 * self.phi)
+ self.marker.set(xdata=[marker_x], ydata=[marker_y])
+
+ def save(self, ev):
+ """
+ Remember the roughness for this layer and the next so that we
+ can restore on Esc.
+ """
+ self._save_x = self._mouse_x
+ self._save_y = self._mouse_y
+
+ def moveend(self, ev):
+ """
+ After a dragging motion reset the flag self.has_move to False
+ :param ev: event
+ """
+ self.has_move = False
+ self.base.moveend(ev)
+
+ def restore(self, ev):
+ """
+ Restore the roughness for this layer.
+ """
+ self._mouse_x = self._save_x
+ self._mouse_y = self._save_y
+
+ def move(self, x, y, ev):
+ """
+ Process move to a new position.
+ """
+ self._mouse_x = x
+ self._mouse_y = y
+ self.radius = np.sqrt(np.power(self._mouse_x, 2) + \
+ np.power(self._mouse_y, 2))
+ self.has_move = True
+ self.base.update()
+ self.base.draw()
+
+ def set_cursor(self, x, y):
+ self.move(x, y, None)
+ self.update()
diff --git a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py b/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py
deleted file mode 100644
index 9ecab61b07..0000000000
--- a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py
+++ /dev/null
@@ -1,266 +0,0 @@
-# TODO: the line slicer should listen to all 2DREFRESH events, get the data and slice it
-# before pushing a new 1D data update.
-#
-# TODO: NEED MAJOR REFACTOR
-#
-import numpy as np
-from sas.qtgui.Plotting.Slicers.Arc import ArcInteractor
-from sas.qtgui.Plotting.Slicers.RadiusInteractor import RadiusInteractor
-from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor
-
-class SectorInteractor(BaseInteractor):
- """
- Select an annulus through a 2D plot
- """
- def __init__(self, base, axes, color='black', zorder=3):
- """
- """
- BaseInteractor.__init__(self, base, axes, color=color)
- self.markers = []
- self.axes = axes
- self.qmax = self.data.xmax
- self.connect = self.base.connect
-
- # # Number of points on the plot
- self.nbins = 100
- theta1 = 2 * np.pi / 3
- theta2 = -2 * np.pi / 3
-
- # Inner circle
- self.inner_circle = ArcInteractor(self, self.base.subplot,
- zorder=zorder,
- r=self.qmax / 2.0,
- theta1=theta1,
- theta2=theta2)
- self.inner_circle.qmax = self.qmax
- self.outer_circle = ArcInteractor(self, self.base.subplot,
- zorder=zorder + 1,
- r=self.qmax / 1.8,
- theta1=theta1,
- theta2=theta2)
- self.outer_circle.qmax = self.qmax * 1.2
- # self.outer_circle.set_cursor(self.base.qmax/1.8, 0)
- self.right_edge = RadiusInteractor(self, self.base.subplot,
- zorder=zorder + 1,
- arc1=self.inner_circle,
- arc2=self.outer_circle,
- theta=theta1)
- self.left_edge = RadiusInteractor(self, self.base.subplot,
- zorder=zorder + 1,
- arc1=self.inner_circle,
- arc2=self.outer_circle,
- theta=theta2)
- self.update()
- self._post_data()
-
- def set_layer(self, n):
- """
- """
- self.layernum = n
- self.update()
-
- def clear(self):
- """
- """
- self.clear_markers()
- self.outer_circle.clear()
- self.inner_circle.clear()
- self.right_edge.clear()
- self.left_edge.clear()
-
- def update(self):
- """
- Respond to changes in the model by recalculating the profiles and
- resetting the widgets.
- """
- # Update locations
- if self.inner_circle.has_move:
- # print "inner circle has moved"
- self.inner_circle.update()
- r1 = self.inner_circle.get_radius()
- r2 = self.outer_circle.get_radius()
- self.right_edge.update(r1, r2)
- self.left_edge.update(r1, r2)
- if self.outer_circle.has_move:
- # print "outer circle has moved"
- self.outer_circle.update()
- r1 = self.inner_circle.get_radius()
- r2 = self.outer_circle.get_radius()
- self.left_edge.update(r1, r2)
- self.right_edge.update(r1, r2)
- if self.right_edge.has_move:
- # print "right edge has moved"
- self.right_edge.update()
- self.inner_circle.update(theta1=self.right_edge.get_angle(),
- theta2=None)
- self.outer_circle.update(theta1=self.right_edge.get_angle(),
- theta2=None)
- if self.left_edge.has_move:
- # print "left Edge has moved"
- self.left_edge.update()
- self.inner_circle.update(theta1=None,
- theta2=self.left_edge.get_angle())
- self.outer_circle.update(theta1=None,
- theta2=self.left_edge.get_angle())
-
- def save(self, ev):
- """
- Remember the roughness for this layer and the next so that we
- can restore on Esc.
- """
- self.base.freeze_axes()
- self.inner_circle.save(ev)
- self.outer_circle.save(ev)
- self.right_edge.save(ev)
- self.left_edge.save(ev)
-
- def _post_data(self):
- pass
-
- def post_data(self, new_sector):
- """ post data averaging in Q"""
- if self.inner_circle.get_radius() < self.outer_circle.get_radius():
- rmin = self.inner_circle.get_radius()
- rmax = self.outer_circle.get_radius()
- else:
- rmin = self.outer_circle.get_radius()
- rmax = self.inner_circle.get_radius()
- if self.right_edge.get_angle() < self.left_edge.get_angle():
- phimin = self.right_edge.get_angle()
- phimax = self.left_edge.get_angle()
- else:
- phimin = self.left_edge.get_angle()
- phimax = self.right_edge.get_angle()
-
- sect = new_sector(r_min=rmin, r_max=rmax,
- phi_min=phimin, phi_max=phimax)
- sector = sect(self.data)
-
- from sas.qtgui.Plotting.PlotterData import Data1D
- if hasattr(sector, "dxl"):
- dxl = sector.dxl
- else:
- dxl = None
- if hasattr(sector, "dxw"):
- dxw = sector.dxw
- else:
- dxw = None
- new_plot = Data1D(x=sector.x, y=sector.y, dy=sector.dy,
- dxl=dxl, dxw=dxw)
- new_plot.name = str(new_sector.__name__) + \
- "(" + self.data.name + ")"
- new_plot.source = self.data.source
- new_plot.interactive = True
- # print "loader output.detector",output.source
- new_plot.detector = self.data.detector
- # If the data file does not tell us what the axes are, just assume...
- new_plot.xaxis("\\rm{Q}", 'rad')
- new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
- new_plot.group_id = str(new_sector.__name__) + self.data.name
-
- def validate(self, param_name, param_value):
- """
- Test the proposed new value "value" for row "row" of parameters
- """
- # Here, always return true
- return True
-
- def moveend(self, ev):
- #TODO: why is this empty?
- pass
-
- def restore(self, ev):
- """
- Restore the roughness for this layer.
- """
- self.inner_circle.restore(ev)
- self.outer_circle.restore(ev)
- self.right_edge.restore(ev)
- self.left_edge.restore(ev)
-
- def move(self, x, y, ev):
- """
- Process move to a new position, making sure that the move is allowed.
- """
- pass
-
- def set_cursor(self, x, y):
- """
- """
- pass
-
- def get_params(self):
- """
- """
- params = {}
- params["r_min"] = self.inner_circle.get_radius()
- params["r_max"] = self.outer_circle.get_radius()
- params["phi_min"] = self.right_edge.get_angle()
- params["phi_max"] = self.left_edge.get_angle()
- params["nbins"] = self.nbins
- return params
-
- def set_params(self, params):
- """
- """
- # print "setparams on main slicer ",params
- inner = params["r_min"]
- outer = params["r_max"]
- phi_min = params["phi_min"]
- phi_max = params["phi_max"]
- self.nbins = int(params["nbins"])
-
- self.inner_circle.set_cursor(inner, phi_min, phi_max, self.nbins)
- self.outer_circle.set_cursor(outer, phi_min, phi_max, self.nbins)
- self.right_edge.set_cursor(inner, outer, phi_min)
- self.left_edge.set_cursor(inner, outer, phi_max)
- self._post_data()
-
- def freeze_axes(self):
- """
- """
- self.base.freeze_axes()
-
- def thaw_axes(self):
- """
- """
- self.base.thaw_axes()
-
- def draw(self):
- """
- """
- self.base.draw()
-
-class SectorInteractorQ(SectorInteractor):
- """
- """
- def __init__(self, base, axes, color='black', zorder=3):
- """
- """
- SectorInteractor.__init__(self, base, axes, color=color)
- self.base = base
- self._post_data()
-
- def _post_data(self):
- """
- """
- from sasdata.data_util.manipulations import SectorQ
- self.post_data(SectorQ)
-
-
-class SectorInteractorPhi(SectorInteractor):
- """
- """
- def __init__(self, base, axes, color='black', zorder=3):
- """
- """
- SectorInteractor.__init__(self, base, axes, color=color)
- self.base = base
- self._post_data()
-
- def _post_data(self):
- """
- """
- from sasdata.data_util.manipulations import SectorPhi
- self.post_data(SectorPhi)
-
diff --git a/src/sas/qtgui/Plotting/Slicers/BaseInteractor.py b/src/sas/qtgui/Plotting/Slicers/BaseInteractor.py
index 9dffd71a74..5d23996837 100755
--- a/src/sas/qtgui/Plotting/Slicers/BaseInteractor.py
+++ b/src/sas/qtgui/Plotting/Slicers/BaseInteractor.py
@@ -140,7 +140,6 @@ def onDrag(self, ev):
self.move(ev.xdata, ev.ydata, ev)
else:
self.restore(ev)
- self.base.update()
return True
def onKey(self, ev):
diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py
index bc93976247..65760e6f66 100644
--- a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py
+++ b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py
@@ -8,8 +8,16 @@
class BoxInteractor(BaseInteractor, SlicerModel):
"""
- BoxInteractor define a rectangle that return data1D average of Data2D
- in a rectangle area defined by -x, x ,y, -y
+ BoxInteractor plots a data1D average of a rectangular area defined in
+ a Data2D object. The data1D averaging itself is performed in sasdata
+ by manipulations.py
+
+ This class uses two other classes, HorizontalLines and VerticalLines,
+ to define the rectangle area: -x, x ,y, -y. It is subclassed by
+ BoxInteractorX and BoxInteracgtorY which define the direction of the
+ average. BoxInteractorX averages all the points from -y to +y as a
+ function of Q_x and BoxInteractorY averages all the points from
+ -x to +x as a function of Q_y
"""
def __init__(self, base, axes, item=None, color='black', zorder=3):
BaseInteractor.__init__(self, base, axes, color=color)
@@ -57,6 +65,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3):
# of averaging data2D
self.update()
self._post_data()
+ self.draw()
self.setModelFromParams()
def update_and_post(self):
@@ -65,6 +74,7 @@ def update_and_post(self):
"""
self.update()
self._post_data()
+ self.draw()
def set_layer(self, n):
"""
@@ -108,12 +118,9 @@ def save(self, ev):
self.vertical_lines.save(ev)
self.horizontal_lines.save(ev)
- def _post_data(self):
- pass
-
- def post_data(self, new_slab=None, nbins=None, direction=None):
+ def _post_data(self, new_slab=None, nbins=None, direction=None):
"""
- post data averaging in Qx or Qy given new_slab type
+ post 1D data averaging in Qx or Qy given new_slab type
:param new_slab: slicer that determine with direction to average
:param nbins: the number of points plotted when averaging
@@ -195,7 +202,6 @@ def post_data(self, new_slab=None, nbins=None, direction=None):
if self.update_model:
self.setModelFromParams()
- self.draw()
def moveend(self, ev):
"""
@@ -250,10 +256,13 @@ def setParams(self, params):
self.horizontal_lines.update(x=self.x, y=self.y)
self.vertical_lines.update(x=self.x, y=self.y)
- self.post_data(nbins=None)
+ self._post_data(nbins=None)
+ self.draw()
def draw(self):
"""
+ Draws the Canvas using the canvas.draw from the calling class
+ that instatiated this object.
"""
self.base.draw()
@@ -261,7 +270,8 @@ def draw(self):
class HorizontalLines(BaseInteractor):
"""
Draw 2 Horizontal lines centered on (0,0) that can move
- on the x- direction and in opposite direction
+ on the x direction. The two lines move symmetrically (in opposite
+ directions). It also defines the x and -x position of a box.
"""
def __init__(self, base, axes, color='black', zorder=5, x=0.5, y=0.5):
"""
@@ -363,12 +373,15 @@ def move(self, x, y, ev):
"""
self.y = y
self.has_move = True
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
class VerticalLines(BaseInteractor):
"""
- Select an annulus through a 2D plot
+ Draw 2 vertical lines centered on (0,0) that can move
+ on the y direction. The two lines move symmetrically (in opposite
+ directions). It also defines the y and -y position of a box.
"""
def __init__(self, base, axes, color='black', zorder=5, x=0.5, y=0.5):
"""
@@ -470,12 +483,15 @@ def move(self, x, y, ev):
"""
self.has_move = True
self.x = x
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
class BoxInteractorX(BoxInteractor):
"""
- Average in Qx direction
+ Average in Qx direction. The data for all Qy at a constant Qx are
+ averaged together to provide a 1D array in Qx (to be plotted as a function
+ of Qx)
"""
def __init__(self, base, axes, item=None, color='black', zorder=3):
BoxInteractor.__init__(self, base, axes, item=item, color=color)
@@ -487,7 +503,7 @@ def _post_data(self):
Post data creating by averaging in Qx direction
"""
from sasdata.data_util.manipulations import SlabX
- self.post_data(SlabX, direction="X")
+ super()._post_data(SlabX, direction="X")
def validate(self, param_name, param_value):
"""
@@ -506,7 +522,9 @@ def validate(self, param_name, param_value):
class BoxInteractorY(BoxInteractor):
"""
- Average in Qy direction
+ Average in Qy direction. The data for all Qx at a constant Qy are
+ averaged together to provide a 1D array in Qy (to be plotted as a function
+ of Qy)
"""
def __init__(self, base, axes, item=None, color='black', zorder=3):
BoxInteractor.__init__(self, base, axes, item=item, color=color)
@@ -518,7 +536,7 @@ def _post_data(self):
Post data creating by averaging in Qy direction
"""
from sasdata.data_util.manipulations import SlabY
- self.post_data(SlabY, direction="Y")
+ super()._post_data(SlabY, direction="Y")
def validate(self, param_name, param_value):
"""
diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSum.py b/src/sas/qtgui/Plotting/Slicers/BoxSum.py
index 9ede747fc1..efb1640b12 100644
--- a/src/sas/qtgui/Plotting/Slicers/BoxSum.py
+++ b/src/sas/qtgui/Plotting/Slicers/BoxSum.py
@@ -1,7 +1,3 @@
-"""
-Boxsum Class: determine 2 rectangular area to compute
-the sum of pixel of a Data.
-"""
import numpy
from PySide6 import QtGui
@@ -15,9 +11,17 @@
class BoxSumCalculator(BaseInteractor):
"""
- Boxsum Class: determine 2 rectangular area to compute
- the sum of pixel of a Data.
- Uses PointerInteractor , VerticalDoubleLine,HorizontalDoubleLine.
+ BoxSumCalculator Class computes properties (such as sum and average of
+ intensities) from a rectangular area defined in a data2D object. The actual
+ calculations are done by manipulations.py
+
+ This class uses three other classes, PointerInteractor to define the center
+ of the rectangle, and VerticalDoubleLine and HorizontalDoubleLine to define
+ the rectangle x1,x2,y1,y2.
+
+ ..TODO: the 3 classes here are the same as used by the BoxSlicer. These
+ should probably be abstracted out.
+
@param zorder: Artists with lower zorder values are drawn first.
@param x_min: the minimum value of the x coordinate
@param x_max: the maximum value of the x coordinate
@@ -226,7 +230,6 @@ def postData(self):
self.total, self.totalerror, self.points = boxtotal(self.data)
if self.update_model:
self.setModelFromParams()
- self.draw()
def moveend(self, ev):
"""
@@ -391,7 +394,8 @@ def move(self, x, y, ev):
self.x = x
self.y = y
self.has_move = True
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
def setCursor(self, x, y):
"""
@@ -558,7 +562,8 @@ def move(self, x, y, ev):
self.x2 = self.center_x - delta
self.half_width = numpy.fabs(self.x1 - self.x2) / 2
self.has_move = True
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
def setCursor(self, x, y):
"""
@@ -569,7 +574,8 @@ def setCursor(self, x, y):
class HorizontalDoubleLine(BaseInteractor):
"""
- Select an annulus through a 2D plot
+ Draw 2 horizontal lines moving in opposite direction and centered on
+ a point (PointInteractor)
"""
def __init__(self, base, axes, color='black', zorder=5, x=0.5, y=0.5,
center_x=0.0, center_y=0.0):
@@ -720,7 +726,8 @@ def move(self, x, y, ev):
self.y2 = self.center_y - delta
self.half_height = numpy.fabs(self.y1) - self.center_y
self.has_move = True
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
def setCursor(self, x, y):
"""
diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py
index 876e18537e..0bacc4a5e2 100755
--- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py
+++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py
@@ -1,66 +1,92 @@
import numpy as np
-from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor
+from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor
class RadiusInteractor(BaseInteractor):
"""
- Select an annulus through a 2D plot
+ Draw a pair of lines radiating from a center at [0,0], between radius
+ values r1 and r2 with and average angle from the x-axis of theta, and an
+ angular diaplacement of phi either side of this average. Used for example
+ to to define the left and right edges of the a wedge area on a plot, see
+ WedgeInteractor. User interaction adjusts the parameter phi.
+
+ :param r1: radius of the inner end of the radial lines
+ :param r2: radius of the outer end of the radial lines
+ :param theta: average angle of the lines from the x-axis
+ :param phi: angular displacement of the lines either side of theta
"""
- def __init__(self, base, axes, color='black', zorder=5, arc1=None,
- arc2=None, theta=np.pi / 8):
- """
- """
- _BaseInteractor.__init__(self, base, axes, color=color)
+ def __init__(self, base, axes, color='black', zorder=5, r1=1.0, r2=2.0,
+ theta=np.pi / 3, phi=np.pi / 8):
+ BaseInteractor.__init__(self, base, axes, color=color)
self.markers = []
self.axes = axes
- self.r1 = arc1.get_radius()
- self.r2 = arc2.get_radius()
+ self.color = color
+ # Key variables used when drawing the interactor element
+ self.r1 = r1
+ self.r2 = r2
self.theta = theta
- self.save_theta = theta
- self.move_stop = False
- self.theta_left = None
- self.theta_right = None
- self.arc1 = arc1
- self.arc2 = arc2
- x1 = self.r1 * np.cos(self.theta)
- y1 = self.r1 * np.sin(self.theta)
- x2 = self.r2 * np.cos(self.theta)
- y2 = self.r2 * np.sin(self.theta)
- self.line = self.axes.plot([x1, x2], [y1, y2],
- linestyle='-', marker='',
- color=self.color,
- visible=True)[0]
- self.phi = theta
- self.npts = 20
+ # Core variable altered by the user
+ self.phi = phi
+ # Last known phi value for when the cursor moves off the plot
+ self.save_phi = phi
+ # Variables for the left and right radial lines
+ l_x1 = self.r1 * np.cos(self.theta + self.phi)
+ l_y1 = self.r1 * np.sin(self.theta + self.phi)
+ l_x2 = self.r2 * np.cos(self.theta + self.phi)
+ l_y2 = self.r2 * np.sin(self.theta + self.phi)
+ r_x1 = self.r1 * np.cos(self.theta - self.phi)
+ r_y1 = self.r1 * np.sin(self.theta - self.phi)
+ r_x2 = self.r2 * np.cos(self.theta - self.phi)
+ r_y2 = self.r2 * np.sin(self.theta - self.phi)
+ # Define the left and right markers
+ self.l_marker = self.axes.plot([(l_x1+l_x2)/2], [(l_y1+l_y2)/2],
+ linestyle='', marker='s', markersize=10,
+ color=self.color, alpha=0.6,
+ pickradius=5, label='pick',
+ zorder=zorder, visible=True)[0]
+ self.r_marker = self.axes.plot([(r_x1+r_x2)/2], [(r_y1+r_y2)/2],
+ linestyle='', marker='s', markersize=10,
+ color=self.color, alpha=0.6,
+ pickradius=5, label='pick',
+ zorder=zorder, visible=True)[0]
+ # Define the left and right lines
+ self.l_line = self.axes.plot([l_x1, l_x2], [l_y1, l_y2],
+ linestyle='-', marker='',
+ color=self.color, visible=True)[0]
+ self.r_line = self.axes.plot([r_x1, r_x2], [r_y1, r_y2],
+ linestyle='-', marker='',
+ color=self.color, visible=True)[0]
+ # Flag to keep track of motion
self.has_move = False
- self.connect_markers([self.line])
+ self.connect_markers([self.l_marker, self.l_line,
+ self.r_marker, self.r_line])
self.update()
def set_layer(self, n):
"""
+ Allow adding plot to the same panel
+ :param n: the number of layer
"""
self.layernum = n
self.update()
def clear(self):
"""
+ Clear this slicer and its markers
"""
self.clear_markers()
- try:
- self.line.remove()
- except:
- # Old version of matplotlib
- for item in range(len(self.axes.lines)):
- del self.axes.lines[0]
+ self.l_marker.remove()
+ self.l_line.remove()
+ self.r_marker.remove()
+ self.r_line.remove()
- def get_angle(self):
- """
- """
- return self.theta
-
- def update(self, r1=None, r2=None, theta=None):
+ def update(self, r1=None, r2=None, theta=None, phi=None):
"""
Draw the new roughness on the graph.
+ :param r1: radius of the inner end of the radial lines
+ :param r2: radius of the outer end of the radial lines
+ :param theta: average angle of the lines from the x-axis
+ :param phi: angular displacement of the lines either side of theta
"""
if r1 is not None:
self.r1 = r1
@@ -68,22 +94,34 @@ def update(self, r1=None, r2=None, theta=None):
self.r2 = r2
if theta is not None:
self.theta = theta
- x1 = self.r1 * np.cos(self.theta)
- y1 = self.r1 * np.sin(self.theta)
- x2 = self.r2 * np.cos(self.theta)
- y2 = self.r2 * np.sin(self.theta)
- self.line.set(xdata=[x1, x2], ydata=[y1, y2])
+ if phi is not None:
+ self.phi = phi
+ # Variables for the left and right radial lines
+ l_x1 = self.r1 * np.cos(self.theta + self.phi)
+ l_y1 = self.r1 * np.sin(self.theta + self.phi)
+ l_x2 = self.r2 * np.cos(self.theta + self.phi)
+ l_y2 = self.r2 * np.sin(self.theta + self.phi)
+ r_x1 = self.r1 * np.cos(self.theta - self.phi)
+ r_y1 = self.r1 * np.sin(self.theta - self.phi)
+ r_x2 = self.r2 * np.cos(self.theta - self.phi)
+ r_y2 = self.r2 * np.sin(self.theta - self.phi)
+ # Draw the updated markers and lines
+ self.l_marker.set(xdata=[(l_x1+l_x2)/2], ydata=[(l_y1+l_y2)/2])
+ self.l_line.set(xdata=[l_x1, l_x2], ydata=[l_y1, l_y2])
+ self.r_marker.set(xdata=[(r_x1+r_x2)/2], ydata=[(r_y1+r_y2)/2])
+ self.r_line.set(xdata=[r_x1, r_x2], ydata=[r_y1, r_y2])
def save(self, ev):
"""
Remember the roughness for this layer and the next so that we
can restore on Esc.
"""
- self.save_theta = np.arctan2(ev.y, ev.x)
- self.base.freeze_axes()
+ self.save_phi = self.phi
def moveend(self, ev):
"""
+ Called when any dragging motion ends.
+ Redraw the plot with new parameters and set self.has_move to False.
"""
self.has_move = False
self.base.moveend(ev)
@@ -92,38 +130,22 @@ def restore(self, ev):
"""
Restore the roughness for this layer.
"""
- self.theta = self.save_theta
+ self.phi = self.save_phi
def move(self, x, y, ev):
"""
- Process move to a new position, making sure that the move is allowed.
+ Process move to a new position.
"""
- self.theta = np.arctan2(y, x)
+ angle = np.arctan2(y, x)
+ phi = np.fabs(angle - self.theta)
+ if phi > np.pi:
+ phi = 2 * np.pi - phi
+ self.phi = phi
self.has_move = True
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
- def set_cursor(self, r_min, r_max, theta):
- """
- """
- self.theta = theta
- self.r1 = r_min
- self.r2 = r_max
+ def set_cursor(self, x, y):
+ self.move(x, y, None)
self.update()
- def get_params(self):
- """
- """
- params = {}
- params["radius1"] = self.r1
- params["radius2"] = self.r2
- params["theta"] = self.theta
- return params
-
- def set_params(self, params):
- """
- """
- x1 = params["radius1"]
- x2 = params["radius2"]
- theta = params["theta"]
- self.set_cursor(x1, x2, theta)
-
diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py
index cb47b09285..5ead14e7bc 100644
--- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py
+++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py
@@ -1,6 +1,3 @@
-"""
- Sector interactor
-"""
import numpy
import logging
@@ -13,7 +10,21 @@
class SectorInteractor(BaseInteractor, SlicerModel):
"""
- Draw a sector slicer.Allow to performQ averaging on data 2D
+ SectorInteractor plots a data1D average of a sector area defined in a
+ Data2D object. The data1D averaging itself is performed in sasdata by
+ manipulations.py. Sectors all go through a single point as (0,0).
+
+ This class uses two other classes, LineInteractor and SideInteractor, to
+ define a sector centered around a main line defined by LineInteractor
+ which goes through 0,0 at some user settable angle theta from 0. The
+ sector itself is defined by the right and left sidelines, both of which
+ also go through (0,0), and set by SideInteractor from -phi to +phi around
+ the center line defined by the main line. All points at a constant Q from
+ -phi to +phi are averaged together to provide a 1D array in Q (to be
+ plotted as a function of Q).
+
+ ..TODO: the 2 subclasses here are the same as used by the BoxSum. These
+ should probably be abstracted out.
"""
def __init__(self, base, axes, item=None, color='black', zorder=3):
@@ -48,15 +59,18 @@ def __init__(self, base, axes, item=None, color='black', zorder=3):
self.right_line = SideInteractor(self, self.axes, color='black',
zorder=zorder, r=self.qmax,
phi=-1 * self.phi, theta2=self.theta2)
+ self.right_line.update(right=True)
self.right_line.qmax = self.qmax
# Left Side line
self.left_line = SideInteractor(self, self.axes, color='black',
zorder=zorder, r=self.qmax,
phi=self.phi, theta2=self.theta2)
+ self.left_line.update(left=True)
self.left_line.qmax = self.qmax
# draw the sector
self.update()
self._post_data()
+ self.draw()
self.setModelFromParams()
def set_layer(self, n):
@@ -89,24 +103,23 @@ def update(self):
if self.main_line.has_move:
self.main_line.update()
self.right_line.update(delta=-self.left_line.phi / 2,
- mline=self.main_line.theta)
+ mline=self.main_line.theta, right=True)
self.left_line.update(delta=self.left_line.phi / 2,
- mline=self.main_line.theta)
+ mline=self.main_line.theta, left=True)
# Check if the left side has moved and update the slicer accordingly
if self.left_line.has_move:
self.main_line.update()
self.left_line.update(phi=None, delta=None, mline=self.main_line,
side=True, left=True)
self.right_line.update(phi=self.left_line.phi, delta=None,
- mline=self.main_line, side=True,
- left=False, right=True)
+ mline=self.main_line, side=True, right=True)
# Check if the right side line has moved and update the slicer accordingly
if self.right_line.has_move:
self.main_line.update()
self.right_line.update(phi=None, delta=None, mline=self.main_line,
- side=True, left=False, right=True)
+ side=True, right=True)
self.left_line.update(phi=self.right_line.phi, delta=None,
- mline=self.main_line, side=True, left=False)
+ mline=self.main_line, side=True, left=True)
def save(self, ev):
"""
@@ -178,7 +191,6 @@ def _post_data(self, nbins=None):
if self.update_model:
self.setModelFromParams()
- self.draw()
def validate(self, param_name, param_value):
"""
@@ -262,10 +274,11 @@ def setParams(self, params):
self.main_line.update()
self.right_line.update(phi=phi, delta=None, mline=self.main_line,
side=True, right=True)
- self.left_line.update(phi=phi, delta=None,
- mline=self.main_line, side=True)
+ self.left_line.update(phi=phi, delta=None, mline=self.main_line,
+ side=True, left=True)
# Post the new corresponding data
self._post_data(nbins=self.nbins)
+ self.draw()
def draw(self):
"""
@@ -276,7 +289,10 @@ def draw(self):
class SideInteractor(BaseInteractor):
"""
- Draw an oblique line
+ Draws a line though 0,0 on a data2D plot with reference to a center line.
+ This is used to define both a left and right line which are always updated
+ together as they must remain symmetric at some phi value around the main
+ line (at -phi and +phi).
:param phi: the phase between the middle line and one side line
:param theta2: the angle between the middle line and x- axis
@@ -445,7 +461,8 @@ def move(self, x, y, ev):
self.phi = numpy.fabs(self.theta2 - self.theta)
if self.phi > numpy.pi:
self.phi = 2 * numpy.pi - numpy.fabs(self.theta2 - self.theta)
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
def set_cursor(self, x, y):
self.move(x, y, None)
@@ -464,10 +481,16 @@ def setParams(self, params):
class LineInteractor(BaseInteractor):
"""
- Select an annulus through a 2D plot
+ Draws a line though 0,0 on a data2D plot. This is used to define the
+ centerline around with other lines can be drawn to define a region of
+ interest (such as a sector).
+
+ :param theta: the angle between the middle line and x- axis
+ :param half_length: Defaults to False. If True, the line is drawn from the
+ origin rather than across the whole graph.
"""
def __init__(self, base, axes, color='black',
- zorder=5, r=1.0, theta=numpy.pi / 4):
+ zorder=5, r=1.0, theta=numpy.pi / 4, half_length=False):
BaseInteractor.__init__(self, base, axes, color=color)
self.markers = []
@@ -477,11 +500,16 @@ def __init__(self, base, axes, color='black',
self.theta = theta
self.radius = r
self.scale = 10.0
+ self.half_length = half_length
# Inner circle
x1 = self.radius * numpy.cos(self.theta)
y1 = self.radius * numpy.sin(self.theta)
- x2 = -1 * self.radius * numpy.cos(self.theta)
- y2 = -1 * self.radius * numpy.sin(self.theta)
+ if not half_length:
+ x2 = -1 * self.radius * numpy.cos(self.theta)
+ y2 = -1 * self.radius * numpy.sin(self.theta)
+ else:
+ x2 = 0
+ y2 = 0
# Inner circle marker
self.inner_marker = self.axes.plot([x1 / 2.5], [y1 / 2.5], linestyle='',
marker='s', markersize=10,
@@ -492,7 +520,6 @@ def __init__(self, base, axes, color='black',
self.line = self.axes.plot([x1, x2], [y1, y2],
linestyle='-', marker='',
color=self.color, visible=True)[0]
- self.npts = 20
self.has_move = False
self.connect_markers([self.inner_marker, self.line])
self.update()
@@ -520,8 +547,12 @@ def update(self, theta=None):
self.theta = theta
x1 = self.radius * numpy.cos(self.theta)
y1 = self.radius * numpy.sin(self.theta)
- x2 = -1 * self.radius * numpy.cos(self.theta)
- y2 = -1 * self.radius * numpy.sin(self.theta)
+ if not self.half_length:
+ x2 = -1 * self.radius * numpy.cos(self.theta)
+ y2 = -1 * self.radius * numpy.sin(self.theta)
+ else:
+ x2 = 0
+ y2 = 0
self.inner_marker.set(xdata=[x1 / 2.5], ydata=[y1 / 2.5])
self.line.set(xdata=[x1, x2], ydata=[y1, y2])
@@ -549,7 +580,8 @@ def move(self, x, y, ev):
"""
self.theta = numpy.arctan2(y, x)
self.has_move = True
- self.base.base.update()
+ self.base.update()
+ self.base.draw()
def set_cursor(self, x, y):
self.move(x, y, None)
diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py
new file mode 100644
index 0000000000..03ac2978a8
--- /dev/null
+++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py
@@ -0,0 +1,360 @@
+import numpy as np
+
+from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor
+from sas.qtgui.Plotting.SlicerModel import SlicerModel
+from sas.qtgui.Plotting.PlotterData import Data1D
+import sas.qtgui.Utilities.GuiUtils as GuiUtils
+
+from sas.qtgui.Plotting.Slicers.ArcInteractor import ArcInteractor
+from sas.qtgui.Plotting.Slicers.RadiusInteractor import RadiusInteractor
+from sas.qtgui.Plotting.Slicers.SectorSlicer import LineInteractor
+
+class WedgeInteractor(BaseInteractor, SlicerModel):
+ """
+ This WedgeInteractor is a cross between the SectorInteractor and the
+ AnnulusInteractor. It plots a data1D average of a wedge area defined in a
+ Data2D object, in either the Q direction or the Phi direction. The data1D
+ averaging itself is performed in sasdata by manipulations.py.
+
+ This class uses three other classes, ArcInteractor (in ArcInteractor.py),
+ RadiusInteractor (in RadiusInteractor.py), and LineInteractor
+ (in SectorSlicer.py), to define a wedge area contained
+ between two radial lines running through (0,0) defining the left and right
+ edges of the wedge (similar to the sector), and two rings at Q1 and Q2
+ (similar to the annulus). The wedge is centred on the line defined by
+ LineInteractor, which the radial lines move symmetrically around.
+ This class is itself subclassed by SectorInteractorPhi and
+ SectorInteractorQ which define the direction of the averaging.
+ SectorInteractorPhi averages all Q points at constant Phi (as for the
+ AnnulusSlicer) and SectorInteractorQ averages all phi points at constant Q
+ (as for the SectorSlicer).
+ """
+ def __init__(self, base, axes, item=None, color='black', zorder=3):
+
+ BaseInteractor.__init__(self, base, axes, color=color)
+ SlicerModel.__init__(self)
+
+ self.markers = []
+ self.axes = axes
+ self._item = item
+ self.qmax = max(self.data.xmax, np.fabs(self.data.xmin),
+ self.data.ymax, np.fabs(self.data.ymin))
+ self.dqmin = min(np.fabs(self.data.qx_data))
+ self.connect = self.base.connect
+
+ # Number of points on the plot
+ self.nbins = 100
+ # Radius of the inner edge of the wedge
+ self.r1 = self.qmax / 2.0
+ # Radius of the outer edge of the wedge
+ self.r2 = self.qmax / 1.6
+ # Angle of the central line
+ self.theta = np.pi / 3
+ # Angle between the central line and the radial lines either side of it
+ self.phi = np.pi / 8
+ # reference of the current data averager
+ self.averager = None
+
+ self.inner_arc = ArcInteractor(self, self.axes, color='black',
+ zorder=zorder, r=self.r1,
+ theta=self.theta, phi=self.phi)
+ self.inner_arc.qmax = self.qmax
+ self.outer_arc = ArcInteractor(self, self.axes, color='black',
+ zorder=zorder + 1, r=self.r2,
+ theta=self.theta, phi=self.phi)
+ self.outer_arc.qmax = self.qmax * 1.2
+ self.radial_lines = RadiusInteractor(self, self.axes, color='black',
+ zorder=zorder + 1,
+ r1=self.r1, r2=self.r2,
+ theta=self.theta, phi=self.phi)
+ self.radial_lines.qmax = self.qmax * 1.2
+ self.central_line = LineInteractor(self, self.axes, color='black',
+ zorder=zorder, r=self.qmax * 1.414,
+ theta=self.theta, half_length=True)
+ self.central_line.qmax = self.qmax * 1.414
+ self.update()
+ self.draw()
+ self._post_data()
+ self.setModelFromParams()
+
+ def set_layer(self, n):
+ """
+ Allow adding plot to the same panel
+ :param n: the number of layer
+ """
+ self.layernum = n
+ self.update()
+
+ def clear(self):
+ """
+ Clear the slicer and all connected events related to this slicer
+ """
+ self.averager = None
+ self.clear_markers()
+ self.outer_arc.clear()
+ self.inner_arc.clear()
+ self.radial_lines.clear()
+ self.central_line.clear()
+ self.base.connect.clearall()
+
+ def update(self):
+ """
+ If one of the interactors has been moved, update it and the parameter
+ it controls, then update the other interactors accordingly
+ """
+ if self.inner_arc.has_move:
+ self.inner_arc.update()
+ self.r1 = self.inner_arc.radius
+ self.radial_lines.update(r1=self.r1)
+ if self.outer_arc.has_move:
+ self.outer_arc.update()
+ self.r2 = self.outer_arc.radius
+ self.radial_lines.update(r2=self.r2)
+ if self.radial_lines.has_move:
+ self.radial_lines.update()
+ self.phi = self.radial_lines.phi
+ self.inner_arc.update(phi=self.phi)
+ self.outer_arc.update(phi=self.phi)
+ if self.central_line.has_move:
+ self.central_line.update()
+ self.theta = self.central_line.theta
+ self.inner_arc.update(theta=self.theta)
+ self.outer_arc.update(theta=self.theta)
+ self.radial_lines.update(theta=self.theta)
+
+ def save(self, ev):
+ """
+ Remember the roughness for this layer and the next so that we
+ can restore on Esc.
+ """
+ self.inner_arc.save(ev)
+ self.outer_arc.save(ev)
+ self.radial_lines.save(ev)
+ self.central_line.save(ev)
+
+ def _post_data(self, new_sector=None, nbins=None):
+ """
+ post 1D data averagin in Q or Phi given new_sector type
+
+ :param new_sector: slicer used for directional averaging in Q or Phi
+ :param nbins: the number of point plotted when averaging
+ :TODO - Unlike other slicers, the two sector types are sufficiently
+ different that this method contains three instances of If (check class name) do x.
+ The point of post_data vs _post_data I think was to avoid this kind of thing and
+ suggests that in this case we may need a new method in the WedgeInteracgtorPhi
+ and WedgeInteracgtorQ to handle these specifics. Probably by creating the 1D plot
+ object in those top level classes along with the specifc attributes.
+ """
+ # Data to average
+ data = self.data
+ if data is None:
+ return
+
+ if self.inner_arc.radius < self.outer_arc.radius:
+ rmin = self.inner_arc.radius
+ rmax = self.outer_arc.radius
+ else:
+ rmin = self.outer_arc.radius
+ rmax = self.inner_arc.radius
+ phimin = self.central_line.theta - self.radial_lines.phi
+ phimax = self.central_line.theta + self.radial_lines.phi
+
+ if nbins is not None:
+ self.nbins = nbins
+ if self.averager is None:
+ if new_sector is None:
+ msg = "post data:cannot average , averager is empty"
+ raise ValueError(msg)
+ self.averager = new_sector
+
+ # Add pi to the angles before invoking sector averaging to transform angular
+ # range from python default of -pi,pi to 0,2pi suitable for manipulations
+ sect = self.averager(r_min=rmin, r_max=rmax, phi_min=phimin + np.pi,
+ phi_max=phimax + np.pi, nbins=self.nbins)
+ sect.fold = False
+ sector = sect(self.data)
+
+ if hasattr(sector, "dxl"):
+ dxl = sector.dxl
+ else:
+ dxl = None
+ if hasattr(sector, "dxw"):
+ dxw = sector.dxw
+ else:
+ dxw = None
+ if self.averager.__name__ == 'SectorPhi':
+ # And here subtract pi when getting angular data back from wedge averaging in
+ # phi in manipulations to get back in the -pi,pi range. Also convert from
+ # radians to degrees for nicer display.
+ sector.x = (sector.x - np.pi) * 180 / np.pi
+ new_plot = Data1D(x=sector.x, y=sector.y, dy=sector.dy, dx=sector.dx)
+ new_plot.dxl = dxl
+ new_plot.dxw = dxw
+ new_plot.name = str(self.averager.__name__) + \
+ "(" + self.data.name + ")"
+ new_plot.source = self.data.source
+ new_plot.interactive = True
+ new_plot.detector = self.data.detector
+ # If the data file does not tell us what the axes are, just assume...
+ if self.averager.__name__ == 'SectorPhi':
+ # angular plots usually require a linear x scale and better with
+ # a linear y scale as well.
+ new_plot.xaxis("\\rm{\phi}", "degrees")
+ new_plot.xtransform = 'x'
+ new_plot.ytransform = 'y'
+ else:
+ new_plot.xaxis("\\rm{Q}", 'A^{-1}')
+ new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}")
+
+
+ new_plot.id = str(self.averager.__name__) + self.data.name
+ new_plot.group_id = new_plot.id
+ new_plot.is_data = True
+ item = self._item
+ if self._item.parent() is not None:
+ item = self._item.parent()
+ GuiUtils.updateModelItemWithPlot(item, new_plot, new_plot.id)
+
+ self.base.manager.communicator.plotUpdateSignal.emit([new_plot])
+ self.base.manager.communicator.forcePlotDisplaySignal.emit([item, new_plot])
+
+ if self.update_model:
+ self.setModelFromParams()
+
+ def validate(self, param_name, param_value):
+ """
+ Validate input from user.
+ Values get checked at apply time.
+ """
+
+ def check_radius_difference(param_name, other_radius_name, param_value):
+ if np.fabs(param_value - self.getParams()[other_radius_name]) < self.dqmin:
+ return "Inner and outer radii too close. Please adjust."
+ elif param_value > self.qmax:
+ return f"{param_name} exceeds maximum range. Please adjust."
+ return None
+
+ def check_phi_difference(param_value):
+ if np.fabs(param_value) < 0.01:
+ return "Sector angles too close. Please adjust."
+ return None
+
+ def check_bins(param_value):
+ if param_value < 1:
+ return "Number of bins cannot be <= 0. Please adjust."
+ return None
+
+ validators = {
+ 'r_min': lambda value: check_radius_difference('r_min', 'r_max', value),
+ 'r_max': lambda value: check_radius_difference('r_max', 'r_min', value),
+ 'delta_phi [deg]': check_phi_difference,
+ 'nbins': check_bins
+ }
+
+ if param_name in validators:
+ error_message = validators[param_name](param_value)
+ if error_message:
+ print(error_message)
+ return False
+
+ return True
+
+ def moveend(self, ev):
+ """
+ Called after a dragging event.
+ Post the slicer new parameters and creates a new Data1D
+ corresponding to the new average
+ """
+ self._post_data()
+
+ def restore(self, ev):
+ """
+ Restore the roughness for this layer.
+ """
+ self.inner_arc.restore(ev)
+ self.outer_arc.restore(ev)
+ self.radial_lines.restore(ev)
+ self.central_line.restore(ev)
+
+ def move(self, x, y, ev):
+ """
+ Process move to a new position.
+ """
+ pass
+
+ def set_cursor(self, x, y):
+ pass
+
+ def getParams(self):
+ """
+ Store a copy of values of parameters of the slicer into a dictionary.
+ :return params: the dictionary created
+ """
+ params = {}
+ params["r_min"] = self.inner_arc.radius
+ params["r_max"] = self.outer_arc.radius
+ params["phi [deg]"] = self.central_line.theta * 180 / np.pi
+ params["delta_phi [deg]"] = self.radial_lines.phi * 180 / np.pi
+ params["nbins"] = self.nbins
+ return params
+
+ def setParams(self, params):
+ """
+ Receive a dictionary and reset the slicer with values contained
+ in the values of the dictionary.
+
+ :param params: a dictionary containing name of slicer parameters and
+ values the user assigned to the slicer.
+ """
+ self.r1 = params["r_min"]
+ self.r2 = params["r_max"]
+ self.theta = params["phi [deg]"] * np.pi / 180
+ self.phi = params["delta_phi [deg]"] * np.pi / 180
+ self.nbins = int(params["nbins"])
+
+ self.inner_arc.update(theta=self.theta, phi=self.phi, r=self.r1)
+ self.outer_arc.update(theta=self.theta, phi=self.phi, r=self.r2)
+ self.radial_lines.update(r1=self.r1, r2=self.r2,
+ theta=self.theta, phi=self.phi)
+ self.central_line.update(theta=self.theta)
+ self._post_data()
+ self.draw()
+
+ def draw(self):
+ """
+ Draws the Canvas using the canvas.draw from the calling class
+ that instantiated this object.
+ """
+ self.base.draw()
+
+class WedgeInteractorQ(WedgeInteractor):
+ """
+ Average in Q direction. The data for all phi at a constant Q are
+ averaged together to provide a 1D array in Q (to be plotted as a function
+ of Q)
+ """
+ def __init__(self, base, axes, item=None, color='black', zorder=3):
+ WedgeInteractor.__init__(self, base, axes, item=item, color=color)
+ self.base = base
+ self._post_data()
+
+ def _post_data(self):
+ from sasdata.data_util.manipulations import SectorQ
+ super()._post_data(SectorQ)
+
+
+class WedgeInteractorPhi(WedgeInteractor):
+ """
+ Average in phi direction. The data for all Q at a constant phi are
+ averaged together to provide a 1D array in phi (to be plotted as a function
+ of phi)
+ """
+ def __init__(self, base, axes, item=None, color='black', zorder=3):
+ WedgeInteractor.__init__(self, base, axes, item=item, color=color)
+ self.base = base
+ self._post_data()
+
+ def _post_data(self):
+ from sasdata.data_util.manipulations import SectorPhi
+ super()._post_data(SectorPhi)
+
diff --git a/src/sas/qtgui/Plotting/UI/SlicerParametersUI.ui b/src/sas/qtgui/Plotting/UI/SlicerParametersUI.ui
index 5cfbcb215e..0acde9cf04 100755
--- a/src/sas/qtgui/Plotting/UI/SlicerParametersUI.ui
+++ b/src/sas/qtgui/Plotting/UI/SlicerParametersUI.ui
@@ -7,7 +7,7 @@
0
0
395
- 458
+ 468
@@ -145,6 +145,16 @@
Box Interactor Y
+ -
+
+ Wedge Interactor Q
+
+
+ -
+
+ Wedge Interactor Phi
+
+
diff --git a/src/sas/qtgui/Utilities/GuiUtils.py b/src/sas/qtgui/Utilities/GuiUtils.py
index 2e439411d9..7166e15c3c 100644
--- a/src/sas/qtgui/Utilities/GuiUtils.py
+++ b/src/sas/qtgui/Utilities/GuiUtils.py
@@ -961,42 +961,67 @@ def replaceHTMLwithASCII(html):
return html
-def convertUnitToUTF8(unit):
- """
- Convert ASCII unit display into UTF-8 symbol
- """
- if unit == "1/A":
- return "Å-1"
- elif unit == "1/cm":
- return "cm-1"
- elif unit == "Ang":
- return "Å"
- elif unit == "1e-6/Ang^2":
- return "10-6/Å2"
- elif unit == "inf":
- return "∞"
- elif unit == "-inf":
- return "-∞"
- else:
- return unit
+def rstToHtml(s):
+ # Extract the unit and replacement parts
+ match_replace = re.match(r'(?:\.\. )?\|(.+?)\| replace:: (.+)', s)
+ match_unit = re.match(r'(?:\.\. )?\|(.+?)\| unicode:: (U\+\w+)', s)
+ unit = None
+ replacement = None
+
+
+ if match_unit:
+ # replace the 'unicode' section
+ unit, unicode_val = match_unit.groups()
+ # Convert the unicode value to actual character representation
+ replacement = chr(int(unicode_val[2:], 16))
+
+ if match_replace:
+ # replace the 'replace' section
+
+ unit, replacement = match_replace.groups()
+
+ # Convert the unit into a valid Python string condition
+ unit = unit.replace("\\", "").replace(" ", "")
+
+ # Convert the replacement into the desired HTML format
+ replacement = replacement.replace("|Ang|", "Å").replace("\\ :sup:`", "").replace("`", "").replace("\\", "")
+ replacement = replacement.replace("|cdot|", "·").replace("|deg|", "°").replace("|pm|", "±")
+
+
+ return unit, replacement
+
+# RST_PROLOG conversion table
+try:
+ from sasmodels.generate import RST_PROLOG
+except ImportError:
+ RST_PROLOG = ""
+RST_PROLOG_DICT = {}
+input_rst_strings = RST_PROLOG.splitlines()
+for line in input_rst_strings:
+ if line.startswith(".. |"):
+ key, value = rstToHtml(line)
+ RST_PROLOG_DICT[key] = value
+# add units not in RST_PROLOG
+# This section will be removed once all these units are added to sasmodels
+RST_PROLOG_DICT["1/A"] = "Å-1"
+RST_PROLOG_DICT["1/Ang"] = "Å-1"
+RST_PROLOG_DICT["1/cm"] = "cm-1"
+RST_PROLOG_DICT["1e-6/Ang^2"] = "10-6/Å2"
+RST_PROLOG_DICT["1e15/cm^3"] = "1015/cm3"
+RST_PROLOG_DICT["inf"] = "∞"
+RST_PROLOG_DICT["-inf"] = "-∞"
+RST_PROLOG_DICT["degrees"] = "°"
+
def convertUnitToHTML(unit):
"""
- Convert ASCII unit display into well rendering HTML
- """
- if unit == "1/A":
- return "Å-1"
- elif unit == "1/cm":
- return "cm-1"
- elif unit == "Ang":
- return "Å"
- elif unit == "1e-6/Ang^2":
- return "10-6/Å2"
- elif unit == "inf":
- return "∞"
- elif unit == "-inf":
- return "-∞"
+ Convert ASCII unit display into HTML symbol
+ """
+ if unit in RST_PROLOG_DICT:
+ return RST_PROLOG_DICT[unit]
else:
+ if unit is None or "None" in unit:
+ return ""
return unit
def parseName(name, expression):
diff --git a/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py b/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py
index abdd1d6143..da535cb4f6 100644
--- a/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py
+++ b/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py
@@ -6,7 +6,10 @@
class DisplayPreferencesWidget(PreferencesWidget):
def __init__(self):
super(DisplayPreferencesWidget, self).__init__("Display Settings")
- self.config_params = ['QT_SCALE_FACTOR', 'QT_AUTO_SCREEN_SCALE_FACTOR']
+ self.config_params = ['QT_SCALE_FACTOR',
+ 'QT_AUTO_SCREEN_SCALE_FACTOR',
+ 'DISABLE_RESIDUAL_PLOT',
+ 'DISABLE_POLYDISPERSITY_PLOT']
self.restart_params = {'QT_SCALE_FACTOR': 'QT Screen Scale Factor',
'QT_AUTO_SCREEN_SCALE_FACTOR': "Enable Automatic Scaling"}
@@ -21,12 +24,26 @@ def _addAllWidgets(self):
checked=config.QT_AUTO_SCREEN_SCALE_FACTOR)
self.autoScaling.clicked.connect(
lambda: self._stageChange('QT_AUTO_SCREEN_SCALE_FACTOR', self.autoScaling.isChecked()))
+ self.disableResidualPlot = self.addCheckBox(
+ title="Disable Residuals Display",
+ checked=config.DISABLE_RESIDUAL_PLOT)
+ self.disableResidualPlot.clicked.connect(
+ lambda: self._stageChange('DISABLE_RESIDUAL_PLOT', self.disableResidualPlot.isChecked()))
+ self.disablePolydispersityPlot = self.addCheckBox(
+ title="Disable Polydispersity Plot Display",
+ checked=config.DISABLE_POLYDISPERSITY_PLOT)
+ self.disablePolydispersityPlot.clicked.connect(
+ lambda: self._stageChange('DISABLE_POLYDISPERSITY_PLOT', self.disablePolydispersityPlot.isChecked()))
def _toggleBlockAllSignaling(self, toggle):
self.qtScaleFactor.blockSignals(toggle)
self.autoScaling.blockSignals(toggle)
+ self.disableResidualPlot.blockSignals(toggle)
+ self.disablePolydispersityPlot.blockSignals(toggle)
def _restoreFromConfig(self):
self.qtScaleFactor.setText(str(config.QT_SCALE_FACTOR))
self.qtScaleFactor.setStyleSheet("background-color: white")
self.autoScaling.setChecked(bool(config.QT_AUTO_SCREEN_SCALE_FACTOR))
+ self.disableResidualPlot.setChecked(config.DISABLE_RESIDUAL_PLOT)
+ self.disablePolydispersityPlot.setChecked(config.DISABLE_POLYDISPERSITY_PLOT)
diff --git a/src/sas/qtgui/Utilities/PythonSyntax.py b/src/sas/qtgui/Utilities/PythonSyntax.py
index 2a5d88e39a..b863d4d993 100644
--- a/src/sas/qtgui/Utilities/PythonSyntax.py
+++ b/src/sas/qtgui/Utilities/PythonSyntax.py
@@ -81,6 +81,8 @@ def __init__(self, document, is_python=True):
# syntax highlighting from this point onward
self.tri_single = (QRegularExpression("'''"), 1, STYLES['string2'])
self.tri_double = (QRegularExpression('"""'), 2, STYLES['string2'])
+ self.tri_single_raw = (QRegularExpression(r'r\'\'\''), 3, STYLES['string2'])
+ self.tri_double_raw = (QRegularExpression(r'r\"\"\"'), 4, STYLES['string2'])
rules = []
@@ -131,14 +133,15 @@ def highlightBlock(self, text):
"""
# Do other syntax formatting
for expression, nth, format in self.rules:
- index = expression.indexIn(text, 0)
+ match = expression.match(text)
+ index = match.capturedStart(0)
while index >= 0:
# We actually want the index of the nth match
- index = expression.pos(nth)
- length = len(expression.cap(nth))
+ index = match.capturedStart(nth)
+ length = match.capturedLength(nth)
self.setFormat(index, length, format)
- index = expression.indexIn(text, index + length)
+ index = match.capturedStart(index+length)
self.setCurrentBlockState(0)
@@ -147,6 +150,9 @@ def highlightBlock(self, text):
if not in_multiline:
in_multiline = self.match_multiline(text, *self.tri_double)
+ in_multiline = in_multiline or self.match_multiline(text, *self.tri_single_raw)
+ if not in_multiline:
+ in_multiline = self.match_multiline(text, *self.tri_double_raw)
def match_multiline(self, text, delimiter, in_state, style):
"""Do highlighting of multi-line strings. ``delimiter`` should be a
@@ -161,17 +167,20 @@ def match_multiline(self, text, delimiter, in_state, style):
add = 0
# Otherwise, look for the delimiter on this line
else:
- start = delimiter.indexIn(text)
+ match = delimiter.match(text)
+ start = match.capturedStart(0)
+ end = match.capturedEnd(0)
# Move past this match
- add = delimiter.matchedLength()
+ add = end - start + 1
# As long as there's a delimiter match on this line...
while start >= 0:
- # Look for the ending delimiter
- end = delimiter.indexIn(text, start + add)
+ # Look for the ending delimiter starting from where the last match ended
+ match = delimiter.match(text, start + add)
+ end = match.capturedEnd(0)
# Ending delimiter on this line?
if end >= add:
- length = end - start + add + delimiter.matchedLength()
+ length = end - start + add
self.setCurrentBlockState(0)
# No; multi-line string
else:
@@ -180,7 +189,7 @@ def match_multiline(self, text, delimiter, in_state, style):
# Apply formatting
self.setFormat(start, length, style)
# Look for the next match
- start = delimiter.indexIn(text, start + length)
+ start = match.capturedStart(start + length)
# Return True if still inside a multi-line string, False otherwise
if self.currentBlockState() == in_state:
diff --git a/src/sas/qtgui/Utilities/Reports/ReportDialog.py b/src/sas/qtgui/Utilities/Reports/ReportDialog.py
index 18d7d24e9a..4c15d2fd0b 100644
--- a/src/sas/qtgui/Utilities/Reports/ReportDialog.py
+++ b/src/sas/qtgui/Utilities/Reports/ReportDialog.py
@@ -127,8 +127,9 @@ def write_string(string, filename):
"""
Write string to file
"""
- with open(filename, 'w') as f:
- f.write(string)
+ with open(filename, 'wb') as f:
+ # weird unit symbols need to be saved as UTF-8
+ f.write(bytes(string, 'utf-8'))
@staticmethod
def save_pdf(data, filename):
diff --git a/src/sas/qtgui/Utilities/SasviewLogger.py b/src/sas/qtgui/Utilities/SasviewLogger.py
index 68cdf4eead..23260be876 100644
--- a/src/sas/qtgui/Utilities/SasviewLogger.py
+++ b/src/sas/qtgui/Utilities/SasviewLogger.py
@@ -4,6 +4,9 @@
from PySide6.QtCore import QObject, Signal
+LOG_FORMAT = "%(asctime)s - %(levelname)s: %(message)s"
+DATE_FORMAT = "%H:%M:%S"
+
class QtPostman(QObject):
messageWritten = Signal(str)
@@ -23,16 +26,18 @@ def emit(self, record):
self.postman.messageWritten.emit(message)
def setup_qt_logging():
- # Define the default logger
- logger = logging.getLogger()
-
# Add the qt-signal logger
+ logger = logging.root
+
+ # If a QtHandler is already defined in log.ini then use it. This allows
+ # config to override the default message formatting. We don't do this
+ # by default because we may be using sasview as a library and don't
+ # want to load Qt.
+ for handler in logger.handlers:
+ if isinstance(handler, QtHandler):
+ return handler
+
handler = QtHandler()
- handler.setFormatter(logging.Formatter(
- fmt="%(asctime)s - %(levelname)s: %(message)s",
- datefmt="%H:%M:%S"
- ))
+ handler.setFormatter(logging.Formatter(fmt=LOG_FORMAT, datefmt=DATE_FORMAT))
logger.addHandler(handler)
-
return handler
-
\ No newline at end of file
diff --git a/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py b/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py
index 3cb45b1c3e..ca649cb915 100644
--- a/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py
+++ b/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py
@@ -507,36 +507,32 @@ def testReplaceHTMLwithASCII(self):
s = "Å ∞ ±"
assert replaceHTMLwithASCII(s) == "Ang inf +/-"
- def testConvertUnitToUTF8(self):
- ''' test unit string replacement'''
+ def testrstToHtml(self):
+ ''' test rst to html conversion'''
s = None
- assert convertUnitToUTF8(s) is None
-
- s = ""
- assert convertUnitToUTF8(s) == s
-
- s = "aaaa"
- assert convertUnitToUTF8(s) == s
-
- s = "1/A"
- assert convertUnitToUTF8(s) == "Å-1"
-
- s = "Ang"
- assert convertUnitToUTF8(s) == "Å"
-
- s = "1e-6/Ang^2"
- assert convertUnitToUTF8(s) == "10-6/Å2"
-
- s = "inf"
- assert convertUnitToUTF8(s) == "∞"
+ with pytest.raises(TypeError):
+ result = rstToHtml(s)
+
+ s = ".. |Ang| unicode:: U+212B"
+ assert rstToHtml(s) == ('Ang', 'Å')
+ s = ".. |Ang^-1| replace:: |Ang|\ :sup:`-1`"
+ assert rstToHtml(s) == ('Ang^-1', 'Å-1')
+ s = ".. |1e-6Ang^-2| replace:: 10\ :sup:`-6`\ |Ang|\ :sup:`-2`"
+ assert rstToHtml(s) == ('1e-6Ang^-2', '10-6 Å-2')
+ s = ".. |cm^-1| replace:: cm\ :sup:`-1`"
+ assert rstToHtml(s) == ('cm^-1', 'cm-1')
+ s = ".. |deg| unicode:: U+00B0"
+ assert rstToHtml(s) == ('deg', '°')
+ s = ".. |cdot| unicode:: U+00B7"
+ assert rstToHtml(s) == ('cdot', '·')
+ s = "bad string"
+ assert rstToHtml(s) == (None, None)
- s = "1/cm"
- assert convertUnitToUTF8(s) == "cm-1"
def testConvertUnitToHTML(self):
''' test unit string replacement'''
s = None
- assert convertUnitToHTML(s) is None
+ assert convertUnitToHTML(s) is ""
s = ""
assert convertUnitToHTML(s) == s
@@ -545,23 +541,26 @@ def testConvertUnitToHTML(self):
assert convertUnitToHTML(s) == s
s = "1/A"
- assert convertUnitToHTML(s) == "Å-1"
+ assert convertUnitToHTML(s) == "Å-1"
s = "Ang"
- assert convertUnitToHTML(s) == "Å"
+ assert convertUnitToHTML(s) == "Å"
s = "1e-6/Ang^2"
- assert convertUnitToHTML(s) == "10-6/Å2"
+ assert convertUnitToHTML(s) == "10-6/Å2"
s = "inf"
- assert convertUnitToHTML(s) == "∞"
+ assert convertUnitToHTML(s) == "∞"
s = "-inf"
- assert convertUnitToHTML(s) == "-∞"
+ assert convertUnitToHTML(s) == "-∞"
s = "1/cm"
assert convertUnitToHTML(s) == "cm-1"
+ s = "degrees"
+ assert convertUnitToHTML(s) == "°"
+
def testParseName(self):
'''test parse out a string from the beinning of a string'''
# good input
diff --git a/src/sas/sascalc/calculator/geni.py b/src/sas/sascalc/calculator/geni.py
index 07553ce394..4b148c395d 100644
--- a/src/sas/sascalc/calculator/geni.py
+++ b/src/sas/sascalc/calculator/geni.py
@@ -11,10 +11,6 @@
try:
if os.environ.get('SAS_NUMBA', '1').lower() in ('1', 'yes', 'true', 't'):
from numba import njit, prange
- # Suppress numba debug info
- import logging
- numba_logger = logging.getLogger('numba')
- numba_logger.setLevel(logging.WARNING)
USE_NUMBA = True
else:
raise ImportError("fail")
diff --git a/src/sas/system/config/config.py b/src/sas/system/config/config.py
index 816110eb90..d3fd556955 100644
--- a/src/sas/system/config/config.py
+++ b/src/sas/system/config/config.py
@@ -197,6 +197,14 @@ def __init__(self):
# sets the maximum number of characters per Fitting plot legend entry.
self.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH = 30
+ # Residuals management
+ # If true, disables residual plot display
+ self.DISABLE_RESIDUAL_PLOT = False
+
+ # Polydispersity plot management
+ # If true, disables polydispersity plot display
+ self.DISABLE_POLYDISPERSITY_PLOT = False
+
# Default fitting optimizer
self.FITTING_DEFAULT_OPTIMIZER = 'lm'
diff --git a/src/sas/system/config/config_meta.py b/src/sas/system/config/config_meta.py
index 5c1f6cd079..c5fb60d444 100644
--- a/src/sas/system/config/config_meta.py
+++ b/src/sas/system/config/config_meta.py
@@ -1,3 +1,4 @@
+import re
from typing import Dict, Any, List, Set
import os
import logging
@@ -150,8 +151,11 @@ def load_from_file_object(self, file):
raise MalformedFile("Malformed config file - no 'sasview_version' key")
try:
- parts = [int(s) for s in data["sasview_version"].split(".")]
- if len(parts) != 3:
+ file_version = data["sasview_version"]
+ # Use the distutils strict version module regex to check if the version string is valid
+ # ref: https://epydoc.sourceforge.net/stdlib/distutils.version.StrictVersion-class.html
+ matcher = re.compile(r'(?x)^(\d+)\.(\d+)(\.(\d+))?([ab](\d+))?$')
+ if not matcher.match(file_version):
raise Exception
except Exception:
@@ -165,7 +169,6 @@ def load_from_file_object(self, file):
# Check major version
- file_version = data["sasview_version"]
file_major_version = file_version.split(".")[0]
sasview_major_version = sas.system.version.__version__.split(".")[0]
diff --git a/src/sas/system/legal.py b/src/sas/system/legal.py
index 1c900b1dbd..5ea2522310 100644
--- a/src/sas/system/legal.py
+++ b/src/sas/system/legal.py
@@ -1,6 +1,6 @@
class Legal:
def __init__(self):
- self.copyright = "Copyright (c) 2009-2022 UTK, UMD, ESS, NIST, ORNL, ISIS, ILL, DLS, TUD, BAM and ANSTO"
+ self.copyright = "Copyright (c) 2009-2023 UTK, UMD, ESS, NIST, ORNL, ISIS, ILL, DLS, TUD, BAM and ANSTO"
legal = Legal()
\ No newline at end of file
diff --git a/src/sas/system/log.ini b/src/sas/system/log.ini
index 295eacd525..d9c6d4d473 100644
--- a/src/sas/system/log.ini
+++ b/src/sas/system/log.ini
@@ -28,58 +28,18 @@ keys=console,log_file
[handler_console]
class=logging.StreamHandler
formatter=simple
-level=WARNING
-args=tuple()
[handler_log_file]
class=logging.FileHandler
-level=WARNING
formatter=detailed
args=(os.path.join(os.path.expanduser("~"),'sasview.log'),"a")
+
###############################################################################
# Loggers
[loggers]
-keys=root,saspr,sasgui,sascalc,sasmodels,h5py,glshaders
+keys=root
[logger_root]
-level=DEBUG
-formatter=default
-handlers=console,log_file
-
-[logger_sasmodels]
-level=INFO
-qualname=sas.models
handlers=console,log_file
-propagate=0
-
-[logger_saspr]
-level=INFO
-qualname=sas.pr
-handlers=console,log_file
-propagate=0
-
-[logger_sasgui]
-level=DEBUG
-qualname=sas.sasgui
-handlers=console,log_file
-propagate=0
-
-[logger_sascalc]
-level=INFO
-qualname=sas.sascalc
-handlers=console,log_file
-propagate=0
-
-[logger_h5py]
-level=DEBUG
-qualname=h5py
-handlers=
-propagate=0
-
-[logger_glshaders]
-level=DEBUG
-qualname=OpenGL.GL.shaders
-handlers=
-propagate=0
\ No newline at end of file
diff --git a/src/sas/system/log.py b/src/sas/system/log.py
index e5e06dfd90..ae31cdf48a 100644
--- a/src/sas/system/log.py
+++ b/src/sas/system/log.py
@@ -12,70 +12,59 @@
Module that manages the global logging
'''
+#BASE_LOGGER = 'sasview'
+TRACED_PACKAGES = ('sas', 'sasmodels', 'sasdata', 'bumps', 'periodictable')
+IGNORED_PACKAGES = {
+ 'matplotlib': 'ERROR',
+ 'numba': 'WARN',
+ 'h5py': 'ERROR',
+ 'ipykernel': 'CRITICAL',
+}
-class SetupLogger(object):
- '''
- Called at the beginning of run.py or sasview.py
- '''
+def setup_logging(level=logging.INFO):
+ # Setup the defaults
+ logging.captureWarnings(True)
+ for package in TRACED_PACKAGES:
+ logging.getLogger(package).setLevel(level)
+ for package, package_level in IGNORED_PACKAGES.items():
+ logging.getLogger(package).setLevel(package_level)
- def __init__(self, logger_name):
- self._config_file = None
- self._find_config_file()
- self.name = logger_name
+ # SasView is often using the root logger to emit error messages. Until
+ # that is fixed we need to set the level of the root logger to the target
+ # level. Unfortunately that means that all broken third party packages
+ # (i.e., those that use the root logger rather than __name__) will also
+ # be set to that level, which is why we have to explicitly override them
+ # with the 'IGNORED_PACKAGES' list. The following regex will find most of
+ # the culprits:
+ # grep -R "logg\(ing\|er\)" src | grep -v .pyc | less
+ # TODO: use __name__ as the logger for all sasview log messages
+ logging.root.setLevel(level)
- def config_production(self):
- logger = logging.getLogger(self.name)
- if not logger.root.handlers:
- self._read_config_file()
- logging.captureWarnings(True)
- logger = logging.getLogger(self.name)
- logging.getLogger('matplotlib').setLevel(logging.WARN)
- logging.getLogger('numba').setLevel(logging.WARN)
- return logger
+ # Apply the logging config after setting the defaults
+ try:
+ fd = importlib.resources.open_text('sas.system', 'log.ini')
+ logging.config.fileConfig(fd)
+ except FileNotFoundError:
+ print(f"ERROR: Log config '{filename}' not found...", file=sys.stderr)
- def config_development(self):
- '''
- '''
- self._read_config_file()
- logger = logging.getLogger(self.name)
- self._update_all_logs_to_debug(logger)
- logging.captureWarnings(True)
- logging.getLogger('matplotlib').setLevel(logging.WARN)
- logging.getLogger('numba').setLevel(logging.WARN)
- return logger
-
- def _read_config_file(self):
- if self._config_file is not None:
- logging.config.fileConfig(self._config_file)
-
- def _update_all_logs_to_debug(self, logger):
- '''
- This updates all loggers and respective handlers to DEBUG
- '''
- for handler in logger.handlers or logger.parent.handlers:
- handler.setLevel(logging.DEBUG)
- for name, _ in logging.Logger.manager.loggerDict.items():
- logging.getLogger(name).setLevel(logging.DEBUG)
-
- def _find_config_file(self, filename="log.ini"):
- '''
- The config file is in:
- Debug ./sasview/
- Packaging: sas/sasview/
- Packaging / production does not work well with absolute paths
- thus importlib is used to find a filehandle to the resource
- wherever it is actually located.
-
- Returns a TextIO instance that is open for reading the resource.
- '''
- self._config_file = None
- try:
- self._config_file = importlib.resources.open_text('sas.system', filename)
- except FileNotFoundError:
- print(f"ERROR: '{filename}' not found...", file=sys.stderr)
+ #print_config()
def production():
- return SetupLogger("sasview").config_production()
+ setup_logging('INFO')
def development():
- return SetupLogger("sasview").config_development()
+ setup_logging('DEBUG')
+
+def print_config(msg="Logger config:"):
+ """
+ When debugging the logging configuration it is handy to see exactly how
+ it is configured. To do so you will need to pip install the logging_tree
+ package and add *log.print_config()* at choice points in the code.
+ """
+ try:
+ from logging_tree import printout
+ except ImportError:
+ print("log.print_config requires the logging_tree package from PyPI")
+ return
+ print(msg)
+ printout()
diff --git a/src/sas/system/version.py b/src/sas/system/version.py
index 1cd06c4312..b3aab77fe1 100644
--- a/src/sas/system/version.py
+++ b/src/sas/system/version.py
@@ -1,4 +1,4 @@
-__version__ = "5.0.5"
-__release_date__ = "2022"
+__version__ = "6.0.0a1"
+__release_date__ = "2023"
__build__ = "GIT_COMMIT"