From 9894cac6891f727770de9f3e7b5368a43c14026c Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 28 Jun 2023 09:58:40 -0400 Subject: [PATCH 01/58] Update version and year --- LICENSE.TXT | 2 +- src/sas/system/legal.py | 2 +- src/sas/system/version.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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/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/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" From 1be5253088d6b5f080d82da930c1b1e6b19be60d Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 28 Jun 2023 10:06:12 -0400 Subject: [PATCH 02/58] Update version and year in conf.py --- docs/sphinx-docs/source/conf.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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. From ffd1d3b6711f392158549eece2996cf76e802087 Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Tue, 20 Dec 2022 06:35:40 +0100 Subject: [PATCH 03/58] Adding Peter to the contributors list --- build_tools/release_automation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/build_tools/release_automation.py b/build_tools/release_automation.py index 06377e3a8e..1791c97de7 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'}, {'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'}, From 7bf4ba24fcf240e6269cfa8b1d59e8c5e82cc618 Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 30 Aug 2022 14:35:31 -0400 Subject: [PATCH 04/58] Add orcid for Jeff Krzywon --- build_tools/release_automation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_tools/release_automation.py b/build_tools/release_automation.py index 1791c97de7..a9e4769a07 100644 --- a/build_tools/release_automation.py +++ b/build_tools/release_automation.py @@ -64,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'}, From 9a3f9426c94e8beabc999d2cee8e6dbf46116544 Mon Sep 17 00:00:00 2001 From: Peter Beaucage Date: Tue, 20 Dec 2022 11:46:32 -0500 Subject: [PATCH 05/58] Add missing ORCIDs --- build_tools/release_automation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_tools/release_automation.py b/build_tools/release_automation.py index a9e4769a07..07e0847cd0 100644 --- a/build_tools/release_automation.py +++ b/build_tools/release_automation.py @@ -49,7 +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'}, + {'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'}, @@ -77,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', From e10a94486e8d7235a0d3eee8fbb35a24f8e0b68e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Jul 2023 23:01:18 +0000 Subject: [PATCH 06/58] Bump scipy from 1.7.3 to 1.10.0 in /build_tools Bumps [scipy](https://github.com/scipy/scipy) from 1.7.3 to 1.10.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.7.3...v1.10.0) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- build_tools/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index b785e11727..3f5edec97e 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -1,5 +1,5 @@ numpy -scipy==1.7.3 +scipy==1.10.0 docutils pytest pytest_qt From 767241964fadbc6c112e611f2b3feb663a56dad1 Mon Sep 17 00:00:00 2001 From: butlerpd Date: Sun, 7 May 2023 21:16:17 +0000 Subject: [PATCH 07/58] reverse order of plot update and plot draw This fixes the error due to the confusion between base and base.base which had all interactors redraw the plot first and updating from the move second. This was resolved in the "moveend" method but made using the mouse to set the interactor rather difficult to do precisely. This was also cleanded up in the three interactor/slicer modules not currently used but that probably will be for the needs of the magnetic/crystal scattering communities. Duplicate draws were removed when appropriate. --- src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py | 7 +++++-- src/sas/qtgui/Plotting/Slicers/Arc.py | 4 ++-- src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py | 1 + src/sas/qtgui/Plotting/Slicers/BaseInteractor.py | 1 - src/sas/qtgui/Plotting/Slicers/BoxSlicer.py | 10 +++++++--- src/sas/qtgui/Plotting/Slicers/BoxSum.py | 10 ++++++---- src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py | 3 ++- src/sas/qtgui/Plotting/Slicers/SectorSlicer.py | 9 ++++++--- 8 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py index af94748200..8717ad53ea 100644 --- a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @@ -48,6 +48,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 +149,6 @@ def _post_data(self, nbins=None): if self.update_model: self.setModelFromParams() - self.draw() def validate(self, param_name, param_value): """ @@ -188,6 +188,7 @@ def moveend(self, ev): Redraw the plot with new parameters. """ self._post_data(self.nbins) + self.draw() def restore(self, ev): """ @@ -232,6 +233,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): """ @@ -353,7 +355,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 index 4616479fb0..0b1568e5d1 100644 --- a/src/sas/qtgui/Plotting/Slicers/Arc.py +++ b/src/sas/qtgui/Plotting/Slicers/Arc.py @@ -108,7 +108,6 @@ def moveend(self, ev): :param ev: event """ self.has_move = False - self.base.moveend(ev) def restore(self, ev): @@ -125,7 +124,8 @@ def move(self, x, y, ev): self._mouse_x = x self._mouse_y = y self.has_move = True - self.base.base.update() + self.base.update() + self.base.draw() def set_cursor(self, radius, phi_min, phi_max, nbins): """ diff --git a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py b/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py index 9ecab61b07..93dc59d7f3 100644 --- a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py @@ -51,6 +51,7 @@ def __init__(self, base, axes, color='black', zorder=3): arc2=self.outer_circle, theta=theta2) self.update() + self.draw() self._post_data() def set_layer(self, n): 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..68a2e24813 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py @@ -57,6 +57,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 +66,7 @@ def update_and_post(self): """ self.update() self._post_data() + self.draw() def set_layer(self, n): """ @@ -195,7 +197,6 @@ def post_data(self, new_slab=None, nbins=None, direction=None): if self.update_model: self.setModelFromParams() - self.draw() def moveend(self, ev): """ @@ -251,6 +252,7 @@ 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.draw() def draw(self): """ @@ -363,7 +365,8 @@ 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): @@ -470,7 +473,8 @@ 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): diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSum.py b/src/sas/qtgui/Plotting/Slicers/BoxSum.py index 9ede747fc1..aedfffbd15 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSum.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSum.py @@ -226,7 +226,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 +390,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 +558,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): """ @@ -720,7 +721,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..07f6f71a77 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -100,7 +100,8 @@ def move(self, x, y, ev): """ self.theta = np.arctan2(y, x) self.has_move = True - self.base.base.update() + self.base.update() + self.base.draw() def set_cursor(self, r_min, r_max, theta): """ diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index cb47b09285..394fed4392 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -57,6 +57,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): # draw the sector self.update() self._post_data() + self.draw() self.setModelFromParams() def set_layer(self, n): @@ -178,7 +179,6 @@ def _post_data(self, nbins=None): if self.update_model: self.setModelFromParams() - self.draw() def validate(self, param_name, param_value): """ @@ -266,6 +266,7 @@ def setParams(self, params): mline=self.main_line, side=True) # Post the new corresponding data self._post_data(nbins=self.nbins) + self.draw() def draw(self): """ @@ -445,7 +446,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) @@ -549,7 +551,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) From 75eb577cdfbf70721072703cd1f3816aadc445c8 Mon Sep 17 00:00:00 2001 From: butlerpd Date: Mon, 8 May 2023 01:28:57 +0000 Subject: [PATCH 08/58] Slicer docstring updates First pass at fixing the slicer classes docstrings. --- .../qtgui/Plotting/Slicers/AnnulusSlicer.py | 15 +++++---- src/sas/qtgui/Plotting/Slicers/Arc.py | 10 +++--- .../qtgui/Plotting/Slicers/AzimutSlicer.py | 21 ++++++++++++- src/sas/qtgui/Plotting/Slicers/BoxSlicer.py | 31 ++++++++++++++----- src/sas/qtgui/Plotting/Slicers/BoxSum.py | 21 ++++++++----- .../Plotting/Slicers/RadiusInteractor.py | 10 +++++- .../qtgui/Plotting/Slicers/SectorSlicer.py | 30 ++++++++++++++---- 7 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py index 8717ad53ea..19a41208c3 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 classe 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): @@ -243,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) diff --git a/src/sas/qtgui/Plotting/Slicers/Arc.py b/src/sas/qtgui/Plotting/Slicers/Arc.py index 0b1568e5d1..9ffde5b7e8 100644 --- a/src/sas/qtgui/Plotting/Slicers/Arc.py +++ b/src/sas/qtgui/Plotting/Slicers/Arc.py @@ -1,13 +1,15 @@ -""" - 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 + Draw an arc on a data2D plot between radial points (centered at [0,0]) at + angles theta1 and theta2. + + param r: radius from (0,0) of the arc on a data2D plot + param theta1: angle from x-axis of right end of arc + param theta2: angle from x-axis of left end of arc """ def __init__(self, base, axes, color='black', zorder=5, r=1.0, theta1=np.pi / 8, theta2=np.pi / 4): diff --git a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py b/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py index 93dc59d7f3..e1c936b1ff 100644 --- a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py @@ -10,7 +10,20 @@ class SectorInteractor(BaseInteractor): """ - Select an annulus through a 2D plot + This SectorInteractor is a cross between the SectorInteractor defined in + SectorSicer.py and the AnnulusInteractor. It plots a data1D average of a + wedge area defined in a Data2D object. The data1D averaging itself is + performed in sasdata by manipulations.py. + + This class uses two other classes, ArcInteractor (in Arc.py) and + RadiusInteractor (in RadiusInteractor.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). 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, color='black', zorder=3): """ @@ -234,6 +247,9 @@ def draw(self): class SectorInteractorQ(SectorInteractor): """ + 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, color='black', zorder=3): """ @@ -251,6 +267,9 @@ def _post_data(self): class SectorInteractorPhi(SectorInteractor): """ + 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, color='black', zorder=3): """ diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py index 68a2e24813..dcd58daff3 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) @@ -115,7 +123,7 @@ def _post_data(self): 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 @@ -256,6 +264,8 @@ def setParams(self, params): def draw(self): """ + Draws the Canvas using the canvas.draw from the calling class + that instatiated this object. """ self.base.draw() @@ -263,7 +273,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): """ @@ -371,7 +382,9 @@ def move(self, x, y, ev): 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): """ @@ -479,7 +492,9 @@ def move(self, x, y, ev): 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) @@ -510,7 +525,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) diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSum.py b/src/sas/qtgui/Plotting/Slicers/BoxSum.py index aedfffbd15..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 @@ -570,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): diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index 07f6f71a77..badfc1a35a 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -4,7 +4,15 @@ class RadiusInteractor(BaseInteractor): """ - Select an annulus through a 2D plot + Draw a radial line (centered at [0,0]) at an angle theta from the x-axis + on a data2D plot from r1 to r2 defined by two arcs (arc1 and arc2). Used + for example to define a wedge area on the plot. + + param theta: angle of the radial line from the x-axis + param arc1: inner arc of radius r1 used to define the starting point of + the radial line + param arc2: outer arc of radius r2 used to define the ending point of + the radial line """ def __init__(self, base, axes, color='black', zorder=5, arc1=None, arc2=None, theta=np.pi / 8): diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index 394fed4392..731d51a3d8 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): @@ -277,7 +288,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 @@ -466,7 +480,11 @@ 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 """ def __init__(self, base, axes, color='black', zorder=5, r=1.0, theta=numpy.pi / 4): From 07b54391e75b01343b10cec07b0ba8b022036206 Mon Sep 17 00:00:00 2001 From: butlerpd Date: Mon, 8 May 2023 02:00:43 +0000 Subject: [PATCH 09/58] Change duplicate class name There are three unused slicer modules that are set up for wedge type slicers. These were never implemented. I suspect that in fact it was orinally planed to use this for sector averaging where perhaps the wedge became a sector by making r1=0 and r2 = qmax. However as it stands it left two slicers classes called SectorInteractor. This unused class is renamed to the more appropriate WedgeInteractor in preparation for being used for crystal type data. --- .../Slicers/{AzimutSlicer.py => WedgeSlicer.py} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename src/sas/qtgui/Plotting/Slicers/{AzimutSlicer.py => WedgeSlicer.py} (96%) diff --git a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py similarity index 96% rename from src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py rename to src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index e1c936b1ff..2669125b5b 100644 --- a/src/sas/qtgui/Plotting/Slicers/AzimutSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -8,9 +8,9 @@ from sas.qtgui.Plotting.Slicers.RadiusInteractor import RadiusInteractor from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor -class SectorInteractor(BaseInteractor): +class WedgeInteractor(BaseInteractor): """ - This SectorInteractor is a cross between the SectorInteractor defined in + This WedgeInteractor is a cross between the SectorInteractor defined in SectorSicer.py and the AnnulusInteractor. It plots a data1D average of a wedge area defined in a Data2D object. The data1D averaging itself is performed in sasdata by manipulations.py. @@ -245,7 +245,7 @@ def draw(self): """ self.base.draw() -class SectorInteractorQ(SectorInteractor): +class SectorInteractorQ(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 @@ -254,7 +254,7 @@ class SectorInteractorQ(SectorInteractor): def __init__(self, base, axes, color='black', zorder=3): """ """ - SectorInteractor.__init__(self, base, axes, color=color) + WedgeInteractor.__init__(self, base, axes, color=color) self.base = base self._post_data() @@ -265,7 +265,7 @@ def _post_data(self): self.post_data(SectorQ) -class SectorInteractorPhi(SectorInteractor): +class SectorInteractorPhi(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 @@ -274,7 +274,7 @@ class SectorInteractorPhi(SectorInteractor): def __init__(self, base, axes, color='black', zorder=3): """ """ - SectorInteractor.__init__(self, base, axes, color=color) + WedgeInteractor.__init__(self, base, axes, color=color) self.base = base self._post_data() From 3e3a3f1d5ffb665b221fe6f2856be9bc38356243 Mon Sep 17 00:00:00 2001 From: Paul Butler Date: Fri, 12 May 2023 22:45:55 +0000 Subject: [PATCH 10/58] Fix two typos in docs after review --- src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py | 2 +- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py index 19a41208c3..2c7e07c077 100644 --- a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @@ -12,7 +12,7 @@ class AnnulusInteractor(BaseInteractor, SlicerModel): Data2D object. The data1D averaging itself is performed in sasdata by manipulations.py - This class uses the RingInteractor classe to define two rings of radius + 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. """ diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index 2669125b5b..c8240ad2e7 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -11,7 +11,7 @@ class WedgeInteractor(BaseInteractor): """ This WedgeInteractor is a cross between the SectorInteractor defined in - SectorSicer.py and the AnnulusInteractor. It plots a data1D average of a + SectorSlicer.py and the AnnulusInteractor. It plots a data1D average of a wedge area defined in a Data2D object. The data1D averaging itself is performed in sasdata by manipulations.py. From c845af89958a3b8eea56c1717619e316f8680b75 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Thu, 13 Jul 2023 15:40:41 +0100 Subject: [PATCH 11/58] Fixed issue with left line not moving reliably by ensuring variables 'left' and 'right' are set correctly when calling SideInteractor.update(). --- src/sas/qtgui/Plotting/Slicers/SectorSlicer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index 731d51a3d8..cdad0cef44 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -59,14 +59,15 @@ 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() @@ -101,24 +102,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): """ From aa192da19a3638865de2855d8a0d25f7167b9d53 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Mon, 17 Jul 2023 17:42:24 +0100 Subject: [PATCH 12/58] Set 'left' parameter correctly in an update call I missed last time. Also added back a superfluous self.update() call for consistency with the other slicers. --- src/sas/qtgui/Plotting/Slicers/SectorSlicer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index cdad0cef44..9378de6c4c 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -68,6 +68,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.left_line.update(left=True) self.left_line.qmax = self.qmax # draw the sector + self.update() self._post_data() self.draw() self.setModelFromParams() @@ -273,8 +274,8 @@ 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() From 1f25aaf548408fad79ffaf2261b693582874378b Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Tue, 18 Jul 2023 09:15:23 +0100 Subject: [PATCH 13/58] Renamed Arc.py to ArcInteractor.py for consistency with the other modules. --- src/sas/qtgui/Plotting/Slicers/{Arc.py => ArcInteractor.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/sas/qtgui/Plotting/Slicers/{Arc.py => ArcInteractor.py} (100%) diff --git a/src/sas/qtgui/Plotting/Slicers/Arc.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py similarity index 100% rename from src/sas/qtgui/Plotting/Slicers/Arc.py rename to src/sas/qtgui/Plotting/Slicers/ArcInteractor.py From 9141ca05b8a1bbc19b06514dad3756f5d267b024 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 18 Jul 2023 21:56:16 -0400 Subject: [PATCH 14/58] Rewrite logging controls so qt console doesn't show debug --- src/sas/cli.py | 12 ++- src/sas/qtgui/Utilities/SasviewLogger.py | 23 +++-- src/sas/system/log.ini | 44 +-------- src/sas/system/log.py | 108 ++++++++++------------- 4 files changed, 75 insertions(+), 112 deletions(-) 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/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/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..637e525fe4 100644 --- a/src/sas/system/log.py +++ b/src/sas/system/log.py @@ -12,70 +12,60 @@ Module that manages the global logging ''' +#BASE_LOGGER = 'sasview' +TRACED_PACKAGES = ('sas', 'sasmodels', 'sasdata', 'bumps', 'periodictable') +IGNORED_PACKAGES = { + 'matplotlib': 'ERROR', + 'numba': 'ERROR', + '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: + 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 + # TODO: remove numba logger setting from sas.sascalc.calculator.geni + 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() From f9bc3d9cae3ecad0ff0b8e3e39d132ccfc59984f Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Tue, 18 Jul 2023 22:14:26 -0400 Subject: [PATCH 15/58] Don't hardcode numba logging level --- src/sas/qtgui/MainWindow/GuiManager.py | 3 +++ src/sas/sascalc/calculator/geni.py | 4 ---- src/sas/system/log.py | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) 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/sascalc/calculator/geni.py b/src/sas/sascalc/calculator/geni.py index 3dc5abe08e..833b18d517 100644 --- a/src/sas/sascalc/calculator/geni.py +++ b/src/sas/sascalc/calculator/geni.py @@ -9,10 +9,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/log.py b/src/sas/system/log.py index 637e525fe4..ae31cdf48a 100644 --- a/src/sas/system/log.py +++ b/src/sas/system/log.py @@ -16,7 +16,7 @@ TRACED_PACKAGES = ('sas', 'sasmodels', 'sasdata', 'bumps', 'periodictable') IGNORED_PACKAGES = { 'matplotlib': 'ERROR', - 'numba': 'ERROR', + 'numba': 'WARN', 'h5py': 'ERROR', 'ipykernel': 'CRITICAL', } @@ -26,7 +26,7 @@ def setup_logging(level=logging.INFO): logging.captureWarnings(True) for package in TRACED_PACKAGES: logging.getLogger(package).setLevel(level) - for package, package_level in IGNORED_PACKAGES: + for package, package_level in IGNORED_PACKAGES.items(): logging.getLogger(package).setLevel(package_level) # SasView is often using the root logger to emit error messages. Until @@ -38,7 +38,6 @@ def setup_logging(level=logging.INFO): # the culprits: # grep -R "logg\(ing\|er\)" src | grep -v .pyc | less # TODO: use __name__ as the logger for all sasview log messages - # TODO: remove numba logger setting from sas.sascalc.calculator.geni logging.root.setLevel(level) # Apply the logging config after setting the defaults From 92f27819abe7f0ca78bab0546a9a26899e3d543f Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Wed, 19 Jul 2023 08:56:08 +0100 Subject: [PATCH 16/58] Refactored the code of ArcInteractor.py to suit my planned WedgeSlicer implementation. --- .../qtgui/Plotting/Slicers/ArcInteractor.py | 92 +++++++++---------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index 9ffde5b7e8..84be541d11 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -4,30 +4,39 @@ class ArcInteractor(BaseInteractor): """ - Draw an arc on a data2D plot between radial points (centered at [0,0]) at - angles theta1 and theta2. + Draw an arc on a data2D plot with a variable radius (centered at [0,0]). - param r: radius from (0,0) of the arc on a data2D plot - param theta1: angle from x-axis of right end of arc - param theta2: angle from x-axis of left end of arc + param r: radius from (0,0) of the arc on a data2D plot + param theta2: 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, - theta1=np.pi / 8, theta2=np.pi / 4): + theta2=np.pi / 3, phi=np.py / 4): BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes + self.color = color 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.phi = phi self.radius = r + # Define the arc's marker + marker_x = self.radius * np.cos(theta2 * 0.8) + marker_y = self.radius * np.sin(theta2 * 0.8) + self.marker = self.axes.plot([marker_x], [marker_y], linestyle='', + marker='s', markersize=10, + color=self.color, alpha=0.6, pickradius=5, + label='pick', zorder=zorder, + visable=True)[0] + # Define the arc [self.arc] = self.axes.plot([], [], linestyle='-', marker='', color=self.color) self.npts = 20 self.has_move = False - self.connect_markers([self.arc]) + self.connect_markers([self.marker, self.arc]) self.update() def set_layer(self, n): @@ -44,8 +53,7 @@ def clear(self): """ self.clear_markers() try: - for item in self.markers: - item.remove() + self.marker.remove() self.arc.remove() except: # Old version of matplotlib @@ -60,39 +68,37 @@ def get_radius(self): np.power(self._mouse_y, 2)) return radius - def update(self, theta1=None, theta2=None, nbins=None, r=None): + def update(self, theta2=None, phi=None, r=None, nbins=120): """ 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 + :param theta2: 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 + :param nbins: number of points drawn for an arc of size pi radians """ - # 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 phi is not None: + self.phi = phi + self.npts = int((2 * self.phi) / (np.pi / nbins)) if r is None: - self.radius = np.sqrt(np.power(self._mouse_x, 2) + \ - np.power(self._mouse_y, 2)) + self.radius = self.get_radius() else: self.radius = r + # Calculate the points on the arc, and draw them 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) - + angleval = 2 * self.phi / (self.npts - 1) * i + (self.theta2 - self.phi) + xval = 1.0 * self.radius * np.cos(angleval) + yval = 1.0 * self.radius * np.sin(angleval) x.append(xval) y.append(yval) + + marker_x = self.radius * np.cos(self.theta2 - 0.2 * self.phi) + marker_y = self.radius * np.sin(self.theta2 - 0.2 * self.phi) + self.marker.set(xdata=[marker_x], ydata=[marker_y]) self.arc.set_data(x, y) def save(self, ev): @@ -102,7 +108,6 @@ def save(self, ev): """ self._save_x = self._mouse_x self._save_y = self._mouse_y - self.base.freeze_axes() def moveend(self, ev): """ @@ -121,35 +126,28 @@ def restore(self, ev): 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._mouse_x = x self._mouse_y = y + self.radius = self.get_radius() self.has_move = True self.base.update() self.base.draw() - def set_cursor(self, radius, phi_min, phi_max, nbins): - """ - """ - self.theta1 = phi_min - self.theta2 = phi_max - self.update(nbins=nbins, r=radius) + # This function is effectively just a move() clone? + def set_cursor(self, x, y): + self.move(x, y, None) + self.update() def get_params(self): - """ - """ params = {} params["radius"] = self.radius - params["theta1"] = self.theta1 params["theta2"] = self.theta2 + params["phi"] = self.phi 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) - + r = params["radius"] + theta = params["theta2"] + self.set_cursor(r*np.cos(theta), r*np.sin(theta)) From 5af2560ffe1afe5222fcc5077fc0145915156074 Mon Sep 17 00:00:00 2001 From: Dorian Lozano Date: Wed, 19 Jul 2023 10:15:39 +0200 Subject: [PATCH 17/58] An additional option to disable the residuals. Note that : 1) the residuals are still computed even if the option is checked 2) the issues 2526 2527 still remains --- src/sas/qtgui/MainWindow/DataExplorer.py | 3 +++ .../Utilities/Preferences/PlottingPreferencesWidget.py | 10 +++++++++- src/sas/system/config/config.py | 4 ++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index d0ebdddf54..bb412f78a4 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1091,6 +1091,9 @@ def displayData(self, data_list, id=None): 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_RESIDUALS: + # Nothing to do if residuals are not plotted + continue elif role in stand_alone_types: # Stand-alone plots should always be separate self.plotData([(plot_item, plot_to_show)]) diff --git a/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py b/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py index 2f9f8dea07..5ed85100da 100644 --- a/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py +++ b/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py @@ -8,7 +8,8 @@ def __init__(self): super(PlottingPreferencesWidget, self).__init__("Plotting Options") self.config_params = ['FITTING_PLOT_FULL_WIDTH_LEGENDS', 'FITTING_PLOT_LEGEND_TRUNCATE', - 'FITTING_PLOT_LEGEND_MAX_LINE_LENGTH'] + 'FITTING_PLOT_LEGEND_MAX_LINE_LENGTH', + 'DISABLE_RESIDUALS'] def _addAllWidgets(self): self.legendFullWidth = self.addCheckBox( @@ -26,14 +27,21 @@ def _addAllWidgets(self): default_number=config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH) self.legendLineLength.textChanged.connect( lambda: self._validate_input_and_stage(self.legendLineLength, 'FITTING_PLOT_LEGEND_MAX_LINE_LENGTH')) + self.disableResiduals = self.addCheckBox( + title="Disable Residuals Display", + checked=config.DISABLE_RESIDUALS) + self.disableResiduals.clicked.connect( + lambda: self._stageChange('DISABLE_RESIDUALS', self.disableResiduals.isChecked())) def _toggleBlockAllSignaling(self, toggle): self.legendFullWidth.blockSignals(toggle) self.legendTruncate.blockSignals(toggle) self.legendLineLength.blockSignals(toggle) + self.disableResiduals.blockSignals(toggle) def _restoreFromConfig(self): self.legendFullWidth.setChecked(bool(config.FITTING_PLOT_FULL_WIDTH_LEGENDS)) self.legendTruncate.setChecked(bool(config.FITTING_PLOT_LEGEND_TRUNCATE)) self.legendLineLength.setText(str(config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH)) self.legendLineLength.setStyleSheet("background-color: white") + self.disableResiduals.setChecked(config.DISABLE_RESIDUALS) diff --git a/src/sas/system/config/config.py b/src/sas/system/config/config.py index 816110eb90..05929c7955 100644 --- a/src/sas/system/config/config.py +++ b/src/sas/system/config/config.py @@ -197,6 +197,10 @@ 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 residuals display + self.DISABLE_RESIDUALS = False + # Default fitting optimizer self.FITTING_DEFAULT_OPTIMIZER = 'lm' From 77034dea9956fedf0dce8079703cba7b1099ae7d Mon Sep 17 00:00:00 2001 From: Dorian Lozano Date: Wed, 19 Jul 2023 10:31:19 +0200 Subject: [PATCH 18/58] An additional option to disable the polydispersity distribution plot. --- src/sas/qtgui/MainWindow/DataExplorer.py | 5 ++++- src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py | 2 +- src/sas/qtgui/Plotting/PlotterData.py | 2 ++ .../Utilities/Preferences/PlottingPreferencesWidget.py | 10 +++++++++- src/sas/system/config/config.py | 4 ++++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index bb412f78a4..0623895a30 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1086,7 +1086,7 @@ 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 @@ -1094,6 +1094,9 @@ def displayData(self, data_list, id=None): elif role == DataRole.ROLE_RESIDUAL and config.DISABLE_RESIDUALS: # 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)]) 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/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/Utilities/Preferences/PlottingPreferencesWidget.py b/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py index 5ed85100da..f4c7869cd9 100644 --- a/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py +++ b/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py @@ -9,7 +9,8 @@ def __init__(self): self.config_params = ['FITTING_PLOT_FULL_WIDTH_LEGENDS', 'FITTING_PLOT_LEGEND_TRUNCATE', 'FITTING_PLOT_LEGEND_MAX_LINE_LENGTH', - 'DISABLE_RESIDUALS'] + 'DISABLE_RESIDUALS', + 'DISABLE_POLYDISPERSITY_PLOT'] def _addAllWidgets(self): self.legendFullWidth = self.addCheckBox( @@ -32,12 +33,18 @@ def _addAllWidgets(self): checked=config.DISABLE_RESIDUALS) self.disableResiduals.clicked.connect( lambda: self._stageChange('DISABLE_RESIDUALS', self.disableResiduals.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.legendFullWidth.blockSignals(toggle) self.legendTruncate.blockSignals(toggle) self.legendLineLength.blockSignals(toggle) self.disableResiduals.blockSignals(toggle) + self.disablePolydispersityPlot.blockSignals(toggle) def _restoreFromConfig(self): self.legendFullWidth.setChecked(bool(config.FITTING_PLOT_FULL_WIDTH_LEGENDS)) @@ -45,3 +52,4 @@ def _restoreFromConfig(self): self.legendLineLength.setText(str(config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH)) self.legendLineLength.setStyleSheet("background-color: white") self.disableResiduals.setChecked(config.DISABLE_RESIDUALS) + self.disablePolydispersityPlot.setChecked(config.DISABLE_POLYDISPERSITY_PLOT) diff --git a/src/sas/system/config/config.py b/src/sas/system/config/config.py index 05929c7955..34efc0bd22 100644 --- a/src/sas/system/config/config.py +++ b/src/sas/system/config/config.py @@ -201,6 +201,10 @@ def __init__(self): # If true, disables residuals display self.DISABLE_RESIDUALS = False + # Polydispersity plot management + # If true, disables polydispersity plot display + self.DISABLE_POLYDISPERSITY_PLOT = False + # Default fitting optimizer self.FITTING_DEFAULT_OPTIMIZER = 'lm' From a627248444d985d91c81481f4e8ac6d025cc0b6e Mon Sep 17 00:00:00 2001 From: Dorian Lozano Date: Wed, 19 Jul 2023 12:54:13 +0200 Subject: [PATCH 19/58] Buttons moved from Plotting Preferences to Display Preferences --- .../Preferences/DisplayPreferencesWidget.py | 19 ++++++++++++++++++- .../Preferences/PlottingPreferencesWidget.py | 18 +----------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py b/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py index abdd1d6143..c81b634c06 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_RESIDUALS', + '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.disableResiduals = self.addCheckBox( + title="Disable Residuals Display", + checked=config.DISABLE_RESIDUALS) + self.disableResiduals.clicked.connect( + lambda: self._stageChange('DISABLE_RESIDUALS', self.disableResiduals.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.disableResiduals.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.disableResiduals.setChecked(config.DISABLE_RESIDUALS) + self.disablePolydispersityPlot.setChecked(config.DISABLE_POLYDISPERSITY_PLOT) diff --git a/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py b/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py index f4c7869cd9..2f9f8dea07 100644 --- a/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py +++ b/src/sas/qtgui/Utilities/Preferences/PlottingPreferencesWidget.py @@ -8,9 +8,7 @@ def __init__(self): super(PlottingPreferencesWidget, self).__init__("Plotting Options") self.config_params = ['FITTING_PLOT_FULL_WIDTH_LEGENDS', 'FITTING_PLOT_LEGEND_TRUNCATE', - 'FITTING_PLOT_LEGEND_MAX_LINE_LENGTH', - 'DISABLE_RESIDUALS', - 'DISABLE_POLYDISPERSITY_PLOT'] + 'FITTING_PLOT_LEGEND_MAX_LINE_LENGTH'] def _addAllWidgets(self): self.legendFullWidth = self.addCheckBox( @@ -28,28 +26,14 @@ def _addAllWidgets(self): default_number=config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH) self.legendLineLength.textChanged.connect( lambda: self._validate_input_and_stage(self.legendLineLength, 'FITTING_PLOT_LEGEND_MAX_LINE_LENGTH')) - self.disableResiduals = self.addCheckBox( - title="Disable Residuals Display", - checked=config.DISABLE_RESIDUALS) - self.disableResiduals.clicked.connect( - lambda: self._stageChange('DISABLE_RESIDUALS', self.disableResiduals.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.legendFullWidth.blockSignals(toggle) self.legendTruncate.blockSignals(toggle) self.legendLineLength.blockSignals(toggle) - self.disableResiduals.blockSignals(toggle) - self.disablePolydispersityPlot.blockSignals(toggle) def _restoreFromConfig(self): self.legendFullWidth.setChecked(bool(config.FITTING_PLOT_FULL_WIDTH_LEGENDS)) self.legendTruncate.setChecked(bool(config.FITTING_PLOT_LEGEND_TRUNCATE)) self.legendLineLength.setText(str(config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH)) self.legendLineLength.setStyleSheet("background-color: white") - self.disableResiduals.setChecked(config.DISABLE_RESIDUALS) - self.disablePolydispersityPlot.setChecked(config.DISABLE_POLYDISPERSITY_PLOT) From 286d44ed0596a906cbd710cc08762c82fe547be2 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 19 Jul 2023 16:42:29 +0200 Subject: [PATCH 20/58] call the widget directly. --- src/sas/qtgui/MainWindow/DataExplorer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index d0ebdddf54..72d5146735 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 @@ -1132,7 +1132,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 +1160,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 From 254ced07d8225315fdccb576c013e9350fa783f6 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Fri, 21 Jul 2023 11:37:41 +0200 Subject: [PATCH 21/58] moving highlighting to QRegularExpression syntax --- src/sas/qtgui/Utilities/PythonSyntax.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/sas/qtgui/Utilities/PythonSyntax.py b/src/sas/qtgui/Utilities/PythonSyntax.py index 2a5d88e39a..2d5ce00bda 100644 --- a/src/sas/qtgui/Utilities/PythonSyntax.py +++ b/src/sas/qtgui/Utilities/PythonSyntax.py @@ -131,14 +131,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) @@ -161,17 +162,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) + match = delimiter.match(text) + 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 +184,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: From 4e3d3afc50d4fd24da2e611f36b404ccf753f8d7 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 21 Jul 2023 13:49:35 +0100 Subject: [PATCH 22/58] Minor changes to ArcInteractor.py --- .../qtgui/Plotting/Slicers/ArcInteractor.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index 84be541d11..411ce716a1 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -68,7 +68,7 @@ def get_radius(self): np.power(self._mouse_y, 2)) return radius - def update(self, theta2=None, phi=None, r=None, nbins=120): + def update(self, theta2=None, phi=None, r=None, nbins=100): """ Update the plotted arc :param theta2: angle from x-axis of the central point on the arc @@ -135,19 +135,18 @@ def move(self, x, y, ev): self.base.update() self.base.draw() - # This function is effectively just a move() clone? def set_cursor(self, x, y): self.move(x, y, None) self.update() - def get_params(self): - params = {} - params["radius"] = self.radius - params["theta2"] = self.theta2 - params["phi"] = self.phi - return params - - def set_params(self, params): - r = params["radius"] - theta = params["theta2"] - self.set_cursor(r*np.cos(theta), r*np.sin(theta)) + # def get_params(self): + # params = {} + # params["radius"] = self.radius + # params["theta2"] = self.theta2 + # params["phi"] = self.phi + # return params + + # def set_params(self, params): + # r = params["radius"] + # theta = params["theta2"] # or = self.theta2 (?) + # self.set_cursor(r*np.cos(theta), r*np.sin(theta)) From b041edd5cabc0a1eb288bcb3444f12efa85bb285 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 21 Jul 2023 13:51:44 +0100 Subject: [PATCH 23/58] Refactored the code of RadiusInteractor.py and WedgeSlicer.py for the new wedge slicer. --- .../Plotting/Slicers/RadiusInteractor.py | 158 +++++---- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 317 ++++++++++-------- 2 files changed, 273 insertions(+), 202 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index badfc1a35a..d7e8f198d5 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -1,4 +1,5 @@ import numpy as np + from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor @@ -15,33 +16,54 @@ class RadiusInteractor(BaseInteractor): the radial line """ def __init__(self, base, axes, color='black', zorder=5, arc1=None, - arc2=None, theta=np.pi / 8): + arc2=None, theta2=np.pi / 8, phi=np.py / 4): """ """ - _BaseInteractor.__init__(self, base, axes, color=color) + BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes + self.color = color self.r1 = arc1.get_radius() self.r2 = arc2.get_radius() - self.theta = theta - self.save_theta = theta - self.move_stop = False - self.theta_left = None - self.theta_right = None + self.theta2 = theta2 + # self.save_theta2 = theta2 + self.phi = phi + self.save_phi = phi 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 + # Variables for the left and right radial lines + l_x1 = self.r1 * np.cos(self.theta2 + self.phi) + l_y1 = self.r1 * np.sin(self.theta2 + self.phi) + l_x2 = self.r2 * np.cos(self.theta2 + self.phi) + l_y2 = self.r2 * np.sin(self.theta2 + self.phi) + r_x1 = self.r1 * np.cos(self.theta2 - self.phi) + r_y1 = self.r1 * np.sin(self.theta2 - self.phi) + r_x2 = self.r2 * np.cos(self.theta2 - self.phi) + r_y2 = self.r2 * np.sin(self.theta2 - 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, visable=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, visable=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 differentiate the left side's motion from the right's + # self.left_moving = False + # 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): @@ -55,40 +77,51 @@ def clear(self): """ self.clear_markers() try: - self.line.remove() + self.l_marker.remove() + self.l_line.remove() + self.r_marker.remove() + self.r_line.remove() except: # Old version of matplotlib for item in range(len(self.axes.lines)): del self.axes.lines[0] - def get_angle(self): - """ - """ - return self.theta + # def get_angle(self): + # """ + # """ + # return self.theta - def update(self, r1=None, r2=None, theta=None): + def update(self, theta2=None, phi=None): """ Draw the new roughness on the graph. """ - if r1 is not None: - self.r1 = r1 - if r2 is not 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]) + # TODO - try out an 'if self.arc1.has_move:' etc + self.r1 = self.arc1.get_radius() + self.r2 = self.arc2.get_radius() + if theta2 is not None: + self.theta2 = theta2 + if phi is not None: + self.phi = phi + l_x1 = self.r1 * np.cos(self.theta2 + self.phi) + l_y1 = self.r1 * np.sin(self.theta2 + self.phi) + l_x2 = self.r2 * np.cos(self.theta2 + self.phi) + l_y2 = self.r2 * np.sin(self.theta2 + self.phi) + r_x1 = self.r1 * np.cos(self.theta2 - self.phi) + r_y1 = self.r1 * np.sin(self.theta2 - self.phi) + r_x2 = self.r2 * np.cos(self.theta2 - self.phi) + r_y2 = self.r2 * np.sin(self.theta2 - self.phi) + 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 + # May also need a save_theta2 variable def moveend(self, ev): """ @@ -100,39 +133,48 @@ def restore(self, ev): """ Restore the roughness for this layer. """ - self.theta = self.save_theta + self.phi = self.save_phi + # May also need a save_theta2 variable def move(self, x, y, ev): """ Process move to a new position, making sure that the move is allowed. """ - self.theta = np.arctan2(y, x) + theta = np.arctan2(y, x) + self.phi = np.fabs(theta - self.theta2) self.has_move = True self.base.update() self.base.draw() - def set_cursor(self, r_min, r_max, theta): + def set_cursor(self, x, y): """ """ - self.theta = theta - self.r1 = r_min - self.r2 = r_max + 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 get_params(self): + # """ + # Store a copy of values of parameters of the slicer into a dictionary. + # :return params: the dictionary created + # """ + # params = {} + # params["radius1"] = self.r1 + # params["radius2"] = self.r2 + # params["theta"] = self.theta2 + # params["phi"] = self.phi + # return params - def set_params(self, params): - """ - """ - x1 = params["radius1"] - x2 = params["radius2"] - theta = params["theta"] - self.set_cursor(x1, x2, theta) + # def set_params(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. + # """ + # r1 = params["radius1"] + # r2 = params["radius2"] + # theta = params["theta"] + # phi = params["phi"] + # self.set_cursor(x1, x2, theta) diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index c8240ad2e7..397f4a8f9d 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -1,86 +1,96 @@ -# 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 +from sas.qtgui.Plotting.SlicerModel import SlicerModel +from sas.qtgui.Plotting.PlotterData import Data1D +import sas.qtgui.Utilities.GuiUtils as GuiUtils -class WedgeInteractor(BaseInteractor): +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): + # TODO - update this docstring, mentioning LineInteractor """ - This WedgeInteractor is a cross between the SectorInteractor defined in - SectorSlicer.py and the AnnulusInteractor. It plots a data1D average of a - wedge area defined in a Data2D object. The data1D averaging itself is - performed in sasdata by manipulations.py. + 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 two other classes, ArcInteractor (in Arc.py) and - RadiusInteractor (in RadiusInteractor.py), to define a wedge area contained + 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). 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). + (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, color='black', zorder=3): - """ - """ + 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.qmax = self.data.xmax + self._item = item + self.qmax = max(self.data.xmax, self.data.xmin, + self.data.ymax, self.data.ymin) 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) + # Angle of the central line + self.theta2 = np.pi / 3 + # Angle between the central line and the radial lines either side of it + self.phi = np.pi / 12 + # reference of the current data averager + self.averager = None + + self.inner_arc = ArcInteractor(self, self.axes, color='black', + zorder=zorder, r=self.qmax / 2.0, + theta2=self.theta2, phi=self.phi) + self.inner_arc.qmax = self.qmax + self.outer_arc = ArcInteractor(self, self.axes, color='black', + zorder=zorder + 1, r=self.qmax / 1.8, + theta2=self.theta2, phi=self.phi) + self.outer_arc.qmax = self.qmax * 1.2 + self.radial_lines = RadiusInteractor(self, self.axes, color='black', + zorder=zorder + 1, + arc1=self.inner_arc, + arc2=self.outer_arc, + theta2=self.theta2, phi=self.phi) + self.central_line = LineInteractor(self, self.axes, color='blue', + zorder=zorder, r=self.qmax, + theta=self.theta2) 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_circle.clear() - self.inner_circle.clear() - self.right_edge.clear() - self.left_edge.clear() + self.outer_arc.clear() + self.inner_arc.clear() + self.radial_lines.clear() + self.central_line.clear() + self.base.connect.clearall() def update(self): """ @@ -88,69 +98,72 @@ def update(self): 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()) + if self.inner_arc.has_move: + self.inner_arc.update() + self.radial_lines.update() + if self.outer_arc.has_move: + self.outer_arc.update() + self.radial_lines.update() + if self.radial_lines.has_move: + self.radial_lines.update() + 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.inner_arc.update(theta2=self.theta2) + self.outer_arc.update(theta2=self.theta2) + self.radial_lines.update() 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) + self.inner_arc.save(ev) + self.outer_arc.save(ev) + self.radial_lines.save(ev) + self.central_line.save(ev) + + ''' + From here onwards we've got the old theta1 & theta2 logic + ''' 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() + 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 + """ + # Data to average + data = self.data + if data is None: + return + + if self.inner_arc.get_radius() < self.outer_arc.get_radius(): + rmin = self.inner_arc.get_radius() + rmax = self.outer_arc.get_radius() else: - phimin = self.left_edge.get_angle() - phimax = self.right_edge.get_angle() + rmin = self.outer_arc.get_radius() + rmax = self.inner_arc.get_radius() + phimin = self.central_line.theta2 - self.radial_lines.phi + phimax = self.central_line.theta2 + 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 - sect = new_sector(r_min=rmin, r_max=rmax, - phi_min=phimin, phi_max=phimax) + sect = self.averager(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: @@ -159,38 +172,65 @@ def post_data(self, new_sector): 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__) + \ + 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 - # 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') + if self.averager.__name__ == 'SectorPhi': + new_plot.xaxis("\\rm{\phi}", "degrees") + else: + new_plot.xaxis("\\rm{Q}", 'A^{-1}') new_plot.yaxis("\\rm{Intensity} ", "cm^{-1}") - new_plot.group_id = str(new_sector.__name__) + self.data.name + + if hasattr(data, "scale") and data.scale == 'linear' and \ + self.data.name.count("Residuals") > 0: + new_plot.ytransform = 'y' + new_plot.yaxis("\\rm{Residuals} ", "/") + + 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): """ - Test the proposed new value "value" for row "row" of parameters + Validate input from user + Values get checked at apply time. """ - # Here, always return true - return True + isValid = True - def moveend(self, ev): - #TODO: why is this empty? - pass + if param_name == 'nbins': + # Can't be 0 + if param_value < 1: + print("Number of bins cannot be <= 0. Please adjust.") + isValid = False + return isValid + + def moveend(self, ev): # Check if that's all I need? + self._post_data() 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) + 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): """ @@ -199,47 +239,36 @@ def move(self, x, y, ev): pass def set_cursor(self, x, y): - """ - """ pass def get_params(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_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["r_min"] = self.inner_arc.get_radius() + params["r_max"] = self.outer_arc.get_radius() + params["phi [deg]"] = self.central_line.theta2 * 180 / np.pi + params["delta_phi [deg]"] = self.radial_lines.phi * 180 / np.pi 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"] + r1 = params["r_min"] + r2 = params["r_max"] + theta = params["phi [deg]"] * np.pi / 180 + phi = params["delta_phi [deg]"] * np.pi / 180 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.inner_arc.update(theta, phi, r1, self.nbins) + self.outer_arc.update(theta, phi, r2, self.nbins) + self.radial_lines.update(theta, phi) + self.central_line.update(theta) self._post_data() - def freeze_axes(self): - """ - """ - self.base.freeze_axes() - - def thaw_axes(self): - """ - """ - self.base.thaw_axes() - def draw(self): """ """ From 1678d689c784a6ae517f28ef04821b8682d41bfa Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 21 Jul 2023 14:04:02 +0100 Subject: [PATCH 24/58] Changed class names for consitency. --- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index 397f4a8f9d..d425d29bca 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -10,7 +10,6 @@ from sas.qtgui.Plotting.Slicers.SectorSlicer import LineInteractor class WedgeInteractor(BaseInteractor, SlicerModel): - # TODO - update this docstring, mentioning LineInteractor """ This WedgeInteractor is a cross between the SectorInteractor and the AnnulusInteractor. It plots a data1D average of a wedge area defined in a @@ -274,7 +273,7 @@ def draw(self): """ self.base.draw() -class SectorInteractorQ(WedgeInteractor): +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 @@ -294,7 +293,7 @@ def _post_data(self): self.post_data(SectorQ) -class SectorInteractorPhi(WedgeInteractor): +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 From 7fd77edfd0e3d531ef7e026261ff1d35af275dbc Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 21 Jul 2023 16:57:51 +0100 Subject: [PATCH 25/58] Corrected a typo. --- src/sas/qtgui/Plotting/Slicers/ArcInteractor.py | 2 +- src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index 411ce716a1..db2ded4d1f 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -11,7 +11,7 @@ class ArcInteractor(BaseInteractor): 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, - theta2=np.pi / 3, phi=np.py / 4): + theta2=np.pi / 3, phi=np.pi / 4): BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index d7e8f198d5..8d2e84b56a 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -16,7 +16,7 @@ class RadiusInteractor(BaseInteractor): the radial line """ def __init__(self, base, axes, color='black', zorder=5, arc1=None, - arc2=None, theta2=np.pi / 8, phi=np.py / 4): + arc2=None, theta2=np.pi / 8, phi=np.pi / 4): """ """ BaseInteractor.__init__(self, base, axes, color=color) From c942693a79fb294c9e87691a47f20de2b0efc527 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 21 Jul 2023 16:59:31 +0100 Subject: [PATCH 26/58] Added the ability to select the wedge slicers from the UI. The slicer will not spawn yet however. --- src/sas/qtgui/Plotting/Plotter2D.py | 21 ++++++++++++++++++- src/sas/qtgui/Plotting/SlicerParameters.py | 6 +++++- .../qtgui/Plotting/UI/SlicerParametersUI.ui | 12 ++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) 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/SlicerParameters.py b/src/sas/qtgui/Plotting/SlicerParameters.py index 274f558f25..c5463fdd96 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) 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 + + From b73da9f8387397c5d66beb9bd75c100c05e0295c Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 24 Jul 2023 07:56:36 +0200 Subject: [PATCH 27/58] fixes for multiline comments with raw strings --- src/sas/qtgui/Utilities/PythonSyntax.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Utilities/PythonSyntax.py b/src/sas/qtgui/Utilities/PythonSyntax.py index 2d5ce00bda..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 = [] @@ -148,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 @@ -170,8 +175,8 @@ def match_multiline(self, text, delimiter, in_state, style): # As long as there's a delimiter match on this line... while start >= 0: - # Look for the ending delimiter - match = delimiter.match(text) + # 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: From 8859dd040bbdc223f3561b98ff69bcb80cedcdb8 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Mon, 24 Jul 2023 16:06:17 +0100 Subject: [PATCH 28/58] Fixed issues causing slicer not to spawn, and edited default parameters --- .../qtgui/Plotting/Slicers/ArcInteractor.py | 8 ++-- .../Plotting/Slicers/RadiusInteractor.py | 6 +-- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 44 +++++++++---------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index db2ded4d1f..7bf1eb8b8a 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -11,7 +11,7 @@ class ArcInteractor(BaseInteractor): 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, - theta2=np.pi / 3, phi=np.pi / 4): + theta2=np.pi / 3, phi=np.pi / 8): BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes @@ -31,7 +31,7 @@ def __init__(self, base, axes, color='black', zorder=5, r=1.0, marker='s', markersize=10, color=self.color, alpha=0.6, pickradius=5, label='pick', zorder=zorder, - visable=True)[0] + visible=True)[0] # Define the arc [self.arc] = self.axes.plot([], [], linestyle='-', marker='', color=self.color) self.npts = 20 @@ -96,8 +96,8 @@ def update(self, theta2=None, phi=None, r=None, nbins=100): x.append(xval) y.append(yval) - marker_x = self.radius * np.cos(self.theta2 - 0.2 * self.phi) - marker_y = self.radius * np.sin(self.theta2 - 0.2 * self.phi) + marker_x = self.radius * np.cos(self.theta2 - 0.5 * self.phi) + marker_y = self.radius * np.sin(self.theta2 - 0.5 * self.phi) self.marker.set(xdata=[marker_x], ydata=[marker_y]) self.arc.set_data(x, y) diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index 8d2e84b56a..30c14d7f5b 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -16,7 +16,7 @@ class RadiusInteractor(BaseInteractor): the radial line """ def __init__(self, base, axes, color='black', zorder=5, arc1=None, - arc2=None, theta2=np.pi / 8, phi=np.pi / 4): + arc2=None, theta2=np.pi / 3, phi=np.pi / 8): """ """ BaseInteractor.__init__(self, base, axes, color=color) @@ -45,12 +45,12 @@ def __init__(self, base, axes, color='black', zorder=5, arc1=None, linestyle='', marker='s', markersize=10, color=self.color, alpha=0.6, pickradius=5, label='pick', - zorder=zorder, visable=True)[0] + 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, visable=True)[0] + 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='', diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index d425d29bca..5e8c21fe15 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -37,8 +37,9 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.markers = [] self.axes = axes self._item = item - self.qmax = max(self.data.xmax, self.data.xmin, - self.data.ymax, self.data.ymin) + xmax2 = np.power(max(self.data.xmax, np.fabs(self.data.xmin)), 2) + ymax2 = np.power(max(self.data.ymax, np.fabs(self.data.ymin)), 2) + self.qmax = np.sqrt(xmax2 + ymax2) self.connect = self.base.connect # # Number of points on the plot @@ -46,7 +47,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): # Angle of the central line self.theta2 = np.pi / 3 # Angle between the central line and the radial lines either side of it - self.phi = np.pi / 12 + self.phi = np.pi / 8 # reference of the current data averager self.averager = None @@ -55,17 +56,19 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): theta2=self.theta2, phi=self.phi) self.inner_arc.qmax = self.qmax self.outer_arc = ArcInteractor(self, self.axes, color='black', - zorder=zorder + 1, r=self.qmax / 1.8, + zorder=zorder + 1, r=self.qmax / 1.6, theta2=self.theta2, phi=self.phi) - self.outer_arc.qmax = self.qmax * 1.2 + self.outer_arc.qmax = self.qmax * 1.2 # TODO - check with someone about this self.radial_lines = RadiusInteractor(self, self.axes, color='black', zorder=zorder + 1, arc1=self.inner_arc, arc2=self.outer_arc, theta2=self.theta2, phi=self.phi) - self.central_line = LineInteractor(self, self.axes, color='blue', + self.radial_lines.qmax = self.qmax + self.central_line = LineInteractor(self, self.axes, color='black', zorder=zorder, r=self.qmax, theta=self.theta2) + self.central_line.qmax = self.qmax self.update() self.draw() self._post_data() @@ -96,6 +99,9 @@ def update(self): Respond to changes in the model by recalculating the profiles and resetting the widgets. """ + # Update parameters + self.theta2 = self.central_line.theta + self.phi = self.radial_lines.phi # Update locations if self.inner_arc.has_move: self.inner_arc.update() @@ -111,7 +117,7 @@ def update(self): self.central_line.update() self.inner_arc.update(theta2=self.theta2) self.outer_arc.update(theta2=self.theta2) - self.radial_lines.update() + self.radial_lines.update(theta2=self.theta2) def save(self, ev): """ @@ -148,8 +154,8 @@ def post_data(self, new_sector=None, nbins=None): else: rmin = self.outer_arc.get_radius() rmax = self.inner_arc.get_radius() - phimin = self.central_line.theta2 - self.radial_lines.phi - phimax = self.central_line.theta2 + self.radial_lines.phi + 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 @@ -240,7 +246,7 @@ def move(self, x, y, ev): def set_cursor(self, x, y): pass - def get_params(self): + def getParams(self): """ Store a copy of values of parameters of the slicer into a dictionary. :return params: the dictionary created @@ -248,12 +254,12 @@ def get_params(self): params = {} params["r_min"] = self.inner_arc.get_radius() params["r_max"] = self.outer_arc.get_radius() - params["phi [deg]"] = self.central_line.theta2 * 180 / np.pi + 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 set_params(self, params): + def setParams(self, params): """ """ r1 = params["r_min"] @@ -279,10 +285,8 @@ class WedgeInteractorQ(WedgeInteractor): averaged together to provide a 1D array in Q (to be plotted as a function of Q) """ - def __init__(self, base, axes, color='black', zorder=3): - """ - """ - WedgeInteractor.__init__(self, base, axes, color=color) + 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() @@ -299,16 +303,12 @@ class WedgeInteractorPhi(WedgeInteractor): averaged together to provide a 1D array in phi (to be plotted as a function of phi) """ - def __init__(self, base, axes, color='black', zorder=3): - """ - """ - WedgeInteractor.__init__(self, base, axes, color=color) + 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 self.post_data(SectorPhi) From 1febb8251248a79c8d3a05485694f9b88b80be69 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 26 Jul 2023 09:59:29 +0200 Subject: [PATCH 29/58] initial version of a unit converter based on sasmodels RST_PROLOG --- src/sas/qtgui/Utilities/GuiUtils.py | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/sas/qtgui/Utilities/GuiUtils.py b/src/sas/qtgui/Utilities/GuiUtils.py index 2e439411d9..a6c3407abf 100644 --- a/src/sas/qtgui/Utilities/GuiUtils.py +++ b/src/sas/qtgui/Utilities/GuiUtils.py @@ -961,7 +961,48 @@ def replaceHTMLwithASCII(html): return html +def rst_to_latex(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_replace: + # replace the 'replace' section + + unit, replacement = match_replace.groups() + + # Convert the unit into a valid Python string condition + unit = unit.replace("Ang", "Å").replace("\\", "").replace(" ", "") + + # Convert the replacement into the desired HTML format + replacement = replacement.replace("|Ang|", "Å").replace("\\ :sup:`", "").replace("`", "").replace("\\", "") + + 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)) + + return unit, replacement + +# RST_PROLOG conversion table +from sasmodels.generate import RST_PROLOG +RST_PROLOG_DICT = {} +input_rst_strings = RST_PROLOG.splitlines() +for line in input_rst_strings: + if line.startswith(".. |"): + key, value = rst_to_latex(line) + RST_PROLOG_DICT[key] = value + def convertUnitToUTF8(unit): + if unit in RST_PROLOG_DICT: + return RST_PROLOG_DICT[unit] + else: + return "" + +def convertUnitToUTF8_old(unit): """ Convert ASCII unit display into UTF-8 symbol """ From 28675a553d206684fade5a319e2b248c67bcd986 Mon Sep 17 00:00:00 2001 From: Dorian Lozano Date: Wed, 26 Jul 2023 10:22:03 +0200 Subject: [PATCH 30/58] Renamed 'DISABLE_RESIDUALS_PLOT' the 'DISABLE_RESIDUALS' variable in config.py and 'disableResidualsPlot' the 'disableResiduals' widget in DisplayPreferencesWidget.py --- src/sas/qtgui/MainWindow/DataExplorer.py | 2 +- .../Preferences/DisplayPreferencesWidget.py | 14 +++++++------- src/sas/system/config/config.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index 0623895a30..35295c7270 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1091,7 +1091,7 @@ def displayData(self, data_list, id=None): 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_RESIDUALS: + elif role == DataRole.ROLE_RESIDUAL and config.DISABLE_RESIDUALS_PLOT: # Nothing to do if residuals are not plotted continue elif role == DataRole.ROLE_POLYDISPERSITY and config.DISABLE_POLYDISPERSITY_PLOT: diff --git a/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py b/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py index c81b634c06..bfefabe479 100644 --- a/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py +++ b/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py @@ -8,7 +8,7 @@ def __init__(self): super(DisplayPreferencesWidget, self).__init__("Display Settings") self.config_params = ['QT_SCALE_FACTOR', 'QT_AUTO_SCREEN_SCALE_FACTOR', - 'DISABLE_RESIDUALS', + 'DISABLE_RESIDUALS_PLOT', 'DISABLE_POLYDISPERSITY_PLOT'] self.restart_params = {'QT_SCALE_FACTOR': 'QT Screen Scale Factor', 'QT_AUTO_SCREEN_SCALE_FACTOR': "Enable Automatic Scaling"} @@ -24,11 +24,11 @@ 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.disableResiduals = self.addCheckBox( + self.disableResidualsPlot = self.addCheckBox( title="Disable Residuals Display", - checked=config.DISABLE_RESIDUALS) - self.disableResiduals.clicked.connect( - lambda: self._stageChange('DISABLE_RESIDUALS', self.disableResiduals.isChecked())) + checked=config.DISABLE_RESIDUALS_PLOT) + self.disableResidualsPlot.clicked.connect( + lambda: self._stageChange('DISABLE_RESIDUALS_PLOT', self.disableResidualsPlot.isChecked())) self.disablePolydispersityPlot = self.addCheckBox( title="Disable Polydispersity Plot Display", checked=config.DISABLE_POLYDISPERSITY_PLOT) @@ -38,12 +38,12 @@ def _addAllWidgets(self): def _toggleBlockAllSignaling(self, toggle): self.qtScaleFactor.blockSignals(toggle) self.autoScaling.blockSignals(toggle) - self.disableResiduals.blockSignals(toggle) + self.disableResidualsPlot.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.disableResiduals.setChecked(config.DISABLE_RESIDUALS) + self.disableResidualsPlot.setChecked(config.DISABLE_RESIDUALS_PLOT) self.disablePolydispersityPlot.setChecked(config.DISABLE_POLYDISPERSITY_PLOT) diff --git a/src/sas/system/config/config.py b/src/sas/system/config/config.py index 34efc0bd22..07b777b92f 100644 --- a/src/sas/system/config/config.py +++ b/src/sas/system/config/config.py @@ -199,7 +199,7 @@ def __init__(self): # Residuals management # If true, disables residuals display - self.DISABLE_RESIDUALS = False + self.DISABLE_RESIDUALS_PLOT = False # Polydispersity plot management # If true, disables polydispersity plot display From a08077a7824f55de8dce83a05695fc1f14f19d93 Mon Sep 17 00:00:00 2001 From: Dorian Lozano Date: Wed, 26 Jul 2023 10:32:36 +0200 Subject: [PATCH 31/58] Typo 'Residuals' -> 'Residual' --- src/sas/qtgui/MainWindow/DataExplorer.py | 2 +- .../Preferences/DisplayPreferencesWidget.py | 14 +++++++------- src/sas/system/config/config.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index 35295c7270..4ad0f51e34 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1091,7 +1091,7 @@ def displayData(self, data_list, id=None): 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_RESIDUALS_PLOT: + 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: diff --git a/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py b/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py index bfefabe479..da535cb4f6 100644 --- a/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py +++ b/src/sas/qtgui/Utilities/Preferences/DisplayPreferencesWidget.py @@ -8,7 +8,7 @@ def __init__(self): super(DisplayPreferencesWidget, self).__init__("Display Settings") self.config_params = ['QT_SCALE_FACTOR', 'QT_AUTO_SCREEN_SCALE_FACTOR', - 'DISABLE_RESIDUALS_PLOT', + 'DISABLE_RESIDUAL_PLOT', 'DISABLE_POLYDISPERSITY_PLOT'] self.restart_params = {'QT_SCALE_FACTOR': 'QT Screen Scale Factor', 'QT_AUTO_SCREEN_SCALE_FACTOR': "Enable Automatic Scaling"} @@ -24,11 +24,11 @@ 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.disableResidualsPlot = self.addCheckBox( + self.disableResidualPlot = self.addCheckBox( title="Disable Residuals Display", - checked=config.DISABLE_RESIDUALS_PLOT) - self.disableResidualsPlot.clicked.connect( - lambda: self._stageChange('DISABLE_RESIDUALS_PLOT', self.disableResidualsPlot.isChecked())) + 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) @@ -38,12 +38,12 @@ def _addAllWidgets(self): def _toggleBlockAllSignaling(self, toggle): self.qtScaleFactor.blockSignals(toggle) self.autoScaling.blockSignals(toggle) - self.disableResidualsPlot.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.disableResidualsPlot.setChecked(config.DISABLE_RESIDUALS_PLOT) + self.disableResidualPlot.setChecked(config.DISABLE_RESIDUAL_PLOT) self.disablePolydispersityPlot.setChecked(config.DISABLE_POLYDISPERSITY_PLOT) diff --git a/src/sas/system/config/config.py b/src/sas/system/config/config.py index 07b777b92f..244603a997 100644 --- a/src/sas/system/config/config.py +++ b/src/sas/system/config/config.py @@ -199,7 +199,7 @@ def __init__(self): # Residuals management # If true, disables residuals display - self.DISABLE_RESIDUALS_PLOT = False + self.DISABLE_RESIDUAL_PLOT = False # Polydispersity plot management # If true, disables polydispersity plot display From cdbd1b7eb781ab4c8af91939f937d03b122b3dc3 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Wed, 26 Jul 2023 10:41:38 +0100 Subject: [PATCH 32/58] Changes to parameter qmax, validate method, some documentation, and cleanup. --- .../qtgui/Plotting/Slicers/ArcInteractor.py | 15 +--- .../Plotting/Slicers/RadiusInteractor.py | 70 ++++++------------- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 53 ++++++++++---- 3 files changed, 65 insertions(+), 73 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index 7bf1eb8b8a..ac050917d2 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -70,7 +70,7 @@ def get_radius(self): def update(self, theta2=None, phi=None, r=None, nbins=100): """ - Update the plotted arc + Draw the new roughness on the graph. :param theta2: 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 @@ -126,7 +126,7 @@ def restore(self, ev): def move(self, x, y, ev): """ - Process move to a new position + Process move to a new position. """ self._mouse_x = x self._mouse_y = y @@ -139,14 +139,3 @@ def set_cursor(self, x, y): self.move(x, y, None) self.update() - # def get_params(self): - # params = {} - # params["radius"] = self.radius - # params["theta2"] = self.theta2 - # params["phi"] = self.phi - # return params - - # def set_params(self, params): - # r = params["radius"] - # theta = params["theta2"] # or = self.theta2 (?) - # self.set_cursor(r*np.cos(theta), r*np.sin(theta)) diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index 30c14d7f5b..24def190ad 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -2,23 +2,21 @@ from sas.qtgui.Plotting.Slicers.BaseInteractor import BaseInteractor - class RadiusInteractor(BaseInteractor): """ - Draw a radial line (centered at [0,0]) at an angle theta from the x-axis - on a data2D plot from r1 to r2 defined by two arcs (arc1 and arc2). Used - for example to define a wedge area on the plot. - - param theta: angle of the radial line from the x-axis - param arc1: inner arc of radius r1 used to define the starting point of - the radial line - param arc2: outer arc of radius r2 used to define the ending point of - the radial line + 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 theta2, 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. + + :param r1: radius of the inner end of the radial lines + :param r2: radius of the outer end of the radial lines + :param theta2: average angle of the lines from the x-axis + :param phi: angular displacement of the lines either side of theta2 """ def __init__(self, base, axes, color='black', zorder=5, arc1=None, arc2=None, theta2=np.pi / 3, phi=np.pi / 8): - """ - """ BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes @@ -68,12 +66,15 @@ def __init__(self, base, axes, color='black', zorder=5, arc1=None, 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: @@ -86,14 +87,13 @@ def clear(self): for item in range(len(self.axes.lines)): del self.axes.lines[0] - # def get_angle(self): - # """ - # """ - # return self.theta - def update(self, theta2=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 theta2: average angle of the lines from the x-axis + :param phi: angular displacement of the lines either side of theta2 """ # TODO - try out an 'if self.arc1.has_move:' etc self.r1 = self.arc1.get_radius() @@ -121,10 +121,12 @@ def save(self, ev): can restore on Esc. """ self.save_phi = self.phi - # May also need a save_theta2 variable + # self.save_theta2 = self.theta2 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) @@ -134,11 +136,11 @@ def restore(self, ev): Restore the roughness for this layer. """ self.phi = self.save_phi - # May also need a save_theta2 variable + # self.theta2 = self.save_theta2 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. """ theta = np.arctan2(y, x) self.phi = np.fabs(theta - self.theta2) @@ -147,34 +149,6 @@ def move(self, x, y, ev): self.base.draw() def set_cursor(self, x, y): - """ - """ self.move(x, y, None) self.update() - # def get_params(self): - # """ - # Store a copy of values of parameters of the slicer into a dictionary. - # :return params: the dictionary created - # """ - # params = {} - # params["radius1"] = self.r1 - # params["radius2"] = self.r2 - # params["theta"] = self.theta2 - # params["phi"] = self.phi - # return params - - # def set_params(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. - # """ - # r1 = params["radius1"] - # r2 = params["radius2"] - # theta = params["theta"] - # phi = params["phi"] - # self.set_cursor(x1, x2, theta) - diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index 5e8c21fe15..4127902c33 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -37,9 +37,9 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.markers = [] self.axes = axes self._item = item - xmax2 = np.power(max(self.data.xmax, np.fabs(self.data.xmin)), 2) - ymax2 = np.power(max(self.data.ymax, np.fabs(self.data.ymin)), 2) - self.qmax = np.sqrt(xmax2 + ymax2) + 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 @@ -58,17 +58,17 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.outer_arc = ArcInteractor(self, self.axes, color='black', zorder=zorder + 1, r=self.qmax / 1.6, theta2=self.theta2, phi=self.phi) - self.outer_arc.qmax = self.qmax * 1.2 # TODO - check with someone about this + self.outer_arc.qmax = self.qmax * 1.2 self.radial_lines = RadiusInteractor(self, self.axes, color='black', zorder=zorder + 1, arc1=self.inner_arc, arc2=self.outer_arc, theta2=self.theta2, phi=self.phi) - self.radial_lines.qmax = self.qmax + self.radial_lines.qmax = self.qmax * 1.2 self.central_line = LineInteractor(self, self.axes, color='black', - zorder=zorder, r=self.qmax, + zorder=zorder, r=self.qmax * 1.414, theta=self.theta2) - self.central_line.qmax = self.qmax + self.central_line.qmax = self.qmax * 1.414 self.update() self.draw() self._post_data() @@ -129,10 +129,6 @@ def save(self, ev): self.radial_lines.save(ev) self.central_line.save(ev) - ''' - From here onwards we've got the old theta1 & theta2 logic - ''' - def _post_data(self): pass @@ -216,9 +212,30 @@ def validate(self, param_name, param_value): Validate input from user Values get checked at apply time. """ + MIN_Q_DIFFERENCE = self.dqmin + MIN_PHI_DIFFERENCE = 0.01 isValid = True - if param_name == 'nbins': + # Stitched together from other slicers. There could be a neater way. + if param_name == 'r_min': + if np.fabs(param_value - self.getParams()['r_max']) < MIN_Q_DIFFERENCE: + print("Inner and outer radii too close. Please adjust.") + isValid = False + elif param_value > self.qmax: + print("Inner radius exceeds maximum range. Please adjust.") + isValid = False + elif param_name == 'r_max': + if np.fabs(param_value - self.getParams()['r_min']) < MIN_Q_DIFFERENCE: + print("Inner and outer radii too close. Please adjust.") + isValid = False + elif param_value > self.qmax: + print("Outer radius exceeds maximum range. Please adjust.") + isValid = False + elif param_name == 'delta_phi [deg]': + if np.fabs(param_value) < MIN_PHI_DIFFERENCE: + print("Sector angles too close. Please adjust.") + isValid = False + elif param_name == 'nbins': # Can't be 0 if param_value < 1: print("Number of bins cannot be <= 0. Please adjust.") @@ -226,6 +243,11 @@ def validate(self, param_name, param_value): return isValid def moveend(self, ev): # Check if that's all I need? + """ + 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): @@ -261,6 +283,11 @@ def getParams(self): 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. """ r1 = params["r_min"] r2 = params["r_max"] @@ -276,6 +303,8 @@ def setParams(self, params): def draw(self): """ + Draws the Canvas using the canvas.draw from the calling class + that instatiated this object. """ self.base.draw() From d0f17f6e999dbb86a618b22c5402baf0c862b89f Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Wed, 26 Jul 2023 11:28:39 +0100 Subject: [PATCH 33/58] Renamed parameter theta2 to theta, plus further polising. --- .../qtgui/Plotting/Slicers/ArcInteractor.py | 29 +++++---- .../Plotting/Slicers/RadiusInteractor.py | 65 +++++++++---------- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 35 ++++------ 3 files changed, 61 insertions(+), 68 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index ac050917d2..418790be34 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -5,13 +5,14 @@ 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 theta2: angle from x-axis of the central point on the arc + 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, - theta2=np.pi / 3, phi=np.pi / 8): + theta=np.pi / 3, phi=np.pi / 8): BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes @@ -21,12 +22,12 @@ def __init__(self, base, axes, color='black', zorder=5, r=1.0, self._save_x = r self._save_y = 0 self.scale = 10.0 - self.theta2 = theta2 + self.theta = theta self.phi = phi self.radius = r - # Define the arc's marker - marker_x = self.radius * np.cos(theta2 * 0.8) - marker_y = self.radius * np.sin(theta2 * 0.8) + # Calculate the marker coordinates and define the marker + marker_x = self.radius * np.cos(theta - 0.5 * phi) + marker_y = self.radius * np.sin(theta - 0.5 * phi) self.marker = self.axes.plot([marker_x], [marker_y], linestyle='', marker='s', markersize=10, color=self.color, alpha=0.6, pickradius=5, @@ -35,6 +36,7 @@ def __init__(self, base, axes, color='black', zorder=5, r=1.0, # Define the arc [self.arc] = self.axes.plot([], [], linestyle='-', marker='', color=self.color) self.npts = 20 + # Flag to keep track of motion self.has_move = False self.connect_markers([self.marker, self.arc]) self.update() @@ -68,18 +70,18 @@ def get_radius(self): np.power(self._mouse_y, 2)) return radius - def update(self, theta2=None, phi=None, r=None, nbins=100): + def update(self, theta=None, phi=None, r=None, nbins=100): """ Draw the new roughness on the graph. - :param theta2: angle from x-axis of the central point on the arc + :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 :param nbins: number of points drawn for an arc of size pi radians """ x = [] y = [] - if theta2 is not None: - self.theta2 = theta2 + if theta is not None: + self.theta = theta if phi is not None: self.phi = phi self.npts = int((2 * self.phi) / (np.pi / nbins)) @@ -90,14 +92,15 @@ def update(self, theta2=None, phi=None, r=None, nbins=100): self.radius = r # Calculate the points on the arc, and draw them for i in range(self.npts): - angleval = 2 * self.phi / (self.npts - 1) * i + (self.theta2 - self.phi) + angleval = 2 * self.phi / (self.npts - 1) * i + (self.theta - self.phi) xval = 1.0 * self.radius * np.cos(angleval) yval = 1.0 * self.radius * np.sin(angleval) x.append(xval) y.append(yval) - marker_x = self.radius * np.cos(self.theta2 - 0.5 * self.phi) - marker_y = self.radius * np.sin(self.theta2 - 0.5 * self.phi) + # 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]) self.arc.set_data(x, y) diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index 24def190ad..5393049141 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -5,39 +5,38 @@ class RadiusInteractor(BaseInteractor): """ 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 theta2, and an + 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. + 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 theta2: average angle of the lines from the x-axis - :param phi: angular displacement of the lines either side of theta2 + :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, theta2=np.pi / 3, phi=np.pi / 8): + arc2=None, theta=np.pi / 3, phi=np.pi / 8): BaseInteractor.__init__(self, base, axes, color=color) self.markers = [] self.axes = axes self.color = color self.r1 = arc1.get_radius() self.r2 = arc2.get_radius() - self.theta2 = theta2 - # self.save_theta2 = theta2 + self.theta = theta self.phi = phi self.save_phi = phi self.arc1 = arc1 self.arc2 = arc2 # Variables for the left and right radial lines - l_x1 = self.r1 * np.cos(self.theta2 + self.phi) - l_y1 = self.r1 * np.sin(self.theta2 + self.phi) - l_x2 = self.r2 * np.cos(self.theta2 + self.phi) - l_y2 = self.r2 * np.sin(self.theta2 + self.phi) - r_x1 = self.r1 * np.cos(self.theta2 - self.phi) - r_y1 = self.r1 * np.sin(self.theta2 - self.phi) - r_x2 = self.r2 * np.cos(self.theta2 - self.phi) - r_y2 = self.r2 * np.sin(self.theta2 - self.phi) + 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, @@ -56,8 +55,6 @@ def __init__(self, base, axes, color='black', zorder=5, arc1=None, self.r_line = self.axes.plot([r_x1, r_x2], [r_y1, r_y2], linestyle='-', marker='', color=self.color, visible=True)[0] - # # Flag to differentiate the left side's motion from the right's - # self.left_moving = False # Flag to keep track of motion self.has_move = False self.connect_markers([self.l_marker, self.l_line, @@ -87,29 +84,31 @@ def clear(self): for item in range(len(self.axes.lines)): del self.axes.lines[0] - def update(self, theta2=None, phi=None): + def update(self, 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 theta2: average angle of the lines from the x-axis - :param phi: angular displacement of the lines either side of theta2 + :param theta: average angle of the lines from the x-axis + :param phi: angular displacement of the lines either side of theta """ # TODO - try out an 'if self.arc1.has_move:' etc self.r1 = self.arc1.get_radius() self.r2 = self.arc2.get_radius() - if theta2 is not None: - self.theta2 = theta2 + if theta is not None: + self.theta = theta if phi is not None: self.phi = phi - l_x1 = self.r1 * np.cos(self.theta2 + self.phi) - l_y1 = self.r1 * np.sin(self.theta2 + self.phi) - l_x2 = self.r2 * np.cos(self.theta2 + self.phi) - l_y2 = self.r2 * np.sin(self.theta2 + self.phi) - r_x1 = self.r1 * np.cos(self.theta2 - self.phi) - r_y1 = self.r1 * np.sin(self.theta2 - self.phi) - r_x2 = self.r2 * np.cos(self.theta2 - self.phi) - r_y2 = self.r2 * np.sin(self.theta2 - self.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]) @@ -121,7 +120,6 @@ def save(self, ev): can restore on Esc. """ self.save_phi = self.phi - # self.save_theta2 = self.theta2 def moveend(self, ev): """ @@ -136,14 +134,13 @@ def restore(self, ev): Restore the roughness for this layer. """ self.phi = self.save_phi - # self.theta2 = self.save_theta2 def move(self, x, y, ev): """ Process move to a new position. """ - theta = np.arctan2(y, x) - self.phi = np.fabs(theta - self.theta2) + angle = np.arctan2(y, x) + self.phi = np.fabs(angle - self.theta) self.has_move = True self.base.update() self.base.draw() diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index 4127902c33..fb5a883cb1 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -42,10 +42,10 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.dqmin = min(np.fabs(self.data.qx_data)) self.connect = self.base.connect - # # Number of points on the plot + # Number of points on the plot self.nbins = 100 # Angle of the central line - self.theta2 = np.pi / 3 + 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 @@ -53,24 +53,24 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.inner_arc = ArcInteractor(self, self.axes, color='black', zorder=zorder, r=self.qmax / 2.0, - theta2=self.theta2, phi=self.phi) + 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.qmax / 1.6, - theta2=self.theta2, phi=self.phi) + 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, arc1=self.inner_arc, arc2=self.outer_arc, - theta2=self.theta2, phi=self.phi) + 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.theta2) + theta=self.theta) self.central_line.qmax = self.qmax * 1.414 self.update() - self.draw() + self.base.draw() self._post_data() self.setModelFromParams() @@ -100,9 +100,9 @@ def update(self): resetting the widgets. """ # Update parameters - self.theta2 = self.central_line.theta + self.theta = self.central_line.theta self.phi = self.radial_lines.phi - # Update locations + # Update interactors if self.inner_arc.has_move: self.inner_arc.update() self.radial_lines.update() @@ -115,9 +115,9 @@ def update(self): self.outer_arc.update(phi=self.phi) if self.central_line.has_move: self.central_line.update() - self.inner_arc.update(theta2=self.theta2) - self.outer_arc.update(theta2=self.theta2) - self.radial_lines.update(theta2=self.theta2) + 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): """ @@ -242,7 +242,7 @@ def validate(self, param_name, param_value): isValid = False return isValid - def moveend(self, ev): # Check if that's all I need? + def moveend(self, ev): """ Called after a dragging event. Post the slicer new parameters and creates a new Data1D @@ -261,7 +261,7 @@ def restore(self, ev): 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. """ pass @@ -301,13 +301,6 @@ def setParams(self, params): self.central_line.update(theta) self._post_data() - def draw(self): - """ - Draws the Canvas using the canvas.draw from the calling class - that instatiated this object. - """ - self.base.draw() - class WedgeInteractorQ(WedgeInteractor): """ Average in Q direction. The data for all phi at a constant Q are From bb8f3b9d809856437d22d1dd3368f82ef5d73dd1 Mon Sep 17 00:00:00 2001 From: Dorian Lozano Date: Wed, 26 Jul 2023 12:51:36 +0200 Subject: [PATCH 34/58] Minor change in a docstring --- src/sas/system/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/system/config/config.py b/src/sas/system/config/config.py index 244603a997..d3fd556955 100644 --- a/src/sas/system/config/config.py +++ b/src/sas/system/config/config.py @@ -198,7 +198,7 @@ def __init__(self): self.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH = 30 # Residuals management - # If true, disables residuals display + # If true, disables residual plot display self.DISABLE_RESIDUAL_PLOT = False # Polydispersity plot management From 32caac5342b4311b37e0998a3f37782269e6bf8c Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Wed, 26 Jul 2023 12:14:07 +0100 Subject: [PATCH 35/58] Changed how RadiusInteractor determines its radii, and how WedgeSlicer sets them for better consistency with the other interactor elements. --- .../Plotting/Slicers/RadiusInteractor.py | 19 +++++---- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 40 ++++++++++++------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index 5393049141..1d4f9f13d9 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -15,19 +15,17 @@ class RadiusInteractor(BaseInteractor): :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 / 3, phi=np.pi / 8): + 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.color = color - self.r1 = arc1.get_radius() - self.r2 = arc2.get_radius() + self.r1 = r1 + self.r2 = r2 self.theta = theta self.phi = phi self.save_phi = phi - self.arc1 = arc1 - self.arc2 = arc2 # 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) @@ -84,7 +82,7 @@ def clear(self): for item in range(len(self.axes.lines)): del self.axes.lines[0] - def update(self, theta=None, phi=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 @@ -92,9 +90,10 @@ def update(self, theta=None, phi=None): :param theta: average angle of the lines from the x-axis :param phi: angular displacement of the lines either side of theta """ - # TODO - try out an 'if self.arc1.has_move:' etc - self.r1 = self.arc1.get_radius() - self.r2 = self.arc2.get_radius() + if r1 is not None: + self.r1 = r1 + if r2 is not None: + self.r2 = r2 if theta is not None: self.theta = theta if phi is not None: diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index fb5a883cb1..ba63f2d488 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -44,6 +44,10 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): # 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 @@ -52,17 +56,16 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self.averager = None self.inner_arc = ArcInteractor(self, self.axes, color='black', - zorder=zorder, r=self.qmax / 2.0, + 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.qmax / 1.6, + 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, - arc1=self.inner_arc, - arc2=self.outer_arc, + 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', @@ -70,7 +73,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): theta=self.theta) self.central_line.qmax = self.qmax * 1.414 self.update() - self.base.draw() + self.draw() self._post_data() self.setModelFromParams() @@ -100,15 +103,17 @@ def update(self): resetting the widgets. """ # Update parameters + self.r1 = self.inner_arc.radius + self.r2 = self.outer_arc.radius self.theta = self.central_line.theta self.phi = self.radial_lines.phi # Update interactors if self.inner_arc.has_move: self.inner_arc.update() - self.radial_lines.update() + self.radial_lines.update(r1=self.r1) if self.outer_arc.has_move: self.outer_arc.update() - self.radial_lines.update() + self.radial_lines.update(r2=self.r2) if self.radial_lines.has_move: self.radial_lines.update() self.inner_arc.update(phi=self.phi) @@ -144,12 +149,12 @@ def post_data(self, new_sector=None, nbins=None): if data is None: return - if self.inner_arc.get_radius() < self.outer_arc.get_radius(): - rmin = self.inner_arc.get_radius() - rmax = self.outer_arc.get_radius() + if self.inner_arc.radius < self.outer_arc.radius: + rmin = self.inner_arc.radius + rmax = self.outer_arc.radius else: - rmin = self.outer_arc.get_radius() - rmax = self.inner_arc.get_radius() + 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 @@ -274,8 +279,8 @@ def getParams(self): :return params: the dictionary created """ params = {} - params["r_min"] = self.inner_arc.get_radius() - params["r_max"] = self.outer_arc.get_radius() + 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 @@ -301,6 +306,13 @@ def setParams(self, params): self.central_line.update(theta) self._post_data() + 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 From e8e68270ea0ae8da83cca0723c6e91090e86f0f2 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Wed, 26 Jul 2023 14:29:20 +0100 Subject: [PATCH 36/58] Added check preventing phi angles > 180 degrees --- src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index 1d4f9f13d9..3ff4f87b85 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -136,10 +136,14 @@ def restore(self, ev): def move(self, x, y, ev): """ - Process move to a new position. + Process move to a new position, making sure that the move is allowed. """ angle = np.arctan2(y, x) - self.phi = np.fabs(angle - self.theta) + phi = np.fabs(angle - self.theta) + if phi > np.pi: + self.restore(ev) + return + self.phi = phi self.has_move = True self.base.update() self.base.draw() From 6800e6c8358d354d6365460b365909e339c3aff1 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Thu, 27 Jul 2023 11:38:10 +0100 Subject: [PATCH 37/58] Implemented proper setting of parameters via SlicerParametersUI window, fixed slicer plotting bugs, and corrected the nbins and npts implementations. --- .../qtgui/Plotting/Slicers/ArcInteractor.py | 12 ++---- .../Plotting/Slicers/RadiusInteractor.py | 5 +-- .../qtgui/Plotting/Slicers/SectorSlicer.py | 1 - src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 38 +++++++++---------- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index 418790be34..b6402bf973 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -35,7 +35,8 @@ def __init__(self, base, axes, color='black', zorder=5, r=1.0, visible=True)[0] # Define the arc [self.arc] = self.axes.plot([], [], linestyle='-', marker='', color=self.color) - self.npts = 20 + # 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]) @@ -70,13 +71,12 @@ def get_radius(self): np.power(self._mouse_y, 2)) return radius - def update(self, theta=None, phi=None, r=None, nbins=100): + 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 - :param nbins: number of points drawn for an arc of size pi radians """ x = [] y = [] @@ -84,11 +84,7 @@ def update(self, theta=None, phi=None, r=None, nbins=100): self.theta = theta if phi is not None: self.phi = phi - self.npts = int((2 * self.phi) / (np.pi / nbins)) - - if r is None: - self.radius = self.get_radius() - else: + if r is not None: self.radius = r # Calculate the points on the arc, and draw them for i in range(self.npts): diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index 3ff4f87b85..ca8ab0a899 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -136,13 +136,12 @@ def restore(self, ev): 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. """ angle = np.arctan2(y, x) phi = np.fabs(angle - self.theta) if phi > np.pi: - self.restore(ev) - return + phi = 2 * np.pi - phi self.phi = phi self.has_move = True self.base.update() diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index 9378de6c4c..3f53f5ab40 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -513,7 +513,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() diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index ba63f2d488..04ffc68dd4 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -99,27 +99,25 @@ def clear(self): def update(self): """ - Respond to changes in the model by recalculating the profiles and - resetting the widgets. + If one of the interactors has been moved, update it and the parameter + it controls, then update the other interactors accordingly """ - # Update parameters - self.r1 = self.inner_arc.radius - self.r2 = self.outer_arc.radius - self.theta = self.central_line.theta - self.phi = self.radial_lines.phi - # Update interactors 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) @@ -166,8 +164,8 @@ def post_data(self, new_sector=None, nbins=None): raise ValueError(msg) self.averager = new_sector - sect = self.averager(r_min=rmin, r_max=rmax, - phi_min=phimin, phi_max=phimax) + sect = self.averager(r_min=rmin, r_max=rmax, phi_min=phimin, + phi_max=phimax, nbins=self.nbins) sector = sect(self.data) if hasattr(sector, "dxl"): @@ -294,17 +292,19 @@ def setParams(self, params): :param params: a dictionary containing name of slicer parameters and values the user assigned to the slicer. """ - r1 = params["r_min"] - r2 = params["r_max"] - theta = params["phi [deg]"] * np.pi / 180 - phi = params["delta_phi [deg]"] * np.pi / 180 + 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, phi, r1, self.nbins) - self.outer_arc.update(theta, phi, r2, self.nbins) - self.radial_lines.update(theta, phi) - self.central_line.update(theta) - self._post_data() + 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): """ From 9741b6aaabce03750087e2c4101d661b3fc82d97 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Thu, 27 Jul 2023 11:59:01 +0100 Subject: [PATCH 38/58] Introduced 'half_length' parameter to LineInteractor, used by WedgeInteractor to draw line from origin instead of across the whole plot. --- .../qtgui/Plotting/Slicers/SectorSlicer.py | 21 ++++++++++++++----- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py index 3f53f5ab40..5ead14e7bc 100644 --- a/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/SectorSlicer.py @@ -486,9 +486,11 @@ class LineInteractor(BaseInteractor): 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 = [] @@ -498,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, @@ -540,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]) diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index 04ffc68dd4..0ad5fa1e92 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -70,7 +70,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): 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) + theta=self.theta, half_length=True) self.central_line.qmax = self.qmax * 1.414 self.update() self.draw() From e0c0092cb2240435c8d8e8e0131b8a98a036378e Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 28 Jul 2023 10:30:28 +0100 Subject: [PATCH 39/58] Disabled folding - this behaviour is not desired for this slicer. Also corrected the angles being sent to manipulations.py --- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index 0ad5fa1e92..f68997791a 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -164,8 +164,10 @@ def post_data(self, new_sector=None, nbins=None): raise ValueError(msg) self.averager = new_sector - sect = self.averager(r_min=rmin, r_max=rmax, phi_min=phimin, - phi_max=phimax, nbins=self.nbins) + # Why do I have to add pi to the angles for it to work properly? + 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"): From 418c42c0bdd8d388cdca5c766e81e7773c2c7bca Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Mon, 31 Jul 2023 09:39:37 +0200 Subject: [PATCH 40/58] Added the model reload signal on data swap --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 2 ++ 1 file changed, 2 insertions(+) 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): """ From ad1914ec907a7462a334768e353f31fa4c95d81e Mon Sep 17 00:00:00 2001 From: rozyczko Date: Mon, 31 Jul 2023 15:00:21 +0200 Subject: [PATCH 41/58] latex->html --- src/sas/qtgui/Utilities/GuiUtils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/Utilities/GuiUtils.py b/src/sas/qtgui/Utilities/GuiUtils.py index a6c3407abf..0d1e1ee4f3 100644 --- a/src/sas/qtgui/Utilities/GuiUtils.py +++ b/src/sas/qtgui/Utilities/GuiUtils.py @@ -961,7 +961,7 @@ def replaceHTMLwithASCII(html): return html -def rst_to_latex(s): +def rst_to_html(s): # Extract the unit and replacement parts match_replace = re.match(r'(?:\.\. )?\|(.+?)\| replace:: (.+)', s) match_unit = re.match(r'(?:\.\. )?\|(.+?)\| unicode:: (U\+\w+)', s) @@ -993,14 +993,14 @@ def rst_to_latex(s): input_rst_strings = RST_PROLOG.splitlines() for line in input_rst_strings: if line.startswith(".. |"): - key, value = rst_to_latex(line) + key, value = rst_to_html(line) RST_PROLOG_DICT[key] = value -def convertUnitToUTF8(unit): +def convertUnitToHTML(unit): if unit in RST_PROLOG_DICT: return RST_PROLOG_DICT[unit] else: - return "" + return unit def convertUnitToUTF8_old(unit): """ From 5357ddd7fb93eb3832f27cf5784a0ab9ba3bbc9c Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Mon, 31 Jul 2023 16:17:26 +0200 Subject: [PATCH 42/58] Updated with missing units, updated tests, corrected report save to txt and html --- src/sas/qtgui/Utilities/GuiUtils.py | 68 +++++++------------ .../qtgui/Utilities/Reports/ReportDialog.py | 5 +- .../Utilities/UnitTesting/GuiUtilsTest.py | 26 ------- 3 files changed, 27 insertions(+), 72 deletions(-) diff --git a/src/sas/qtgui/Utilities/GuiUtils.py b/src/sas/qtgui/Utilities/GuiUtils.py index 0d1e1ee4f3..11324b9136 100644 --- a/src/sas/qtgui/Utilities/GuiUtils.py +++ b/src/sas/qtgui/Utilities/GuiUtils.py @@ -968,22 +968,25 @@ def rst_to_html(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("Ang", "Å").replace("\\", "").replace(" ", "") + 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|", "±") - 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)) return unit, replacement @@ -995,49 +998,26 @@ def rst_to_html(s): if line.startswith(".. |"): key, value = rst_to_html(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-62" +RST_PROLOG_DICT["1e15/cm^3"] = "1015/cm3" +RST_PROLOG_DICT["inf"] = "∞" +RST_PROLOG_DICT["-inf"] = "-∞" -def convertUnitToHTML(unit): - if unit in RST_PROLOG_DICT: - return RST_PROLOG_DICT[unit] - else: - return unit - -def convertUnitToUTF8_old(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-62" - elif unit == "inf": - return "∞" - elif unit == "-inf": - return "-∞" - else: - return unit 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 "None" in unit: + return "" return unit def parseName(name, expression): 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/UnitTesting/GuiUtilsTest.py b/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py index 3cb45b1c3e..b0849a62a9 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py @@ -507,32 +507,6 @@ def testReplaceHTMLwithASCII(self): s = "Å ∞ ±" assert replaceHTMLwithASCII(s) == "Ang inf +/-" - def testConvertUnitToUTF8(self): - ''' test unit string replacement''' - 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-62" - - s = "inf" - assert convertUnitToUTF8(s) == "∞" - - s = "1/cm" - assert convertUnitToUTF8(s) == "cm-1" - def testConvertUnitToHTML(self): ''' test unit string replacement''' s = None From 3db40134fd395b44af0b8db2df6e2a6192b613dc Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Mon, 31 Jul 2023 16:21:51 +0200 Subject: [PATCH 43/58] missed the degrees -> U+00B7 conversion --- src/sas/qtgui/Utilities/GuiUtils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sas/qtgui/Utilities/GuiUtils.py b/src/sas/qtgui/Utilities/GuiUtils.py index 11324b9136..4c5f8b36ea 100644 --- a/src/sas/qtgui/Utilities/GuiUtils.py +++ b/src/sas/qtgui/Utilities/GuiUtils.py @@ -1007,6 +1007,7 @@ def rst_to_html(s): RST_PROLOG_DICT["1e15/cm^3"] = "1015/cm3" RST_PROLOG_DICT["inf"] = "∞" RST_PROLOG_DICT["-inf"] = "-∞" +RST_PROLOG_DICT["degrees"] = "°" def convertUnitToHTML(unit): From 2b3edf870818247809fb238fe60d7e34a12dd76a Mon Sep 17 00:00:00 2001 From: Piotr Rozyczko Date: Mon, 31 Jul 2023 16:24:07 +0200 Subject: [PATCH 44/58] in case the import doesn't succeed, we shouldn't be crashing. --- src/sas/qtgui/Utilities/GuiUtils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Utilities/GuiUtils.py b/src/sas/qtgui/Utilities/GuiUtils.py index 4c5f8b36ea..30e5b25afc 100644 --- a/src/sas/qtgui/Utilities/GuiUtils.py +++ b/src/sas/qtgui/Utilities/GuiUtils.py @@ -991,7 +991,10 @@ def rst_to_html(s): return unit, replacement # RST_PROLOG conversion table -from sasmodels.generate import RST_PROLOG +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: From fdc0ceff0465be9045297a2bc8e648700c352bf1 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Tue, 1 Aug 2023 10:45:42 +0100 Subject: [PATCH 45/58] Added some extra comments --- src/sas/qtgui/Plotting/Slicers/ArcInteractor.py | 15 +++++---------- .../qtgui/Plotting/Slicers/RadiusInteractor.py | 3 +++ src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 2 -- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index b6402bf973..c5c3ef91d8 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -17,11 +17,14 @@ def __init__(self, base, axes, color='black', zorder=5, r=1.0, 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 @@ -63,14 +66,6 @@ def clear(self): 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, theta=None, phi=None, r=None): """ Draw the new roughness on the graph. @@ -129,7 +124,8 @@ def move(self, x, y, ev): """ self._mouse_x = x self._mouse_y = y - self.radius = self.get_radius() + 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() @@ -137,4 +133,3 @@ def move(self, x, y, ev): def set_cursor(self, x, y): self.move(x, y, None) self.update() - diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index ca8ab0a899..d7a2475ef1 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -21,10 +21,13 @@ def __init__(self, base, axes, color='black', zorder=5, r1=1.0, r2=2.0, self.markers = [] self.axes = axes self.color = color + # Key variables used when drawing the interactor element self.r1 = r1 self.r2 = r2 self.theta = theta + # 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) diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index f68997791a..a14c5a3e58 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -327,8 +327,6 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): self._post_data() def _post_data(self): - """ - """ from sasdata.data_util.manipulations import SectorQ self.post_data(SectorQ) From fcc91ccb74adb97ae55ad056fe23b51a0d077f5c Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Tue, 1 Aug 2023 11:20:42 +0100 Subject: [PATCH 46/58] Cleaned up inheritance of _post_data and related methods --- src/sas/qtgui/Plotting/Slicers/BoxSlicer.py | 11 ++++------- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py index dcd58daff3..65760e6f66 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSlicer.py @@ -118,10 +118,7 @@ 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 1D data averaging in Qx or Qy given new_slab type @@ -259,7 +256,7 @@ 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): @@ -506,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): """ @@ -539,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/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index a14c5a3e58..fb7e13446a 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -132,10 +132,7 @@ def save(self, ev): self.radial_lines.save(ev) self.central_line.save(ev) - def _post_data(self): - pass - - def post_data(self, new_sector=None, nbins=None): + def _post_data(self, new_sector=None, nbins=None): """ post 1D data averagin in Q or Phi given new_sector type @@ -305,7 +302,7 @@ def setParams(self, params): 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._post_data() self.draw() def draw(self): @@ -328,7 +325,7 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): def _post_data(self): from sasdata.data_util.manipulations import SectorQ - self.post_data(SectorQ) + super()._post_data(SectorQ) class WedgeInteractorPhi(WedgeInteractor): @@ -344,5 +341,5 @@ def __init__(self, base, axes, item=None, color='black', zorder=3): def _post_data(self): from sasdata.data_util.manipulations import SectorPhi - self.post_data(SectorPhi) + super()._post_data(SectorPhi) From e5fd0910ef6aae99da692eed2977d38979eeaba5 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Tue, 1 Aug 2023 15:56:59 +0100 Subject: [PATCH 47/58] Readability improvements and optimizations, suggested by Piotr Rozyczko. --- .../qtgui/Plotting/Slicers/ArcInteractor.py | 29 +++------ .../Plotting/Slicers/RadiusInteractor.py | 13 ++-- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 59 ++++++++++--------- 3 files changed, 44 insertions(+), 57 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index c5c3ef91d8..ef0c3716f7 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -29,15 +29,13 @@ def __init__(self, base, axes, color='black', zorder=5, r=1.0, self.phi = phi self.radius = r # Calculate the marker coordinates and define the marker - marker_x = self.radius * np.cos(theta - 0.5 * phi) - marker_y = self.radius * np.sin(theta - 0.5 * phi) - self.marker = self.axes.plot([marker_x], [marker_y], linestyle='', + 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) + 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 @@ -58,13 +56,8 @@ def clear(self): Clear this slicer and its markers """ self.clear_markers() - try: - self.marker.remove() - self.arc.remove() - except: - # Old version of matplotlib - for item in range(len(self.axes.lines)): - del self.axes.lines[0] + self.marker.remove() + self.arc.remove() def update(self, theta=None, phi=None, r=None): """ @@ -73,8 +66,6 @@ def update(self, theta=None, phi=None, r=None): :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 """ - x = [] - y = [] if theta is not None: self.theta = theta if phi is not None: @@ -82,18 +73,16 @@ def update(self, theta=None, phi=None, r=None): if r is not None: self.radius = r # Calculate the points on the arc, and draw them - for i in range(self.npts): - angleval = 2 * self.phi / (self.npts - 1) * i + (self.theta - self.phi) - xval = 1.0 * self.radius * np.cos(angleval) - yval = 1.0 * self.radius * np.sin(angleval) - x.append(xval) - y.append(yval) + angle_factor = 2 * self.phi / (self.npts - 1) + angle_offset = self.theta - self.phi + x = [self.radius * np.cos(angle_factor * i + angle_offset) for i in range(self.npts)] + y = [self.radius * np.sin(angle_factor * i + angle_offset) for i in range(self.npts)] + self.arc.set_data(x, y) # 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]) - self.arc.set_data(x, y) def save(self, ev): """ diff --git a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py index d7a2475ef1..0bacc4a5e2 100755 --- a/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/RadiusInteractor.py @@ -75,15 +75,10 @@ def clear(self): Clear this slicer and its markers """ self.clear_markers() - try: - self.l_marker.remove() - self.l_line.remove() - self.r_marker.remove() - self.r_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 update(self, r1=None, r2=None, theta=None, phi=None): """ diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index fb7e13446a..5da6fa0e34 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -211,38 +211,41 @@ def _post_data(self, new_sector=None, nbins=None): def validate(self, param_name, param_value): """ - Validate input from user + Validate input from user. Values get checked at apply time. """ - MIN_Q_DIFFERENCE = self.dqmin - MIN_PHI_DIFFERENCE = 0.01 - isValid = True - - # Stitched together from other slicers. There could be a neater way. - if param_name == 'r_min': - if np.fabs(param_value - self.getParams()['r_max']) < MIN_Q_DIFFERENCE: - print("Inner and outer radii too close. Please adjust.") - isValid = False - elif param_value > self.qmax: - print("Inner radius exceeds maximum range. Please adjust.") - isValid = False - elif param_name == 'r_max': - if np.fabs(param_value - self.getParams()['r_min']) < MIN_Q_DIFFERENCE: - print("Inner and outer radii too close. Please adjust.") - isValid = False + + 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: - print("Outer radius exceeds maximum range. Please adjust.") - isValid = False - elif param_name == 'delta_phi [deg]': - if np.fabs(param_value) < MIN_PHI_DIFFERENCE: - print("Sector angles too close. Please adjust.") - isValid = False - elif param_name == 'nbins': - # Can't be 0 + 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: - print("Number of bins cannot be <= 0. Please adjust.") - isValid = False - return isValid + 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): """ From 9bebc4fb2fd21bf47d1f61502054265fdb98c148 Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 1 Aug 2023 14:12:17 -0400 Subject: [PATCH 48/58] Use the regex used by distutils to check the version string is valid instead of coercing to ints --- src/sas/system/config/config_meta.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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] From 46016392fcedd50a8c44b0d7c2352522342a3fe5 Mon Sep 17 00:00:00 2001 From: ehewins <108800436+ehewins@users.noreply.github.com> Date: Thu, 3 Aug 2023 10:21:28 +0100 Subject: [PATCH 49/58] Further suggestion by Jeff Krzywon to minimise the number of list comprehensions. Co-authored-by: Jeff Krzywon --- src/sas/qtgui/Plotting/Slicers/ArcInteractor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py index ef0c3716f7..0a44e25d42 100644 --- a/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py +++ b/src/sas/qtgui/Plotting/Slicers/ArcInteractor.py @@ -73,11 +73,11 @@ def update(self, theta=None, phi=None, r=None): if r is not None: self.radius = r # Calculate the points on the arc, and draw them - angle_factor = 2 * self.phi / (self.npts - 1) angle_offset = self.theta - self.phi - x = [self.radius * np.cos(angle_factor * i + angle_offset) for i in range(self.npts)] - y = [self.radius * np.sin(angle_factor * i + angle_offset) for i in range(self.npts)] - self.arc.set_data(x, y) + 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) From 1171caffce44816ac308bb81efe49c01bfc31971 Mon Sep 17 00:00:00 2001 From: butlerpd Date: Sat, 5 Aug 2023 21:02:31 +0000 Subject: [PATCH 50/58] Fix two typos in docs after review --- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index fb7e13446a..4e42325614 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -161,7 +161,8 @@ def _post_data(self, new_sector=None, nbins=None): raise ValueError(msg) self.averager = new_sector - # Why do I have to add pi to the angles for it to work properly? + # 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 @@ -175,7 +176,10 @@ def _post_data(self, new_sector=None, nbins=None): dxw = sector.dxw else: dxw = None - new_plot = Data1D(x=sector.x, y=sector.y, dy=sector.dy, dx=sector.dx) + # And here subtract pi when getting angular data back from sector averaging in + # manipulations to get back in the -pi,pi range. Also convert from radians to + # degrees for nicer display. + new_plot = Data1D(x=(sector.x - np.pi) * 180 / np.pi, y=sector.y, dy=sector.dy, dx=sector.dx) new_plot.dxl = dxl new_plot.dxw = dxw new_plot.name = str(self.averager.__name__) + \ @@ -185,15 +189,15 @@ def _post_data(self, new_sector=None, nbins=None): 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}") - if hasattr(data, "scale") and data.scale == 'linear' and \ - self.data.name.count("Residuals") > 0: - new_plot.ytransform = 'y' - new_plot.yaxis("\\rm{Residuals} ", "/") new_plot.id = str(self.averager.__name__) + self.data.name new_plot.group_id = new_plot.id From 846b4675b30f75e9daabfb0f97a00db2508adbcc Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 8 Aug 2023 11:34:19 +0200 Subject: [PATCH 51/58] Added/fixed unit tests --- src/sas/qtgui/Utilities/GuiUtils.py | 6 +-- .../Utilities/UnitTesting/GuiUtilsTest.py | 37 ++++++++++++++++--- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/sas/qtgui/Utilities/GuiUtils.py b/src/sas/qtgui/Utilities/GuiUtils.py index 30e5b25afc..7166e15c3c 100644 --- a/src/sas/qtgui/Utilities/GuiUtils.py +++ b/src/sas/qtgui/Utilities/GuiUtils.py @@ -961,7 +961,7 @@ def replaceHTMLwithASCII(html): return html -def rst_to_html(s): +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) @@ -999,7 +999,7 @@ def rst_to_html(s): input_rst_strings = RST_PROLOG.splitlines() for line in input_rst_strings: if line.startswith(".. |"): - key, value = rst_to_html(line) + 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 @@ -1020,7 +1020,7 @@ def convertUnitToHTML(unit): if unit in RST_PROLOG_DICT: return RST_PROLOG_DICT[unit] else: - if "None" in unit: + if unit is None or "None" in unit: return "" return unit diff --git a/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py b/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py index b0849a62a9..ca649cb915 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/GuiUtilsTest.py @@ -507,10 +507,32 @@ def testReplaceHTMLwithASCII(self): s = "Å ∞ ±" assert replaceHTMLwithASCII(s) == "Ang inf +/-" + def testrstToHtml(self): + ''' test rst to html conversion''' + s = None + 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) + + def testConvertUnitToHTML(self): ''' test unit string replacement''' s = None - assert convertUnitToHTML(s) is None + assert convertUnitToHTML(s) is "" s = "" assert convertUnitToHTML(s) == s @@ -519,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-62" 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 From 82e3b99a8ff8cab4afd6b79cc07376ce6486bdc8 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Tue, 8 Aug 2023 12:04:36 +0200 Subject: [PATCH 52/58] fix dialog sizes for some calculators. #2437 --- src/sas/qtgui/Calculators/DensityPanel.py | 3 +-- src/sas/qtgui/Calculators/GenericScatteringCalculator.py | 1 + src/sas/qtgui/Calculators/ResolutionCalculatorPanel.py | 1 + src/sas/qtgui/Calculators/SldPanel.py | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) 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 6a8c37c183..31c963b785 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -54,6 +54,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() From 5de4bfe4f51b40fc4b696854c8f84f346a529411 Mon Sep 17 00:00:00 2001 From: butlerpd Date: Fri, 11 Aug 2023 19:07:24 +0000 Subject: [PATCH 53/58] Temporary fix to issue with the two classes called being a bit too different The purpose of this method is to create and update the 1D plot from the sector selections. The problem is that the phi and Q averaging are different enough 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 an suggests that in this case we may need a new method in the WedgeInteracgtorPhi and WedgeInteracgtorQ to handle these specifics? But that seems a bit complicated -- leaving as is for now by adding this third "if class name" check to fix the immediate problem. --- src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py index 4e42325614..b2eda4e732 100644 --- a/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/WedgeSlicer.py @@ -138,6 +138,12 @@ def _post_data(self, new_sector=None, nbins=None): :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 @@ -176,10 +182,12 @@ def _post_data(self, new_sector=None, nbins=None): dxw = sector.dxw else: dxw = None - # And here subtract pi when getting angular data back from sector averaging in - # manipulations to get back in the -pi,pi range. Also convert from radians to - # degrees for nicer display. - new_plot = Data1D(x=(sector.x - np.pi) * 180 / np.pi, y=sector.y, dy=sector.dy, dx=sector.dx) + 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__) + \ From eeee79aa46316164b7c33c63589faa2c83c1e124 Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Tue, 15 Aug 2023 09:40:48 +0200 Subject: [PATCH 54/58] Testing with numpy 1.24 (We should provide proper fix) --- build_tools/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index 3f5edec97e..e5d3f2124e 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -1,4 +1,4 @@ -numpy +numpy==1.24 scipy==1.10.0 docutils pytest From d937169391b52446933a5e55cbeb71877406a963 Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Tue, 15 Aug 2023 10:11:49 +0200 Subject: [PATCH 55/58] Trying with numpy < 1.24 --- build_tools/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_tools/requirements.txt b/build_tools/requirements.txt index e5d3f2124e..c00355eca1 100644 --- a/build_tools/requirements.txt +++ b/build_tools/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.24 +numpy<1.24 scipy==1.10.0 docutils pytest From 6d1f2ceef0b09dbace2b89cbcbe8a47331c86bac Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 16 Aug 2023 09:30:08 +0200 Subject: [PATCH 56/58] Plot2D instances are now of `Plotter2DWidget` type. Fixes #2586 --- src/sas/qtgui/MainWindow/DataExplorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index ba2729ea42..756607f432 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1251,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) From 04185ee505c979ef90dd3428b1b08c9f9ac33c87 Mon Sep 17 00:00:00 2001 From: rozyczko Date: Wed, 23 Aug 2023 11:13:23 +0200 Subject: [PATCH 57/58] corrected signature of getExistingDirectory --- src/sas/qtgui/MainWindow/DataExplorer.py | 2 +- src/sas/qtgui/Plotting/SlicerParameters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index 756607f432..93148b6efb 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -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 diff --git a/src/sas/qtgui/Plotting/SlicerParameters.py b/src/sas/qtgui/Plotting/SlicerParameters.py index c5463fdd96..de33f8721d 100644 --- a/src/sas/qtgui/Plotting/SlicerParameters.py +++ b/src/sas/qtgui/Plotting/SlicerParameters.py @@ -220,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 From 55b1e9f6db58e33729f2a93b7dd1d8bf255b46f7 Mon Sep 17 00:00:00 2001 From: Wojciech Potrzebowski Date: Wed, 23 Aug 2023 14:25:23 +0200 Subject: [PATCH 58/58] Update nightly-build.yml --- .github/workflows/nightly-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"