diff --git a/installers/sasview.spec b/installers/sasview.spec index f3c463ce24..ac9701e381 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -18,6 +18,7 @@ datas = [ ('../src/sas/qtgui/Utilities/Reports/report_style.css', 'sas/qtgui/Utilities/Reports'), ('../src/sas/qtgui/Perspectives/Fitting/plugin_models', 'plugin_models'), ('../src/sas/qtgui/Utilities/WhatsNew/messages', 'sas/qtgui/Utilities/WhatsNew/messages'), + ('../src/sas/qtgui/Utilities/WhatsNew/css/style.css', 'sas/qtgui/Utilities/WhatsNew/css'), ('../src/sas/qtgui/Utilities/About/images', 'sas/qtgui/Utilities/About/images'), ('../src/sas/system/log.ini', 'sas/system/'), ('../../sasmodels/sasmodels','sasmodels'), diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index f4e006f5aa..f505297451 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -662,6 +662,7 @@ def actionWelcome(self): self.welcomePanel.show() def actionWhatsNew(self): + self.WhatsNew = WhatsNew(strictly_newer=False) self.WhatsNew.show() def showWelcomeMessage(self): diff --git a/src/sas/qtgui/Perspectives/ParticleEditor/datamodel/calculation.py b/src/sas/qtgui/Perspectives/ParticleEditor/datamodel/calculation.py index c55ce7f1e1..60b29574e7 100644 --- a/src/sas/qtgui/Perspectives/ParticleEditor/datamodel/calculation.py +++ b/src/sas/qtgui/Perspectives/ParticleEditor/datamodel/calculation.py @@ -1,6 +1,5 @@ -from typing import Optional, Callable, Tuple, Protocol, List +from typing import Optional, Tuple, List import numpy as np -from enum import Enum from dataclasses import dataclass from abc import ABC, abstractmethod @@ -32,7 +31,12 @@ def __call__(self): class SpatialDistribution(ABC): - """ Base class for point generators """ + """ Base class for point generators + + Batches need to be usable for bootstrapping, the run needs to be split into + n_bootstrap sections, and the output saved for each section, so that it can + be rebuilt in different combinations to establish a variance. + """ def __init__(self, radius: float, n_points: int, n_desired_points): self.radius = radius @@ -40,7 +44,11 @@ def __init__(self, radius: float, n_points: int, n_desired_points): self.n_points = n_points @property - def info(self): + def allows_bootstrap(self) -> bool: + return False + + @property + def info(self) -> str: """ Information to be displayed in the settings window next to the point number input """ return "" @@ -56,6 +64,7 @@ def bounding_surface_check_points(self) -> VectorComponents3: pts = self._bounding_surface_check_points() return pts[:, 0], pts[:, 1], pts[:, 2] + class AngularDistribution(ABC): """ Base class for angular distributions """ @@ -126,6 +135,8 @@ class SamplingDistribution: class QSpaceScattering: abscissa: QSample ordinate: np.ndarray + upper_error: np.ndarray | None = None + lower_error: np.ndarray | None = None @dataclass class RealSpaceScattering: diff --git a/src/sas/qtgui/Perspectives/ParticleEditor/sampling/points.py b/src/sas/qtgui/Perspectives/ParticleEditor/sampling/points.py index b07f41eb35..2f438d95ef 100644 --- a/src/sas/qtgui/Perspectives/ParticleEditor/sampling/points.py +++ b/src/sas/qtgui/Perspectives/ParticleEditor/sampling/points.py @@ -2,6 +2,8 @@ Instances of the spatial sampler + + """ import math @@ -72,6 +74,9 @@ def __init__(self, radius: float, desired_points: int, seed=None): # Accessing this will generate random seeds if they don't exist and store them to be accessed if they do self.seeds = defaultdict(lambda: self._seed_rng.integers(0, 0x7fff_ffff_ffff_ffff)) + def allows_bootstrap(self) -> bool: + return True + def generate(self, start_index: int, end_index: int) -> VectorComponents3: # This method of tracking seeds only works if the same start_indices are used each time points # in a region are requested - i.e. if they're chunks form a grid @@ -90,9 +95,10 @@ def generate(self, start_index: int, end_index: int) -> VectorComponents3: class PointGeneratorStepper: - """ Generate batches of step_size points from a PointGenerator instance""" + """ Generate batches of step_size points from a PointGenerator instance + """ - def __init__(self, point_generator: SpatialDistribution, step_size: int): + def __init__(self, point_generator: SpatialDistribution, step_size: int, bootstrap_sections: int): self.point_generator = point_generator self.step_size = step_size diff --git a/src/sas/qtgui/Utilities/WhatsNew/WhatsNew.py b/src/sas/qtgui/Utilities/WhatsNew/WhatsNew.py index c271e506a6..bc8205db4d 100644 --- a/src/sas/qtgui/Utilities/WhatsNew/WhatsNew.py +++ b/src/sas/qtgui/Utilities/WhatsNew/WhatsNew.py @@ -1,7 +1,9 @@ from collections import defaultdict from PySide6 import QtWidgets +from PySide6.QtGui import QImage, QPixmap from PySide6.QtWidgets import QDialog, QWidget, QTextBrowser, QVBoxLayout, QHBoxLayout, QPushButton, QCheckBox +from PySide6.QtCore import QUrl from sas.system.version import __version__ as sasview_version import importlib.resources as resources @@ -11,8 +13,14 @@ from sas.qtgui.Utilities.WhatsNew.newer import strictly_newer_than, reduced_version, newest -def whats_new_messages(): - """ Accumulate all files that are newer than the value in the config""" + + +def whats_new_messages(strictly_newer=True): + """ Accumulate all files that are newer than the value in the config + + :param strictly_newer: require strictly newer stuff, strictness is needed for showing new things + when there is an update, non-strictness is needed for the menu access. + """ out = defaultdict(list) message_dir = resources.files("sas.qtgui.Utilities.WhatsNew.messages") @@ -23,7 +31,11 @@ def whats_new_messages(): newer = False try: - newer = strictly_newer_than(message_dir.name, config.LAST_WHATS_NEW_HIDDEN_VERSION) + if strictly_newer: + newer = strictly_newer_than(message_dir.name, config.LAST_WHATS_NEW_HIDDEN_VERSION) + else: + # Include current version + newer = not strictly_newer_than(config.LAST_WHATS_NEW_HIDDEN_VERSION, message_dir.name) except ValueError: pass @@ -36,6 +48,44 @@ def whats_new_messages(): return out +class WhatsNewBrowser(QTextBrowser): + def __init__(self, parent=None): + super().__init__(parent) + + css = resources.read_text("sas.qtgui.Utilities.WhatsNew.css", "style.css") + self.css_data = "" + + def setHtml(self, text: str) -> None: + """ Overriden to inject CSS into HTML files - very, very ugly""" + super().setHtml(text.replace("", self.css_data)) + + def loadResource(self, kind: int, url: QUrl | str): + + """ Override the resource discovery to get the images we need """ + + if isinstance(url, QUrl): + name = url.toString() + else: + name = url + + if kind == 2: + + # This is pretty nasty, there's probably a better way + if name.startswith("whatsnew/"): + parts = name.split("/")[1:] + + location = resources.files("sas.qtgui.Utilities.WhatsNew.messages") + + for part in parts: + location = location.joinpath(part) + + im = QPixmap(str(location)) + + return im + + return super().loadResource(kind, url) + + class WhatsNew(QDialog): """ What's New window: displays messages about what is new in this version of SasView @@ -46,12 +96,12 @@ class WhatsNew(QDialog): To add new messages, just dump a (self-contained) html file into the appropriate folder """ - def __init__(self, parent=None): + def __init__(self, parent=None, strictly_newer=True): super().__init__() self.setWindowTitle(f"What's New in SasView {sasview_version}") - self.browser = QTextBrowser() + self.browser = WhatsNewBrowser() self.browser.setOpenLinks(True) self.browser.setOpenExternalLinks(True) @@ -67,15 +117,23 @@ def __init__(self, parent=None): self.closeButton = QPushButton("Close") self.nextButton = QPushButton("Next") - self.showAgain = QCheckBox("Show on Startup") - self.showAgain.setChecked(True) + # Only show the show on startup checkbox if we're not up-to-date + self.buttonLayout.addWidget(self.closeButton) + + if strictly_newer_than(sasview_version, config.LAST_WHATS_NEW_HIDDEN_VERSION): + + self.showAgain = QCheckBox("Show on Startup") + self.showAgain.setChecked(True) + self.buttonLayout.addWidget(self.showAgain) - # add a horizontal spacer - self.buttonLayout.addWidget(self.showAgain) + else: + self.showAgain = None + + # other buttons self.buttonLayout.addSpacerItem(QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)) - self.buttonLayout.addWidget(self.closeButton) self.buttonLayout.addWidget(self.nextButton) + # Viewer self.setLayout(self.mainLayout) self.mainLayout.addWidget(self.browser) @@ -86,7 +144,7 @@ def __init__(self, parent=None): self.nextButton.clicked.connect(self.next_file) # # Gather new files - new_messages = whats_new_messages() + new_messages = whats_new_messages(strictly_newer=strictly_newer) new_message_directories = [key for key in new_messages.keys()] new_message_directories.sort(key=reduced_version) @@ -100,6 +158,7 @@ def __init__(self, parent=None): self.show_file() + self.setFixedSize(800, 600) self.setModal(True) def next_file(self): @@ -112,14 +171,15 @@ def show_file(self): filename = self.all_messages[self.current_index] with open(filename, 'r') as fid: data = fid.read() - self.browser.setText(data) + self.browser.setHtml(data) else: - self.browser.setText("
SasView is now more configurable, and a preference dialog is available though the File menu.
+ +There are settings for graphics, GPU utilisation and more!
+-
+
+ +