From 781f6d9413fa86955f69f51f1f6caf8d5e3c1975 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 10:27:49 +0200 Subject: [PATCH 01/37] use equal aspect ratio in shape preview --- .../Calculators/Shape2SAS/ViewerModel.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py b/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py index f189653070..8d5da35ce0 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py @@ -143,13 +143,24 @@ def initialiseAxis(self): self.scatter.setAxisZ(self.Z_ax) def setAxis(self, x_range: (float, float), y_range: (float, float), z_range: (float, float)): - """Set axis for the model""" - - #FIXME: even if min and max are the same for X, Y, Z, a sphere still looks like an ellipsoid - #Tried with global min and max, and by centering the model, but no success. - self.X_ax.setRange(*x_range) - self.Y_ax.setRange(*y_range) - self.Z_ax.setRange(*z_range) + """Set axis for the model with equal aspect ratio""" + + # Calculate the overall range to ensure equal aspect ratio + x_min, x_max = x_range + y_min, y_max = y_range + z_min, z_max = z_range + x_center = (x_min + x_max) / 2 + y_center = (y_min + y_max) / 2 + z_center = (z_min + z_max) / 2 + max_range = max(x_max - x_min, y_max - y_min, z_max - z_min) + + # Add some padding + half_range = (max_range*1.1) / 2 + + # Set equal ranges for all axes centered on their respective centers + self.X_ax.setRange(x_center - half_range, x_center + half_range) + self.Y_ax.setRange(y_center - half_range, y_center + half_range) + self.Z_ax.setRange(z_center - half_range, z_center + half_range) self.scatter.setAxisX(self.X_ax) self.scatter.setAxisY(self.Y_ax) From c8aa64fcca45f4cc456d66f02bedf1d04042ca07 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 11:51:29 +0200 Subject: [PATCH 02/37] separated models out into separate files --- src/sas/sascalc/shape2sas/Typing.py | 6 + src/sas/sascalc/shape2sas/helpfunctions.py | 504 +----------------- src/sas/sascalc/shape2sas/models/Cube.py | 27 + src/sas/sascalc/shape2sas/models/Cuboid.py | 26 + src/sas/sascalc/shape2sas/models/Cylinder.py | 34 ++ .../sascalc/shape2sas/models/CylinderRing.py | 58 ++ src/sas/sascalc/shape2sas/models/Disc.py | 4 + src/sas/sascalc/shape2sas/models/DiscRing.py | 4 + src/sas/sascalc/shape2sas/models/Ellipsoid.py | 36 ++ .../shape2sas/models/EllipticalCylinder.py | 36 ++ .../sascalc/shape2sas/models/HollowCube.py | 78 +++ .../sascalc/shape2sas/models/HollowSphere.py | 61 +++ src/sas/sascalc/shape2sas/models/Sphere.py | 36 ++ .../shape2sas/models/SuperEllipsoid.py | 49 ++ src/sas/sascalc/shape2sas/models/Template.txt | 37 ++ src/sas/sascalc/shape2sas/models/__init__.py | 18 + 16 files changed, 514 insertions(+), 500 deletions(-) create mode 100644 src/sas/sascalc/shape2sas/Typing.py create mode 100644 src/sas/sascalc/shape2sas/models/Cube.py create mode 100644 src/sas/sascalc/shape2sas/models/Cuboid.py create mode 100644 src/sas/sascalc/shape2sas/models/Cylinder.py create mode 100644 src/sas/sascalc/shape2sas/models/CylinderRing.py create mode 100644 src/sas/sascalc/shape2sas/models/Disc.py create mode 100644 src/sas/sascalc/shape2sas/models/DiscRing.py create mode 100644 src/sas/sascalc/shape2sas/models/Ellipsoid.py create mode 100644 src/sas/sascalc/shape2sas/models/EllipticalCylinder.py create mode 100644 src/sas/sascalc/shape2sas/models/HollowCube.py create mode 100644 src/sas/sascalc/shape2sas/models/HollowSphere.py create mode 100644 src/sas/sascalc/shape2sas/models/Sphere.py create mode 100644 src/sas/sascalc/shape2sas/models/SuperEllipsoid.py create mode 100644 src/sas/sascalc/shape2sas/models/Template.txt create mode 100644 src/sas/sascalc/shape2sas/models/__init__.py diff --git a/src/sas/sascalc/shape2sas/Typing.py b/src/sas/sascalc/shape2sas/Typing.py new file mode 100644 index 0000000000..f4ac940165 --- /dev/null +++ b/src/sas/sascalc/shape2sas/Typing.py @@ -0,0 +1,6 @@ +import numpy as np +from typing import Optional, Tuple, List, Any + +Vector2D = Tuple[np.ndarray, np.ndarray] +Vector3D = Tuple[np.ndarray, np.ndarray, np.ndarray] +Vector4D = Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/helpfunctions.py b/src/sas/sascalc/shape2sas/helpfunctions.py index 9f633ccc64..a062b3b9b3 100644 --- a/src/sas/sascalc/shape2sas/helpfunctions.py +++ b/src/sas/sascalc/shape2sas/helpfunctions.py @@ -1,17 +1,8 @@ from typing import Any import matplotlib.pyplot as plt -import numpy as np -from scipy.special import gamma - -#from dataclasses import dataclass - - -################################ Type Hints ################################ -Vector2D = tuple[np.ndarray, np.ndarray] -Vector3D = tuple[np.ndarray, np.ndarray, np.ndarray] -Vector4D = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] - +from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.models import * ################################ Shape2SAS helper functions ################################### def sinc(x) -> np.ndarray: @@ -21,493 +12,6 @@ def sinc(x) -> np.ndarray: """ return np.sinc(x / np.pi) -'''#template to write a subunit class -class : - def __init__(self, dimensions: List[float]): - #PARAMERERS HERE - self. = dimensions[0] - self. = dimensions[1] - self. = dimensions[2] - - def getVolume(self) -> float: - """Returns the volume of the subunit""" - - - - return - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of the subunit""" - - Volume = self.getVolume() - Volume_max = ###Box around the subunit - Vratio = Volume_max/Volume - - N = int(Vratio * Npoints) - - - - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within the subunit""" - - - - return idx -''' - - -class Sphere: - def __init__(self, dimensions: list[float]): - self.R = dimensions[0] - - def getVolume(self) -> float: - """Returns the volume of a sphere""" - return (4 / 3) * np.pi * self.R**3 - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of a sphere""" - Volume = self.getVolume() - Volume_max = (2*self.R)**3 ###Box around sphere. - Vratio = Volume_max/Volume - - N = int(Vratio * Npoints) - x = np.random.uniform(-self.R, self.R, N) - y = np.random.uniform(-self.R, self.R, N) - z = np.random.uniform(-self.R, self.R, N) - d = np.sqrt(x**2 + y**2 + z**2) - - idx = np.where(d < self.R) #save points inside sphere - x_add,y_add,z_add = x[idx], y[idx], z[idx] - - return x_add, y_add, z_add - - def checkOverlap(self, - x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a sphere""" - - d = np.sqrt(x_eff**2+y_eff**2+z_eff**2) - idx = np.where(d > self.R) - return idx - - -class HollowSphere: - def __init__(self, dimensions: list[float]): - self.R = dimensions[0] - self.r = dimensions[1] - - def getVolume(self) -> float: - """Returns the volume of a hollow sphere""" - if self.r > self.R: - self.R, self.r = self.r, self.R - - if self.r == self.R: - return 4 * np.pi * self.R**2 #surface area of a sphere - else: - return (4 / 3) * np.pi * (self.R**3 - self.r**3) - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of a hollow sphere""" - Volume = self.getVolume() - - if self.r == self.R: - #The hollow sphere is a shell - phi = np.random.uniform(0,2 * np.pi, Npoints) - costheta = np.random.uniform(-1, 1, Npoints) - theta = np.arccos(costheta) - - x_add = self.R * np.sin(theta) * np.cos(phi) - y_add = self.R * np.sin(theta) * np.sin(phi) - z_add = self.R * np.cos(theta) - return x_add, y_add, z_add - - Volume_max = (2*self.R)**3 ###Box around the sphere - Vratio = Volume_max/Volume - - N = int(Vratio * Npoints) - x = np.random.uniform(-self.R, self.R, N) - y = np.random.uniform(-self.R, self.R, N) - z = np.random.uniform(-self.R, self.R, N) - d = np.sqrt(x**2 + y**2 + z**2) - - idx = np.where((d < self.R) & (d > self.r)) - x_add, y_add, z_add = x[idx], y[idx], z[idx] - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a hollow sphere""" - - d = np.sqrt(x_eff**2+y_eff**2+z_eff**2) - if self.r > self.R: - self.r, self.R = self.R, self.r - - if self.r == self.R: - idx = np.where(d != self.R) - return idx - - else: - idx = np.where((d > self.R) | (d < self.r)) - return idx - - -class Cylinder: - def __init__(self, dimensions: list[float]): - self.R = dimensions[0] - self.l = dimensions[1] - - def getVolume(self) -> float: - """Returns the volume of a cylinder""" - return np.pi * self.R**2 * self.l - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of a cylinder""" - Volume = self.getVolume() - Volume_max = 2 * self.R * 2 * self.R * self.l - Vratio = Volume_max / Volume - - N = int(Vratio * Npoints) - x = np.random.uniform(-self.R, self.R, N) - y = np.random.uniform(-self.R, self.R, N) - z = np.random.uniform(-self.l / 2, self.l / 2, N) - d = np.sqrt(x**2 + y**2) - idx = np.where(d < self.R) - x_add,y_add,z_add = x[idx],y[idx],z[idx] - - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a cylinder""" - d = np.sqrt(x_eff**2+y_eff**2) - idx = np.where((d > self.R) | (abs(z_eff) > self.l / 2)) - return idx - - -class Ellipsoid: - def __init__(self, dimensions: list[float]): - self.a = dimensions[0] - self.b = dimensions[1] - self.c = dimensions[2] - - def getVolume(self) -> float: - """Returns the volume of an ellipsoid""" - return (4 / 3) * np.pi * self.a * self.b * self.c - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of an ellipsoid""" - Volume = self.getVolume() - Volume_max = 2 * self.a * 2 * self.b * 2 * self.c - Vratio = Volume_max / Volume - - N = int(Vratio * Npoints) - x = np.random.uniform(-self.a, self.a, N) - y = np.random.uniform(-self.b, self.b, N) - z = np.random.uniform(-self.c, self.c, N) - - d2 = x**2 / self.a**2 + y**2 / self.b**2 + z**2 / self.c**2 - idx = np.where(d2 < 1) - x_add, y_add, z_add = x[idx], y[idx], z[idx] - - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """check for points within a ellipsoid""" - d2 = x_eff**2 / self.a**2 + y_eff**2 / self.b**2 + z_eff**2 / self.c**2 - idx = np.where(d2 > 1) - return idx - - -class EllipticalCylinder: - def __init__(self, dimensions: list[float]): - self.a = dimensions[0] - self.b = dimensions[1] - self.l = dimensions[2] - - def getVolume(self) -> float: - """Returns the volume of an elliptical cylinder""" - return np.pi * self.a * self.b * self.l - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of an elliptical cylinder""" - Volume = self.getVolume() - Volume_max = 2 * self.a * 2 * self.b * self.l - Vratio = Volume_max / Volume - - N = int(Vratio * Npoints) - x = np.random.uniform(-self.a, self.a, N) - y = np.random.uniform(-self.b, self.b, N) - z = np.random.uniform(-self.l / 2, self.l / 2, N) - - d2 = x**2 / self.a**2 + y**2 / self.b**2 - idx = np.where(d2 < 1) - x_add, y_add, z_add = x[idx], y[idx], z[idx] - - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a Elliptical cylinder""" - d2 = x_eff**2 / self.a**2 + y_eff**2 / self.b**2 - idx = np.where((d2 > 1) | (abs(z_eff) > self.l / 2)) - return idx - - -class Disc(EllipticalCylinder): - pass - - -class Cube: - def __init__(self, dimensions: list[float]): - self.a = dimensions[0] - - def getVolume(self) -> float: - """Returns the volume of a cube""" - return self.a**3 - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of a cube""" - - #Volume = self.getVolume() - N = Npoints - x_add = np.random.uniform(-self.a / 2, self.a / 2, N) - y_add = np.random.uniform(-self.a / 2, self.a / 2, N) - z_add = np.random.uniform(-self.a / 2, self.a / 2, N) - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a cube""" - idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.a/2) | - (abs(z_eff) >= self.a/2)) - return idx - - -class HollowCube: - def __init__(self, dimensions: list[float]): - self.a = dimensions[0] - self.b = dimensions[1] - - def getVolume(self) -> float: - """Returns the volume of a hollow cube""" - - if self.a < self.b: - self.a, self.b = self.b, self.a - - if self.a == self.b: - return 6 * self.a**2 #surface area of a cube - - else: - return (self.a - self.b)**3 - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of a hollow cube""" - - Volume = self.getVolume() - - if self.a == self.b: - #The hollow cube is a shell - d = self.a / 2 - N = int(Npoints / 6) - one = np.ones(N) - - #make each side of the cube at a time - x_add, y_add, z_add = [], [], [] - for sign in [-1, 1]: - x_add = np.concatenate((x_add, sign * one * d)) - y_add = np.concatenate((y_add, np.random.uniform(-d, d, N))) - z_add = np.concatenate((z_add, np.random.uniform(-d, d, N))) - - x_add = np.concatenate((x_add, np.random.uniform(-d, d, N))) - y_add = np.concatenate((y_add, sign * one * d)) - z_add = np.concatenate((z_add, np.random.uniform(-d, d, N))) - - x_add = np.concatenate((x_add, np.random.uniform(-d, d, N))) - y_add = np.concatenate((y_add, np.random.uniform(-d, d, N))) - z_add = np.concatenate((z_add, sign * one * d)) - return x_add, y_add, z_add - - Volume_max = self.a**3 - Vratio = Volume_max / Volume - N = int(Vratio * Npoints) - - x = np.random.uniform(-self.a / 2,self.a / 2, N) - y = np.random.uniform(-self.a / 2,self.a / 2, N) - z = np.random.uniform(-self.a / 2,self.a / 2, N) - - d = np.maximum.reduce([abs(x), abs(y), abs(z)]) - idx = np.where(d >= self.b / 2) - x_add,y_add,z_add = x[idx], y[idx], z[idx] - - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a hollow cube""" - - if self.a < self.b: - self.a, self.b = self.b, self.a - - if self.a == self.b: - idx = np.where((abs(x_eff)!=self.a/2) | (abs(y_eff)!=self.a/2) | (abs(z_eff)!=self.a/2)) - return idx - - else: - idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.a/2) | - (abs(z_eff) >= self.a/2) | ((abs(x_eff) <= self.b/2) - & (abs(y_eff) <= self.b/2) & (abs(z_eff) <= self.b/2))) - - return idx - - -class Cuboid: - def __init__(self, dimensions: list[float]): - self.a = dimensions[0] - self.b = dimensions[1] - self.c = dimensions[2] - - def getVolume(self) -> float: - """Returns the volume of a cuboid""" - return self.a * self.b * self.c - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of a cuboid""" - x_add = np.random.uniform(-self.a, self.a, Npoints) - y_add = np.random.uniform(-self.b, self.b, Npoints) - z_add = np.random.uniform(-self.c, self.c, Npoints) - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a Cuboid""" - idx = np.where((abs(x_eff) >= self.a/2) - | (abs(y_eff) >= self.b/2) | (abs(z_eff) >= self.c/2)) - return idx - - -class CylinderRing: - def __init__(self, dimensions: list[float]): - self.R = dimensions[0] - self.r = dimensions[1] - self.l = dimensions[2] - - def getVolume(self) -> float: - """Returns the volume of a cylinder ring""" - - if self.r > self.R: - self.R, self.r = self.r, self.R - - if self.r == self.R: - return 2 * np.pi * self.R * self.l #surface area of a cylinder - - else: - return np.pi * (self.R**2 - self.r**2) * self.l - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of a cylinder ring""" - Volume = self.getVolume() - - if self.r == self.R: - #The cylinder ring is a shell - phi = np.random.uniform(0, 2 * np.pi, Npoints) - x_add = self.R * np.cos(phi) - y_add = self.R * np.sin(phi) - z_add = np.random.uniform(-self.l / 2, self.l / 2, Npoints) - return x_add, y_add, z_add - - Volume_max = 2 * self.R * 2 * self.R * self.l - Vratio = Volume_max / Volume - N = int(Vratio * Npoints) - x = np.random.uniform(-self.R, self.R, N) - y = np.random.uniform(-self.R, self.R, N) - z = np.random.uniform(-self.l / 2, self.l / 2, N) - d = np.sqrt(x**2 + y**2) - idx = np.where((d < self.R) & (d > self.r)) - x_add, y_add, z_add = x[idx], y[idx], z[idx] - - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a cylinder ring""" - d = np.sqrt(x_eff**2 + y_eff**2) - if self.r > self.R: - self.R, self.r = self.r, self.R - - if self.r == self.R: - idx = np.where((d != self.R) | (abs(z_eff) > self.l / 2)) - return idx - else: - idx = np.where((d > self.R) | (d < self.r) | (abs(z_eff) > self.l / 2)) - return idx - - -class DiscRing(CylinderRing): - pass - - -class Superellipsoid: - def __init__(self, dimensions: list[float]): - self.R = dimensions[0] - self.eps = dimensions[1] - self.t = dimensions[2] - self.s = dimensions[3] - - @staticmethod - def beta(a, b) -> float: - """beta function""" - - return gamma(a) * gamma(b) / gamma(a + b) - - def getVolume(self) -> float: - """Returns the volume of a superellipsoid""" - - return (8 / (3 * self.t * self.s) * self.R**3 * self.eps * - self.beta(1 / self.s, 1 / self.s) * self.beta(2 / self.t, 1 / self.t)) - - def getPointDistribution(self, Npoints: int) -> Vector3D: - """Returns the point distribution of a superellipsoid""" - Volume = self.getVolume() - Volume_max = 2 * self.R * self.eps * 2 * self.R * 2 * self.R - Vratio = Volume_max / Volume - - N = int(Vratio * Npoints) - x = np.random.uniform(-self.R, self.R, N) - y = np.random.uniform(-self.R, self.R, N) - z = np.random.uniform(-self.R * self.eps, self.R * self.eps, N) - - d = ((np.abs(x)**self.s + np.abs(y)**self.s)**(self.t/ self.s) - + np.abs(z / self.eps)**self.t) - idx = np.where(d < np.abs(self.R)**self.t) - x_add, y_add, z_add = x[idx], y[idx], z[idx] - - return x_add, y_add, z_add - - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, - z_eff: np.ndarray) -> np.ndarray: - """Check for points within a superellipsoid""" - d = ((np.abs(x_eff)**self.s + np.abs(y_eff)**self.s)**(self.t / self.s) - + np.abs(z_eff / self.eps)**self.t) - idx = np.where(d >= np.abs(self.R)**self.t) - - return idx - - class Qsampling: def onQsampling(qmin: float, qmax: float, Nq: int) -> np.ndarray: """Returns uniform q sampling""" @@ -688,8 +192,8 @@ def setAvailableSubunits(self): "disc_ring": DiscRing, "Disc ring": DiscRing, - - "superellipsoid": Superellipsoid} + + "superellipsoid": SuperEllipsoid} def getSubunitClass(self, key: str): if key in self.subunitClasses: diff --git a/src/sas/sascalc/shape2sas/models/Cube.py b/src/sas/sascalc/shape2sas/models/Cube.py new file mode 100644 index 0000000000..f5a657d636 --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/Cube.py @@ -0,0 +1,27 @@ +from sas.sascalc.shape2sas.Typing import * + +class Cube: + def __init__(self, dimensions: List[float]): + self.a = dimensions[0] + + def getVolume(self) -> float: + """Returns the volume of a cube""" + return self.a**3 + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of a cube""" + + #Volume = self.getVolume() + N = Npoints + x_add = np.random.uniform(-self.a / 2, self.a / 2, N) + y_add = np.random.uniform(-self.a / 2, self.a / 2, N) + z_add = np.random.uniform(-self.a / 2, self.a / 2, N) + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a cube""" + idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.a/2) | + (abs(z_eff) >= self.a/2)) + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/Cuboid.py b/src/sas/sascalc/shape2sas/models/Cuboid.py new file mode 100644 index 0000000000..5e74740e7e --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/Cuboid.py @@ -0,0 +1,26 @@ +from sas.sascalc.shape2sas.Typing import * + +class Cuboid: + def __init__(self, dimensions: List[float]): + self.a = dimensions[0] + self.b = dimensions[1] + self.c = dimensions[2] + + def getVolume(self) -> float: + """Returns the volume of a cuboid""" + return self.a * self.b * self.c + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of a cuboid""" + x_add = np.random.uniform(-self.a, self.a, Npoints) + y_add = np.random.uniform(-self.b, self.b, Npoints) + z_add = np.random.uniform(-self.c, self.c, Npoints) + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a Cuboid""" + idx = np.where((abs(x_eff) >= self.a/2) + | (abs(y_eff) >= self.b/2) | (abs(z_eff) >= self.c/2)) + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/Cylinder.py b/src/sas/sascalc/shape2sas/models/Cylinder.py new file mode 100644 index 0000000000..59ce916e83 --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/Cylinder.py @@ -0,0 +1,34 @@ +from sas.sascalc.shape2sas.Typing import * + +class Cylinder: + def __init__(self, dimensions: List[float]): + self.R = dimensions[0] + self.l = dimensions[1] + + def getVolume(self) -> float: + """Returns the volume of a cylinder""" + return np.pi * self.R**2 * self.l + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of a cylinder""" + Volume = self.getVolume() + Volume_max = 2 * self.R * 2 * self.R * self.l + Vratio = Volume_max / Volume + + N = int(Vratio * Npoints) + x = np.random.uniform(-self.R, self.R, N) + y = np.random.uniform(-self.R, self.R, N) + z = np.random.uniform(-self.l / 2, self.l / 2, N) + d = np.sqrt(x**2 + y**2) + idx = np.where(d < self.R) + x_add,y_add,z_add = x[idx],y[idx],z[idx] + + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a cylinder""" + d = np.sqrt(x_eff**2+y_eff**2) + idx = np.where((d > self.R) | (abs(z_eff) > self.l / 2)) + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/CylinderRing.py b/src/sas/sascalc/shape2sas/models/CylinderRing.py new file mode 100644 index 0000000000..d4906635a0 --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/CylinderRing.py @@ -0,0 +1,58 @@ +from sas.sascalc.shape2sas.Typing import * + +class CylinderRing: + def __init__(self, dimensions: List[float]): + self.R = dimensions[0] + self.r = dimensions[1] + self.l = dimensions[2] + + def getVolume(self) -> float: + """Returns the volume of a cylinder ring""" + + if self.r > self.R: + self.R, self.r = self.r, self.R + + if self.r == self.R: + return 2 * np.pi * self.R * self.l #surface area of a cylinder + + else: + return np.pi * (self.R**2 - self.r**2) * self.l + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of a cylinder ring""" + Volume = self.getVolume() + + if self.r == self.R: + #The cylinder ring is a shell + phi = np.random.uniform(0, 2 * np.pi, Npoints) + x_add = self.R * np.cos(phi) + y_add = self.R * np.sin(phi) + z_add = np.random.uniform(-self.l / 2, self.l / 2, Npoints) + return x_add, y_add, z_add + + Volume_max = 2 * self.R * 2 * self.R * self.l + Vratio = Volume_max / Volume + N = int(Vratio * Npoints) + x = np.random.uniform(-self.R, self.R, N) + y = np.random.uniform(-self.R, self.R, N) + z = np.random.uniform(-self.l / 2, self.l / 2, N) + d = np.sqrt(x**2 + y**2) + idx = np.where((d < self.R) & (d > self.r)) + x_add, y_add, z_add = x[idx], y[idx], z[idx] + + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a cylinder ring""" + d = np.sqrt(x_eff**2 + y_eff**2) + if self.r > self.R: + self.R, self.r = self.r, self.R + + if self.r == self.R: + idx = np.where((d != self.R) | (abs(z_eff) > self.l / 2)) + return idx + else: + idx = np.where((d > self.R) | (d < self.r) | (abs(z_eff) > self.l / 2)) + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/Disc.py b/src/sas/sascalc/shape2sas/models/Disc.py new file mode 100644 index 0000000000..459c484edb --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/Disc.py @@ -0,0 +1,4 @@ +from sas.sascalc.shape2sas.models.EllipticalCylinder import EllipticalCylinder + +class Disc(EllipticalCylinder): + pass \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/DiscRing.py b/src/sas/sascalc/shape2sas/models/DiscRing.py new file mode 100644 index 0000000000..dc8c5756ed --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/DiscRing.py @@ -0,0 +1,4 @@ +from sas.sascalc.shape2sas.models.CylinderRing import CylinderRing + +class DiscRing(CylinderRing): + pass diff --git a/src/sas/sascalc/shape2sas/models/Ellipsoid.py b/src/sas/sascalc/shape2sas/models/Ellipsoid.py new file mode 100644 index 0000000000..f7be308c74 --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/Ellipsoid.py @@ -0,0 +1,36 @@ +from sas.sascalc.shape2sas.Typing import * + +class Ellipsoid: + def __init__(self, dimensions: List[float]): + self.a = dimensions[0] + self.b = dimensions[1] + self.c = dimensions[2] + + def getVolume(self) -> float: + """Returns the volume of an ellipsoid""" + return (4 / 3) * np.pi * self.a * self.b * self.c + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of an ellipsoid""" + Volume = self.getVolume() + Volume_max = 2 * self.a * 2 * self.b * 2 * self.c + Vratio = Volume_max / Volume + + N = int(Vratio * Npoints) + x = np.random.uniform(-self.a, self.a, N) + y = np.random.uniform(-self.b, self.b, N) + z = np.random.uniform(-self.c, self.c, N) + + d2 = x**2 / self.a**2 + y**2 / self.b**2 + z**2 / self.c**2 + idx = np.where(d2 < 1) + x_add, y_add, z_add = x[idx], y[idx], z[idx] + + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """check for points within a ellipsoid""" + d2 = x_eff**2 / self.a**2 + y_eff**2 / self.b**2 + z_eff**2 / self.c**2 + idx = np.where(d2 > 1) + return idx diff --git a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py new file mode 100644 index 0000000000..f069b84a17 --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py @@ -0,0 +1,36 @@ +from sas.sascalc.shape2sas.Typing import * + +class EllipticalCylinder: + def __init__(self, dimensions: List[float]): + self.a = dimensions[0] + self.b = dimensions[1] + self.l = dimensions[2] + + def getVolume(self) -> float: + """Returns the volume of an elliptical cylinder""" + return np.pi * self.a * self.b * self.l + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of an elliptical cylinder""" + Volume = self.getVolume() + Volume_max = 2 * self.a * 2 * self.b * self.l + Vratio = Volume_max / Volume + + N = int(Vratio * Npoints) + x = np.random.uniform(-self.a, self.a, N) + y = np.random.uniform(-self.b, self.b, N) + z = np.random.uniform(-self.l / 2, self.l / 2, N) + + d2 = x**2 / self.a**2 + y**2 / self.b**2 + idx = np.where(d2 < 1) + x_add, y_add, z_add = x[idx], y[idx], z[idx] + + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a Elliptical cylinder""" + d2 = x_eff**2 / self.a**2 + y_eff**2 / self.b**2 + idx = np.where((d2 > 1) | (abs(z_eff) > self.l / 2)) + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/HollowCube.py b/src/sas/sascalc/shape2sas/models/HollowCube.py new file mode 100644 index 0000000000..5edafb1f3d --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/HollowCube.py @@ -0,0 +1,78 @@ +from sas.sascalc.shape2sas.Typing import * + +class HollowCube: + def __init__(self, dimensions: List[float]): + self.a = dimensions[0] + self.b = dimensions[1] + + def getVolume(self) -> float: + """Returns the volume of a hollow cube""" + + if self.a < self.b: + self.a, self.b = self.b, self.a + + if self.a == self.b: + return 6 * self.a**2 #surface area of a cube + + else: + return (self.a - self.b)**3 + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of a hollow cube""" + + Volume = self.getVolume() + + if self.a == self.b: + #The hollow cube is a shell + d = self.a / 2 + N = int(Npoints / 6) + one = np.ones(N) + + #make each side of the cube at a time + x_add, y_add, z_add = [], [], [] + for sign in [-1, 1]: + x_add = np.concatenate((x_add, sign * one * d)) + y_add = np.concatenate((y_add, np.random.uniform(-d, d, N))) + z_add = np.concatenate((z_add, np.random.uniform(-d, d, N))) + + x_add = np.concatenate((x_add, np.random.uniform(-d, d, N))) + y_add = np.concatenate((y_add, sign * one * d)) + z_add = np.concatenate((z_add, np.random.uniform(-d, d, N))) + + x_add = np.concatenate((x_add, np.random.uniform(-d, d, N))) + y_add = np.concatenate((y_add, np.random.uniform(-d, d, N))) + z_add = np.concatenate((z_add, sign * one * d)) + return x_add, y_add, z_add + + Volume_max = self.a**3 + Vratio = Volume_max / Volume + N = int(Vratio * Npoints) + + x = np.random.uniform(-self.a / 2,self.a / 2, N) + y = np.random.uniform(-self.a / 2,self.a / 2, N) + z = np.random.uniform(-self.a / 2,self.a / 2, N) + + d = np.maximum.reduce([abs(x), abs(y), abs(z)]) + idx = np.where(d >= self.b / 2) + x_add,y_add,z_add = x[idx], y[idx], z[idx] + + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a hollow cube""" + + if self.a < self.b: + self.a, self.b = self.b, self.a + + if self.a == self.b: + idx = np.where((abs(x_eff)!=self.a/2) | (abs(y_eff)!=self.a/2) | (abs(z_eff)!=self.a/2)) + return idx + + else: + idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.a/2) | + (abs(z_eff) >= self.a/2) | ((abs(x_eff) <= self.b/2) + & (abs(y_eff) <= self.b/2) & (abs(z_eff) <= self.b/2))) + + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/HollowSphere.py b/src/sas/sascalc/shape2sas/models/HollowSphere.py new file mode 100644 index 0000000000..9794633b66 --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/HollowSphere.py @@ -0,0 +1,61 @@ +from sas.sascalc.shape2sas.Typing import * + +class HollowSphere: + def __init__(self, dimensions: List[float]): + self.R = dimensions[0] + self.r = dimensions[1] + + def getVolume(self) -> float: + """Returns the volume of a hollow sphere""" + if self.r > self.R: + self.R, self.r = self.r, self.R + + if self.r == self.R: + return 4 * np.pi * self.R**2 #surface area of a sphere + else: + return (4 / 3) * np.pi * (self.R**3 - self.r**3) + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of a hollow sphere""" + Volume = self.getVolume() + + if self.r == self.R: + #The hollow sphere is a shell + phi = np.random.uniform(0,2 * np.pi, Npoints) + costheta = np.random.uniform(-1, 1, Npoints) + theta = np.arccos(costheta) + + x_add = self.R * np.sin(theta) * np.cos(phi) + y_add = self.R * np.sin(theta) * np.sin(phi) + z_add = self.R * np.cos(theta) + return x_add, y_add, z_add + + Volume_max = (2*self.R)**3 ###Box around the sphere + Vratio = Volume_max/Volume + + N = int(Vratio * Npoints) + x = np.random.uniform(-self.R, self.R, N) + y = np.random.uniform(-self.R, self.R, N) + z = np.random.uniform(-self.R, self.R, N) + d = np.sqrt(x**2 + y**2 + z**2) + + idx = np.where((d < self.R) & (d > self.r)) + x_add, y_add, z_add = x[idx], y[idx], z[idx] + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a hollow sphere""" + + d = np.sqrt(x_eff**2+y_eff**2+z_eff**2) + if self.r > self.R: + self.r, self.R = self.R, self.r + + if self.r == self.R: + idx = np.where(d != self.R) + return idx + + else: + idx = np.where((d > self.R) | (d < self.r)) + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/Sphere.py b/src/sas/sascalc/shape2sas/models/Sphere.py new file mode 100644 index 0000000000..291cf2e577 --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/Sphere.py @@ -0,0 +1,36 @@ +from sas.sascalc.shape2sas.Typing import * + +class Sphere: + def __init__(self, dimensions: List[float]): + self.R = dimensions[0] + + def getVolume(self) -> float: + """Returns the volume of a sphere""" + return (4 / 3) * np.pi * self.R**3 + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of a sphere""" + Volume = self.getVolume() + Volume_max = (2*self.R)**3 ###Box around sphere. + Vratio = Volume_max/Volume + + N = int(Vratio * Npoints) + x = np.random.uniform(-self.R, self.R, N) + y = np.random.uniform(-self.R, self.R, N) + z = np.random.uniform(-self.R, self.R, N) + d = np.sqrt(x**2 + y**2 + z**2) + + idx = np.where(d < self.R) #save points inside sphere + x_add,y_add,z_add = x[idx], y[idx], z[idx] + + return x_add, y_add, z_add + + def checkOverlap(self, + x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a sphere""" + + d = np.sqrt(x_eff**2+y_eff**2+z_eff**2) + idx = np.where(d > self.R) + return idx diff --git a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py new file mode 100644 index 0000000000..d2ead1be04 --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py @@ -0,0 +1,49 @@ +from sas.sascalc.shape2sas.Typing import * +from scipy.special import gamma + +class SuperEllipsoid: + def __init__(self, dimensions: List[float]): + self.R = dimensions[0] + self.eps = dimensions[1] + self.t = dimensions[2] + self.s = dimensions[3] + + @staticmethod + def beta(a, b) -> float: + """beta function""" + + return gamma(a) * gamma(b) / gamma(a + b) + + def getVolume(self) -> float: + """Returns the volume of a superellipsoid""" + + return (8 / (3 * self.t * self.s) * self.R**3 * self.eps * + self.beta(1 / self.s, 1 / self.s) * self.beta(2 / self.t, 1 / self.t)) + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of a superellipsoid""" + Volume = self.getVolume() + Volume_max = 2 * self.R * self.eps * 2 * self.R * 2 * self.R + Vratio = Volume_max / Volume + + N = int(Vratio * Npoints) + x = np.random.uniform(-self.R, self.R, N) + y = np.random.uniform(-self.R, self.R, N) + z = np.random.uniform(-self.R * self.eps, self.R * self.eps, N) + + d = ((np.abs(x)**self.s + np.abs(y)**self.s)**(self.t/ self.s) + + np.abs(z / self.eps)**self.t) + idx = np.where(d < np.abs(self.R)**self.t) + x_add, y_add, z_add = x[idx], y[idx], z[idx] + + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within a superellipsoid""" + d = ((np.abs(x_eff)**self.s + np.abs(y_eff)**self.s)**(self.t / self.s) + + np.abs(z_eff / self.eps)**self.t) + idx = np.where(d >= np.abs(self.R)**self.t) + + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/Template.txt b/src/sas/sascalc/shape2sas/models/Template.txt new file mode 100644 index 0000000000..718fdbd31e --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/Template.txt @@ -0,0 +1,37 @@ +Template to write a subunit class + +class : + def __init__(self, dimensions: List[float]): + #PARAMERERS HERE + self. = dimensions[0] + self. = dimensions[1] + self. = dimensions[2] + + def getVolume(self) -> float: + """Returns the volume of the subunit""" + + + + return + + def getPointDistribution(self, Npoints: int) -> Vector3D: + """Returns the point distribution of the subunit""" + + Volume = self.getVolume() + Volume_max = ###Box around the subunit + Vratio = Volume_max/Volume + + N = int(Vratio * Npoints) + + + + return x_add, y_add, z_add + + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, + z_eff: np.ndarray) -> np.ndarray: + """Check for points within the subunit""" + + + + return idx \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/__init__.py b/src/sas/sascalc/shape2sas/models/__init__.py new file mode 100644 index 0000000000..4c92b0bada --- /dev/null +++ b/src/sas/sascalc/shape2sas/models/__init__.py @@ -0,0 +1,18 @@ +from .Cube import Cube +from .Cuboid import Cuboid +from .Cylinder import Cylinder +from .CylinderRing import CylinderRing +from .Disc import Disc +from .DiscRing import DiscRing +from .Ellipsoid import Ellipsoid +from .EllipticalCylinder import EllipticalCylinder +from .HollowCube import HollowCube +from .HollowSphere import HollowSphere +from .Sphere import Sphere +from .SuperEllipsoid import SuperEllipsoid + +__all__ = [ + 'Cube', 'Cuboid', 'Cylinder', 'CylinderRing', + 'Disc', 'DiscRing', 'Ellipsoid', 'EllipticalCylinder', + 'HollowCube', 'HollowSphere', 'Sphere', 'SuperEllipsoid' +] \ No newline at end of file From 5772f8d4aebd5d2965d33d59a3d89d23a74f0b59 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 11:52:00 +0200 Subject: [PATCH 03/37] re-enabled plugin model button --- src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index 62830ade42..d2bc085ea1 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -91,10 +91,6 @@ def __init__(self, parent=None): self.plugin.setEnabled(False) self.modelTabButtonOptions.horizontalLayout_5.insertWidget(1, self.plugin) - # TODO: Remove these lines to enable the plugin model generation window - hidden for v6.1.0 - self.line2.setHidden(True) - self.plugin.setHidden(True) - #connect buttons self.modelTabButtonOptions.reset.clicked.connect(self.onSubunitTableReset) self.modelTabButtonOptions.closePage.clicked.connect(self.onClickingClose) From be085197a9af64a97f5b068fdee080273be7a84f Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 12:36:57 +0200 Subject: [PATCH 04/37] separated out structure factors --- src/sas/sascalc/shape2sas/Math.py | 8 + src/sas/sascalc/shape2sas/Shape2SAS.py | 19 +- src/sas/sascalc/shape2sas/StructureFactor.py | 66 ++++ src/sas/sascalc/shape2sas/helpfunctions.py | 298 +----------------- .../structure_factors/Aggregation.py | 48 +++ .../structure_factors/HardSphereStructure.py | 74 +++++ .../structure_factors/NoStructure.py | 17 + .../StructureDecouplingApprox.py | 95 ++++++ .../shape2sas/structure_factors/__init__.py | 10 + 9 files changed, 323 insertions(+), 312 deletions(-) create mode 100644 src/sas/sascalc/shape2sas/Math.py create mode 100644 src/sas/sascalc/shape2sas/StructureFactor.py create mode 100644 src/sas/sascalc/shape2sas/structure_factors/Aggregation.py create mode 100644 src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py create mode 100644 src/sas/sascalc/shape2sas/structure_factors/NoStructure.py create mode 100644 src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py create mode 100644 src/sas/sascalc/shape2sas/structure_factors/__init__.py diff --git a/src/sas/sascalc/shape2sas/Math.py b/src/sas/sascalc/shape2sas/Math.py new file mode 100644 index 0000000000..a6cbcdfd77 --- /dev/null +++ b/src/sas/sascalc/shape2sas/Math.py @@ -0,0 +1,8 @@ +import numpy as np + +def sinc(x) -> np.ndarray: + """ + function for calculating sinc = sin(x)/x + numpy.sinc is defined as sinc(x) = sin(pi*x)/(pi*x) + """ + return np.sinc(x / np.pi) \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index 6d48ca43cf..d279c2514b 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -4,18 +4,10 @@ import warnings from dataclasses import dataclass, field -import numpy as np - +from sas.sascalc.shape2sas.StructureFactor import StructureFactor from sas.sascalc.shape2sas.helpfunctions import ( - GenerateAllPoints, - IExperimental, - ITheoretical, - Qsampling, - StructureFactor, - WeightedPairDistribution, - generate_pdb, - plot_2D, - plot_results, + GenerateAllPoints, WeightedPairDistribution, ITheoretical, IExperimental, Qsampling, + plot_2D, plot_results, generate_pdb ) Vectors = list[list[float]] @@ -94,9 +86,6 @@ class TheoreticalScattering: I0: np.ndarray I: np.ndarray S_eff: np.ndarray - r: np.ndarray #pair distance distribution - pr: np.ndarray #pair distance distribution - pr_norm: np.ndarray #normalized pair distance distribution @dataclass @@ -152,7 +141,7 @@ def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> Theoret I = I_theory.calc_Iq(Pq, S_eff, sys.sigma_r) - return TheoreticalScattering(q=q, I=I, I0=I0, S_eff=S_eff, r=r, pr=pr, pr_norm=pr_norm) + return TheoreticalScattering(q=q, I=I, I0=I0, S_eff=S_eff) def getSimulatedScattering(scalc: SimulateScattering) -> SimulatedScattering: diff --git a/src/sas/sascalc/shape2sas/StructureFactor.py b/src/sas/sascalc/shape2sas/StructureFactor.py new file mode 100644 index 0000000000..273ee4b25b --- /dev/null +++ b/src/sas/sascalc/shape2sas/StructureFactor.py @@ -0,0 +1,66 @@ +from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.structure_factors import * +import numpy as np + +class StructureFactor: + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray, + Stype: str, + par: Optional[List[float]]): + self.q = q + self.x_new = x_new + self.y_new = y_new + self.z_new = z_new + self.p_new = p_new + self.Stype = Stype + self.par = par + self.setAvailableStructureFactors() + + def setAvailableStructureFactors(self): + """Available structure factors""" + self.structureFactor = { + 'HS': HardSphereStructure, + 'Hard Sphere': HardSphereStructure, + 'aggregation': Aggregation, + 'Aggregation': Aggregation, + 'None': NoStructure + } + + def getStructureFactorClass(self): + """Return chosen structure factor""" + if self.Stype in self.structureFactor: + return self.structureFactor[self.Stype](self.q, self.x_new, self.y_new, self.z_new, self.p_new, self.par) + + else: + try: + return globals()[self.Stype](self.q, self.x_new, self.y_new, self.z_new, self.p_new, self.par) + except KeyError: + ValueError(f"Structure factor '{self.Stype}' was not found in structureFactor or global scope.") + + @staticmethod + def getparname(name: str) -> List[str]: + """Return the name of the parameters""" + pars = { + 'HS': {'conc': 0.02,'r_hs': 50}, + 'Hard Sphere': {'conc': 0.02,'r_hs': 50}, + 'Aggregation': {'R_eff': 50, 'N_aggr': 80, 'frac': 0.1}, + 'aggregation': {'R_eff': 50, 'N_aggr': 80, 'frac': 0.1}, + 'None': {} + } + return pars[name] + + @staticmethod + def save_S(q: np.ndarray, S_eff: np.ndarray, Model: str): + """ + save S to file + """ + + with open('Sq%s.dat' % Model,'w') as f: + f.write('# Structure factor, S(q), used in: I(q) = P(q)*S(q)\n') + f.write('# Default: S(q) = 1.0\n') + f.write('# %-17s %-17s\n' % ('q','S(q)')) + for (q_i, S_i) in zip(q, S_eff): + f.write(' %-17.5e%-17.5e\n' % (q_i, S_i)) diff --git a/src/sas/sascalc/shape2sas/helpfunctions.py b/src/sas/sascalc/shape2sas/helpfunctions.py index a062b3b9b3..565e00cd26 100644 --- a/src/sas/sascalc/shape2sas/helpfunctions.py +++ b/src/sas/sascalc/shape2sas/helpfunctions.py @@ -3,15 +3,9 @@ import matplotlib.pyplot as plt from sas.sascalc.shape2sas.Typing import * from sas.sascalc.shape2sas.models import * +from sas.sascalc.shape2sas.Math import sinc ################################ Shape2SAS helper functions ################################### -def sinc(x) -> np.ndarray: - """ - function for calculating sinc = sin(x)/x - numpy.sinc is defined as sinc(x) = sin(pi*x)/(pi*x) - """ - return np.sinc(x / np.pi) - class Qsampling: def onQsampling(qmin: float, qmax: float, Nq: int) -> np.ndarray: """Returns uniform q sampling""" @@ -584,296 +578,6 @@ def save_pr(Nbins: int, f.write(' %-17.5e %-17.5e\n' % (r[i], pr_norm[i])) -class StructureDecouplingApprox: - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray): - self.q = q - self.x_new = x_new - self.y_new = y_new - self.z_new = z_new - self.p_new = p_new - - def calc_com_dist(self) -> np.ndarray: - """ - calc contrast-weighted com distance - """ - w = np.abs(self.p_new) - - if np.sum(w) == 0: - w = np.ones(len(self.x_new)) - - x_com, y_com, z_com = np.average(self.x_new, weights=w), np.average(self.y_new, weights=w), np.average(self.z_new, weights=w) - dx, dy, dz = self.x_new - x_com, self.y_new - y_com, self.z_new - z_com - com_dist = np.sqrt(dx**2 + dy**2 + dz**2) - - return com_dist - - def calc_A00(self) -> np.ndarray: - """ - calc zeroth order sph harm, for decoupling approximation - """ - d_new = self.calc_com_dist() - M = len(self.q) - A00 = np.zeros(M) - - for i in range(M): - qr = self.q[i] * d_new - - A00[i] = sum(self.p_new * sinc(qr)) - A00 = A00 / A00[0] # normalise, A00[0] = 1 - - return A00 - - def decoupling_approx(self, Pq: np.ndarray, S: np.ndarray) -> np.ndarray: - """ - modify structure factor with the decoupling approximation - for combining structure factors with non-spherical (or polydisperse) particles - - see, for example, Larsen et al 2020: https://doi.org/10.1107/S1600576720006500 - and refs therein - - input - q - x,y,z,p : coordinates and contrasts - Pq : form factor - S : structure factor - - output - S_eff : effective structure factor, after applying decoupl. approx - """ - - A00 = self.calc_A00() - const = 1e-3 # add constant in nominator and denominator, for stability (numerical errors for small values dampened) - Beta = (A00**2 + const) / (Pq + const) - S_eff = 1 + Beta * (S - 1) - - return S_eff - - -'''#template for the structure factor classes -class (StructureDecouplingApprox): - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray, - par: List[float]): - super(, self).__init__(q, x_new, y_new, z_new, p_new) - self.q = q - self.x_new = x_new - self.y_new = y_new - self.z_new = z_new - self.p_new = p_new - self.par = par[0] - - def structure_eff(self, Pq: np.ndarray) -> np.ndarray: - S = - S_eff = self.decoupling_approx(Pq, S) - - return S_eff -''' - - -class HardSphereStructure(StructureDecouplingApprox): - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray, - par: list[float]): - super(HardSphereStructure, self).__init__(q, x_new, y_new, z_new, p_new) - self.q = q - self.x_new = x_new - self.y_new = y_new - self.z_new = z_new - self.p_new = p_new - self.conc = par[0] - self.R_HS = par[1] - - def calc_S_HS(self) -> np.ndarray: - """ - calculate the hard-sphere structure factor - calls function calc_G() - - input - q : momentum transfer - eta : volume fraction - R : estimation of the hard-sphere radius - - output - S_HS : hard-sphere structure factor - """ - - if self.conc > 0.0: - A = 2 * self.R_HS * self.q - G = self.calc_G(A, self.conc) - S_HS = 1 / (1 + 24 * self.conc * G / A) #percus-yevick approximation for - else: #calculating the structure factor - S_HS = np.ones(len(self.q)) - - return S_HS - - @staticmethod - def calc_G(A: np.ndarray, eta: float) -> np.ndarray: - """ - calculate G in the hard-sphere potential - - input - A : 2*R*q - q : momentum transfer - R : hard-sphere radius - eta: volume fraction - - output: - G - """ - - a = (1 + 2 * eta)**2 / (1 - eta)**4 - b = -6 * eta * (1 + eta / 2)**2/(1 - eta)**4 - c = eta * a / 2 - sinA = np.sin(A) - cosA = np.cos(A) - fa = sinA - A * cosA - fb = 2 * A * sinA + (2 - A**2) * cosA-2 - fc = -A**4 * cosA + 4 * ((3 * A**2 - 6) * cosA + (A**3 - 6 * A) * sinA + 6) - G = a * fa / A**2 + b * fb / A**3 + c * fc / A**5 - - return G - - def structure_eff(self, Pq: np.ndarray) -> np.ndarray: - S = self.calc_S_HS() - S_eff = self.decoupling_approx(Pq, S) - return S_eff - - -class Aggregation(StructureDecouplingApprox): - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray, - par: list[float]): - super(Aggregation, self).__init__(q, x_new, y_new, z_new, p_new) - self.q = q - self.x_new = x_new - self.y_new = y_new - self.z_new = z_new - self.p_new = p_new - self.Reff = par[0] - self.Naggr = par[1] - self.fracs_aggr = par[2] - - def calc_S_aggr(self) -> np.ndarray: - """ - calculates fractal aggregate structure factor with dimensionality 2 - - S_{2,D=2} in Larsen et al 2020, https://doi.org/10.1107/S1600576720006500 - - input - q : - Naggr : number of particles per aggregate - Reff : effective radius of one particle - - output - S_aggr : - """ - - qR = self.q * self.Reff - S_aggr = 1 + (self.Naggr - 1)/(1 + qR**2 * self.Naggr / 3) - - return S_aggr - - def structure_eff(self, Pq: np.ndarray) -> np.ndarray: - """Return effective structure factor for aggregation""" - - S = self.calc_S_aggr() - S_eff = self.decoupling_approx(Pq, S) - S_eff = (1 - self.fracs_aggr) + self.fracs_aggr * S_eff - return S_eff - - -class NoStructure(StructureDecouplingApprox): - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray, - par: Any): - super(NoStructure, self).__init__(q, x_new, y_new, z_new, p_new) - self.q = q - - def structure_eff(self, Pq: Any) -> np.ndarray: - """Return effective structure factor for no structure""" - return np.ones(len(self.q)) - - -class StructureFactor: - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray, - Stype: str, - par: list[float] | None): - self.q = q - self.x_new = x_new - self.y_new = y_new - self.z_new = z_new - self.p_new = p_new - self.Stype = Stype - self.par = par - self.setAvailableStructureFactors() - - def setAvailableStructureFactors(self): - """Available structure factors""" - self.structureFactor = { - 'HS': HardSphereStructure, - 'Hard Sphere': HardSphereStructure, - 'aggregation': Aggregation, - 'Aggregation': Aggregation, - 'None': NoStructure - } - - def getStructureFactorClass(self): - """Return chosen structure factor""" - if self.Stype in self.structureFactor: - return self.structureFactor[self.Stype](self.q, self.x_new, self.y_new, self.z_new, self.p_new, self.par) - - else: - try: - return globals()[self.Stype](self.q, self.x_new, self.y_new, self.z_new, self.p_new, self.par) - except KeyError: - ValueError(f"Structure factor '{self.Stype}' was not found in structureFactor or global scope.") - - @staticmethod - def getparname(name: str) -> list[str]: - """Return the name of the parameters""" - pars = { - 'HS': {'conc': 0.02,'r_hs': 50}, - 'Hard Sphere': {'conc': 0.02,'r_hs': 50}, - 'Aggregation': {'R_eff': 50, 'N_aggr': 80, 'frac': 0.1}, - 'aggregation': {'R_eff': 50, 'N_aggr': 80, 'frac': 0.1}, - 'None': {} - } - return pars[name] - - @staticmethod - def save_S(q: np.ndarray, S_eff: np.ndarray, Model: str): - """ - save S to file - """ - - with open('Sq%s.dat' % Model,'w') as f: - f.write('# Structure factor, S(q), used in: I(q) = P(q)*S(q)\n') - f.write('# Default: S(q) = 1.0\n') - f.write('# %-17s %-17s\n' % ('q','S(q)')) - for (q_i, S_i) in zip(q, S_eff): - f.write(' %-17.5e%-17.5e\n' % (q_i, S_i)) - - class ITheoretical: def __init__(self, q: np.ndarray): self.q = q diff --git a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py new file mode 100644 index 0000000000..c8d6720186 --- /dev/null +++ b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py @@ -0,0 +1,48 @@ +from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox +import numpy as np + +class Aggregation(StructureDecouplingApprox): + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray, + par: List[float]): + super(Aggregation, self).__init__(q, x_new, y_new, z_new, p_new) + self.q = q + self.x_new = x_new + self.y_new = y_new + self.z_new = z_new + self.p_new = p_new + self.Reff = par[0] + self.Naggr = par[1] + self.fracs_aggr = par[2] + + def calc_S_aggr(self) -> np.ndarray: + """ + calculates fractal aggregate structure factor with dimensionality 2 + + S_{2,D=2} in Larsen et al 2020, https://doi.org/10.1107/S1600576720006500 + + input + q : + Naggr : number of particles per aggregate + Reff : effective radius of one particle + + output + S_aggr : + """ + + qR = self.q * self.Reff + S_aggr = 1 + (self.Naggr - 1)/(1 + qR**2 * self.Naggr / 3) + + return S_aggr + + def structure_eff(self, Pq: np.ndarray) -> np.ndarray: + """Return effective structure factor for aggregation""" + + S = self.calc_S_aggr() + S_eff = self.decoupling_approx(Pq, S) + S_eff = (1 - self.fracs_aggr) + self.fracs_aggr * S_eff + return S_eff diff --git a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py new file mode 100644 index 0000000000..3ab9c0073e --- /dev/null +++ b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py @@ -0,0 +1,74 @@ +from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox +import numpy as np + +class HardSphereStructure(StructureDecouplingApprox): + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray, + par: List[float]): + super(HardSphereStructure, self).__init__(q, x_new, y_new, z_new, p_new) + self.q = q + self.x_new = x_new + self.y_new = y_new + self.z_new = z_new + self.p_new = p_new + self.conc = par[0] + self.R_HS = par[1] + + def calc_S_HS(self) -> np.ndarray: + """ + calculate the hard-sphere structure factor + calls function calc_G() + + input + q : momentum transfer + eta : volume fraction + R : estimation of the hard-sphere radius + + output + S_HS : hard-sphere structure factor + """ + + if self.conc > 0.0: + A = 2 * self.R_HS * self.q + G = self.calc_G(A, self.conc) + S_HS = 1 / (1 + 24 * self.conc * G / A) #percus-yevick approximation for + else: #calculating the structure factor + S_HS = np.ones(len(self.q)) + + return S_HS + + @staticmethod + def calc_G(A: np.ndarray, eta: float) -> np.ndarray: + """ + calculate G in the hard-sphere potential + + input + A : 2*R*q + q : momentum transfer + R : hard-sphere radius + eta: volume fraction + + output: + G + """ + + a = (1 + 2 * eta)**2 / (1 - eta)**4 + b = -6 * eta * (1 + eta / 2)**2/(1 - eta)**4 + c = eta * a / 2 + sinA = np.sin(A) + cosA = np.cos(A) + fa = sinA - A * cosA + fb = 2 * A * sinA + (2 - A**2) * cosA-2 + fc = -A**4 * cosA + 4 * ((3 * A**2 - 6) * cosA + (A**3 - 6 * A) * sinA + 6) + G = a * fa / A**2 + b * fb / A**3 + c * fc / A**5 + + return G + + def structure_eff(self, Pq: np.ndarray) -> np.ndarray: + S = self.calc_S_HS() + S_eff = self.decoupling_approx(Pq, S) + return S_eff diff --git a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py new file mode 100644 index 0000000000..0a13a24917 --- /dev/null +++ b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py @@ -0,0 +1,17 @@ +from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox +import numpy as np + +class NoStructure(StructureDecouplingApprox): + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray, + par: Any): + super(NoStructure, self).__init__(q, x_new, y_new, z_new, p_new) + self.q = q + + def structure_eff(self, Pq: Any) -> np.ndarray: + """Return effective structure factor for no structure""" + return np.ones(len(self.q)) diff --git a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py new file mode 100644 index 0000000000..c9fb033693 --- /dev/null +++ b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py @@ -0,0 +1,95 @@ +from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.Math import sinc +import numpy as np + +class StructureDecouplingApprox: + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray): + self.q = q + self.x_new = x_new + self.y_new = y_new + self.z_new = z_new + self.p_new = p_new + + def calc_com_dist(self) -> np.ndarray: + """ + calc contrast-weighted com distance + """ + w = np.abs(self.p_new) + + if np.sum(w) == 0: + w = np.ones(len(self.x_new)) + + x_com, y_com, z_com = np.average(self.x_new, weights=w), np.average(self.y_new, weights=w), np.average(self.z_new, weights=w) + dx, dy, dz = self.x_new - x_com, self.y_new - y_com, self.z_new - z_com + com_dist = np.sqrt(dx**2 + dy**2 + dz**2) + + return com_dist + + def calc_A00(self) -> np.ndarray: + """ + calc zeroth order sph harm, for decoupling approximation + """ + d_new = self.calc_com_dist() + M = len(self.q) + A00 = np.zeros(M) + + for i in range(M): + qr = self.q[i] * d_new + + A00[i] = sum(self.p_new * sinc(qr)) + A00 = A00 / A00[0] # normalise, A00[0] = 1 + + return A00 + + def decoupling_approx(self, Pq: np.ndarray, S: np.ndarray) -> np.ndarray: + """ + modify structure factor with the decoupling approximation + for combining structure factors with non-spherical (or polydisperse) particles + + see, for example, Larsen et al 2020: https://doi.org/10.1107/S1600576720006500 + and refs therein + + input + q + x,y,z,p : coordinates and contrasts + Pq : form factor + S : structure factor + + output + S_eff : effective structure factor, after applying decoupl. approx + """ + + A00 = self.calc_A00() + const = 1e-3 # add constant in nominator and denominator, for stability (numerical errors for small values dampened) + Beta = (A00**2 + const) / (Pq + const) + S_eff = 1 + Beta * (S - 1) + + return S_eff + + +'''#template for the structure factor classes +class (StructureDecouplingApprox): + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray, + par: List[float]): + super(, self).__init__(q, x_new, y_new, z_new, p_new) + self.q = q + self.x_new = x_new + self.y_new = y_new + self.z_new = z_new + self.p_new = p_new + self.par = par[0] + + def structure_eff(self, Pq: np.ndarray) -> np.ndarray: + S = + S_eff = self.decoupling_approx(Pq, S) + + return S_eff +''' \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/structure_factors/__init__.py b/src/sas/sascalc/shape2sas/structure_factors/__init__.py new file mode 100644 index 0000000000..41d8641349 --- /dev/null +++ b/src/sas/sascalc/shape2sas/structure_factors/__init__.py @@ -0,0 +1,10 @@ +from .Aggregation import Aggregation +from .HardSphereStructure import HardSphereStructure +from .NoStructure import NoStructure +from .StructureDecouplingApprox import StructureDecouplingApprox + +__all__ = [ + 'Aggregation', + 'HardSphereStructure', + 'NoStructure' +] \ No newline at end of file From e08e4d3e082941e03c585f13c593c801758d9069 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 12:44:12 +0200 Subject: [PATCH 05/37] separated out Experimental & Theoretical scattering --- .../shape2sas/ExperimentalScattering.py | 87 +++++ src/sas/sascalc/shape2sas/Shape2SAS.py | 4 +- .../shape2sas/TheoreticalScattering.py | 250 +++++++++++++ src/sas/sascalc/shape2sas/helpfunctions.py | 333 ------------------ 4 files changed, 340 insertions(+), 334 deletions(-) create mode 100644 src/sas/sascalc/shape2sas/ExperimentalScattering.py create mode 100644 src/sas/sascalc/shape2sas/TheoreticalScattering.py diff --git a/src/sas/sascalc/shape2sas/ExperimentalScattering.py b/src/sas/sascalc/shape2sas/ExperimentalScattering.py new file mode 100644 index 0000000000..85aa9d3811 --- /dev/null +++ b/src/sas/sascalc/shape2sas/ExperimentalScattering.py @@ -0,0 +1,87 @@ +from sas.sascalc.shape2sas.Typing import * +import numpy as np + +class IExperimental: + def __init__(self, + q: np.ndarray, + I0: np.ndarray, + I: np.ndarray, + exposure: float): + self.q = q + self.I0 = I0 + self.I = I + self.exposure = exposure + + def simulate_data(self) -> Vector2D: + """ + Simulate SAXS data using calculated scattering and empirical expression for sigma + + input + q,I : calculated scattering, normalized + I0 : forward scattering + #noise : relative noise (scales the simulated sigmas by a factor) + exposure : exposure (in arbitrary units) - affects the noise level of data + + output + sigma : simulated noise + Isim : simulated data + + data is also written to a file + """ + + ## simulate exp error + #input, sedlak errors (https://doi.org/10.1107/S1600576717003077) + #k = 5000000 + #c = 0.05 + #sigma = noise*np.sqrt((I+c)/(k*q)) + + ## simulate exp error, other approach, also sedlak errors + + # set constants + k = 4500 + c = 0.85 + + # convert from intensity units to counts + scale = self.exposure + I_sed = scale * self.I0 * self.I + + # make N + N = k * self.q # original expression from Sedlak2017 paper + + qt = 1.4 # threshold - above this q value, the linear expression do not hold + a = 3.0 # empirical constant + b = 0.6 # empirical constant + idx = np.where(self.q > qt) + N[idx] = k * qt * np.exp(-0.5 * ((self.q[idx] - qt) / b)**a) + + # make I(q_arb) + q_max = np.amax(self.q) + q_arb = 0.3 + if q_max <= q_arb: + I_sed_arb = I_sed[-2] + else: + idx_arb = np.where(self.q > q_arb)[0][0] + I_sed_arb = I_sed[idx_arb] + + # calc variance and sigma + v_sed = (I_sed + 2 * c * I_sed_arb / (1 - c)) / N + sigma_sed = np.sqrt(v_sed) + + # rescale + #sigma = noise * sigma_sed/scale + sigma = sigma_sed / scale + + ## simulate data using errors + mu = self.I0 * self.I + Isim = np.random.normal(mu, sigma) + + return Isim, sigma + + def save_Iexperimental(self, Isim: np.ndarray, sigma: np.ndarray, Model: str): + with open('Isim%s.dat' % Model,'w') as f: + f.write('# Simulated data\n') + f.write('# sigma generated using Sedlak et al, k=100000, c=0.55, https://doi.org/10.1107/S1600576717003077, and rebinned with 10 per bin)\n') + f.write('# %-12s %-12s %-12s\n' % ('q','I','sigma')) + for i in range(len(Isim)): + f.write(' %-12.5e %-12.5e %-12.5e\n' % (self.q[i], Isim[i], sigma[i])) + diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index d279c2514b..39b1c8d2ee 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -5,8 +5,10 @@ from dataclasses import dataclass, field from sas.sascalc.shape2sas.StructureFactor import StructureFactor +from sas.sascalc.shape2sas.TheoreticalScattering import ITheoretical, WeightedPairDistribution +from sas.sascalc.shape2sas.ExperimentalScattering import IExperimental from sas.sascalc.shape2sas.helpfunctions import ( - GenerateAllPoints, WeightedPairDistribution, ITheoretical, IExperimental, Qsampling, + GenerateAllPoints, Qsampling, plot_2D, plot_results, generate_pdb ) diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py new file mode 100644 index 0000000000..36d63313f0 --- /dev/null +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -0,0 +1,250 @@ +from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.Math import sinc +import numpy as np + +class WeightedPairDistribution: + def __init__(self, x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + p: np.ndarray): + self.x = x + self.y = y + self.z = z + self.p = p #contrast + + @staticmethod + def calc_dist(x: np.ndarray) -> np.ndarray: + """ + calculate all distances between points in an array + """ + # mesh this array so that you will have all combinations + m, n = np.meshgrid(x, x, sparse=True) + # get the distance via the norm + dist = abs(m - n) + return dist + + def calc_all_dist(self) -> np.ndarray: + """ + calculate all pairwise distances + calls calc_dist() for each set of coordinates: x,y,z + does a square sum of coordinates + convert from matrix to + """ + + square_sum = 0 + for arr in [self.x, self.y, self.z]: + square_sum += self.calc_dist(arr)**2 #arr will input x_new, then y_new and z_new so you get + #x_new^2 + y_new^2 + z_new^2 + d = np.sqrt(square_sum) #then the square root is taken to get avector for the distance + # convert from matrix to array + # reshape is slightly faster than flatten() and ravel() + dist = d.reshape(-1) + # reduce precision, for computational speed + dist = dist.astype('float32') + + return dist + + def calc_all_contrasts(self) -> np.ndarray: + """ + calculate all pairwise contrast products + of p: all contrasts + """ + + dp = np.outer(self.p, self.p) + contrast = dp.reshape(-1) + contrast = contrast.astype('float32') + return contrast + + @staticmethod + def generate_histogram(dist: np.ndarray, contrast: np.ndarray, r_max: float, Nbins: int) -> Vector2D: + """ + make histogram of point pairs, h(r), binned after pair-distances, r + used for calculating scattering (fast Debye) + + input + dist : all pairwise distances + Nbins : number of bins in h(r) + contrast : contrast of points + r_max : max distance to include in histogram + + output + r : distances of bins + histo : histogram, weighted by contrast + + """ + + histo, bin_edges = np.histogram(dist, bins=Nbins, weights=contrast, range=(0, r_max)) + dr = bin_edges[2] - bin_edges[1] + r = bin_edges[0:-1] + dr / 2 + + return r, histo + + @staticmethod + def calc_Rg(r: np.ndarray, pr: np.ndarray) -> float: + """ + calculate Rg from r and p(r) + """ + sum_pr_r2 = np.sum(pr * r**2) + sum_pr = np.sum(pr) + Rg = np.sqrt(abs(sum_pr_r2 / sum_pr) / 2) + + return Rg + + def calc_hr(self, + dist: np.ndarray, + Nbins: int, + contrast: np.ndarray, + polydispersity: float) -> Vector2D: + """ + calculate h(r) + h(r) is the contrast-weighted histogram of distances, including self-terms (dist = 0) + + input: + dist : all pairwise distances + contrast : all pair-wise contrast products + polydispersity: relative polydispersity, float + + output: + hr : pair distance distribution function + """ + + ## make r range in h(r) histogram slightly larger than Dmax + ratio_rmax_dmax = 1.05 + + ## calc h(r) with/without polydispersity + if polydispersity > 0.0: + Dmax = np.amax(dist) * (1 + 3 * polydispersity) + r_max = Dmax * ratio_rmax_dmax + r, hr_1 = self.generate_histogram(dist, contrast, r_max, Nbins) + N_poly_integral = 10 + factor_range = 1 + np.linspace(-3, 3, N_poly_integral) * polydispersity + hr, norm = 0, 0 + for factor_d in factor_range: + if factor_d == 1.0: + hr += hr_1 + norm += 1 + else: + _, dhr = self.generate_histogram(dist * factor_d, contrast, r_max, Nbins) + #dhr = histogram1d(dist * factor_d, bins=Nbins, weights=contrast, range=(0,r_max)) + res = (1.0 - factor_d) / polydispersity + w = np.exp(-res**2 / 2.0) # weight: normal distribution + vol = factor_d**3 # weight: relative volume, because larger particles scatter more + hr += dhr * w * vol**2 + norm += w * vol**2 + hr /= norm + else: + Dmax = np.amax(dist) + r_max = Dmax * ratio_rmax_dmax + r, hr = self.generate_histogram(dist, contrast, r_max, Nbins) + + # print Dmax + print(f" Dmax: {Dmax:.3e} A") + + return r, hr + + def calc_pr(self, Nbins: int, polydispersity: float) -> Vector3D: + """ + calculate p(r) + p(r) is the contrast-weighted histogram of distances, without the self-terms (dist = 0) + + input: + dist : all pairwise distances + contrast : all pair-wise contrast products + polydispersity: boolian, True or False + + output: + pr : pair distance distribution function + """ + dist = self.calc_all_dist() + contrast = self.calc_all_contrasts() + + ## calculate pr + idx_nonzero = np.where(dist > 0.0) # nonzero elements + r, pr = self.calc_hr(dist[idx_nonzero], Nbins, contrast[idx_nonzero], polydispersity) + + ## normalize so pr_max = 1 + pr_norm = pr / np.amax(pr) + + ## calculate Rg + Rg = self.calc_Rg(r, pr_norm) + print(f" Rg : {Rg:.3e} A") + + #returned N values after generating + pr /= len(self.x)**2 #NOTE: N_total**2 + + #NOTE: If Nreps is to be added from the original code + #Then r_sum, pr_sum and pr_norm_sum should be added here + + return r, pr, pr_norm + + @staticmethod + def save_pr(Nbins: int, + r: np.ndarray, + pr_norm: np.ndarray, + Model: str): + """ + save p(r) to textfile + """ + with open('pr%s.dat' % Model,'w') as f: + f.write('# %-17s %-17s\n' % ('r','p(r)')) + for i in range(Nbins): + f.write(' %-17.5e %-17.5e\n' % (r[i], pr_norm[i])) + + +class ITheoretical: + def __init__(self, q: np.ndarray): + self.q = q + + def calc_Pq(self, r: np.ndarray, pr: np.ndarray, conc: float, volume_total: float) -> Vector2D: + """ + calculate form factor, P(q), and forward scattering, I(0), using pair distribution, p(r) + """ + ## calculate P(q) and I(0) from p(r) + I0, Pq = 0, 0 + for (r_i, pr_i) in zip(r, pr): + I0 += pr_i + qr = self.q * r_i + Pq += pr_i * sinc(qr) + + # normalization, P(0) = 1 + if I0 == 0: + I0 = 1E-5 + elif I0 < 0: + I0 = abs(I0) + Pq /= I0 + + # make I0 scale with volume fraction (concentration) and + # volume squared and scale so default values gives I(0) of approx unity + + I0 *= conc * volume_total * 1E-4 + + return I0, Pq + + def calc_Iq(self, Pq: np.ndarray, + S_eff: np.ndarray, + sigma_r: float) -> np.ndarray: + """ + calculates intensity + """ + + ## save structure factor to file + #self.save_S(self.q, S_eff, Model) + + ## multiply formfactor with structure factor + I = Pq * S_eff + + ## interface roughness (Skar-Gislinge et al. 2011, DOI: 10.1039/c0cp01074j) + if sigma_r > 0.0: + roughness = np.exp(-(self.q * sigma_r)**2 / 2) + I *= roughness + + return I + + def save_I(self, I: np.ndarray, Model: str): + """Save theoretical intensity to file""" + + with open('Iq%s.dat' % Model,'w') as f: + f.write('# Calculated data\n') + f.write('# %-12s %-12s\n' % ('q','I')) + for i in range(len(I)): + f.write(' %-12.5e %-12.5e\n' % (self.q[i], I[i])) diff --git a/src/sas/sascalc/shape2sas/helpfunctions.py b/src/sas/sascalc/shape2sas/helpfunctions.py index 565e00cd26..2838f9dd85 100644 --- a/src/sas/sascalc/shape2sas/helpfunctions.py +++ b/src/sas/sascalc/shape2sas/helpfunctions.py @@ -389,339 +389,6 @@ def onGeneratingAllPoints(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np. return x_new, y_new, z_new, p_new, volume_total -class WeightedPairDistribution: - def __init__(self, x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - p: np.ndarray): - self.x = x - self.y = y - self.z = z - self.p = p #contrast - - @staticmethod - def calc_dist(x: np.ndarray) -> np.ndarray: - """ - calculate all distances between points in an array - """ - # mesh this array so that you will have all combinations - m, n = np.meshgrid(x, x, sparse=True) - # get the distance via the norm - dist = abs(m - n) - return dist - - def calc_all_dist(self) -> np.ndarray: - """ - calculate all pairwise distances - calls calc_dist() for each set of coordinates: x,y,z - does a square sum of coordinates - convert from matrix to - """ - - square_sum = 0 - for arr in [self.x, self.y, self.z]: - square_sum += self.calc_dist(arr)**2 #arr will input x_new, then y_new and z_new so you get - #x_new^2 + y_new^2 + z_new^2 - d = np.sqrt(square_sum) #then the square root is taken to get avector for the distance - # convert from matrix to array - # reshape is slightly faster than flatten() and ravel() - dist = d.reshape(-1) - # reduce precision, for computational speed - dist = dist.astype('float32') - - return dist - - def calc_all_contrasts(self) -> np.ndarray: - """ - calculate all pairwise contrast products - of p: all contrasts - """ - - dp = np.outer(self.p, self.p) - contrast = dp.reshape(-1) - contrast = contrast.astype('float32') - return contrast - - @staticmethod - def generate_histogram(dist: np.ndarray, contrast: np.ndarray, r_max: float, Nbins: int) -> Vector2D: - """ - make histogram of point pairs, h(r), binned after pair-distances, r - used for calculating scattering (fast Debye) - - input - dist : all pairwise distances - Nbins : number of bins in h(r) - contrast : contrast of points - r_max : max distance to include in histogram - - output - r : distances of bins - histo : histogram, weighted by contrast - - """ - - histo, bin_edges = np.histogram(dist, bins=Nbins, weights=contrast, range=(0, r_max)) - dr = bin_edges[2] - bin_edges[1] - r = bin_edges[0:-1] + dr / 2 - - return r, histo - - @staticmethod - def calc_Rg(r: np.ndarray, pr: np.ndarray) -> float: - """ - calculate Rg from r and p(r) - """ - sum_pr_r2 = np.sum(pr * r**2) - sum_pr = np.sum(pr) - Rg = np.sqrt(abs(sum_pr_r2 / sum_pr) / 2) - - return Rg - - def calc_hr(self, - dist: np.ndarray, - Nbins: int, - contrast: np.ndarray, - polydispersity: float) -> Vector2D: - """ - calculate h(r) - h(r) is the contrast-weighted histogram of distances, including self-terms (dist = 0) - - input: - dist : all pairwise distances - contrast : all pair-wise contrast products - polydispersity: relative polydispersity, float - - output: - hr : pair distance distribution function - """ - - ## make r range in h(r) histogram slightly larger than Dmax - ratio_rmax_dmax = 1.05 - - ## calc h(r) with/without polydispersity - if polydispersity > 0.0: - Dmax = np.amax(dist) * (1 + 3 * polydispersity) - r_max = Dmax * ratio_rmax_dmax - r, hr_1 = self.generate_histogram(dist, contrast, r_max, Nbins) - N_poly_integral = 10 - factor_range = 1 + np.linspace(-3, 3, N_poly_integral) * polydispersity - hr, norm = 0, 0 - for factor_d in factor_range: - if factor_d == 1.0: - hr += hr_1 - norm += 1 - else: - _, dhr = self.generate_histogram(dist * factor_d, contrast, r_max, Nbins) - #dhr = histogram1d(dist * factor_d, bins=Nbins, weights=contrast, range=(0,r_max)) - res = (1.0 - factor_d) / polydispersity - w = np.exp(-res**2 / 2.0) # weight: normal distribution - vol = factor_d**3 # weight: relative volume, because larger particles scatter more - hr += dhr * w * vol**2 - norm += w * vol**2 - hr /= norm - else: - Dmax = np.amax(dist) - r_max = Dmax * ratio_rmax_dmax - r, hr = self.generate_histogram(dist, contrast, r_max, Nbins) - - # print Dmax - print(f" Dmax: {Dmax:.3e} A") - - return r, hr - - def calc_pr(self, Nbins: int, polydispersity: float) -> Vector3D: - """ - calculate p(r) - p(r) is the contrast-weighted histogram of distances, without the self-terms (dist = 0) - - input: - dist : all pairwise distances - contrast : all pair-wise contrast products - polydispersity: boolian, True or False - - output: - pr : pair distance distribution function - """ - dist = self.calc_all_dist() - contrast = self.calc_all_contrasts() - - ## calculate pr - idx_nonzero = np.where(dist > 0.0) # nonzero elements - r, pr = self.calc_hr(dist[idx_nonzero], Nbins, contrast[idx_nonzero], polydispersity) - - ## normalize so pr_max = 1 - pr_norm = pr / np.amax(pr) - - ## calculate Rg - Rg = self.calc_Rg(r, pr_norm) - print(f" Rg : {Rg:.3e} A") - - #returned N values after generating - pr /= len(self.x)**2 #NOTE: N_total**2 - - #NOTE: If Nreps is to be added from the original code - #Then r_sum, pr_sum and pr_norm_sum should be added here - - return r, pr, pr_norm - - @staticmethod - def save_pr(Nbins: int, - r: np.ndarray, - pr_norm: np.ndarray, - Model: str): - """ - save p(r) to textfile - """ - with open('pr%s.dat' % Model,'w') as f: - f.write('# %-17s %-17s\n' % ('r','p(r)')) - for i in range(Nbins): - f.write(' %-17.5e %-17.5e\n' % (r[i], pr_norm[i])) - - -class ITheoretical: - def __init__(self, q: np.ndarray): - self.q = q - - def calc_Pq(self, r: np.ndarray, pr: np.ndarray, conc: float, volume_total: float) -> Vector2D: - """ - calculate form factor, P(q), and forward scattering, I(0), using pair distribution, p(r) - """ - ## calculate P(q) and I(0) from p(r) - I0, Pq = 0, 0 - for (r_i, pr_i) in zip(r, pr): - I0 += pr_i - qr = self.q * r_i - Pq += pr_i * sinc(qr) - - # normalization, P(0) = 1 - if I0 == 0: - I0 = 1E-5 - elif I0 < 0: - I0 = abs(I0) - Pq /= I0 - - # make I0 scale with volume fraction (concentration) and - # volume squared and scale so default values gives I(0) of approx unity - - I0 *= conc * volume_total * 1E-4 - - return I0, Pq - - def calc_Iq(self, Pq: np.ndarray, - S_eff: np.ndarray, - sigma_r: float) -> np.ndarray: - """ - calculates intensity - """ - - ## save structure factor to file - #self.save_S(self.q, S_eff, Model) - - ## multiply formfactor with structure factor - I = Pq * S_eff - - ## interface roughness (Skar-Gislinge et al. 2011, DOI: 10.1039/c0cp01074j) - if sigma_r > 0.0: - roughness = np.exp(-(self.q * sigma_r)**2 / 2) - I *= roughness - - return I - - def save_I(self, I: np.ndarray, Model: str): - """Save theoretical intensity to file""" - - with open('Iq%s.dat' % Model,'w') as f: - f.write('# Calculated data\n') - f.write('# %-12s %-12s\n' % ('q','I')) - for i in range(len(I)): - f.write(' %-12.5e %-12.5e\n' % (self.q[i], I[i])) - - -class IExperimental: - def __init__(self, - q: np.ndarray, - I0: np.ndarray, - I: np.ndarray, - exposure: float): - self.q = q - self.I0 = I0 - self.I = I - self.exposure = exposure - - def simulate_data(self) -> Vector2D: - """ - Simulate SAXS data using calculated scattering and empirical expression for sigma - - input - q,I : calculated scattering, normalized - I0 : forward scattering - #noise : relative noise (scales the simulated sigmas by a factor) - exposure : exposure (in arbitrary units) - affects the noise level of data - - output - sigma : simulated noise - Isim : simulated data - - data is also written to a file - """ - - ## simulate exp error - #input, sedlak errors (https://doi.org/10.1107/S1600576717003077) - #k = 5000000 - #c = 0.05 - #sigma = noise*np.sqrt((I+c)/(k*q)) - - ## simulate exp error, other approach, also sedlak errors - - # set constants - k = 4500 - c = 0.85 - - # convert from intensity units to counts - scale = self.exposure - I_sed = scale * self.I0 * self.I - - # make N - N = k * self.q # original expression from Sedlak2017 paper - - qt = 1.4 # threshold - above this q value, the linear expression do not hold - a = 3.0 # empirical constant - b = 0.6 # empirical constant - idx = np.where(self.q > qt) - N[idx] = k * qt * np.exp(-0.5 * ((self.q[idx] - qt) / b)**a) - - # make I(q_arb) - q_max = np.amax(self.q) - q_arb = 0.3 - if q_max <= q_arb: - I_sed_arb = I_sed[-2] - else: - idx_arb = np.where(self.q > q_arb)[0][0] - I_sed_arb = I_sed[idx_arb] - - # calc variance and sigma - v_sed = (I_sed + 2 * c * I_sed_arb / (1 - c)) / N - sigma_sed = np.sqrt(v_sed) - - # rescale - #sigma = noise * sigma_sed/scale - sigma = sigma_sed / scale - - ## simulate data using errors - mu = self.I0 * self.I - Isim = np.random.normal(mu, sigma) - - return Isim, sigma - - def save_Iexperimental(self, Isim: np.ndarray, sigma: np.ndarray, Model: str): - with open('Isim%s.dat' % Model,'w') as f: - f.write('# Simulated data\n') - f.write('# sigma generated using Sedlak et al, k=100000, c=0.55, https://doi.org/10.1107/S1600576717003077, and rebinned with 10 per bin)\n') - f.write('# %-12s %-12s %-12s\n' % ('q','I','sigma')) - for i in range(len(Isim)): - f.write(' %-12.5e %-12.5e %-12.5e\n' % (self.q[i], Isim[i], sigma[i])) - - def get_max_dimension(x_list: np.ndarray, y_list: np.ndarray, z_list: np.ndarray) -> float: """ find max dimensions of n models From eeeb86c2d76b6c7b5d28398b7fd4540dbd554e63 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 13:09:23 +0200 Subject: [PATCH 06/37] separated out DataClasses from Shape2SAS main script; should be the end of refactoring for now --- .../shape2sas/ExperimentalScattering.py | 23 ++ src/sas/sascalc/shape2sas/HelperFunctions.py | 259 +++++++++++++++ src/sas/sascalc/shape2sas/Math.py | 8 - .../shape2sas/{helpfunctions.py => Models.py} | 312 ++++-------------- src/sas/sascalc/shape2sas/Shape2SAS.py | 110 +----- src/sas/sascalc/shape2sas/StructureFactor.py | 2 + .../shape2sas/TheoreticalScattering.py | 24 +- src/sas/sascalc/shape2sas/Typing.py | 3 +- .../structure_factors/NoStructure.py | 2 + .../StructureDecouplingApprox.py | 3 +- 10 files changed, 378 insertions(+), 368 deletions(-) create mode 100644 src/sas/sascalc/shape2sas/HelperFunctions.py delete mode 100644 src/sas/sascalc/shape2sas/Math.py rename src/sas/sascalc/shape2sas/{helpfunctions.py => Models.py} (59%) diff --git a/src/sas/sascalc/shape2sas/ExperimentalScattering.py b/src/sas/sascalc/shape2sas/ExperimentalScattering.py index 85aa9d3811..c5bf536ed8 100644 --- a/src/sas/sascalc/shape2sas/ExperimentalScattering.py +++ b/src/sas/sascalc/shape2sas/ExperimentalScattering.py @@ -1,6 +1,29 @@ from sas.sascalc.shape2sas.Typing import * + +from dataclasses import dataclass, field +from typing import Optional import numpy as np +@dataclass +class SimulateScattering: + """Class containing parameters for + simulating scattering""" + + q: np.ndarray + I0: np.ndarray + I: np.ndarray + exposure: Optional[float] = field(default_factory=lambda:500.0) + + +@dataclass +class SimulatedScattering: + """Class containing parameters for + simulated scattering""" + + I_sim: np.ndarray + q: np.ndarray + I_err: np.ndarray + class IExperimental: def __init__(self, q: np.ndarray, diff --git a/src/sas/sascalc/shape2sas/HelperFunctions.py b/src/sas/sascalc/shape2sas/HelperFunctions.py new file mode 100644 index 0000000000..f0ba9586c4 --- /dev/null +++ b/src/sas/sascalc/shape2sas/HelperFunctions.py @@ -0,0 +1,259 @@ +from sas.sascalc.shape2sas.Typing import * + +import numpy as np +import matplotlib.pyplot as plt + +################################ Shape2SAS helper functions ################################### +class Qsampling: + def onQsampling(qmin: float, qmax: float, Nq: int) -> np.ndarray: + """Returns uniform q sampling""" + return np.linspace(qmin, qmax, Nq) + + def onUserSampledQ(q: np.ndarray) -> np.ndarray: + """Returns user sampled q""" + if isinstance(q, list): + q = np.array(q) + return q + + def qMethodsNames(name: str): + methods = { + "Uniform": Qsampling.onQsampling, + "User_sampled": Qsampling.onUserSampledQ + } + return methods[name] + + def qMethodsInput(name: str): + inputs = { + "Uniform": {"qmin": 0.001, "qmax": 0.5, "Nq": 400}, + "User_sampled": {"q": Qsampling.onQsampling(0.001, 0.5, 400)} #if the user does not input q + } + return inputs[name] + + +def sinc(x) -> np.ndarray: + """ + function for calculating sinc = sin(x)/x + numpy.sinc is defined as sinc(x) = sin(pi*x)/(pi*x) + """ + return np.sinc(x / np.pi) + + +def get_max_dimension(x_list: np.ndarray, y_list: np.ndarray, z_list: np.ndarray) -> float: + """ + find max dimensions of n models + used for determining plot limits + """ + + max_x,max_y,max_z = 0, 0, 0 + for i in range(len(x_list)): + tmp_x = np.amax(abs(x_list[i])) + tmp_y = np.amax(abs(y_list[i])) + tmp_z = np.amax(abs(z_list[i])) + if tmp_x>max_x: + max_x = tmp_x + if tmp_y>max_y: + max_y = tmp_y + if tmp_z>max_z: + max_z = tmp_z + + max_l = np.amax([max_x,max_y,max_z]) + + return max_l + + +def plot_2D(x_list: np.ndarray, + y_list: np.ndarray, + z_list: np.ndarray, + p_list: np.ndarray, + Models: np.ndarray, + high_res: bool) -> None: + """ + plot 2D-projections of generated points (shapes) using matplotlib: + positive contrast in red (Model 1) or blue (Model 2) or yellow (Model 3) or green (Model 4) + zero contrast in grey + negative contrast in black + + input + (x_list,y_list,z_list) : coordinates of simulated points + p_list : excess scattering length densities (contrast) of simulated points + Model : Model number + + output + plot : points.png + + """ + + ## figure settings + markersize = 0.5 + max_l = get_max_dimension(x_list, y_list, z_list)*1.1 + lim = [-max_l, max_l] + + for x,y,z,p,Model in zip(x_list,y_list,z_list,p_list,Models): + + ## find indices of positive, zero and negatative contrast + idx_neg = np.where(p < 0.0) + idx_pos = np.where(p > 0.0) + idx_nul = np.where(p == 0.0) + + f,ax = plt.subplots(1, 3, figsize=(12,4)) + + ## plot, perspective 1 + ax[0].plot(x[idx_pos], z[idx_pos], linestyle='none', marker='.', markersize=markersize) + ax[0].plot(x[idx_neg], z[idx_neg], linestyle='none', marker='.', markersize=markersize, color='black') + ax[0].plot(x[idx_nul], z[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') + ax[0].set_xlim(lim) + ax[0].set_ylim(lim) + ax[0].set_xlabel('x') + ax[0].set_ylabel('z') + ax[0].set_title('pointmodel, (x,z), "front"') + + ## plot, perspective 2 + ax[1].plot(y[idx_pos], z[idx_pos], linestyle='none', marker='.', markersize=markersize) + ax[1].plot(y[idx_neg], z[idx_neg], linestyle='none', marker='.', markersize=markersize, color='black') + ax[1].plot(y[idx_nul], z[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') + ax[1].set_xlim(lim) + ax[1].set_ylim(lim) + ax[1].set_xlabel('y') + ax[1].set_ylabel('z') + ax[1].set_title('pointmodel, (y,z), "side"') + + ## plot, perspective 3 + ax[2].plot(x[idx_pos], y[idx_pos], linestyle='none', marker='.', markersize=markersize) + ax[2].plot(x[idx_neg], y[idx_neg], linestyle='none', marker='.', markersize=markersize, color='black') + ax[2].plot(x[idx_nul], y[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') + ax[2].set_xlim(lim) + ax[2].set_ylim(lim) + ax[2].set_xlabel('x') + ax[2].set_ylabel('y') + ax[2].set_title('pointmodel, (x,y), "bottom"') + + plt.tight_layout() + if high_res: + plt.savefig('points%s.png' % Model,dpi=600) + else: + plt.savefig('points%s.png' % Model) + plt.close() + + +def plot_results(q: np.ndarray, + r_list: List[np.ndarray], + pr_list: List[np.ndarray], + I_list: List[np.ndarray], + Isim_list: List[np.ndarray], + sigma_list: List[np.ndarray], + S_list: List[np.ndarray], + names: List[str], + scales: List[float], + xscale_log: bool, + high_res: bool) -> None: + """ + plot results for all models, using matplotlib: + - p(r) + - calculated formfactor, P(r) on log-log or log-lin scale + - simulated noisy data on log-log or log-lin scale + + """ + fig, ax = plt.subplots(1,3,figsize=(12,4)) + + zo = 1 + for (r, pr, I, Isim, sigma, S, model_name, scale) in zip (r_list, pr_list, I_list, Isim_list, sigma_list, S_list, names, scales): + ax[0].plot(r,pr,zorder=zo,label='p(r), %s' % model_name) + + if scale > 1: + ax[2].errorbar(q,Isim*scale,yerr=sigma*scale,linestyle='none',marker='.',label=r'$I_\mathrm{sim}(q)$, %s, scaled by %d' % (model_name,scale),zorder=1/zo) + else: + ax[2].errorbar(q,Isim*scale,yerr=sigma*scale,linestyle='none',marker='.',label=r'$I_\mathrm{sim}(q)$, %s' % model_name,zorder=zo) + + if S[0] != 1.0 or S[-1] != 1.0: + ax[1].plot(q, S, linestyle='--', label=r'$S(q)$, %s' % model_name,zorder=0) + ax[1].plot(q, I, zorder=zo, label=r'$I(q)=P(q)S(q)$, %s' % model_name) + ax[1].set_ylabel(r'$I(q)=P(q)S(q)$') + else: + ax[1].plot(q, I, zorder=zo, label=r'$P(q)=I(q)/I(0)$, %s' % model_name) + ax[1].set_ylabel(r'$P(q)=I(q)/I(0)$') + zo += 1 + + ## figure settings, p(r) + ax[0].set_xlabel(r'$r$ [$\mathrm{\AA}$]') + ax[0].set_ylabel(r'$p(r)$') + ax[0].set_title('pair distance distribution function') + ax[0].legend(frameon=False) + + ## figure settings, calculated scattering + if xscale_log: + ax[1].set_xscale('log') + ax[1].set_yscale('log') + ax[1].set_xlabel(r'$q$ [$\mathrm{\AA}^{-1}$]') + ax[1].set_title('normalized scattering, no noise') + ax[1].legend(frameon=False) + + ## figure settings, simulated scattering + if xscale_log: + ax[2].set_xscale('log') + ax[2].set_yscale('log') + ax[2].set_xlabel(r'$q$ [$\mathrm{\AA}^{-1}$]') + ax[2].set_ylabel(r'$I(q)$ [a.u.]') + ax[2].set_title('simulated scattering, with noise') + ax[2].legend(frameon=True) + + ## figure settings + plt.tight_layout() + if high_res: + plt.savefig('plot.png', dpi=600) + else: + plt.savefig('plot.png') + plt.close() + + +def generate_pdb(x_list: List[np.ndarray], + y_list: List[np.ndarray], + z_list: List[np.ndarray], + p_list: List[np.ndarray], + Model_list: List[str]) -> None: + """ + Generates a visualisation file in PDB format with the simulated points (coordinates) and contrasts + ONLY FOR VISUALIZATION! + Each bead is represented as a dummy atom + Carbon, C : positive contrast + Hydrogen, H : zero contrast + Oxygen, O : negateive contrast + information of accurate contrasts not included, only sign + IMPORTANT: IT WILL NOT GIVE THE CORRECT RESULTS IF SCATTERING IS CACLLUATED FROM THIS MODEL WITH E.G. CRYSOL, PEPSI-SAXS, FOXS, CAPP OR THE LIKE! + """ + + for (x,y,z,p,Model) in zip(x_list, y_list, z_list, p_list, Model_list): + with open('model%s.pdb' % Model,'w') as f: + f.write('TITLE POINT SCATTER : MODEL%s\n' % Model) + f.write('REMARK GENERATED WITH Shape2SAS\n') + f.write('REMARK EACH BEAD REPRESENTED BY DUMMY ATOM\n') + f.write('REMARK CARBON, C : POSITIVE EXCESS SCATTERING LENGTH\n') + f.write('REMARK HYDROGEN, H : ZERO EXCESS SCATTERING LENGTH\n') + f.write('REMARK OXYGEN, O : NEGATIVE EXCESS SCATTERING LENGTH\n') + f.write('REMARK ACCURATE SCATTERING LENGTH DENSITY INFORMATION NOT INCLUDED\n') + f.write('REMARK OBS: WILL NOT GIVE CORRECT RESULTS IF SCATTERING IS CALCULATED FROM THIS MODEL WITH E.G CRYSOL, PEPSI-SAXS, FOXS, CAPP OR THE LIKE!\n') + f.write('REMARK ONLY FOR VISUALIZATION, E.G. WITH PYMOL\n') + f.write('REMARK \n') + for i in range(len(x)): + if p[i] > 0: + atom = 'C' + elif p[i] == 0: + atom = 'H' + else: + atom = 'O' + f.write('ATOM %6i %s ALA A%6i %8.3f%8.3f%8.3f 1.00 0.00 %s \n' % (i,atom,i,x[i],y[i],z[i],atom)) + f.write('END') + + +def check_unique(A_list: List[float]) -> bool: + """ + if all elements in a list are unique then return True, else return False + """ + unique = True + N = len(A_list) + for i in range(N): + for j in range(N): + if i != j: + if A_list[i] == A_list[j]: + unique = False + + return unique diff --git a/src/sas/sascalc/shape2sas/Math.py b/src/sas/sascalc/shape2sas/Math.py deleted file mode 100644 index a6cbcdfd77..0000000000 --- a/src/sas/sascalc/shape2sas/Math.py +++ /dev/null @@ -1,8 +0,0 @@ -import numpy as np - -def sinc(x) -> np.ndarray: - """ - function for calculating sinc = sin(x)/x - numpy.sinc is defined as sinc(x) = sin(pi*x)/(pi*x) - """ - return np.sinc(x / np.pi) \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/helpfunctions.py b/src/sas/sascalc/shape2sas/Models.py similarity index 59% rename from src/sas/sascalc/shape2sas/helpfunctions.py rename to src/sas/sascalc/shape2sas/Models.py index 2838f9dd85..16824ad0d0 100644 --- a/src/sas/sascalc/shape2sas/helpfunctions.py +++ b/src/sas/sascalc/shape2sas/Models.py @@ -1,35 +1,64 @@ -from typing import Any - -import matplotlib.pyplot as plt from sas.sascalc.shape2sas.Typing import * from sas.sascalc.shape2sas.models import * -from sas.sascalc.shape2sas.Math import sinc - -################################ Shape2SAS helper functions ################################### -class Qsampling: - def onQsampling(qmin: float, qmax: float, Nq: int) -> np.ndarray: - """Returns uniform q sampling""" - return np.linspace(qmin, qmax, Nq) - - def onUserSampledQ(q: np.ndarray) -> np.ndarray: - """Returns user sampled q""" - if isinstance(q, list): - q = np.array(q) - return q - - def qMethodsNames(name: str): - methods = { - "Uniform": Qsampling.onQsampling, - "User_sampled": Qsampling.onUserSampledQ - } - return methods[name] - - def qMethodsInput(name: str): - inputs = { - "Uniform": {"qmin": 0.001, "qmax": 0.5, "Nq": 400}, - "User_sampled": {"q": Qsampling.onQsampling(0.001, 0.5, 400)} #if the user does not input q - } - return inputs[name] +from sas.sascalc.shape2sas.HelperFunctions import Qsampling + +from dataclasses import dataclass, field +from typing import Optional, List +import numpy as np + +@dataclass +class ModelProfile: + """Class containing parameters for + creating a particle + + NOTE: Default values create a sphere with a + radius of 50 Ã… at the origin. + """ + + subunits: List[str] = field(default_factory=lambda: ['sphere']) + p_s: List[float] = field(default_factory=lambda: [1.0]) # scattering length density + dimensions: Vectors = field(default_factory=lambda: [[50]]) + com: Vectors = field(default_factory=lambda: [[0, 0, 0]]) + rotation_points: Vectors = field(default_factory=lambda: [[0, 0, 0]]) + rotation: Vectors = field(default_factory=lambda: [[0, 0, 0]]) + exclude_overlap: Optional[bool] = field(default_factory=lambda: True) + + +@dataclass +class ModelPointDistribution: + """Point distribution of a model""" + + x: np.ndarray + y: np.ndarray + z: np.ndarray + p: np.ndarray #scattering length density for each point + volume_total: float + + +@dataclass +class SimulationParameters: + """Class containing parameters for + the simulation itself""" + + q: Optional[np.ndarray] = field(default_factory=lambda: Qsampling.onQsampling(0.001, 0.5, 400)) + prpoints: Optional[int] = field(default_factory=lambda: 100) + Npoints: Optional[int] = field(default_factory=lambda: 3000) + #seed: Optional[int] #TODO:Add for future projects + #method: Optional[str] #generation of point method #TODO: Add for future projects + model_name: Optional[List[str]] = field(default_factory=lambda: ['Model_1']) + + +@dataclass +class ModelSystem: + """Class containing parameters for + the system""" + + PointDistribution: ModelPointDistribution + Stype: str = field(default_factory=lambda: "None") #structure factor + par: List[float] = field(default_factory=lambda: np.array([]))#parameters for structure factor + polydispersity: float = field(default_factory=lambda: 0.0)#polydispersity + conc: float = field(default_factory=lambda: 0.02) #concentration + sigma_r: float = field(default_factory=lambda: 0.0) #interface roughness class Rotation: @@ -386,225 +415,4 @@ def onGeneratingAllPoints(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np. print(f" Total volume of model: {volume_total:.3e} A^3") print(" ") - return x_new, y_new, z_new, p_new, volume_total - - -def get_max_dimension(x_list: np.ndarray, y_list: np.ndarray, z_list: np.ndarray) -> float: - """ - find max dimensions of n models - used for determining plot limits - """ - - max_x,max_y,max_z = 0, 0, 0 - for i in range(len(x_list)): - tmp_x = np.amax(abs(x_list[i])) - tmp_y = np.amax(abs(y_list[i])) - tmp_z = np.amax(abs(z_list[i])) - if tmp_x>max_x: - max_x = tmp_x - if tmp_y>max_y: - max_y = tmp_y - if tmp_z>max_z: - max_z = tmp_z - - max_l = np.amax([max_x,max_y,max_z]) - - return max_l - - -def plot_2D(x_list: np.ndarray, - y_list: np.ndarray, - z_list: np.ndarray, - p_list: np.ndarray, - Models: np.ndarray, - high_res: bool) -> None: - """ - plot 2D-projections of generated points (shapes) using matplotlib: - positive contrast in red (Model 1) or blue (Model 2) or yellow (Model 3) or green (Model 4) - zero contrast in grey - negative contrast in black - - input - (x_list,y_list,z_list) : coordinates of simulated points - p_list : excess scattering length densities (contrast) of simulated points - Model : Model number - - output - plot : points.png - - """ - - ## figure settings - markersize = 0.5 - max_l = get_max_dimension(x_list, y_list, z_list)*1.1 - lim = [-max_l, max_l] - - for x,y,z,p,Model in zip(x_list,y_list,z_list,p_list,Models): - - ## find indices of positive, zero and negatative contrast - idx_neg = np.where(p < 0.0) - idx_pos = np.where(p > 0.0) - idx_nul = np.where(p == 0.0) - - f,ax = plt.subplots(1, 3, figsize=(12,4)) - - ## plot, perspective 1 - ax[0].plot(x[idx_pos], z[idx_pos], linestyle='none', marker='.', markersize=markersize) - ax[0].plot(x[idx_neg], z[idx_neg], linestyle='none', marker='.', markersize=markersize, color='black') - ax[0].plot(x[idx_nul], z[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') - ax[0].set_xlim(lim) - ax[0].set_ylim(lim) - ax[0].set_xlabel('x') - ax[0].set_ylabel('z') - ax[0].set_title('pointmodel, (x,z), "front"') - - ## plot, perspective 2 - ax[1].plot(y[idx_pos], z[idx_pos], linestyle='none', marker='.', markersize=markersize) - ax[1].plot(y[idx_neg], z[idx_neg], linestyle='none', marker='.', markersize=markersize, color='black') - ax[1].plot(y[idx_nul], z[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') - ax[1].set_xlim(lim) - ax[1].set_ylim(lim) - ax[1].set_xlabel('y') - ax[1].set_ylabel('z') - ax[1].set_title('pointmodel, (y,z), "side"') - - ## plot, perspective 3 - ax[2].plot(x[idx_pos], y[idx_pos], linestyle='none', marker='.', markersize=markersize) - ax[2].plot(x[idx_neg], y[idx_neg], linestyle='none', marker='.', markersize=markersize, color='black') - ax[2].plot(x[idx_nul], y[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') - ax[2].set_xlim(lim) - ax[2].set_ylim(lim) - ax[2].set_xlabel('x') - ax[2].set_ylabel('y') - ax[2].set_title('pointmodel, (x,y), "bottom"') - - plt.tight_layout() - if high_res: - plt.savefig('points%s.png' % Model,dpi=600) - else: - plt.savefig('points%s.png' % Model) - plt.close() - - -def plot_results(q: np.ndarray, - r_list: list[np.ndarray], - pr_list: list[np.ndarray], - I_list: list[np.ndarray], - Isim_list: list[np.ndarray], - sigma_list: list[np.ndarray], - S_list: list[np.ndarray], - names: list[str], - scales: list[float], - xscale_log: bool, - high_res: bool) -> None: - """ - plot results for all models, using matplotlib: - - p(r) - - calculated formfactor, P(r) on log-log or log-lin scale - - simulated noisy data on log-log or log-lin scale - - """ - fig, ax = plt.subplots(1,3,figsize=(12,4)) - - zo = 1 - for (r, pr, I, Isim, sigma, S, model_name, scale) in zip (r_list, pr_list, I_list, Isim_list, sigma_list, S_list, names, scales): - ax[0].plot(r,pr,zorder=zo,label='p(r), %s' % model_name) - - if scale > 1: - ax[2].errorbar(q,Isim*scale,yerr=sigma*scale,linestyle='none',marker='.',label=r'$I_\mathrm{sim}(q)$, %s, scaled by %d' % (model_name,scale),zorder=1/zo) - else: - ax[2].errorbar(q,Isim*scale,yerr=sigma*scale,linestyle='none',marker='.',label=r'$I_\mathrm{sim}(q)$, %s' % model_name,zorder=zo) - - if S[0] != 1.0 or S[-1] != 1.0: - ax[1].plot(q, S, linestyle='--', label=r'$S(q)$, %s' % model_name,zorder=0) - ax[1].plot(q, I, zorder=zo, label=r'$I(q)=P(q)S(q)$, %s' % model_name) - ax[1].set_ylabel(r'$I(q)=P(q)S(q)$') - else: - ax[1].plot(q, I, zorder=zo, label=r'$P(q)=I(q)/I(0)$, %s' % model_name) - ax[1].set_ylabel(r'$P(q)=I(q)/I(0)$') - zo += 1 - - ## figure settings, p(r) - ax[0].set_xlabel(r'$r$ [$\mathrm{\AA}$]') - ax[0].set_ylabel(r'$p(r)$') - ax[0].set_title('pair distance distribution function') - ax[0].legend(frameon=False) - - ## figure settings, calculated scattering - if xscale_log: - ax[1].set_xscale('log') - ax[1].set_yscale('log') - ax[1].set_xlabel(r'$q$ [$\mathrm{\AA}^{-1}$]') - ax[1].set_title('normalized scattering, no noise') - ax[1].legend(frameon=False) - - ## figure settings, simulated scattering - if xscale_log: - ax[2].set_xscale('log') - ax[2].set_yscale('log') - ax[2].set_xlabel(r'$q$ [$\mathrm{\AA}^{-1}$]') - ax[2].set_ylabel(r'$I(q)$ [a.u.]') - ax[2].set_title('simulated scattering, with noise') - ax[2].legend(frameon=True) - - ## figure settings - plt.tight_layout() - if high_res: - plt.savefig('plot.png', dpi=600) - else: - plt.savefig('plot.png') - plt.close() - - -def generate_pdb(x_list: list[np.ndarray], - y_list: list[np.ndarray], - z_list: list[np.ndarray], - p_list: list[np.ndarray], - Model_list: list[str]) -> None: - """ - Generates a visualisation file in PDB format with the simulated points (coordinates) and contrasts - ONLY FOR VISUALIZATION! - Each bead is represented as a dummy atom - Carbon, C : positive contrast - Hydrogen, H : zero contrast - Oxygen, O : negateive contrast - information of accurate contrasts not included, only sign - IMPORTANT: IT WILL NOT GIVE THE CORRECT RESULTS IF SCATTERING IS CACLLUATED FROM THIS MODEL WITH E.G. CRYSOL, PEPSI-SAXS, FOXS, CAPP OR THE LIKE! - """ - - for (x,y,z,p,Model) in zip(x_list, y_list, z_list, p_list, Model_list): - with open('model%s.pdb' % Model,'w') as f: - f.write('TITLE POINT SCATTER : MODEL%s\n' % Model) - f.write('REMARK GENERATED WITH Shape2SAS\n') - f.write('REMARK EACH BEAD REPRESENTED BY DUMMY ATOM\n') - f.write('REMARK CARBON, C : POSITIVE EXCESS SCATTERING LENGTH\n') - f.write('REMARK HYDROGEN, H : ZERO EXCESS SCATTERING LENGTH\n') - f.write('REMARK OXYGEN, O : NEGATIVE EXCESS SCATTERING LENGTH\n') - f.write('REMARK ACCURATE SCATTERING LENGTH DENSITY INFORMATION NOT INCLUDED\n') - f.write('REMARK OBS: WILL NOT GIVE CORRECT RESULTS IF SCATTERING IS CALCULATED FROM THIS MODEL WITH E.G CRYSOL, PEPSI-SAXS, FOXS, CAPP OR THE LIKE!\n') - f.write('REMARK ONLY FOR VISUALIZATION, E.G. WITH PYMOL\n') - f.write('REMARK \n') - for i in range(len(x)): - if p[i] > 0: - atom = 'C' - elif p[i] == 0: - atom = 'H' - else: - atom = 'O' - f.write('ATOM %6i %s ALA A%6i %8.3f%8.3f%8.3f 1.00 0.00 %s \n' % (i,atom,i,x[i],y[i],z[i],atom)) - f.write('END') - - -def check_unique(A_list: list[float]) -> bool: - """ - if all elements in a list are unique then return True, else return False - """ - unique = True - N = len(A_list) - for i in range(N): - for j in range(N): - if i != j: - if A_list[i] == A_list[j]: - unique = False - - return unique + return x_new, y_new, z_new, p_new, volume_total \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index 39b1c8d2ee..66ed11a978 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -1,115 +1,15 @@ import argparse import re -import time -import warnings -from dataclasses import dataclass, field +import numpy as np from sas.sascalc.shape2sas.StructureFactor import StructureFactor -from sas.sascalc.shape2sas.TheoreticalScattering import ITheoretical, WeightedPairDistribution -from sas.sascalc.shape2sas.ExperimentalScattering import IExperimental -from sas.sascalc.shape2sas.helpfunctions import ( - GenerateAllPoints, Qsampling, +from sas.sascalc.shape2sas.TheoreticalScattering import * +from sas.sascalc.shape2sas.ExperimentalScattering import * +from sas.sascalc.shape2sas.Models import * +from sas.sascalc.shape2sas.HelperFunctions import ( plot_2D, plot_results, generate_pdb ) -Vectors = list[list[float]] - - -@dataclass -class ModelProfile: - """Class containing parameters for - creating a particle - - NOTE: Default values create a sphere with a - radius of 50 Ã… at the origin. - """ - - subunits: list[str] = field(default_factory=lambda: ['sphere']) - p_s: list[float] = field(default_factory=lambda: [1.0]) # scattering length density - dimensions: Vectors = field(default_factory=lambda: [[50]]) - com: Vectors = field(default_factory=lambda: [[0, 0, 0]]) - rotation_points: Vectors = field(default_factory=lambda: [[0, 0, 0]]) - rotation: Vectors = field(default_factory=lambda: [[0, 0, 0]]) - exclude_overlap: bool | None = field(default_factory=lambda: True) - - -@dataclass -class ModelPointDistribution: - """Point distribution of a model""" - - x: np.ndarray - y: np.ndarray - z: np.ndarray - p: np.ndarray #scattering length density for each point - volume_total: float - - -@dataclass -class SimulationParameters: - """Class containing parameters for - the simulation itself""" - - q: np.ndarray | None = field(default_factory=lambda: Qsampling.onQsampling(0.001, 0.5, 400)) - prpoints: int | None = field(default_factory=lambda: 100) - Npoints: int | None = field(default_factory=lambda: 3000) - #seed: Optional[int] #TODO:Add for future projects - #method: Optional[str] #generation of point method #TODO: Add for future projects - model_name: list[str] | None = field(default_factory=lambda: ['Model_1']) - - -@dataclass -class ModelSystem: - """Class containing parameters for - the system""" - - PointDistribution: ModelPointDistribution - Stype: str = field(default_factory=lambda: "None") #structure factor - par: list[float] = field(default_factory=lambda: np.array([]))#parameters for structure factor - polydispersity: float = field(default_factory=lambda: 0.0)#polydispersity - conc: float = field(default_factory=lambda: 0.02) #concentration - sigma_r: float = field(default_factory=lambda: 0.0) #interface roughness - - -@dataclass -class TheoreticalScatteringCalculation: - """Class containing parameters for simulating - scattering for a given model system""" - - System: ModelSystem - Calculation: SimulationParameters - - -@dataclass -class TheoreticalScattering: - """Class containing parameters for - theoretical scattering""" - - q: np.ndarray - I0: np.ndarray - I: np.ndarray - S_eff: np.ndarray - - -@dataclass -class SimulateScattering: - """Class containing parameters for - simulating scattering""" - - q: np.ndarray - I0: np.ndarray - I: np.ndarray - exposure: float | None = field(default_factory=lambda:500.0) - - -@dataclass -class SimulatedScattering: - """Class containing parameters for - simulated scattering""" - - I_sim: np.ndarray - q: np.ndarray - I_err: np.ndarray - ################################ Shape2SAS functions ################################ def getPointDistribution(prof: ModelProfile, Npoints): diff --git a/src/sas/sascalc/shape2sas/StructureFactor.py b/src/sas/sascalc/shape2sas/StructureFactor.py index 273ee4b25b..82ad1f810f 100644 --- a/src/sas/sascalc/shape2sas/StructureFactor.py +++ b/src/sas/sascalc/shape2sas/StructureFactor.py @@ -1,5 +1,7 @@ from sas.sascalc.shape2sas.Typing import * from sas.sascalc.shape2sas.structure_factors import * + +from typing import Optional import numpy as np class StructureFactor: diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index 36d63313f0..026db619cf 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -1,7 +1,29 @@ from sas.sascalc.shape2sas.Typing import * -from sas.sascalc.shape2sas.Math import sinc +from sas.sascalc.shape2sas.HelperFunctions import sinc + +from dataclasses import dataclass +from sas.sascalc.shape2sas.Models import ModelSystem, SimulationParameters import numpy as np +@dataclass +class TheoreticalScatteringCalculation: + """Class containing parameters for simulating + scattering for a given model system""" + + System: ModelSystem + Calculation: SimulationParameters + + +@dataclass +class TheoreticalScattering: + """Class containing parameters for + theoretical scattering""" + + q: np.ndarray + I0: np.ndarray + I: np.ndarray + S_eff: np.ndarray + class WeightedPairDistribution: def __init__(self, x: np.ndarray, y: np.ndarray, diff --git a/src/sas/sascalc/shape2sas/Typing.py b/src/sas/sascalc/shape2sas/Typing.py index f4ac940165..aca1808949 100644 --- a/src/sas/sascalc/shape2sas/Typing.py +++ b/src/sas/sascalc/shape2sas/Typing.py @@ -1,6 +1,7 @@ import numpy as np -from typing import Optional, Tuple, List, Any +from typing import Tuple, List +Vectors = List[List[float]] Vector2D = Tuple[np.ndarray, np.ndarray] Vector3D = Tuple[np.ndarray, np.ndarray, np.ndarray] Vector4D = Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py index 0a13a24917..ba2597716d 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py +++ b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py @@ -1,6 +1,8 @@ from sas.sascalc.shape2sas.Typing import * from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox + import numpy as np +from typing import Any class NoStructure(StructureDecouplingApprox): def __init__(self, q: np.ndarray, diff --git a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py index c9fb033693..16c4b3843e 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py +++ b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * -from sas.sascalc.shape2sas.Math import sinc +from sas.sascalc.shape2sas.HelperFunctions import sinc + import numpy as np class StructureDecouplingApprox: From 9ab0dec1281f875b43b45ca30b86c3a05b1f0670 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 14:12:25 +0200 Subject: [PATCH 07/37] removed external references to WeightedPairDistribution --- .../shape2sas/ExperimentalScattering.py | 8 +++ src/sas/sascalc/shape2sas/Models.py | 11 ++- src/sas/sascalc/shape2sas/Shape2SAS.py | 70 +++++-------------- .../shape2sas/TheoreticalScattering.py | 46 ++++++++++++ 4 files changed, 82 insertions(+), 53 deletions(-) diff --git a/src/sas/sascalc/shape2sas/ExperimentalScattering.py b/src/sas/sascalc/shape2sas/ExperimentalScattering.py index c5bf536ed8..d2b4758e32 100644 --- a/src/sas/sascalc/shape2sas/ExperimentalScattering.py +++ b/src/sas/sascalc/shape2sas/ExperimentalScattering.py @@ -108,3 +108,11 @@ def save_Iexperimental(self, Isim: np.ndarray, sigma: np.ndarray, Model: str): for i in range(len(Isim)): f.write(' %-12.5e %-12.5e %-12.5e\n' % (self.q[i], Isim[i], sigma[i])) + +def getSimulatedScattering(scalc: SimulateScattering) -> SimulatedScattering: + """Simulate scattering for a given theoretical scattering.""" + + Isim_class = IExperimental(scalc.q, scalc.I0, scalc.I, scalc.exposure) + I_sim, I_err = Isim_class.simulate_data() + + return SimulatedScattering(I_sim=I_sim, q=scalc.q, I_err=I_err) \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/Models.py b/src/sas/sascalc/shape2sas/Models.py index 16824ad0d0..c1ae2d4988 100644 --- a/src/sas/sascalc/shape2sas/Models.py +++ b/src/sas/sascalc/shape2sas/Models.py @@ -415,4 +415,13 @@ def onGeneratingAllPoints(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np. print(f" Total volume of model: {volume_total:.3e} A^3") print(" ") - return x_new, y_new, z_new, p_new, volume_total \ No newline at end of file + return x_new, y_new, z_new, p_new, volume_total + + +def getPointDistribution(prof: ModelProfile, Npoints): + """Generate points for a given model profile.""" + x_new, y_new, z_new, p_new, volume_total = GenerateAllPoints(Npoints, prof.com, prof.subunits, + prof.dimensions, prof.rotation, prof.rotation_points, + prof.p_s, prof.exclude_overlap).onGeneratingAllPointsSeparately() + + return ModelPointDistribution(x=x_new, y=y_new, z=z_new, p=p_new, volume_total=volume_total) \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index 66ed11a978..e5692b272f 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -11,50 +11,6 @@ ) -################################ Shape2SAS functions ################################ -def getPointDistribution(prof: ModelProfile, Npoints): - """Generate points for a given model profile.""" - x_new, y_new, z_new, p_new, volume_total = GenerateAllPoints(Npoints, prof.com, prof.subunits, - prof.dimensions, prof.rotation, prof.rotation_points, - prof.p_s, prof.exclude_overlap).onGeneratingAllPointsSeparately() - - return ModelPointDistribution(x=x_new, y=y_new, z=z_new, p=p_new, volume_total=volume_total) - - -def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> TheoreticalScattering: - """Calculate theoretical scattering for a given model profile.""" - sys = scalc.System - prof = sys.PointDistribution - calc = scalc.Calculation - x = np.concatenate(prof.x) - y = np.concatenate(prof.y) - z = np.concatenate(prof.z) - p = np.concatenate(prof.p) - - r, pr, pr_norm = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) - - q = calc.q - I_theory = ITheoretical(q) - I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) - - S_class = StructureFactor(q, x, y, z, p, sys.Stype, sys.par) - - S_eff = S_class.getStructureFactorClass().structure_eff(Pq) - - I = I_theory.calc_Iq(Pq, S_eff, sys.sigma_r) - - return TheoreticalScattering(q=q, I=I, I0=I0, S_eff=S_eff) - - -def getSimulatedScattering(scalc: SimulateScattering) -> SimulatedScattering: - """Simulate scattering for a given theoretical scattering.""" - - Isim_class = IExperimental(scalc.q, scalc.I0, scalc.I, scalc.exposure) - I_sim, I_err = Isim_class.simulate_data() - - return SimulatedScattering(I_sim=I_sim, q=scalc.q, I_err=I_err) - - ################################ Shape2SAS batch version ################################ if __name__ == "__main__": ################################ Read argparse input ################################ @@ -313,16 +269,26 @@ def check_input(input: float, default: float, name: str, i: int): sigma_r = check_input(args.sigma_r, 0.0, "sigma_r", i) #calculate theoretical scattering - Theo_calc = TheoreticalScatteringCalculation(System=ModelSystem(PointDistribution=Distr, - Stype=Stype, par=par, - polydispersity=pd, conc=conc, - sigma_r=sigma_r), - Calculation=Sim_par) - Theo_I = getTheoreticalScattering(Theo_calc) + model = ModelSystem( + PointDistribution=Distr, + Stype=Stype, par=par, + polydispersity=pd, conc=conc, + sigma_r=sigma_r + ) + + Theo_I = getTheoreticalScattering( + TheoreticalScatteringCalculation( + System=model, + Calculation=Sim_par + ) + ) + + # calculate pair distance distribution function p(r) for plotting + r, pr, pr_norm = getTheoreticalHistogram(model, Sim_par) #save models Model = f'{i}' - WeightedPairDistribution.save_pr(Nbins, Theo_I.r, Theo_I.pr, Model) + WeightedPairDistribution.save_pr(Nbins, r, pr, Model) StructureFactor.save_S(Theo_I.q, Theo_I.S_eff, Model) ITheoretical(Theo_I.q).save_I(Theo_I.I, Model) @@ -342,7 +308,7 @@ def check_input(input: float, default: float, name: str, i: int): p_list.append(np.concatenate(Distr.p)) r_list.append(Theo_I.r) - pr_norm_list.append(Theo_I.pr_norm) + pr_norm_list.append(pr_norm) I_list.append(Theo_I.I) S_eff_list.append(Theo_I.S_eff) diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index 026db619cf..91fc127be1 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * from sas.sascalc.shape2sas.HelperFunctions import sinc +from sas.sascalc.shape2sas.StructureFactor import StructureFactor from dataclasses import dataclass from sas.sascalc.shape2sas.Models import ModelSystem, SimulationParameters @@ -24,6 +25,7 @@ class TheoreticalScattering: I: np.ndarray S_eff: np.ndarray + class WeightedPairDistribution: def __init__(self, x: np.ndarray, y: np.ndarray, @@ -207,6 +209,9 @@ def save_pr(Nbins: int, """ save p(r) to textfile """ + + + with open('pr%s.dat' % Model,'w') as f: f.write('# %-17s %-17s\n' % ('r','p(r)')) for i in range(Nbins): @@ -270,3 +275,44 @@ def save_I(self, I: np.ndarray, Model: str): f.write('# %-12s %-12s\n' % ('q','I')) for i in range(len(I)): f.write(' %-12.5e %-12.5e\n' % (self.q[i], I[i])) + + +def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> TheoreticalScattering: + """Calculate theoretical scattering for a given model profile.""" + sys = scalc.System + prof = sys.PointDistribution + calc = scalc.Calculation + x = np.concatenate(prof.x) + y = np.concatenate(prof.y) + z = np.concatenate(prof.z) + p = np.concatenate(prof.p) + + r, pr, pr_norm = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) + + q = calc.q + I_theory = ITheoretical(q) + I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) + + S_class = StructureFactor(q, x, y, z, p, sys.Stype, sys.par) + + S_eff = S_class.getStructureFactorClass().structure_eff(Pq) + + I = I_theory.calc_Iq(Pq, S_eff, sys.sigma_r) + + return TheoreticalScattering(q=q, I=I, I0=I0, S_eff=S_eff) + +def getTheoreticalHistogram(model: ModelSystem, sim_pars: SimulationParameters) -> Vector3D: + """ + Get theoretical histogram for a given model system and simulation parameters. + This function is used to calculate the pair distribution function (p(r)). + + :param model: ModelSystem object containing the model parameters. + :param sim_pars: SimulationParameters object containing the simulation parameters. + :return: r, pr, pr_norm - pair distance distribution function. + """ + prof = model.PointDistribution + x = np.concatenate(prof.x) + y = np.concatenate(prof.y) + z = np.concatenate(prof.z) + p = np.concatenate(prof.p) + return WeightedPairDistribution(x, y, z, p).calc_pr(sim_pars.prpoints, model.polydispersity) \ No newline at end of file From 4c533ee405c19d4ebe60e70db94262fd6f53c1fa Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 15:08:13 +0200 Subject: [PATCH 08/37] changed scattering calc from gui --- .../Calculators/Shape2SAS/DesignWindow.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index d2bc085ea1..756165d138 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -695,13 +695,20 @@ def getSimulatedSAXSData(self): Distr = getPointDistribution(Profile, N) - Theo_calc = TheoreticalScatteringCalculation(System=ModelSystem(PointDistribution=Distr, - Stype=Stype, par=par, - polydispersity=polydispersity, - conc=conc, - sigma_r=sigma_r), - Calculation=Sim_par) - Theo_I = getTheoreticalScattering(Theo_calc) + model = ModelSystem( + PointDistribution=Distr, + Stype=Stype, par=par, + polydispersity=polydispersity, + conc=conc, + sigma_r=sigma_r + ) + + Theo_I = getTheoreticalScattering( + TheoreticalScatteringCalculation( + System=model, + Calculation=Sim_par + ) + ) Sim_calc = SimulateScattering(q=Theo_I.q, I0=Theo_I.I0, I=Theo_I.I, exposure=exposure) Sim_SAXS = getSimulatedScattering(Sim_calc) From 4833b5332d3f2016a7279efa2f2684ea66037b91 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 15:08:27 +0200 Subject: [PATCH 09/37] disabled shadows on preview plot --- src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py b/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py index 8d5da35ce0..b8fdc2030b 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py @@ -22,6 +22,9 @@ def __init__(self, parent=None): ###3D plot view of model self.scatter = Q3DScatter() + # remove shadows + self.scatter.setShadowQuality(Q3DScatter.ShadowQuality.ShadowQualityNone) + """ NOTE: Orignal intend was to create QScatter3DSeries() in setPlot() method. However, From 99dd2d3adcd58bb323b5c0e13acfc14958a5faf3 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 15:51:51 +0200 Subject: [PATCH 10/37] added initial support for forwarding debye calc to ausaxs --- .../shape2sas/TheoreticalScattering.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index 91fc127be1..ffd15fe13d 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -246,6 +246,13 @@ def calc_Pq(self, r: np.ndarray, pr: np.ndarray, conc: float, volume_total: floa I0 *= conc * volume_total * 1E-4 return I0, Pq + + def calc_Pq_ausaxs(self, q: np.ndarray, x: np.ndarray, y: np.ndarray, z: np.ndarray, p: np.ndarray) -> np.ndarray: + """ + calculate form factor, P(q), using ausaxs SANS Debye method + """ + from sas.sascalc.calculator.ausaxs.ausaxs_sans_debye import evaluate_sans_debye + return evaluate_sans_debye(q, np.array([x, y, z]), p) def calc_Iq(self, Pq: np.ndarray, S_eff: np.ndarray, @@ -277,8 +284,10 @@ def save_I(self, I: np.ndarray, Model: str): f.write(' %-12.5e %-12.5e\n' % (self.q[i], I[i])) +use_ausaxs = True def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> TheoreticalScattering: """Calculate theoretical scattering for a given model profile.""" + sys = scalc.System prof = sys.PointDistribution calc = scalc.Calculation @@ -286,19 +295,20 @@ def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> Theoret y = np.concatenate(prof.y) z = np.concatenate(prof.z) p = np.concatenate(prof.p) - - r, pr, pr_norm = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) - q = calc.q I_theory = ITheoretical(q) - I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) - S_class = StructureFactor(q, x, y, z, p, sys.Stype, sys.par) + if use_ausaxs: + Pq = I_theory.calc_Pq_ausaxs(q, x, y, z, p) + I0 = np.square(np.sum(p)) * sys.conc * prof.volume_total * 1E-4 + + else: + r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) + I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) + S_class = StructureFactor(q, x, y, z, p, sys.Stype, sys.par) S_eff = S_class.getStructureFactorClass().structure_eff(Pq) - I = I_theory.calc_Iq(Pq, S_eff, sys.sigma_r) - return TheoreticalScattering(q=q, I=I, I0=I0, S_eff=S_eff) def getTheoreticalHistogram(model: ModelSystem, sim_pars: SimulationParameters) -> Vector3D: From 6621209790959f1e8b042c3ec6fff7823f085a16 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Thu, 19 Jun 2025 16:50:39 +0200 Subject: [PATCH 11/37] improved ausaxs support --- .../shape2sas/TheoreticalScattering.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index ffd15fe13d..abd2543eda 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -209,9 +209,6 @@ def save_pr(Nbins: int, """ save p(r) to textfile """ - - - with open('pr%s.dat' % Model,'w') as f: f.write('# %-17s %-17s\n' % ('r','p(r)')) for i in range(Nbins): @@ -299,9 +296,23 @@ def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> Theoret I_theory = ITheoretical(q) if use_ausaxs: - Pq = I_theory.calc_Pq_ausaxs(q, x, y, z, p) - I0 = np.square(np.sum(p)) * sys.conc * prof.volume_total * 1E-4 - + import time + t_start = time.time() + I0 = np.square(np.sum(p)) * sys.conc * prof.volume_total + Pq = I_theory.calc_Pq_ausaxs(q, x, y, z, p)/I0 + I0 = 1 + print(f"AUSAXS I0: {I0}") + print(f"AUSAXS P: {Pq[0]}, {Pq[1]}, {Pq[2]}, {Pq[3]}, {Pq[4]}") + t_end_ausaxs = time.time() + + r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) + I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) + t_end_shape2sas = time.time() + print(f"Shape2SAS I0: {I0}") + print(f"Shape2SAS P: {Pq[0]}, {Pq[1]}, {Pq[2]}, {Pq[3]}, {Pq[4]}") + print(f"AUSAXS time: {(t_end_ausaxs - t_start)*1000:.2f} ms") + print(f"Shape2SAS time: {(t_end_shape2sas - t_start)*1000:.2f} ms") + else: r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) From 69279fe1687eaef255b989171459da5cfd5abd3d Mon Sep 17 00:00:00 2001 From: Krelle Date: Wed, 2 Jul 2025 15:22:38 +0200 Subject: [PATCH 12/37] moved genPlugin from qtgui to sascalc; changed signatures to match --- .../Calculators/Shape2SAS/DesignWindow.py | 37 ++++++++---- .../shape2sas/PluginGenerator.py} | 60 ++++++++++--------- 2 files changed, 57 insertions(+), 40 deletions(-) rename src/sas/{qtgui/Calculators/Shape2SAS/genPlugin.py => sascalc/shape2sas/PluginGenerator.py} (66%) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index 756165d138..11ea7fa964 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -28,17 +28,23 @@ # Local SasView from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor -from sas.sascalc.shape2sas.Shape2SAS import ( - ModelProfile, - ModelSystem, - Qsampling, - SimulateScattering, - SimulationParameters, - TheoreticalScatteringCalculation, - getPointDistribution, - getSimulatedScattering, - getTheoreticalScattering, -) +from sas.qtgui.Perspectives.perspective import Perspective +from sas.qtgui.Utilities.GuiUtils import createModelItemWithPlot +from sas.qtgui.Plotting.PlotterData import Data1D + +from sas.qtgui.Calculators.Shape2SAS.UI.DesignWindowUI import Ui_Shape2SAS +from sas.qtgui.Calculators.Shape2SAS.ViewerModel import ViewerModel +from sas.qtgui.Calculators.Shape2SAS.ButtonOptions import ButtonOptions +from sas.qtgui.Calculators.Shape2SAS.Tables.subunitTable import SubunitTable, OptionLayout +from sas.qtgui.Calculators.Shape2SAS.Constraints import Constraints, logger +from sas.qtgui.Calculators.Shape2SAS.PlotAspects.plotAspects import Canvas + +from sas.sascalc.shape2sas.Shape2SAS import (getTheoreticalScattering, getPointDistribution, getSimulatedScattering, + ModelProfile, ModelSystem, SimulationParameters, + Qsampling, TheoreticalScatteringCalculation, + SimulateScattering) +from sas.qtgui.Calculators.Shape2SAS.PlotAspects.plotAspects import ViewerPlotDesign +from sas.sascalc.shape2sas.PluginGenerator import generate_plugin class DesignWindow(QDialog, Ui_Shape2SAS, Perspective): @@ -641,7 +647,14 @@ def getPluginModel(self): #conditional subunit table parameters modelProfile = self.getModelProfile(self.ifFitPar, conditionBool=checkedPars, conditionFitPar=parNames) - model_str, full_path = generatePlugin(modelProfile, [importStatement, parameters, translation], fitPar, Npoints, prPoints, modelName) + model_str, full_path = generate_plugin( + modelProfile, + [importStatement, parameters, translation], + fitPar, + Npoints, + prPoints, + modelName + ) #Write file to plugin model folder TabbedModelEditor.writeFile(full_path, model_str) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/genPlugin.py b/src/sas/sascalc/shape2sas/PluginGenerator.py similarity index 66% rename from src/sas/qtgui/Calculators/Shape2SAS/genPlugin.py rename to src/sas/sascalc/shape2sas/PluginGenerator.py index 894821e88a..2617a83749 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/genPlugin.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -1,27 +1,27 @@ -#Global import textwrap from pathlib import Path +import logging #Global SasView #Local Perspectives +from sas.sascalc.fit import models from sas.sascalc.shape2sas.Shape2SAS import ModelProfile from sas.system.user import find_plugins_dir - -def generatePlugin(prof: ModelProfile, constrainParameters: (str), fitPar: [str], - Npoints: int, pr_points: int, file_name: str) -> (str, Path): +def generate_plugin(prof: ModelProfile, constrainParameters: (str), fitPar: list[str], + Npoints: int, pr_points: int, file_name: str) -> tuple[str, Path]: """Generates a theoretical scattering plugin model""" - plugin_location = Path(find_plugins_dir()) - full_path = plugin_location / file_name - full_path = full_path.with_suffix('.py') + plugin_location = Path(models.find_plugins_dir()) + full_path = plugin_location.joinpath(file_name).with_suffix('.py') + logging.info(f"Plugin model will be saved to: {full_path}") - model_str = generateModel(prof, constrainParameters, fitPar, Npoints, pr_points, file_name) + model_str = generate_model(prof, constrainParameters, fitPar, Npoints, pr_points, file_name) return model_str, full_path -def parListFormat(par: [[str | float]]) -> str: +def format_parameter_list(par: list[list[str | float]]) -> str: """ Format a list of parameters to the model string. In this case the list is on element for each shape. For a single shape there will be only @@ -30,7 +30,7 @@ def parListFormat(par: [[str | float]]) -> str: return f"[{', '.join(str(x) for x in par)}]" -def parListsFormat(par: [str | float]) -> str: +def format_parameter_list_of_list(par: list[str | float]) -> str: """ Format a list of list containing parameters to the model string. This is used for single shape parameter lists like the center of mass of the @@ -41,7 +41,7 @@ def parListsFormat(par: [str | float]) -> str: return f"[[{'],['.join(sub_pars_join)}]]" -def generateModel(prof: ModelProfile, constrainParameters: (str), fitPar: [str], +def generate_model(prof: ModelProfile, constrainParameters: (str), fitPar: list[str], Npoints: int, pr_points: int, model_name: str) -> str: """Generates a theoretical model""" importStatement, parameters, translation = constrainParameters @@ -88,30 +88,34 @@ def Iq({', '.join(fitPar)}): {textwrap.indent(translation, ' ')} - modelProfile = ModelProfile(subunits={prof.subunits}, - p_s={parListFormat(prof.p_s)}, - dimensions={parListsFormat(prof.dimensions)}, - com={parListsFormat(prof.com)}, - rotation_points={parListsFormat(prof.rotation_points)}, - rotation={parListsFormat(prof.rotation)}, - exclude_overlap={prof.exclude_overlap}) + modelProfile = ModelProfile( + subunits={prof.subunits}, + p_s={format_parameter_list(prof.p_s)}, + dimensions={format_parameter_list_of_list(prof.dimensions)}, + com={format_parameter_list_of_list(prof.com)}, + rotation_points={format_parameter_list_of_list(prof.rotation_points)}, + rotation={format_parameter_list_of_list(prof.rotation)}, + exclude_overlap={prof.exclude_overlap} + ) simPar = SimulationParameters(q=q, prpoints={pr_points}, Npoints={Npoints}, model_name="{model_name.replace('.py', '')}") dist = getPointDistribution(modelProfile, {Npoints}) - scattering = TheoreticalScatteringCalculation(System=ModelSystem(PointDistribution=dist, - Stype="None", par=[], - polydispersity=0.0, conc=1, - sigma_r=0.0), - Calculation=simPar) + scattering = TheoreticalScatteringCalculation( + System=ModelSystem( + PointDistribution=dist, + Stype="None", par=[], + polydispersity=0.0, conc=1, + sigma_r=0.0 + ), + Calculation=simPar + ) theoreticalScattering = getTheoreticalScattering(scattering) return theoreticalScattering.I Iq.vectorized = True -''').lstrip().rstrip() - - return model_str - - +''') + + return model_str \ No newline at end of file From 47648d6921e2a22befdd662149bacd1a30e7716e Mon Sep 17 00:00:00 2001 From: Krelle Date: Wed, 2 Jul 2025 16:12:20 +0200 Subject: [PATCH 13/37] 'create plugin model' is now available without specifying constraints --- .../Calculators/Shape2SAS/Constraints.py | 25 ++++++++++--------- .../Calculators/Shape2SAS/DesignWindow.py | 5 ++-- .../Shape2SAS/Tables/variableTable.py | 6 +++++ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index fa1e79edae..9c2c238def 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -47,6 +47,7 @@ def __init__(self, parent=None): self.createPlugin.setMaximumSize(110, 24) self.createPlugin.setToolTip("Create and send the plugin model to the Plugin Models Category in Fit panel") self.createPlugin.setEnabled(False) + self.variableTable.on_item_changed_callback = lambda _: self.createPlugin.setEnabled(True) self.buttonOptions.horizontalLayout_5.insertWidget(1, self.createPlugin) self.buttonOptions.horizontalLayout_5.setContentsMargins(0, 0, 0, 0) @@ -97,8 +98,8 @@ def checkPythonSyntax(self, text: str): #send to log logger.error(traceback_to_show) - def getConstraints(self, constraintsStr: str, fitPar: [str], modelPars: [str], modelVals: [[float]], - checkedPars: [str]) -> ([str], str, str, [[bool]]): + def getConstraints(self, constraintsStr: str, fitPar: list[str], modelPars: list[str], modelVals: list[list[float]], + checkedPars: list[str]) -> tuple[list[str], str, str, list[list[bool]]]: """Read inputs from text editor""" self.checkPythonSyntax(constraintsStr) @@ -115,7 +116,7 @@ def getConstraints(self, constraintsStr: str, fitPar: [str], modelPars: [str], m return importStatement, parameters, translation, checkedPars @staticmethod - def getPosition(item: VAL_TYPE, itemLists: [[VAL_TYPE]]) -> (int, int): + def getPosition(item: VAL_TYPE, itemLists: list[list[VAL_TYPE]]) -> tuple[int, int]: """Find position of an item in lists""" for i, sublist in enumerate(itemLists): @@ -123,7 +124,7 @@ def getPosition(item: VAL_TYPE, itemLists: [[VAL_TYPE]]) -> (int, int): return i, sublist.index(item) @staticmethod - def removeFromList(listItems: [VAL_TYPE], listToCompare: [VAL_TYPE]): + def removeFromList(listItems: list[VAL_TYPE], listToCompare: list[VAL_TYPE]): """Remove items from a list if in another list""" finalpars = [] @@ -135,7 +136,7 @@ def removeFromList(listItems: [VAL_TYPE], listToCompare: [VAL_TYPE]): # Explicilty modify list sent to the method listItems = finalpars - def ifParameterExists(self, lineNames: [str], modelPars: [[str]]) -> bool: + def ifParameterExists(self, lineNames: list[str], modelPars: list[list[str]]) -> bool: """Check if parameter exists in model parameters""" for par in lineNames: @@ -145,8 +146,8 @@ def ifParameterExists(self, lineNames: [str], modelPars: [[str]]) -> bool: return False - def getTranslation(self, constraintsStr: str, importStatement: [str], modelPars: [[str]], modelVals: [[float]], - checkedPars: [[str]]) -> (str, [[bool]]): + def getTranslation(self, constraintsStr: str, importStatement: list[str], modelPars: list[list[str]], + modelVals: list[list[float]], checkedPars: list[list[str]]) -> tuple[str, list[list[bool]]]: """Get translation from constraints""" #see if translation is in constraints @@ -239,7 +240,7 @@ def getParametersFromConstraints(self, constraints_str: str, targetName: str) -> logger.warn(f"No {targetName} variable found in constraints") - def getParameters(self, constraintsStr: str, fitPar: [str]) -> str: + def getParameters(self, constraintsStr: str, fitPar: list[str]) -> str: """Get parameters from constraints""" #Is anything in parameters? @@ -260,7 +261,7 @@ def getParameters(self, constraintsStr: str, fitPar: [str]) -> str: return parameters_str - def isImportFromStatement(self, node: ast.ImportFrom) -> [str]: + def isImportFromStatement(self, node: ast.ImportFrom) -> list[str]: """Return list of ImportFrom statements""" #Check if library exists @@ -281,7 +282,7 @@ def isImportFromStatement(self, node: ast.ImportFrom) -> [str]: return [f"from {node.module} import {', '.join(imports)}"] - def isImportStatement(self, node: ast.Import) -> [str]: + def isImportStatement(self, node: ast.Import) -> list[str]: """Return list of Import statements""" imports = [] @@ -297,8 +298,8 @@ def isImportStatement(self, node: ast.Import) -> [str]: return [f"import {', '.join(imports)}"] - def getImportStatements(self, text: str) -> [str]: - """return all import statements that were + def getImportStatements(self, text: str) -> list[str]: + """return all import statements that were written in the text editor""" importStatements = [] diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index 11ea7fa964..a11f234b45 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -639,11 +639,11 @@ def getPluginModel(self): #get chosen fit parameters fitPar = self.getFitParameters() - logger.info("Retrieving and verifying constraints. . .") + logger.info("Retrieving and verifying constraints.") #get parameters constraints importStatement, parameters, translation, checkedPars = self.checkStateOfConstraints(fitPar, parNames, parVals, checkedPars) - logger.info("Retrieving Model. . .") + logger.info("Retrieving Model.") #conditional subunit table parameters modelProfile = self.getModelProfile(self.ifFitPar, conditionBool=checkedPars, conditionFitPar=parNames) @@ -660,6 +660,7 @@ def getPluginModel(self): TabbedModelEditor.writeFile(full_path, model_str) self.communicator.customModelDirectoryChanged.emit() logger.info(f"Successfully generated model {modelName}!") + self.constraint.createPlugin.setEnabled(False) def onCheckingInput(self, input: str, default: str) -> str: """Check if the input not None. Otherwise, return default value""" diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Tables/variableTable.py b/src/sas/qtgui/Calculators/Shape2SAS/Tables/variableTable.py index a02b4b2cfc..f214c46fe6 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Tables/variableTable.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Tables/variableTable.py @@ -58,6 +58,7 @@ def __init__(self): self.initializeVariableModel() self.setDefaultLayout() + self.on_item_changed_callback = None def initializeVariableModel(self): @@ -99,8 +100,13 @@ def setVariableTableData(self, names: list[str], column: int): itemNum.setTextAlignment(Qt.AlignCenter) itemNum.setFont(font) self.variableModel.insertRow(numrow, [itemName, itemNum]) + self.variableModel.itemChanged.connect(self.onItemChanged) itemNum.setFlags(itemNum.flags() & ~Qt.ItemIsSelectable & ~Qt.ItemIsEditable) + def onItemChanged(self, item): + """Handle item changes in the variable table""" + if self.on_item_changed_callback: + self.on_item_changed_callback(item) def removeTableData(self, row): """Remove data from table""" From 27ae5242fa3bbc5bc587d86eba3d898e6b0cdd2a Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sun, 13 Jul 2025 13:23:50 +0200 Subject: [PATCH 14/37] simplified constraint script parsing --- .../Calculators/Shape2SAS/Constraints.py | 306 +++++------------- .../Calculators/Shape2SAS/DesignWindow.py | 4 +- src/sas/sascalc/shape2sas/PluginGenerator.py | 35 +- 3 files changed, 105 insertions(+), 240 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 9c2c238def..02201e7ce6 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -45,7 +45,7 @@ def __init__(self, parent=None): self.createPlugin = QPushButton("Create Plugin") self.createPlugin.setMinimumSize(110, 24) self.createPlugin.setMaximumSize(110, 24) - self.createPlugin.setToolTip("Create and send the plugin model to the Plugin Models Category in Fit panel") + self.createPlugin.setToolTip("Create the plugin model. It will be available in the Plugin Models category in the Fit panel.") self.createPlugin.setEnabled(False) self.variableTable.on_item_changed_callback = lambda _: self.createPlugin.setEnabled(True) @@ -57,63 +57,94 @@ def __init__(self, parent=None): self.textEdit_2.append(defaultText) - def getConstraintText(self, constraints: str) -> str: - """Get default text for constraints""" - - self.constraintText = (f''' -#Write libraries to be imported here. -from numpy import inf - -#Modify fit parameters here. -parameters = {constraints} - -#Set constraints here. -translation = """ - -""" - - ''').lstrip().rstrip() + def getConstraintText(self, fit_params: str) -> str: + """Get the default text for the constraints editor""" + + self.constraintText = ( + "# Write libraries to be imported here.\n" + "from numpy import inf\n" + "\n" + "# Modify fit parameters here.\n" + f"parameters = {fit_params}\n" + "\n" + "# Define your constraints here.\n" + "# Both absolute and relative parameters can be used.\n" + "# Example: dCOMX2 = dCOMX1 will make COMX2 track changes in COMX1\n" + ) return self.constraintText - def setConstraints(self, constraints: str): - """Set text to QTextEdit""" + def setConstraints(self, fit_params: str): + """Insert the text into the constraints editor""" - constraints = self.getConstraintText(constraints) + constraints = self.getConstraintText(fit_params) self.constraintTextEditor.txtEditor.setPlainText(constraints) self.createPlugin.setEnabled(True) - def checkPythonSyntax(self, text: str): - """Check if text is valid python syntax""" - - try: - ast.parse(text) - - except SyntaxError: - #Get last line of traceback - all_lines = traceback.format_exc().split('\n') - last_lines = all_lines[-1:] - traceback_to_show = '\n'.join(last_lines) - - #send to log - logger.error(traceback_to_show) - - def getConstraints(self, constraintsStr: str, fitPar: list[str], modelPars: list[str], modelVals: list[list[float]], - checkedPars: list[str]) -> tuple[list[str], str, str, list[list[bool]]]: - """Read inputs from text editor""" - - self.checkPythonSyntax(constraintsStr) - - #Get and check import statements - importStatement = self.getImportStatements(constraintsStr) - - #Get and check parameters - parameters = self.getParameters(constraintsStr, fitPar) - - #Get and check translation - translation, checkedPars = self.getTranslation(constraintsStr, importStatement, modelPars, modelVals, checkedPars) + @staticmethod + def parseConstraintsText( + text: str, fitPar: list[str], modelPars: list[str], modelVals: list[list[float]], checkedPars: list[str] + ) -> tuple[list[str], str, str, list[list[bool]]]: + """Parse the text in the constraints editor and return a dictionary of parameters""" + + print("Parsing constraints text.") + print("Received input:") + print(f"fitPar: {fitPar}") + print(f"modelPars: {modelPars}") + print(f"modelVals: {modelVals}") + print(f"checkedPars: {checkedPars}") + + def as_ast(text: str): + try: + return ast.parse(text) + except SyntaxError as e: + # log most recent traceback error + all_lines = traceback.format_exc().split('\n') + last_lines = all_lines[-1:] + traceback_to_show = '\n'.join(last_lines) + logger.error(traceback_to_show) + return None + + def parse_ast(tree: ast.AST): + params = None + imports = [] + constraints = [] - return importStatement, parameters, translation, checkedPars + for node in ast.walk(tree): + match node: + case ast.ImportFrom() | ast.Import(): + imports.append(node) + + case ast.Assign(): + if node.targets[0].id == 'parameters': + params = node + else: + constraints.append(node) + + # params must be defined + if params is None: + logger.error("No parameters found in constraints text.") + return None, None, None + + # ensure imports are valid + #! not implemented yet + + return [ + ast.unparse(params), + [ast.unparse(imp) for imp in imports], + [ast.unparse(constraint) for constraint in constraints] + ] + + tree = as_ast(text) + if tree is None: + return None + + params, imports, constraints = parse_ast(tree) + print("Finished parsing constraints text.") + print(f"Parsed parameters: {params}") + print(f"Parsed imports: {imports}") + print(f"Parsed constraints: {constraints}") + return imports, params, constraints, checkedPars @staticmethod def getPosition(item: VAL_TYPE, itemLists: list[list[VAL_TYPE]]) -> tuple[int, int]: @@ -146,181 +177,6 @@ def ifParameterExists(self, lineNames: list[str], modelPars: list[list[str]]) -> return False - def getTranslation(self, constraintsStr: str, importStatement: list[str], modelPars: list[list[str]], - modelVals: list[list[float]], checkedPars: list[list[str]]) -> tuple[str, list[list[bool]]]: - """Get translation from constraints""" - - #see if translation is in constraints - if not re.search(r'translation\s*=', constraintsStr): - logger.warn("No variable translation found in constraints") - - #TODO: make getParametersFromConstraints general, so translation can be inputted - #NOTE: re.search() a bit slow, ast faster - translation = re.search(r'translation\s*=\s*"""(.*\n(?:.*\n)*?)"""', constraintsStr, re.DOTALL) - translationInput = translation.group(1) if translation else "" - - #Check syntax - self.checkPythonSyntax(translationInput) #TODO: fix wrong line number output for translation - lines = translationInput.split('\n') - - #remove empty lines and tabs - lines = [line.replace('\t', '').strip() for line in lines if line.strip()] - translationInput = '' - - #Check parameters and update checkedPars - for line in lines: - if line.count('=') != 1: - logger.warn(f"Constraints may only have a single '=' sign in them. Please fix {line}.") - - #split line - leftLine, rightLine = line.split('=') - - #unicode greek letters: \u0370-\u03FF - rightPars = re.findall(r'(?<=)[a-zA-Z_\u0370-\u03FF]\w*\b', rightLine) - leftPars = re.findall(r'(?<=)[a-zA-Z_\u0370-\u03FF]\w*\b', leftLine) - - #check for import statements (I can't imagine a case where it would be to the left) - self.removeFromList(rightPars, importStatement) - - #check if parameters exist in model parameters - self.ifParameterExists(rightPars + leftPars, modelPars) - - #Translate - notes = "" - for par in rightPars: - j, k = self.getPosition(par, modelPars) - if not checkedPars[j][k]: - #if parameter is a constant, set inputted value - inputVal = modelVals[j][k] - - #update line with input value - rightLine = re.sub(r'\b' + re.escape(par) + r'\b', str(inputVal), rightLine) - notes += f"{par} = {inputVal}," - #any constants added to notes? - if notes: - notes = f" #{notes}" - - for par in leftPars: - j, k = self.getPosition(par, modelPars) - #check if paramater are to be set in ModelProfile - checkedPars[j][k] = True - line = leftLine + '=' + rightLine + notes - translationInput += line + "\n" - - return translationInput, checkedPars - - def extractValues(self, elt: ast.AST) -> VAL_TYPE: - if isinstance(elt, ast.Constant): - return elt.value - elif isinstance(elt, ast.List): - return [self.extractValues(elt) for elt in elt.elts] - #statements for the the boundary list: - elif isinstance(elt, ast.Name) and elt.id == 'inf': - return float('inf') - elif isinstance(elt, ast.Name): - return elt.id - #check for negative values in boundary list - elif isinstance(elt.op, ast.USub) and self.extractValues(elt.operand) == float('inf'): - return float('-inf') - elif isinstance(elt.op, ast.USub) and isinstance(self.extractValues(elt.operand), (int, float)): - return -1*self.extractValues(elt.operand) - return None - - def getParametersFromConstraints(self, constraints_str: str, targetName: str) -> []: - """Extract parameters from constraints string""" - tree = ast.parse(constraints_str) #get abstract syntax tree - - parametersNode = None - for node in ast.walk(tree): - #is the node an assignment and does it have the target name? - if isinstance(node, ast.Assign) and node.targets[0].id == targetName: - parametersNode = node.value - parameters = [self.extractValues(elt) for elt in parametersNode.elts] - return parameters - - logger.warn(f"No {targetName} variable found in constraints") - - def getParameters(self, constraintsStr: str, fitPar: list[str]) -> str: - """Get parameters from constraints""" - - #Is anything in parameters? - parameters = self.getParametersFromConstraints(constraintsStr, 'parameters') - names = [parameter[0] for parameter in parameters] - - #Check parameters in constraints - if len(names) != len(fitPar): - logger.error("Number of parameters in variable parameters does not match checked parameters in table") - - #Check if parameter exists in checked parameters - for name in names: - if name not in fitPar: - logger.error(f"{name} does not exists in checked parameters") - - description = 'parameters =' + '[' + '\n' + '# name, units, default, [min, max], type, description,' + '\n' - parameters_str = description + ',\n'.join(str(sublist) for sublist in parameters) + "\n]" - - return parameters_str - - def isImportFromStatement(self, node: ast.ImportFrom) -> list[str]: - """Return list of ImportFrom statements""" - - #Check if library exists - if not importlib.util.find_spec(node.module): - raise ModuleNotFoundError(f"No module named {node.module}") - - imports = [] - module = importlib.import_module(node.module) - - for alias in node.names: - #check if library has the attribute - if not hasattr(module, f"{alias.name}"): - raise AttributeError(f"module {node.module} has no attribute {alias.name}") - if alias.asname: - imports.append(f"{alias.name} as {alias.asname}") - else: - imports.append(f"{alias.name}") - - return [f"from {node.module} import {', '.join(imports)}"] - - def isImportStatement(self, node: ast.Import) -> list[str]: - """Return list of Import statements""" - - imports = [] - for alias in node.names: - #check if library exists - if not importlib.util.find_spec(alias.name): - raise ModuleNotFoundError(f"No module named {alias.name}") - #get name and asname - if alias.asname: - imports.append(f"{alias.name} as {alias.asname}") - else: - imports.append(f"{alias.name}") - - return [f"import {', '.join(imports)}"] - - def getImportStatements(self, text: str) -> list[str]: - """return all import statements that were - written in the text editor""" - - importStatements = [] - - try: - tree = ast.parse(text) - #look for import statements - for node in ast.walk(tree): - #check statement type - if isinstance(node, ast.ImportFrom): - importStatements.extend(self.isImportFromStatement(node)) - - elif isinstance(node, ast.Import): - importStatements.extend(self.isImportStatement(node)) - - return importStatements - - except SyntaxError as e: - error_line = text.splitlines()[e.lineno - 1] - raise SyntaxError(f"Syntax error: {e.msg} at line {e.lineno}: {error_line}") - def clearConstraints(self): """Clear text editor containing constraints""" self.constraintTextEditor.txtEditor.clear() diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index a11f234b45..b89a132852 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -258,8 +258,8 @@ def checkStateOfConstraints(self, fitPar: list[str], modelPars: list[list[str]], #Has anything been written to the text editor if constraintsStr: #TODO: print to GUI output texteditor - return self.constraint.getConstraints(constraintsStr, fitPar, modelPars, modelVals, checkedPars) - + return self.constraint.parseConstraintsText(constraintsStr, fitPar, modelPars, modelVals, checkedPars) + #Did the user only check parameters and click generate plugin elif fitPar: #Get default constraints diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 2617a83749..2a0baf1c4b 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -50,7 +50,9 @@ def generate_model(prof: ModelProfile, constrainParameters: (str), fitPar: list[ fitPar.insert(0, "q") - model_str = (f''' + model_str = ( +# file header +f'''\ r""" This plugin model uses Shape2SAS to generate theoretical 1D small-angle scattering. Shape2SAS is a program built by Larsen and Brookes @@ -67,26 +69,33 @@ def generate_model(prof: ModelProfile, constrainParameters: (str), fitPar: list[ {', '.join(prof.subunits)} """ +''' +# imports +f'''\ {nl.join(importStatement)} -from sas.sascalc.shape2sas.Shape2SAS import (ModelProfile, SimulationParameters, - ModelSystem, getPointDistribution, - TheoreticalScatteringCalculation, - getTheoreticalScattering) - -name = "{model_name.replace('.py', '')}" +from sas.sascalc.shape2sas.Shape2SAS import ( + ModelProfile, SimulationParameters, ModelSystem, getPointDistribution, + TheoreticalScatteringCalculation, getTheoreticalScattering +) +''' + +# model description +f'''\ +name = "{model_name.replace('.py', '')}"' title = "Shape2SAS Model" -description = """ -Theoretical generation of P(q) using Shape2SAS -""" +description = "Theoretical generation of P(q) using Shape2SAS" category = "plugin" +''' -{parameters} +# parameter list +f"{parameters}\n" +# define Iq +f'''\ def Iq({', '.join(fitPar)}): """Fit function using Shape2SAS to calculate the scattering intensity.""" - -{textwrap.indent(translation, ' ')} + {nl.join(translation)} modelProfile = ModelProfile( subunits={prof.subunits}, From 8a2f25be4ccd4a42b58590a134081fa1bc503dce Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sun, 13 Jul 2025 13:56:38 +0200 Subject: [PATCH 15/37] defined dX pars in plugin model --- .../Calculators/Shape2SAS/Constraints.py | 2 +- .../Calculators/Shape2SAS/DesignWindow.py | 1 + src/sas/sascalc/shape2sas/PluginGenerator.py | 70 ++++++++++++++++--- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 02201e7ce6..b1a74a4ccf 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -114,7 +114,7 @@ def parse_ast(tree: ast.AST): match node: case ast.ImportFrom() | ast.Import(): imports.append(node) - + case ast.Assign(): if node.targets[0].id == 'parameters': params = node diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index b89a132852..ce6bbf9bb1 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -649,6 +649,7 @@ def getPluginModel(self): model_str, full_path = generate_plugin( modelProfile, + [parNames, parVals], [importStatement, parameters, translation], fitPar, Npoints, diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 2a0baf1c4b..6b5c06a7c9 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -8,15 +8,22 @@ from sas.sascalc.shape2sas.Shape2SAS import ModelProfile from sas.system.user import find_plugins_dir -def generate_plugin(prof: ModelProfile, constrainParameters: (str), fitPar: list[str], - Npoints: int, pr_points: int, file_name: str) -> tuple[str, Path]: +def generate_plugin( + prof: ModelProfile, + modelPars: list[list[str], list[str | float]], + constrainParameters: (str), + fitPar: list[str], + Npoints: int, + pr_points: int, + file_name: str +) -> tuple[str, Path]: """Generates a theoretical scattering plugin model""" plugin_location = Path(models.find_plugins_dir()) full_path = plugin_location.joinpath(file_name).with_suffix('.py') logging.info(f"Plugin model will be saved to: {full_path}") - model_str = generate_model(prof, constrainParameters, fitPar, Npoints, pr_points, file_name) + model_str = generate_model(prof, modelPars, constrainParameters, fitPar, Npoints, pr_points, file_name) return model_str, full_path @@ -41,16 +48,58 @@ def format_parameter_list_of_list(par: list[str | float]) -> str: return f"[[{'],['.join(sub_pars_join)}]]" -def generate_model(prof: ModelProfile, constrainParameters: (str), fitPar: list[str], - Npoints: int, pr_points: int, model_name: str) -> str: +def delta_parameters_script_inserts(fitPar: list[str], modelPars: list[list[str | float]]) -> str: + """ + Format the code section defining and updating the delta parameters. + """ + par_names, par_vals = modelPars[0], modelPars[1] + + prev_pars_def = [] + shape_index, par_index = -1, -1 + for par in fitPar: + name = "prev_" + par + for shape_index in range(len(modelPars)): + if par in par_names[shape_index]: + par_index = par_names[shape_index].index(par) + break + if par_index == -1: + raise ValueError(f"Parameter '{par}' not found in model parameters.") + val = par_vals[shape_index][par_index] + prev_pars_def.append(f"{name} = {val}") + prev_pars_def = "\n".join(prev_pars_def) + + globals = "global " + ", ".join([f"prev_{par}" for par in fitPar]) + delta_pars_def = [] + prev_pars_update = [] + for par in fitPar: + delta_pars_def.append(f"d{par} = {par} - prev_{par}") + prev_pars_update.append(f"prev_{par} = {par}") + delta_pars_def = "\n ".join(delta_pars_def) # indentation for the function body + prev_pars_update = "\n ".join(prev_pars_update) + + return ( + f"{prev_pars_def}", + f" {globals}\n" + f" {delta_pars_def}\n" + f" {prev_pars_update}\n" + ) + +def generate_model( + prof: ModelProfile, + modelPars: list[list[str], list[str | float]], + constrainParameters: (str), + fitPar: list[str], + Npoints: int, + pr_points: int, + model_name: str +) -> str: """Generates a theoretical model""" importStatement, parameters, translation = constrainParameters - + delta_parameters_def, delta_parameters_update = delta_parameters_script_inserts(fitPar, modelPars) nl = '\n' - fitPar.insert(0, "q") - model_str = ( + # file header f'''\ r""" @@ -93,8 +142,13 @@ def generate_model(prof: ModelProfile, constrainParameters: (str), fitPar: list[ # define Iq f'''\ + +# previous fit parameter values +{delta_parameters_def} + def Iq({', '.join(fitPar)}): """Fit function using Shape2SAS to calculate the scattering intensity.""" +{delta_parameters_update} {nl.join(translation)} modelProfile = ModelProfile( From 1ea2782232ca085b9a7dfc746edf156e9e9f4c9f Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sun, 13 Jul 2025 16:30:19 +0200 Subject: [PATCH 16/37] added param update functionality to plugin --- .../Calculators/Shape2SAS/Constraints.py | 70 ++++++++++---- .../Calculators/Shape2SAS/DesignWindow.py | 4 +- src/sas/sascalc/shape2sas/PluginGenerator.py | 91 +++++++++++++------ src/sas/sascalc/shape2sas/UserText.py | 9 ++ 4 files changed, 126 insertions(+), 48 deletions(-) create mode 100644 src/sas/sascalc/shape2sas/UserText.py diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index b1a74a4ccf..9676505fae 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -8,14 +8,11 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QPushButton, QWidget -from sas.qtgui.Calculators.Shape2SAS.ButtonOptions import ButtonOptions -from sas.qtgui.Calculators.Shape2SAS.Tables.variableTable import VariableTable - -#Local Perspectives -from sas.qtgui.Calculators.Shape2SAS.UI.ConstraintsUI import Ui_Constraints - -#Global SasView from sas.qtgui.Utilities.ModelEditors.TabbedEditor.ModelEditor import ModelEditor +from sas.qtgui.Calculators.Shape2SAS.UI.ConstraintsUI import Ui_Constraints +from sas.qtgui.Calculators.Shape2SAS.Tables.variableTable import VariableTable +from sas.qtgui.Calculators.Shape2SAS.ButtonOptions import ButtonOptions +from sas.sascalc.shape2sas.UserText import UserText logger = logging.getLogger(__name__) @@ -129,22 +126,63 @@ def parse_ast(tree: ast.AST): # ensure imports are valid #! not implemented yet - return [ - ast.unparse(params), - [ast.unparse(imp) for imp in imports], - [ast.unparse(constraint) for constraint in constraints] - ] + return params, imports, constraints + + def extract_symbols(constraints: list[ast.AST]) -> tuple[list[str], list[str]]: + """Extract all symbols used in the constraints.""" + lhs, rhs = set(), set() + for node in constraints: + # left-hand side of assignment + for target in node.targets: + if isinstance(target, ast.Name): + lhs.add(target.id) + + # right-hand side of assignment + for value in ast.walk(node.value): + if isinstance(value, ast.Name): + rhs.add(value.id) + + return lhs, rhs + + def validate_symbols(lhs: list[str], rhs: list[str], fitPars: list[str]): + """Check if all symbols in lhs and rhs are valid parameters.""" + # lhs is not allowed to contain fit parameters + for symbol in lhs: + if symbol in fitPars or symbol[1:] in fitPars: + logger.error(f"Symbol '{symbol}' is a fit parameter and cannot be used in constraints.") + raise ValueError(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to.") + + def validate_imports(imports: list[ast.ImportFrom | ast.Import]): + """Check if all imports are valid.""" + for imp in imports: + if isinstance(imp, ast.ImportFrom): + if not importlib.util.find_spec(imp.module): + logger.error(f"Module '{imp.module}' not found.") + raise ModuleNotFoundError(f"No module named {imp.module}") + elif isinstance(imp, ast.Import): + for name in imp.names: + if not importlib.util.find_spec(name.name): + logger.error(f"Module '{name.name}' not found.") + raise ModuleNotFoundError(f"No module named {name.name}") tree = as_ast(text) - if tree is None: - return None - params, imports, constraints = parse_ast(tree) + lhs, rhs = extract_symbols(constraints) + validate_symbols(lhs, rhs, fitPar) + validate_imports(imports) + + params = ast.unparse(params) + imports = [ast.unparse(imp) for imp in imports] + constraints = [ast.unparse(constraint) for constraint in constraints] + symbols = (lhs, rhs) + print("Finished parsing constraints text.") print(f"Parsed parameters: {params}") print(f"Parsed imports: {imports}") print(f"Parsed constraints: {constraints}") - return imports, params, constraints, checkedPars + print(f"Symbols used: {symbols}") + + return UserText(imports, params, constraints, symbols), checkedPars @staticmethod def getPosition(item: VAL_TYPE, itemLists: list[list[VAL_TYPE]]) -> tuple[int, int]: diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index ce6bbf9bb1..5159539048 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -641,7 +641,7 @@ def getPluginModel(self): logger.info("Retrieving and verifying constraints.") #get parameters constraints - importStatement, parameters, translation, checkedPars = self.checkStateOfConstraints(fitPar, parNames, parVals, checkedPars) + usertext, checkedPars = self.checkStateOfConstraints(fitPar, parNames, parVals, checkedPars) logger.info("Retrieving Model.") #conditional subunit table parameters @@ -650,7 +650,7 @@ def getPluginModel(self): model_str, full_path = generate_plugin( modelProfile, [parNames, parVals], - [importStatement, parameters, translation], + usertext, fitPar, Npoints, prPoints, diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 6b5c06a7c9..52994ac7ba 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -6,12 +6,12 @@ #Local Perspectives from sas.sascalc.fit import models from sas.sascalc.shape2sas.Shape2SAS import ModelProfile -from sas.system.user import find_plugins_dir +from sas.sascalc.shape2sas.UserText import UserText def generate_plugin( prof: ModelProfile, modelPars: list[list[str], list[str | float]], - constrainParameters: (str), + usertext: UserText, fitPar: list[str], Npoints: int, pr_points: int, @@ -23,7 +23,7 @@ def generate_plugin( full_path = plugin_location.joinpath(file_name).with_suffix('.py') logging.info(f"Plugin model will be saved to: {full_path}") - model_str = generate_model(prof, modelPars, constrainParameters, fitPar, Npoints, pr_points, file_name) + model_str = generate_model(prof, modelPars, usertext, fitPar, Npoints, pr_points, file_name) return model_str, full_path @@ -48,54 +48,83 @@ def format_parameter_list_of_list(par: list[str | float]) -> str: return f"[[{'],['.join(sub_pars_join)}]]" -def delta_parameters_script_inserts(fitPar: list[str], modelPars: list[list[str | float]]) -> str: +def script_insert_delta_parameters(modelPars: list[list[str | float]], symbols: tuple[set[str], set[str]]) -> tuple[str, str]: """ - Format the code section defining and updating the delta parameters. + Create the code sections defining and updating the delta parameters. + Only parameters declared in the symbol list will be included. """ par_names, par_vals = modelPars[0], modelPars[1] + symbols = symbols[0].union(symbols[1]) # combine lhs and rhs symbols + globals = [] prev_pars_def = [] - shape_index, par_index = -1, -1 - for par in fitPar: - name = "prev_" + par - for shape_index in range(len(modelPars)): - if par in par_names[shape_index]: - par_index = par_names[shape_index].index(par) + delta_pars_def = [] + prev_pars_update = [] + for symbol in symbols: + if symbol[0] != 'd': + continue # skip if symbol is not a delta parameter + symbol = symbol[1:] # remove 'd' prefix + + # find the list index of the parameter + par_index = -1 + for shape_index in range(len(par_names)): + shape = par_names[shape_index] + if symbol in shape: + par_index = par_names[shape_index].index(symbol) break if par_index == -1: - raise ValueError(f"Parameter '{par}' not found in model parameters.") + raise ValueError(f"Parameter '{symbol}' not found in model parameters.") + + # create the variable names val = par_vals[shape_index][par_index] - prev_pars_def.append(f"{name} = {val}") + + prev_name = "prev_" + symbol + globals.append(f"{prev_name}") + prev_pars_def.append(f"{prev_name} = {val}") + delta_pars_def.append(f"d{symbol} = {prev_name} - {symbol}") + prev_pars_update.append(f"{prev_name} = {symbol}") + + if not delta_pars_def: + return False, "", "" + + # convert to strings + globals = "global " + ", ".join(globals) prev_pars_def = "\n".join(prev_pars_def) - - globals = "global " + ", ".join([f"prev_{par}" for par in fitPar]) - delta_pars_def = [] - prev_pars_update = [] - for par in fitPar: - delta_pars_def.append(f"d{par} = {par} - prev_{par}") - prev_pars_update.append(f"prev_{par} = {par}") delta_pars_def = "\n ".join(delta_pars_def) # indentation for the function body prev_pars_update = "\n ".join(prev_pars_update) return ( + True, f"{prev_pars_def}", - f" {globals}\n" + f"{globals}\n" # insertion site is already indented, so only newlines should be manually indented f" {delta_pars_def}\n" f" {prev_pars_update}\n" ) +def script_insert_apply_constraints(lhs_symbols: set[str]) -> str: + """ Create the code responsible for updating constraints.""" + + text = [] + for symbol in lhs_symbols: + if symbol[0] != 'd': + continue + symbol = symbol[1:] # remove 'd' prefix + text.append(f"{symbol} += d{symbol}") + return bool(text), "\n ".join(text) # indentation for the function body + def generate_model( prof: ModelProfile, modelPars: list[list[str], list[str | float]], - constrainParameters: (str), + usertext: UserText, fitPar: list[str], Npoints: int, pr_points: int, model_name: str ) -> str: """Generates a theoretical model""" - importStatement, parameters, translation = constrainParameters - delta_parameters_def, delta_parameters_update = delta_parameters_script_inserts(fitPar, modelPars) + importStatement, parameters, translation = usertext.imports, usertext.params, usertext.constraints + insert_delta, delta_parameters_def, delta_parameters_update = script_insert_delta_parameters(modelPars, usertext.symbols) + insert_constraint_update, constraint_update = script_insert_apply_constraints(usertext.symbols[0]) nl = '\n' fitPar.insert(0, "q") model_str = ( @@ -138,18 +167,20 @@ def generate_model( ''' # parameter list -f"{parameters}\n" +f"{parameters}\n\n" -# define Iq +# define prev_X vars f'''\ +{"# previous fit parameter values" + nl + delta_parameters_def if insert_delta else ""} +''' -# previous fit parameter values -{delta_parameters_def} - +# define Iq +f'''\ def Iq({', '.join(fitPar)}): """Fit function using Shape2SAS to calculate the scattering intensity.""" -{delta_parameters_update} + {delta_parameters_update if insert_delta else ""} {nl.join(translation)} + {constraint_update if insert_constraint_update else ""} modelProfile = ModelProfile( subunits={prof.subunits}, diff --git a/src/sas/sascalc/shape2sas/UserText.py b/src/sas/sascalc/shape2sas/UserText.py new file mode 100644 index 0000000000..34dcb9ce56 --- /dev/null +++ b/src/sas/sascalc/shape2sas/UserText.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + +@dataclass +class UserText: + def __init__(self, imports: list[str], params: list[str], constraints: list[str], symbols: tuple[set[str], set[str]]): + self.imports = imports + self.params = params + self.constraints = constraints + self.symbols = symbols \ No newline at end of file From 17bc110ea2b760fc3cdc49a25b6a9f29d8a71364 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sun, 13 Jul 2025 17:15:54 +0200 Subject: [PATCH 17/37] constraint window now opens on top --- src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index 5159539048..f6dfbe905e 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -166,7 +166,8 @@ def __init__(self, parent=None): #TODO: implement in a future project ###Building Constraint window - self.constraint = Constraints() + self.constraint = Constraints(parent=self) + self.constraint.setWindowFlags(Qt.Window | Qt.Tool) self.subunitTable.add.clicked.connect(self.addToVariableTable) self.subunitTable.deleteButton.clicked.connect(self.deleteFromVariableTable) self.subunitTable.table.clicked.connect(self.updateDeleteButton) @@ -183,6 +184,8 @@ def __init__(self, parent=None): def showConstraintWindow(self): """Get the Constraint window""" + self.constraint.setScreen(self.screen()) + self.constraint.move(self.pos().x()+50, self.pos().y()+50) self.constraint.show() def checkedVariables(self): From 339ec00e7c03f9fa255322ee8a060ac5be0785bd Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sun, 13 Jul 2025 17:22:48 +0200 Subject: [PATCH 18/37] 'create plugin' button is now enabled upon modifying the constraint text --- src/sas/qtgui/Calculators/Shape2SAS/Constraints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 9676505fae..c5402dc88a 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -26,6 +26,7 @@ def __init__(self, parent=None): #Setup GUI for Constraints self.constraintTextEditor = ModelEditor() #SasView's standard text editor + self.constraintTextEditor.modelModified.connect(lambda: self.createPlugin.setEnabled(True)) self.constraintTextEditor.gridLayout.setContentsMargins(0, 0, 0, 0) self.constraintTextEditor.gridLayout_16.setContentsMargins(5, 5, 5, 5) self.constraintTextEditor.groupBox_13.setTitle("Constraints") #override title From 06b695d7f68494b5fffa2c7b790645aa869a5a7c Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sun, 13 Jul 2025 18:36:22 +0200 Subject: [PATCH 19/37] update --- .../Calculators/Shape2SAS/Constraints.py | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index c5402dc88a..e9ca3a0f6b 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -72,11 +72,61 @@ def getConstraintText(self, fit_params: str) -> str: return self.constraintText - def setConstraints(self, fit_params: str): + def setConstraints(self, parameter_text: str): """Insert the text into the constraints editor""" - constraints = self.getConstraintText(fit_params) - self.constraintTextEditor.txtEditor.setPlainText(constraints) + def get_default(parameter_text: str): + return self.getConstraintText(parameter_text) + + def merge_text(current_text: str, parameter_text: str): + if not current_text: + return get_default(parameter_text) + + # search for 'parameters =' in the current text + found = False + current_text_lines = current_text.splitlines() + for start, line in enumerate(current_text_lines): + if line.startswith("parameters ="): + break + + # find closing bracket of the parameters list + bracket_count = 0 + for end, line in enumerate(current_text_lines[start:]): + bracket_count += line.count('[') - line.count(']') + if bracket_count == 0: + # found the closing bracket + found = True + old_lines = current_text_lines[start:start+end+1] + break + + if not found: + return get_default(parameter_text) + + new_lines = parameter_text.split("\n") + # fit_param string is formatted as: + # [ + # # header + # ['name1', 'unit1', ...], + # ['name2', 'unit2', ...], + # ... + # ] + # extract names from the first element of each sublist, skipping the first two and last lines + table = str.maketrans("", "", "[]' ") + old_names = [line.translate(table).split(",")[0] for line in old_lines[2:-1]] + new_names = [line.translate(table).split(",")[0] for line in new_lines[2:-1]] + + # create new list of parameters, replacing existing old lines in the new list + for i, name in enumerate(new_names): + if name in old_names: + new_lines[i+2] = old_lines[old_names.index(name)+2] + + # remove old lines from the current text and insert the new ones in the middle + current_text = "\n".join(current_text_lines[:start+1] + new_lines[2:-1] + current_text_lines[start+end:]) + return current_text + + current_text = self.constraintTextEditor.txtEditor.toPlainText() + text = merge_text(current_text, parameter_text) + self.constraintTextEditor.txtEditor.setPlainText(text) self.createPlugin.setEnabled(True) @staticmethod From 419e4c27a2f6c66944f80b09402c7f6a78c197ab Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sun, 13 Jul 2025 21:06:11 +0200 Subject: [PATCH 20/37] plugin script seems finished; fitter complains about negative vals --- .../Calculators/Shape2SAS/Constraints.py | 33 ++++++++----- src/sas/sascalc/shape2sas/PluginGenerator.py | 47 ++++++++++++++++--- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index e9ca3a0f6b..7a4ac0b771 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -101,7 +101,7 @@ def merge_text(current_text: str, parameter_text: str): if not found: return get_default(parameter_text) - + new_lines = parameter_text.split("\n") # fit_param string is formatted as: # [ @@ -123,7 +123,7 @@ def merge_text(current_text: str, parameter_text: str): # remove old lines from the current text and insert the new ones in the middle current_text = "\n".join(current_text_lines[:start+1] + new_lines[2:-1] + current_text_lines[start+end:]) return current_text - + current_text = self.constraintTextEditor.txtEditor.toPlainText() text = merge_text(current_text, parameter_text) self.constraintTextEditor.txtEditor.setPlainText(text) @@ -131,7 +131,7 @@ def merge_text(current_text: str, parameter_text: str): @staticmethod def parseConstraintsText( - text: str, fitPar: list[str], modelPars: list[str], modelVals: list[list[float]], checkedPars: list[str] + text: str, fitPar: list[str], modelPars: list[list[str]], modelVals: list[list[float]], checkedPars: list[list[bool]] ) -> tuple[list[str], str, str, list[list[bool]]]: """Parse the text in the constraints editor and return a dictionary of parameters""" @@ -169,14 +169,6 @@ def parse_ast(tree: ast.AST): else: constraints.append(node) - # params must be defined - if params is None: - logger.error("No parameters found in constraints text.") - return None, None, None - - # ensure imports are valid - #! not implemented yet - return params, imports, constraints def extract_symbols(constraints: list[ast.AST]) -> tuple[list[str], list[str]]: @@ -194,6 +186,11 @@ def extract_symbols(constraints: list[ast.AST]) -> tuple[list[str], list[str]]: rhs.add(value.id) return lhs, rhs + + def validate_params(params: ast.AST): + if params is None: + logger.error("No parameters found in constraints text.") + raise ValueError("No parameters found in constraints text.") def validate_symbols(lhs: list[str], rhs: list[str], fitPars: list[str]): """Check if all symbols in lhs and rhs are valid parameters.""" @@ -216,9 +213,21 @@ def validate_imports(imports: list[ast.ImportFrom | ast.Import]): logger.error(f"Module '{name.name}' not found.") raise ModuleNotFoundError(f"No module named {name.name}") + def mark_named_parameters(checkedPars: list[list[bool]], modelPars: list[str], symbols: set[str]): + """Mark parameters in the modelPars as checked if they are in symbols_lhs.""" + for i, shape in enumerate(modelPars): + for j, par in enumerate(shape): + if par is None: + continue + in_symbols = par in symbols + d_in_symbols = "d" + par in symbols + checkedPars[i][j] = checkedPars[i][j] or in_symbols or d_in_symbols + return checkedPars + tree = as_ast(text) params, imports, constraints = parse_ast(tree) lhs, rhs = extract_symbols(constraints) + validate_params(params) validate_symbols(lhs, rhs, fitPar) validate_imports(imports) @@ -233,7 +242,7 @@ def validate_imports(imports: list[ast.ImportFrom | ast.Import]): print(f"Parsed constraints: {constraints}") print(f"Symbols used: {symbols}") - return UserText(imports, params, constraints, symbols), checkedPars + return UserText(imports, params, constraints, symbols), mark_named_parameters(checkedPars, modelPars, lhs.union(rhs)) @staticmethod def getPosition(item: VAL_TYPE, itemLists: list[list[VAL_TYPE]]) -> tuple[int, int]: diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 52994ac7ba..3a5da44a5b 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -48,7 +48,7 @@ def format_parameter_list_of_list(par: list[str | float]) -> str: return f"[[{'],['.join(sub_pars_join)}]]" -def script_insert_delta_parameters(modelPars: list[list[str | float]], symbols: tuple[set[str], set[str]]) -> tuple[str, str]: +def script_insert_delta_parameters(modelPars: list[list[str | float]], fitPars: list[str], symbols: tuple[set[str], set[str]]) -> tuple[str, str]: """ Create the code sections defining and updating the delta parameters. Only parameters declared in the symbol list will be included. @@ -61,6 +61,7 @@ def script_insert_delta_parameters(modelPars: list[list[str | float]], symbols: delta_pars_def = [] prev_pars_update = [] for symbol in symbols: + print(f"Processing symbol: {symbol}") if symbol[0] != 'd': continue # skip if symbol is not a delta parameter symbol = symbol[1:] # remove 'd' prefix @@ -78,12 +79,18 @@ def script_insert_delta_parameters(modelPars: list[list[str | float]], symbols: # create the variable names val = par_vals[shape_index][par_index] + # add base variable to globals if not a fit parameter + if symbol not in fitPars: + globals.append(symbol) + prev_name = "prev_" + symbol globals.append(f"{prev_name}") prev_pars_def.append(f"{prev_name} = {val}") delta_pars_def.append(f"d{symbol} = {prev_name} - {symbol}") prev_pars_update.append(f"{prev_name} = {symbol}") + print(f"Globals: {globals}") + if not delta_pars_def: return False, "", "" @@ -112,6 +119,30 @@ def script_insert_apply_constraints(lhs_symbols: set[str]) -> str: text.append(f"{symbol} += d{symbol}") return bool(text), "\n ".join(text) # indentation for the function body +def script_insert_constrained_parameters(symbols: set[str], modelPars: list[list[str], list[str | float]]) -> str: + """ Create the code defining the constrained parameters.""" + par_names, par_vals = modelPars[0], modelPars[1] + symbols = symbols[0].union(symbols[1]) # combine lhs and rhs symbols + + text = [] + for symbol in symbols: + if symbol[0] == 'd': + if symbol[1:] in symbols: + continue + symbol = symbol[1:] # remove 'd' prefix + + # find the list index of the parameter + par_index = -1 + for shape_index in range(len(par_names)): + shape = par_names[shape_index] + if symbol in shape: + par_index = par_names[shape_index].index(symbol) + break + if par_index == -1: + raise ValueError(f"Parameter '{symbol}' not found in model parameters.") + text.append(f"{symbol} = {par_vals[shape_index][par_index]}") + return bool(text), "\n".join(text) # indentation for the function body + def generate_model( prof: ModelProfile, modelPars: list[list[str], list[str | float]], @@ -123,8 +154,9 @@ def generate_model( ) -> str: """Generates a theoretical model""" importStatement, parameters, translation = usertext.imports, usertext.params, usertext.constraints - insert_delta, delta_parameters_def, delta_parameters_update = script_insert_delta_parameters(modelPars, usertext.symbols) + insert_delta, delta_parameters_def, delta_parameters_update = script_insert_delta_parameters(modelPars, fitPar, usertext.symbols) insert_constraint_update, constraint_update = script_insert_apply_constraints(usertext.symbols[0]) + insert_constrained_defs, constrained_parameters = script_insert_constrained_parameters(usertext.symbols, modelPars) nl = '\n' fitPar.insert(0, "q") model_str = ( @@ -160,18 +192,19 @@ def generate_model( # model description f'''\ -name = "{model_name.replace('.py', '')}"' +name = "{model_name.replace('.py', '')}" title = "Shape2SAS Model" description = "Theoretical generation of P(q) using Shape2SAS" category = "plugin" ''' # parameter list -f"{parameters}\n\n" +f"{parameters}\n" -# define prev_X vars +# define prev_X vars and constrained parameters f'''\ -{"# previous fit parameter values" + nl + delta_parameters_def if insert_delta else ""} +{nl + "# previous fit parameter values" + nl + delta_parameters_def if insert_delta else ""} +{nl + "# constrained parameters" + nl + constrained_parameters if insert_constrained_defs else ""} ''' # define Iq @@ -179,7 +212,7 @@ def generate_model( def Iq({', '.join(fitPar)}): """Fit function using Shape2SAS to calculate the scattering intensity.""" {delta_parameters_update if insert_delta else ""} - {nl.join(translation)} + {(nl + " ").join(translation)} {constraint_update if insert_constraint_update else ""} modelProfile = ModelProfile( From 6ac755ae865dc697cf265ce61dab6d11e89f0f6d Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 26 Jul 2025 13:37:16 +0200 Subject: [PATCH 21/37] fixed plugin generation; seems to work now --- src/sas/sascalc/shape2sas/Models.py | 86 ++++++++++--------- src/sas/sascalc/shape2sas/PluginGenerator.py | 40 +++++++-- .../shape2sas/TheoreticalScattering.py | 22 ++--- 3 files changed, 89 insertions(+), 59 deletions(-) diff --git a/src/sas/sascalc/shape2sas/Models.py b/src/sas/sascalc/shape2sas/Models.py index c1ae2d4988..22af313ada 100644 --- a/src/sas/sascalc/shape2sas/Models.py +++ b/src/sas/sascalc/shape2sas/Models.py @@ -62,15 +62,12 @@ class ModelSystem: class Rotation: - def __init__(self, x_add: np.ndarray, - y_add: np.ndarray, - z_add: np.ndarray, - alpha: float, - beta: float, - gam: float, - rotp_x: float, - rotp_y: float, - rotp_z: float): + def __init__( + self, + x_add: np.ndarray, y_add: np.ndarray, z_add: np.ndarray, + alpha: float, beta: float, gam: float, + rotp_x: float, rotp_y: float,rotp_z: float + ): self.x_add = x_add self.y_add = y_add self.z_add = z_add @@ -145,6 +142,7 @@ def __init__(self, com: list[float], def onGeneratingPoints(self) -> Vector3D: """Generates the points""" + print(f"Generating {self.Npoints} points for subunit {self.subunitClass.__name__} with dimensions {self.dimensions}") x, y, z= self.subunitClass(self.dimensions).getPointDistribution(self.Npoints) x, y, z = self.onTransformingPoints(x, y, z) return x, y, z @@ -188,35 +186,36 @@ def __init__(self, Npoints: int, def setAvailableSubunits(self): """Returns the available subunits""" self.subunitClasses = { - "sphere": Sphere, - "ball": Sphere, + "sphere": Sphere, + "ball": Sphere, - "hollow_sphere": HollowSphere, - "Hollow sphere": HollowSphere, + "hollow_sphere": HollowSphere, + "Hollow sphere": HollowSphere, - "cylinder": Cylinder, + "cylinder": Cylinder, - "ellipsoid": Ellipsoid, + "ellipsoid": Ellipsoid, - "elliptical_cylinder": EllipticalCylinder, - "Elliptical cylinder": EllipticalCylinder, + "elliptical_cylinder": EllipticalCylinder, + "Elliptical cylinder": EllipticalCylinder, - "disc": Disc, + "disc": Disc, - "cube": Cube, + "cube": Cube, - "hollow_cube": HollowCube, - "Hollow cube": HollowCube, + "hollow_cube": HollowCube, + "Hollow cube": HollowCube, - "cuboid": Cuboid, + "cuboid": Cuboid, - "cyl_ring": CylinderRing, - "Cylinder ring": CylinderRing, + "cyl_ring": CylinderRing, + "Cylinder ring": CylinderRing, - "disc_ring": DiscRing, - "Disc ring": DiscRing, - - "superellipsoid": SuperEllipsoid} + "disc_ring": DiscRing, + "Disc ring": DiscRing, + + "superellipsoid": SuperEllipsoid + } def getSubunitClass(self, key: str): if key in self.subunitClasses: @@ -254,16 +253,18 @@ def onAppendingPoints(x_new: np.ndarray, return x_new, y_new, z_new, p_new @staticmethod - def onCheckOverlap(x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - p: np.ndarray, - rotation: list[float], - rotation_point: list[float], - com: list[float], - subunitClass: object, - dimensions: list[float]): - """check for overlap with previous subunits. + def onCheckOverlap( + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + p: np.ndarray, + rotation: List[float], + rotation_point: list[float], + com: List[float], + subunitClass: object, + dimensions: List[float] + ): + """check for overlap with previous subunits. if overlap, the point is removed""" if sum(rotation) != 0: @@ -330,7 +331,7 @@ def onGeneratingAllPointsSeparately(self) -> Vector3D: N.append(N_subunit) rho.append(rho_subunit) N_exclude.append(N_x_sum) - fraction_left = (N_subunit-N_x_sum) / N_subunit + fraction_left = (N_subunit-N_x_sum) / max(N_subunit, 1) volume_total += volume[i] * fraction_left x_new.append(x_add) @@ -420,6 +421,13 @@ def onGeneratingAllPoints(self) -> tuple[np.ndarray, np.ndarray, np.ndarray, np. def getPointDistribution(prof: ModelProfile, Npoints): """Generate points for a given model profile.""" + + print(f"Generating points for model profile: {prof.subunits}") + print(f"Number of subunits: {len(prof.subunits)}") + for i, subunit in enumerate(prof.subunits): + print(f" Subunit {i}: {subunit} with dimensions {prof.dimensions[i]} at COM {prof.com[i]}") + print(f" Rotation: {prof.rotation[i]} at rotation point {prof.rotation_points[i]} with SLD {prof.p_s[i]}") + x_new, y_new, z_new, p_new, volume_total = GenerateAllPoints(Npoints, prof.com, prof.subunits, prof.dimensions, prof.rotation, prof.rotation_points, prof.p_s, prof.exclude_overlap).onGeneratingAllPointsSeparately() diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 3a5da44a5b..83a46e4a40 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -9,13 +9,13 @@ from sas.sascalc.shape2sas.UserText import UserText def generate_plugin( - prof: ModelProfile, - modelPars: list[list[str], list[str | float]], - usertext: UserText, - fitPar: list[str], - Npoints: int, - pr_points: int, - file_name: str + prof: ModelProfile, + modelPars: list[list[str], list[str | float]], + usertext: UserText, + fitPar: list[str], + Npoints: int, + pr_points: int, + file_name: str ) -> tuple[str, Path]: """Generates a theoretical scattering plugin model""" @@ -48,6 +48,28 @@ def format_parameter_list_of_list(par: list[str | float]) -> str: return f"[[{'],['.join(sub_pars_join)}]]" +def format_parameter_list_of_list_dimension(par: list[list[str | float]]) -> str: + """ + Format a list of lists containing dimensional parameters to the model string. + Variables will be enclosed in 'min(abs(x), 1)' for safety. + """ + def format_parameter(p): + if isinstance(p, str): + return f"min(abs({p}), 1)" + elif isinstance(p, (int, float)): + if p < 0: + raise ValueError(f"PluginGenerator: Got value {p}, but dimensional scalars cannot be negative!") + return str(p) + else: + return str(p) + + def format_sublist(sub_par): + return ", ".join(format_parameter(p) for p in sub_par) + + formatted_sublists = [format_sublist(sub_par) for sub_par in par] + return f"[[{'],['.join(formatted_sublists)}]]" + + def script_insert_delta_parameters(modelPars: list[list[str | float]], fitPars: list[str], symbols: tuple[set[str], set[str]]) -> tuple[str, str]: """ Create the code sections defining and updating the delta parameters. @@ -86,7 +108,7 @@ def script_insert_delta_parameters(modelPars: list[list[str | float]], fitPars: prev_name = "prev_" + symbol globals.append(f"{prev_name}") prev_pars_def.append(f"{prev_name} = {val}") - delta_pars_def.append(f"d{symbol} = {prev_name} - {symbol}") + delta_pars_def.append(f"d{symbol} = {symbol} - {prev_name}") prev_pars_update.append(f"{prev_name} = {symbol}") print(f"Globals: {globals}") @@ -218,7 +240,7 @@ def Iq({', '.join(fitPar)}): modelProfile = ModelProfile( subunits={prof.subunits}, p_s={format_parameter_list(prof.p_s)}, - dimensions={format_parameter_list_of_list(prof.dimensions)}, + dimensions={format_parameter_list_of_list_dimension(prof.dimensions)}, com={format_parameter_list_of_list(prof.com)}, rotation_points={format_parameter_list_of_list(prof.rotation_points)}, rotation={format_parameter_list_of_list(prof.rotation)}, diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index abd2543eda..68e96e04ef 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -296,22 +296,22 @@ def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> Theoret I_theory = ITheoretical(q) if use_ausaxs: - import time - t_start = time.time() + # import time + # t_start = time.time() I0 = np.square(np.sum(p)) * sys.conc * prof.volume_total Pq = I_theory.calc_Pq_ausaxs(q, x, y, z, p)/I0 I0 = 1 print(f"AUSAXS I0: {I0}") print(f"AUSAXS P: {Pq[0]}, {Pq[1]}, {Pq[2]}, {Pq[3]}, {Pq[4]}") - t_end_ausaxs = time.time() - - r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) - I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) - t_end_shape2sas = time.time() - print(f"Shape2SAS I0: {I0}") - print(f"Shape2SAS P: {Pq[0]}, {Pq[1]}, {Pq[2]}, {Pq[3]}, {Pq[4]}") - print(f"AUSAXS time: {(t_end_ausaxs - t_start)*1000:.2f} ms") - print(f"Shape2SAS time: {(t_end_shape2sas - t_start)*1000:.2f} ms") + # t_end_ausaxs = time.time() + + # r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) + # I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) + # t_end_shape2sas = time.time() + # print(f"Shape2SAS I0: {I0}") + # print(f"Shape2SAS P: {Pq[0]}, {Pq[1]}, {Pq[2]}, {Pq[3]}, {Pq[4]}") + # print(f"AUSAXS time: {(t_end_ausaxs - t_start)*1000:.2f} ms") + # print(f"Shape2SAS time: {(t_end_shape2sas - t_start)*1000:.2f} ms") else: r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) From f6b33c1b922966f85d23db24e44719949238eafa Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 26 Jul 2025 13:55:55 +0200 Subject: [PATCH 22/37] live update of plot --- .../Calculators/Shape2SAS/DesignWindow.py | 4 ++- .../Shape2SAS/Tables/subunitTable.py | 25 +++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index f6dfbe905e..cf7757434f 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -105,7 +105,7 @@ def __init__(self, parent=None): #create layout for build model tab self.viewerModel = ViewerModel() - self.subunitTable = SubunitTable() + self.subunitTable = SubunitTable(self.onClickingPlot) modelVbox = QVBoxLayout() modelHbox = QHBoxLayout() @@ -492,6 +492,8 @@ def onClickingPlot(self): self.plugin.setEnabled(columns > 0) if not self.subunitTable.model.item(1, columns - 1): + self.viewerModel.setClearScatteringPlot() + self.viewerModel.setClearModelPlot() return modelProfile = self.getModelProfile(self.ifEmptyValue) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Tables/subunitTable.py b/src/sas/qtgui/Calculators/Shape2SAS/Tables/subunitTable.py index 3949b32c9e..d6dd54d194 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Tables/subunitTable.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Tables/subunitTable.py @@ -333,11 +333,12 @@ class CustomStandardItem(QStandardItem): """Custom QStandardItem to set initial values, roles and take care of subunit and colour case""" - def __init__(self, prefix="", unit="", tooltip="", default_value=None): + def __init__(self, prefix="", unit="", tooltip="", default_value=None, plot_callback=None): super().__init__(str(default_value)) self.prefix = prefix self.tooltip = tooltip self.unit = unit + self.plot_callback = plot_callback self.setData(default_value, Qt.EditRole) @@ -350,7 +351,6 @@ def data(self, role=Qt.DisplayRole): return f"{self.prefix}{value}{self.unit}" elif role == Qt.ToolTipRole: return self.tooltip - return value @@ -366,6 +366,8 @@ def setData(self, value, role=Qt.EditRole): super().setData(value, Qt.DisplayRole) else: super().setData(value, role) + if self.plot_callback: + self.plot_callback() class CustomDelegate(QStyledItemDelegate): @@ -433,13 +435,14 @@ def __init__(self, parent=None): class SubunitTable(QWidget, Ui_SubunitTableController): """Subunit table functionality and design for the model tab""" - def __init__(self): + def __init__(self, updatePlotCallback=None): super(SubunitTable, self).__init__() self.setupUi(self) self.columnEyeKeeper = [] self.restrictedRowsPos = [] + self.updatePlotCallback = updatePlotCallback self.initializeModel() self.initializeSignals() self.setSubunitOptions() @@ -508,8 +511,11 @@ def onAdding(self): #if row is contained in the subunit if row in subunitName.keys(): paintedName = subunitName[row] + f"{to_column_name}" + " = " - item = CustomStandardItem(paintedName, subunitUnits[row], - subunitTooltip[row], subunitDefault_value[row]) + item = CustomStandardItem( + paintedName, subunitUnits[row], + subunitTooltip[row], subunitDefault_value[row], + plot_callback=self.updatePlotCallback + ) else: #no input for this row item = CustomStandardItem("", "", "", "") @@ -529,8 +535,11 @@ def onAdding(self): attr = getattr(OptionLayout, row.value) method = MethodType(attr, OptionLayout) name, defaultVal, units, tooltip, _, _ = method() - item = CustomStandardItem(name[row] + f"{to_column_name}" + " = ", units[row], - tooltip[row], defaultVal[row]) + item = CustomStandardItem( + name[row] + f"{to_column_name}" + " = ", units[row], + tooltip[row], defaultVal[row], + plot_callback=self.updatePlotCallback + ) items.append(item) @@ -539,6 +548,7 @@ def onAdding(self): self.setSubunitRestriction(subunitName.keys()) self.table.resizeColumnsToContents() self.setButtonSpinboxBounds() + self.updatePlotCallback() # Update the plot after adding a subunit def onDeleting(self): @@ -554,6 +564,7 @@ def onDeleting(self): #clear the table if no columns are left if not self.model.columnCount(): self.onClearSubunitTable() + self.updatePlotCallback() # Update the plot after removing a subunit def setButtonSpinboxBounds(self): From c6a2ebdaae9d5828318bc4a64775890a2206c145 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 26 Jul 2025 18:05:38 +0200 Subject: [PATCH 23/37] added COM support --- .../Calculators/Shape2SAS/Constraints.py | 86 ++++++++++++++++--- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 7a4ac0b771..55d2945cae 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -88,7 +88,7 @@ def merge_text(current_text: str, parameter_text: str): for start, line in enumerate(current_text_lines): if line.startswith("parameters ="): break - + # find closing bracket of the parameters list bracket_count = 0 for end, line in enumerate(current_text_lines[start:]): @@ -152,7 +152,57 @@ def as_ast(text: str): traceback_to_show = '\n'.join(last_lines) logger.error(traceback_to_show) return None - + + def expand_center_of_mass_pars(constraint: ast.Assign) -> list[ast.Assign]: + """Expand center of mass parameters to include all components.""" + + # check if this is a COM assignment we need to expand + if (len(constraint.targets) != 1 or + not isinstance(constraint.targets[0], ast.Name) or + not isinstance(constraint.value, ast.Name)): + return constraint + + lhs = constraint.targets[0].id + rhs = constraint.value.id + + # check if lhs is a COM parameter (with or without 'd' prefix) + lhs_is_delta = lhs.startswith('d') + lhs_base = lhs[1:] if lhs_is_delta else lhs + + if not (lhs_base.startswith("COM") and lhs_base[3:].isdigit()): + return constraint + + # check rhs + rhs_is_delta = rhs.startswith('d') + rhs_base = rhs[1:] if rhs_is_delta else rhs + + new_targets, new_values = [], [] + if rhs_base.startswith("COM") and rhs_base[3:].isdigit(): + print("CASE DOUBLE COM") + # rhs is also a COM parameter: COM2 = COM1 -> COMX2, COMY2, COMZ2 =COMX1, COMY1, COMZ1 + lhs_shape_num = lhs_base[3:] + rhs_shape_num = rhs_base[3:] + + for axis in ['X', 'Y', 'Z']: + lhs_full = f"{'d' if lhs_is_delta else ''}COM{axis}{lhs_shape_num}" + rhs_full = f"{'d' if rhs_is_delta else ''}COM{axis}{rhs_shape_num}" + new_targets.append(ast.Name(id=lhs_full, ctx=ast.Store())) + new_values.append(ast.Name(id=rhs_full, ctx=ast.Load())) + + else: + print("CASE SINGLE COM") + # rhs is a regular parameter: COM2 = X -> COMX2, COMY2, COMZ2 = X, X, X + lhs_shape_num = lhs_base[3:] + rhs_full = f"{'d' if rhs_is_delta else ''}{rhs_base}" + for axis in ['X', 'Y', 'Z']: + lhs_full = f"{'d' if lhs_is_delta else ''}COM{axis}{lhs_shape_num}" + new_targets.append(ast.Name(id=lhs_full, ctx=ast.Store())) + new_values.append(ast.Name(id=rhs_full, ctx=ast.Load())) + + constraint.targets = [ast.Tuple(elts=new_targets, ctx=ast.Store())] + constraint.value = ast.Tuple(elts=new_values, ctx=ast.Load()) + return constraint + def parse_ast(tree: ast.AST): params = None imports = [] @@ -166,24 +216,37 @@ def parse_ast(tree: ast.AST): case ast.Assign(): if node.targets[0].id == 'parameters': params = node + elif node.targets[0].id.startswith('dCOM') or node.targets[0].id.startswith('COM'): + constraints.append(expand_center_of_mass_pars(node)) else: constraints.append(node) + print(f"Parsed constraints: {constraints}") return params, imports, constraints - + def extract_symbols(constraints: list[ast.AST]) -> tuple[list[str], list[str]]: """Extract all symbols used in the constraints.""" lhs, rhs = set(), set() for node in constraints: # left-hand side of assignment for target in node.targets: - if isinstance(target, ast.Name): - lhs.add(target.id) + match target: + case ast.Name(): + lhs.add(target.id) + case ast.Tuple(): + for elt in target.elts: + if isinstance(elt, ast.Name): + lhs.add(elt.id) # right-hand side of assignment for value in ast.walk(node.value): - if isinstance(value, ast.Name): - rhs.add(value.id) + match value: + case ast.Name(): + rhs.add(value.id) + case ast.Tuple: + for elt in value.elts: + if isinstance(elt, ast.Name): + rhs.add(elt.id) return lhs, rhs @@ -215,13 +278,16 @@ def validate_imports(imports: list[ast.ImportFrom | ast.Import]): def mark_named_parameters(checkedPars: list[list[bool]], modelPars: list[str], symbols: set[str]): """Mark parameters in the modelPars as checked if they are in symbols_lhs.""" + def in_symbols(par: str): + if par in symbols: return True + if 'd' + par in symbols: return True + return False + for i, shape in enumerate(modelPars): for j, par in enumerate(shape): if par is None: continue - in_symbols = par in symbols - d_in_symbols = "d" + par in symbols - checkedPars[i][j] = checkedPars[i][j] or in_symbols or d_in_symbols + checkedPars[i][j] = checkedPars[i][j] or in_symbols(par) return checkedPars tree = as_ast(text) From edfd2533b1e31bc3f60afa74b42184769748518f Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 26 Jul 2025 18:44:32 +0200 Subject: [PATCH 24/37] fixed minor plugin gen issue --- src/sas/sascalc/shape2sas/PluginGenerator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 83a46e4a40..4a251d4f49 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -55,17 +55,17 @@ def format_parameter_list_of_list_dimension(par: list[list[str | float]]) -> str """ def format_parameter(p): if isinstance(p, str): - return f"min(abs({p}), 1)" + return f"max({p}, 1)" elif isinstance(p, (int, float)): if p < 0: raise ValueError(f"PluginGenerator: Got value {p}, but dimensional scalars cannot be negative!") return str(p) else: return str(p) - + def format_sublist(sub_par): return ", ".join(format_parameter(p) for p in sub_par) - + formatted_sublists = [format_sublist(sub_par) for sub_par in par] return f"[[{'],['.join(formatted_sublists)}]]" From 0c7094f09e5bab1806a136ce11fffccd25db94df Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 26 Jul 2025 19:24:56 +0200 Subject: [PATCH 25/37] embedded log is now being used --- .../Calculators/Shape2SAS/Constraints.py | 40 ++++++++++++++----- .../Calculators/Shape2SAS/DesignWindow.py | 9 +++-- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 55d2945cae..388899b8b9 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -54,6 +54,15 @@ def __init__(self, parent=None): defaultText = "Shape2SAS plugin constraints log

" self.textEdit_2.append(defaultText) + def log_embedded_error(self, message: str): + """Log an error message in the embedded logbook.""" + self.textEdit_2.append(f"{message}") + self.textEdit_2.verticalScrollBar().setValue(self.textEdit_2.verticalScrollBar().maximum()) + + def log_embedded(self, message: str): + """Log a message in the embedded logbook.""" + self.textEdit_2.append(f"{message}") + self.textEdit_2.verticalScrollBar().setValue(self.textEdit_2.verticalScrollBar().maximum()) def getConstraintText(self, fit_params: str) -> str: """Get the default text for the constraints editor""" @@ -118,7 +127,8 @@ def merge_text(current_text: str, parameter_text: str): # create new list of parameters, replacing existing old lines in the new list for i, name in enumerate(new_names): if name in old_names: - new_lines[i+2] = old_lines[old_names.index(name)+2] + entry = old_lines[old_names.index(name)+2] + new_lines[i+2] = entry + ',' if entry[-1] != ',' else entry # remove old lines from the current text and insert the new ones in the middle current_text = "\n".join(current_text_lines[:start+1] + new_lines[2:-1] + current_text_lines[start+end:]) @@ -129,8 +139,7 @@ def merge_text(current_text: str, parameter_text: str): self.constraintTextEditor.txtEditor.setPlainText(text) self.createPlugin.setEnabled(True) - @staticmethod - def parseConstraintsText( + def parseConstraintsText(self, text: str, fitPar: list[str], modelPars: list[list[str]], modelVals: list[list[float]], checkedPars: list[list[bool]] ) -> tuple[list[str], str, str, list[list[bool]]]: """Parse the text in the constraints editor and return a dictionary of parameters""" @@ -151,6 +160,7 @@ def as_ast(text: str): last_lines = all_lines[-1:] traceback_to_show = '\n'.join(last_lines) logger.error(traceback_to_show) + self.log_embedded_error(f"Error parsing constraints text: {e}") return None def expand_center_of_mass_pars(constraint: ast.Assign) -> list[ast.Assign]: @@ -178,7 +188,6 @@ def expand_center_of_mass_pars(constraint: ast.Assign) -> list[ast.Assign]: new_targets, new_values = [], [] if rhs_base.startswith("COM") and rhs_base[3:].isdigit(): - print("CASE DOUBLE COM") # rhs is also a COM parameter: COM2 = COM1 -> COMX2, COMY2, COMZ2 =COMX1, COMY1, COMZ1 lhs_shape_num = lhs_base[3:] rhs_shape_num = rhs_base[3:] @@ -190,7 +199,6 @@ def expand_center_of_mass_pars(constraint: ast.Assign) -> list[ast.Assign]: new_values.append(ast.Name(id=rhs_full, ctx=ast.Load())) else: - print("CASE SINGLE COM") # rhs is a regular parameter: COM2 = X -> COMX2, COMY2, COMZ2 = X, X, X lhs_shape_num = lhs_base[3:] rhs_full = f"{'d' if rhs_is_delta else ''}{rhs_base}" @@ -220,8 +228,6 @@ def parse_ast(tree: ast.AST): constraints.append(expand_center_of_mass_pars(node)) else: constraints.append(node) - - print(f"Parsed constraints: {constraints}") return params, imports, constraints def extract_symbols(constraints: list[ast.AST]) -> tuple[list[str], list[str]]: @@ -249,10 +255,11 @@ def extract_symbols(constraints: list[ast.AST]) -> tuple[list[str], list[str]]: rhs.add(elt.id) return lhs, rhs - + def validate_params(params: ast.AST): if params is None: logger.error("No parameters found in constraints text.") + self.log_embedded_error("No parameters found in constraints text.") raise ValueError("No parameters found in constraints text.") def validate_symbols(lhs: list[str], rhs: list[str], fitPars: list[str]): @@ -260,20 +267,31 @@ def validate_symbols(lhs: list[str], rhs: list[str], fitPars: list[str]): # lhs is not allowed to contain fit parameters for symbol in lhs: if symbol in fitPars or symbol[1:] in fitPars: - logger.error(f"Symbol '{symbol}' is a fit parameter and cannot be used in constraints.") + logger.error(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to.") + self.log_embedded_error(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to.") raise ValueError(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to.") - + + for symbol in rhs: + is_fit_par = symbol in fitPars or symbol[1:] in fitPars + is_defined = symbol in lhs + if not is_fit_par and not is_defined: + logger.error(f"Symbol '{symbol}' is undefined.") + self.log_embedded_error(f"Symbol '{symbol}' is undefined.") + raise ValueError(f"Symbol '{symbol}' is undefined.") + def validate_imports(imports: list[ast.ImportFrom | ast.Import]): """Check if all imports are valid.""" for imp in imports: if isinstance(imp, ast.ImportFrom): if not importlib.util.find_spec(imp.module): logger.error(f"Module '{imp.module}' not found.") + self.log_embedded_error(f"Module '{imp.module}' not found.") raise ModuleNotFoundError(f"No module named {imp.module}") elif isinstance(imp, ast.Import): for name in imp.names: if not importlib.util.find_spec(name.name): logger.error(f"Module '{name.name}' not found.") + self.log_embedded_error(f"Module '{name.name}' not found.") raise ModuleNotFoundError(f"No module named {name.name}") def mark_named_parameters(checkedPars: list[list[bool]], modelPars: list[str], symbols: set[str]): @@ -302,7 +320,7 @@ def in_symbols(par: str): constraints = [ast.unparse(constraint) for constraint in constraints] symbols = (lhs, rhs) - print("Finished parsing constraints text.") + self.log_embedded("Successfully parsed user text. Generating plugin model...") print(f"Parsed parameters: {params}") print(f"Parsed imports: {imports}") print(f"Parsed constraints: {constraints}") diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index cf7757434f..d769eb7f57 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -260,21 +260,21 @@ def checkStateOfConstraints(self, fitPar: list[str], modelPars: list[list[str]], #Has anything been written to the text editor if constraintsStr: - #TODO: print to GUI output texteditor + self.constraint.log_embedded("Parsing constraints...") return self.constraint.parseConstraintsText(constraintsStr, fitPar, modelPars, modelVals, checkedPars) - + #Did the user only check parameters and click generate plugin elif fitPar: #Get default constraints fitParLists = self.getConstraintsToTextEditor() defaultConstraintsStr = self.constraint.getConstraintText(fitParLists) - #TODO: print to GUI output texteditor + self.constraint.log_embedded("No constraints text found. Creating unconstrained model") return self.constraint.getConstraints(defaultConstraintsStr, fitPar, modelPars, modelVals, checkedPars) #If not, return empty else: #all parameters are constant - #TODO: print to GUI output texteditor + self.constraint.log_embedded("Creating unconstrained model.") return "", "", "", checkedPars def enableButtons(self, toggle: bool): @@ -666,6 +666,7 @@ def getPluginModel(self): TabbedModelEditor.writeFile(full_path, model_str) self.communicator.customModelDirectoryChanged.emit() logger.info(f"Successfully generated model {modelName}!") + self.constraint.log_embedded(f"Plugin model {modelName} has been generated and is now available in the Fit panel.") self.constraint.createPlugin.setEnabled(False) def onCheckingInput(self, input: str, default: str) -> str: From 0840db80314e373373b07b59bc881bf573473b4b Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 26 Jul 2025 19:42:32 +0200 Subject: [PATCH 26/37] support intermediate variables --- src/sas/sascalc/shape2sas/PluginGenerator.py | 31 ++++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 4a251d4f49..b1f1f5ecc9 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -28,6 +28,30 @@ def generate_plugin( return model_str, full_path +def get_shape_symbols(symbols: tuple[set[str], set[str]], modelPars: list[list[str], list[str | float]]) -> tuple[set[str], set[str]]: + """ + Get the symbols used in the model, discarding user-defined variables + """ + shape_symbols = set() + for shape in modelPars[0]: # iterate over shape names + for symbol in shape[1:]: # skip shape name + shape_symbols.add(symbol) + + # filter out user-defined symbols + lhs_symbols, rhs_symbols = set(), set() + for symbol in symbols[0]: + if symbol in shape_symbols or symbol[1:] in shape_symbols: + lhs_symbols.add(symbol) + + for symbol in symbols[1]: + if symbol in shape_symbols or symbol[1:] in shape_symbols: + rhs_symbols.add(symbol) + + print(f"LHS: {lhs_symbols}") + print(f"RHS: {rhs_symbols}") + + return lhs_symbols, rhs_symbols + def format_parameter_list(par: list[list[str | float]]) -> str: """ Format a list of parameters to the model string. In this case the list @@ -176,9 +200,10 @@ def generate_model( ) -> str: """Generates a theoretical model""" importStatement, parameters, translation = usertext.imports, usertext.params, usertext.constraints - insert_delta, delta_parameters_def, delta_parameters_update = script_insert_delta_parameters(modelPars, fitPar, usertext.symbols) - insert_constraint_update, constraint_update = script_insert_apply_constraints(usertext.symbols[0]) - insert_constrained_defs, constrained_parameters = script_insert_constrained_parameters(usertext.symbols, modelPars) + symbols = get_shape_symbols(usertext.symbols, modelPars) + insert_delta, delta_parameters_def, delta_parameters_update = script_insert_delta_parameters(modelPars, fitPar, symbols) + insert_constraint_update, constraint_update = script_insert_apply_constraints(symbols[0]) + insert_constrained_defs, constrained_parameters = script_insert_constrained_parameters(symbols, modelPars) nl = '\n' fitPar.insert(0, "q") model_str = ( From cf0178b69c30cbf3fe3e3d057b97d94acc9bf4e4 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Sat, 26 Jul 2025 20:32:48 +0200 Subject: [PATCH 27/37] improved log warnings with linenos --- .../Calculators/Shape2SAS/Constraints.py | 45 +++++++++++-------- src/sas/sascalc/shape2sas/PluginGenerator.py | 3 -- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 388899b8b9..b75f6f212e 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -58,7 +58,8 @@ def log_embedded_error(self, message: str): """Log an error message in the embedded logbook.""" self.textEdit_2.append(f"{message}") self.textEdit_2.verticalScrollBar().setValue(self.textEdit_2.verticalScrollBar().maximum()) - + logger.error(message) + def log_embedded(self, message: str): """Log a message in the embedded logbook.""" self.textEdit_2.append(f"{message}") @@ -66,7 +67,7 @@ def log_embedded(self, message: str): def getConstraintText(self, fit_params: str) -> str: """Get the default text for the constraints editor""" - + self.constraintText = ( "# Write libraries to be imported here.\n" "from numpy import inf\n" @@ -160,7 +161,7 @@ def as_ast(text: str): last_lines = all_lines[-1:] traceback_to_show = '\n'.join(last_lines) logger.error(traceback_to_show) - self.log_embedded_error(f"Error parsing constraints text: {e}") + self.log_embedded_error(f"{e}") return None def expand_center_of_mass_pars(constraint: ast.Assign) -> list[ast.Assign]: @@ -222,9 +223,15 @@ def parse_ast(tree: ast.AST): imports.append(node) case ast.Assign(): + if len(node.targets) != 1 or isinstance(node.targets[0], ast.Tuple) or isinstance(node.value, ast.Tuple): + self.log_embedded_error(f"Tuple assignment is not supported (line {node.lineno}).") + raise ValueError(f"Tuple assignment is not supported (line {node.lineno}).") + if node.targets[0].id == 'parameters': params = node - elif node.targets[0].id.startswith('dCOM') or node.targets[0].id.startswith('COM'): + continue + + if node.targets[0].id.startswith('dCOM') or node.targets[0].id.startswith('COM'): constraints.append(expand_center_of_mass_pars(node)) else: constraints.append(node) @@ -233,65 +240,65 @@ def parse_ast(tree: ast.AST): def extract_symbols(constraints: list[ast.AST]) -> tuple[list[str], list[str]]: """Extract all symbols used in the constraints.""" lhs, rhs = set(), set() + lineno = {} for node in constraints: # left-hand side of assignment for target in node.targets: match target: case ast.Name(): lhs.add(target.id) + lineno[target.id] = target.lineno case ast.Tuple(): for elt in target.elts: if isinstance(elt, ast.Name): lhs.add(elt.id) + lineno[elt.id] = elt.lineno # right-hand side of assignment for value in ast.walk(node.value): match value: case ast.Name(): rhs.add(value.id) + lineno[value.id] = value.lineno case ast.Tuple: for elt in value.elts: if isinstance(elt, ast.Name): rhs.add(elt.id) + lineno[elt.id] = elt.lineno - return lhs, rhs + return lhs, rhs, lineno def validate_params(params: ast.AST): if params is None: - logger.error("No parameters found in constraints text.") self.log_embedded_error("No parameters found in constraints text.") raise ValueError("No parameters found in constraints text.") - def validate_symbols(lhs: list[str], rhs: list[str], fitPars: list[str]): + def validate_symbols(lhs: list[str], rhs: list[str], symbol_linenos: dict[str, int], fitPars: list[str]): """Check if all symbols in lhs and rhs are valid parameters.""" # lhs is not allowed to contain fit parameters for symbol in lhs: if symbol in fitPars or symbol[1:] in fitPars: - logger.error(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to.") - self.log_embedded_error(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to.") - raise ValueError(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to.") + self.log_embedded_error(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to (line {symbol_linenos[symbol]}).") + raise ValueError(f"Symbol '{symbol}' is a fit parameter and cannot be assigned to (line {symbol_linenos[symbol]}).") for symbol in rhs: is_fit_par = symbol in fitPars or symbol[1:] in fitPars is_defined = symbol in lhs if not is_fit_par and not is_defined: - logger.error(f"Symbol '{symbol}' is undefined.") - self.log_embedded_error(f"Symbol '{symbol}' is undefined.") - raise ValueError(f"Symbol '{symbol}' is undefined.") + self.log_embedded_error(f"Symbol '{symbol}' is undefined (line {symbol_linenos[symbol]}).") + raise ValueError(f"Symbol '{symbol}' is undefined (line {symbol_linenos[symbol]}).") def validate_imports(imports: list[ast.ImportFrom | ast.Import]): """Check if all imports are valid.""" for imp in imports: if isinstance(imp, ast.ImportFrom): if not importlib.util.find_spec(imp.module): - logger.error(f"Module '{imp.module}' not found.") - self.log_embedded_error(f"Module '{imp.module}' not found.") + self.log_embedded_error(f"Module '{imp.module}' not found (line {imp.lineno}).") raise ModuleNotFoundError(f"No module named {imp.module}") elif isinstance(imp, ast.Import): for name in imp.names: if not importlib.util.find_spec(name.name): - logger.error(f"Module '{name.name}' not found.") - self.log_embedded_error(f"Module '{name.name}' not found.") + self.log_embedded_error(f"Module '{name.name}' not found (line {imp.lineno}).") raise ModuleNotFoundError(f"No module named {name.name}") def mark_named_parameters(checkedPars: list[list[bool]], modelPars: list[str], symbols: set[str]): @@ -310,9 +317,9 @@ def in_symbols(par: str): tree = as_ast(text) params, imports, constraints = parse_ast(tree) - lhs, rhs = extract_symbols(constraints) + lhs, rhs, symbol_linenos = extract_symbols(constraints) validate_params(params) - validate_symbols(lhs, rhs, fitPar) + validate_symbols(lhs, rhs, symbol_linenos, fitPar) validate_imports(imports) params = ast.unparse(params) diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index b1f1f5ecc9..59da084961 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -47,9 +47,6 @@ def get_shape_symbols(symbols: tuple[set[str], set[str]], modelPars: list[list[s if symbol in shape_symbols or symbol[1:] in shape_symbols: rhs_symbols.add(symbol) - print(f"LHS: {lhs_symbols}") - print(f"RHS: {rhs_symbols}") - return lhs_symbols, rhs_symbols def format_parameter_list(par: list[list[str | float]]) -> str: From b8a9a12b0da42f1124ea7cd79a609b3218411d19 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 27 Jul 2025 09:43:50 +0000 Subject: [PATCH 28/37] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- src/sas/qtgui/Calculators/Shape2SAS/Constraints.py | 3 --- src/sas/sascalc/shape2sas/PluginGenerator.py | 1 - 2 files changed, 4 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index b75f6f212e..8131657859 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -1,9 +1,6 @@ #Global import ast import importlib.util -import logging -import re -import traceback from PySide6.QtCore import Qt from PySide6.QtWidgets import QPushButton, QWidget diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 59da084961..e26a29b0b0 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -1,4 +1,3 @@ -import textwrap from pathlib import Path import logging From 118b90bd6fe30e60ad1895272a0162c4b49ec3b8 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Fri, 22 Aug 2025 10:46:58 +0200 Subject: [PATCH 29/37] compatibility update after rebase --- .../Calculators/Shape2SAS/Constraints.py | 2 ++ .../Calculators/Shape2SAS/DesignWindow.py | 19 ++++--------------- src/sas/sascalc/shape2sas/PluginGenerator.py | 4 ++-- src/sas/sascalc/shape2sas/Shape2SAS.py | 2 ++ .../shape2sas/TheoreticalScattering.py | 13 ------------- 5 files changed, 10 insertions(+), 30 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 8131657859..7ef4616d70 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -1,6 +1,8 @@ #Global import ast import importlib.util +import logging +import traceback from PySide6.QtCore import Qt from PySide6.QtWidgets import QPushButton, QWidget diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index d769eb7f57..0c7c639a9a 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -15,17 +15,6 @@ QWidget, ) -from sas.qtgui.Calculators.Shape2SAS.ButtonOptions import ButtonOptions -from sas.qtgui.Calculators.Shape2SAS.Constraints import Constraints, logger -from sas.qtgui.Calculators.Shape2SAS.genPlugin import generatePlugin -from sas.qtgui.Calculators.Shape2SAS.PlotAspects.plotAspects import Canvas, ViewerPlotDesign -from sas.qtgui.Calculators.Shape2SAS.Tables.subunitTable import OptionLayout, SubunitTable -from sas.qtgui.Calculators.Shape2SAS.UI.DesignWindowUI import Ui_Shape2SAS -from sas.qtgui.Calculators.Shape2SAS.ViewerModel import ViewerModel -from sas.qtgui.Perspectives.perspective import Perspective -from sas.qtgui.Plotting.PlotterData import Data1D -from sas.qtgui.Utilities.GuiUtils import createModelItemWithPlot - # Local SasView from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor from sas.qtgui.Perspectives.perspective import Perspective @@ -39,10 +28,10 @@ from sas.qtgui.Calculators.Shape2SAS.Constraints import Constraints, logger from sas.qtgui.Calculators.Shape2SAS.PlotAspects.plotAspects import Canvas -from sas.sascalc.shape2sas.Shape2SAS import (getTheoreticalScattering, getPointDistribution, getSimulatedScattering, - ModelProfile, ModelSystem, SimulationParameters, - Qsampling, TheoreticalScatteringCalculation, - SimulateScattering) +from sas.sascalc.shape2sas.Shape2SAS import ( + getTheoreticalScattering, getPointDistribution, getSimulatedScattering, + ModelProfile, ModelSystem, SimulationParameters, Qsampling, TheoreticalScatteringCalculation, SimulateScattering +) from sas.qtgui.Calculators.Shape2SAS.PlotAspects.plotAspects import ViewerPlotDesign from sas.sascalc.shape2sas.PluginGenerator import generate_plugin diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index e26a29b0b0..0cd58ea661 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -3,9 +3,9 @@ #Global SasView #Local Perspectives -from sas.sascalc.fit import models from sas.sascalc.shape2sas.Shape2SAS import ModelProfile from sas.sascalc.shape2sas.UserText import UserText +from sas.system.user import get_plugin_dir def generate_plugin( prof: ModelProfile, @@ -18,7 +18,7 @@ def generate_plugin( ) -> tuple[str, Path]: """Generates a theoretical scattering plugin model""" - plugin_location = Path(models.find_plugins_dir()) + plugin_location = Path(get_plugin_dir()) full_path = plugin_location.joinpath(file_name).with_suffix('.py') logging.info(f"Plugin model will be saved to: {full_path}") diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index e5692b272f..37f842e3ec 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -1,6 +1,8 @@ import argparse import re import numpy as np +import warnings +import time from sas.sascalc.shape2sas.StructureFactor import StructureFactor from sas.sascalc.shape2sas.TheoreticalScattering import * diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index 68e96e04ef..da909e3c56 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -296,22 +296,9 @@ def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> Theoret I_theory = ITheoretical(q) if use_ausaxs: - # import time - # t_start = time.time() I0 = np.square(np.sum(p)) * sys.conc * prof.volume_total Pq = I_theory.calc_Pq_ausaxs(q, x, y, z, p)/I0 I0 = 1 - print(f"AUSAXS I0: {I0}") - print(f"AUSAXS P: {Pq[0]}, {Pq[1]}, {Pq[2]}, {Pq[3]}, {Pq[4]}") - # t_end_ausaxs = time.time() - - # r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) - # I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) - # t_end_shape2sas = time.time() - # print(f"Shape2SAS I0: {I0}") - # print(f"Shape2SAS P: {Pq[0]}, {Pq[1]}, {Pq[2]}, {Pq[3]}, {Pq[4]}") - # print(f"AUSAXS time: {(t_end_ausaxs - t_start)*1000:.2f} ms") - # print(f"Shape2SAS time: {(t_end_shape2sas - t_start)*1000:.2f} ms") else: r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) From 644ee6ea57a6f7018dafb33a72eb7bc86ca8c034 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:47:40 +0000 Subject: [PATCH 30/37] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- .../Calculators/Shape2SAS/Constraints.py | 6 ++-- .../Calculators/Shape2SAS/DesignWindow.py | 33 +++++++++++-------- .../shape2sas/ExperimentalScattering.py | 9 ++--- src/sas/sascalc/shape2sas/HelperFunctions.py | 5 +-- src/sas/sascalc/shape2sas/Models.py | 33 ++++++++++--------- src/sas/sascalc/shape2sas/PluginGenerator.py | 3 +- src/sas/sascalc/shape2sas/Shape2SAS.py | 14 ++++---- src/sas/sascalc/shape2sas/StructureFactor.py | 9 ++--- .../shape2sas/TheoreticalScattering.py | 10 +++--- src/sas/sascalc/shape2sas/Typing.py | 10 +++--- src/sas/sascalc/shape2sas/UserText.py | 1 + src/sas/sascalc/shape2sas/models/Cube.py | 1 + src/sas/sascalc/shape2sas/models/Cuboid.py | 1 + src/sas/sascalc/shape2sas/models/Cylinder.py | 1 + .../sascalc/shape2sas/models/CylinderRing.py | 1 + src/sas/sascalc/shape2sas/models/Disc.py | 1 + src/sas/sascalc/shape2sas/models/DiscRing.py | 1 + src/sas/sascalc/shape2sas/models/Ellipsoid.py | 1 + .../shape2sas/models/EllipticalCylinder.py | 1 + .../sascalc/shape2sas/models/HollowCube.py | 1 + .../sascalc/shape2sas/models/HollowSphere.py | 1 + src/sas/sascalc/shape2sas/models/Sphere.py | 1 + .../shape2sas/models/SuperEllipsoid.py | 4 ++- .../structure_factors/Aggregation.py | 6 ++-- .../structure_factors/HardSphereStructure.py | 6 ++-- .../structure_factors/NoStructure.py | 8 +++-- .../StructureDecouplingApprox.py | 5 +-- 27 files changed, 102 insertions(+), 71 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 7ef4616d70..25f2c0f619 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -7,10 +7,10 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QPushButton, QWidget -from sas.qtgui.Utilities.ModelEditors.TabbedEditor.ModelEditor import ModelEditor -from sas.qtgui.Calculators.Shape2SAS.UI.ConstraintsUI import Ui_Constraints -from sas.qtgui.Calculators.Shape2SAS.Tables.variableTable import VariableTable from sas.qtgui.Calculators.Shape2SAS.ButtonOptions import ButtonOptions +from sas.qtgui.Calculators.Shape2SAS.Tables.variableTable import VariableTable +from sas.qtgui.Calculators.Shape2SAS.UI.ConstraintsUI import Ui_Constraints +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.ModelEditor import ModelEditor from sas.sascalc.shape2sas.UserText import UserText logger = logging.getLogger(__name__) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index 0c7c639a9a..eaf8f74037 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -15,25 +15,30 @@ QWidget, ) -# Local SasView -from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor -from sas.qtgui.Perspectives.perspective import Perspective -from sas.qtgui.Utilities.GuiUtils import createModelItemWithPlot -from sas.qtgui.Plotting.PlotterData import Data1D - -from sas.qtgui.Calculators.Shape2SAS.UI.DesignWindowUI import Ui_Shape2SAS -from sas.qtgui.Calculators.Shape2SAS.ViewerModel import ViewerModel from sas.qtgui.Calculators.Shape2SAS.ButtonOptions import ButtonOptions -from sas.qtgui.Calculators.Shape2SAS.Tables.subunitTable import SubunitTable, OptionLayout from sas.qtgui.Calculators.Shape2SAS.Constraints import Constraints, logger -from sas.qtgui.Calculators.Shape2SAS.PlotAspects.plotAspects import Canvas +from sas.qtgui.Calculators.Shape2SAS.PlotAspects.plotAspects import Canvas, ViewerPlotDesign +from sas.qtgui.Calculators.Shape2SAS.Tables.subunitTable import OptionLayout, SubunitTable +from sas.qtgui.Calculators.Shape2SAS.UI.DesignWindowUI import Ui_Shape2SAS +from sas.qtgui.Calculators.Shape2SAS.ViewerModel import ViewerModel +from sas.qtgui.Perspectives.perspective import Perspective +from sas.qtgui.Plotting.PlotterData import Data1D +from sas.qtgui.Utilities.GuiUtils import createModelItemWithPlot +# Local SasView +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor +from sas.sascalc.shape2sas.PluginGenerator import generate_plugin from sas.sascalc.shape2sas.Shape2SAS import ( - getTheoreticalScattering, getPointDistribution, getSimulatedScattering, - ModelProfile, ModelSystem, SimulationParameters, Qsampling, TheoreticalScatteringCalculation, SimulateScattering + ModelProfile, + ModelSystem, + Qsampling, + SimulateScattering, + SimulationParameters, + TheoreticalScatteringCalculation, + getPointDistribution, + getSimulatedScattering, + getTheoreticalScattering, ) -from sas.qtgui.Calculators.Shape2SAS.PlotAspects.plotAspects import ViewerPlotDesign -from sas.sascalc.shape2sas.PluginGenerator import generate_plugin class DesignWindow(QDialog, Ui_Shape2SAS, Perspective): diff --git a/src/sas/sascalc/shape2sas/ExperimentalScattering.py b/src/sas/sascalc/shape2sas/ExperimentalScattering.py index d2b4758e32..65a019d590 100644 --- a/src/sas/sascalc/shape2sas/ExperimentalScattering.py +++ b/src/sas/sascalc/shape2sas/ExperimentalScattering.py @@ -1,9 +1,10 @@ -from sas.sascalc.shape2sas.Typing import * - from dataclasses import dataclass, field -from typing import Optional + import numpy as np +from sas.sascalc.shape2sas.Typing import * + + @dataclass class SimulateScattering: """Class containing parameters for @@ -12,7 +13,7 @@ class SimulateScattering: q: np.ndarray I0: np.ndarray I: np.ndarray - exposure: Optional[float] = field(default_factory=lambda:500.0) + exposure: float | None = field(default_factory=lambda:500.0) @dataclass diff --git a/src/sas/sascalc/shape2sas/HelperFunctions.py b/src/sas/sascalc/shape2sas/HelperFunctions.py index f0ba9586c4..843524f6d7 100644 --- a/src/sas/sascalc/shape2sas/HelperFunctions.py +++ b/src/sas/sascalc/shape2sas/HelperFunctions.py @@ -1,7 +1,8 @@ +import matplotlib.pyplot as plt +import numpy as np + from sas.sascalc.shape2sas.Typing import * -import numpy as np -import matplotlib.pyplot as plt ################################ Shape2SAS helper functions ################################### class Qsampling: diff --git a/src/sas/sascalc/shape2sas/Models.py b/src/sas/sascalc/shape2sas/Models.py index 22af313ada..c4b873eae5 100644 --- a/src/sas/sascalc/shape2sas/Models.py +++ b/src/sas/sascalc/shape2sas/Models.py @@ -1,11 +1,12 @@ -from sas.sascalc.shape2sas.Typing import * -from sas.sascalc.shape2sas.models import * -from sas.sascalc.shape2sas.HelperFunctions import Qsampling - from dataclasses import dataclass, field -from typing import Optional, List + import numpy as np +from sas.sascalc.shape2sas.HelperFunctions import Qsampling +from sas.sascalc.shape2sas.models import * +from sas.sascalc.shape2sas.Typing import * + + @dataclass class ModelProfile: """Class containing parameters for @@ -15,13 +16,13 @@ class ModelProfile: radius of 50 Ã… at the origin. """ - subunits: List[str] = field(default_factory=lambda: ['sphere']) - p_s: List[float] = field(default_factory=lambda: [1.0]) # scattering length density + subunits: list[str] = field(default_factory=lambda: ['sphere']) + p_s: list[float] = field(default_factory=lambda: [1.0]) # scattering length density dimensions: Vectors = field(default_factory=lambda: [[50]]) com: Vectors = field(default_factory=lambda: [[0, 0, 0]]) rotation_points: Vectors = field(default_factory=lambda: [[0, 0, 0]]) rotation: Vectors = field(default_factory=lambda: [[0, 0, 0]]) - exclude_overlap: Optional[bool] = field(default_factory=lambda: True) + exclude_overlap: bool | None = field(default_factory=lambda: True) @dataclass @@ -40,12 +41,12 @@ class SimulationParameters: """Class containing parameters for the simulation itself""" - q: Optional[np.ndarray] = field(default_factory=lambda: Qsampling.onQsampling(0.001, 0.5, 400)) - prpoints: Optional[int] = field(default_factory=lambda: 100) - Npoints: Optional[int] = field(default_factory=lambda: 3000) + q: np.ndarray | None = field(default_factory=lambda: Qsampling.onQsampling(0.001, 0.5, 400)) + prpoints: int | None = field(default_factory=lambda: 100) + Npoints: int | None = field(default_factory=lambda: 3000) #seed: Optional[int] #TODO:Add for future projects #method: Optional[str] #generation of point method #TODO: Add for future projects - model_name: Optional[List[str]] = field(default_factory=lambda: ['Model_1']) + model_name: list[str] | None = field(default_factory=lambda: ['Model_1']) @dataclass @@ -55,7 +56,7 @@ class ModelSystem: PointDistribution: ModelPointDistribution Stype: str = field(default_factory=lambda: "None") #structure factor - par: List[float] = field(default_factory=lambda: np.array([]))#parameters for structure factor + par: list[float] = field(default_factory=lambda: np.array([]))#parameters for structure factor polydispersity: float = field(default_factory=lambda: 0.0)#polydispersity conc: float = field(default_factory=lambda: 0.02) #concentration sigma_r: float = field(default_factory=lambda: 0.0) #interface roughness @@ -258,11 +259,11 @@ def onCheckOverlap( y: np.ndarray, z: np.ndarray, p: np.ndarray, - rotation: List[float], + rotation: list[float], rotation_point: list[float], - com: List[float], + com: list[float], subunitClass: object, - dimensions: List[float] + dimensions: list[float] ): """check for overlap with previous subunits. if overlap, the point is removed""" diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index 0cd58ea661..ce9624d494 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -1,5 +1,5 @@ -from pathlib import Path import logging +from pathlib import Path #Global SasView #Local Perspectives @@ -7,6 +7,7 @@ from sas.sascalc.shape2sas.UserText import UserText from sas.system.user import get_plugin_dir + def generate_plugin( prof: ModelProfile, modelPars: list[list[str], list[str | float]], diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index 37f842e3ec..5fc00aa79d 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -1,17 +1,15 @@ import argparse import re -import numpy as np -import warnings import time +import warnings + +import numpy as np -from sas.sascalc.shape2sas.StructureFactor import StructureFactor -from sas.sascalc.shape2sas.TheoreticalScattering import * from sas.sascalc.shape2sas.ExperimentalScattering import * +from sas.sascalc.shape2sas.HelperFunctions import generate_pdb, plot_2D, plot_results from sas.sascalc.shape2sas.Models import * -from sas.sascalc.shape2sas.HelperFunctions import ( - plot_2D, plot_results, generate_pdb -) - +from sas.sascalc.shape2sas.StructureFactor import StructureFactor +from sas.sascalc.shape2sas.TheoreticalScattering import * ################################ Shape2SAS batch version ################################ if __name__ == "__main__": diff --git a/src/sas/sascalc/shape2sas/StructureFactor.py b/src/sas/sascalc/shape2sas/StructureFactor.py index 82ad1f810f..b85340c3c6 100644 --- a/src/sas/sascalc/shape2sas/StructureFactor.py +++ b/src/sas/sascalc/shape2sas/StructureFactor.py @@ -1,9 +1,10 @@ -from sas.sascalc.shape2sas.Typing import * -from sas.sascalc.shape2sas.structure_factors import * -from typing import Optional import numpy as np +from sas.sascalc.shape2sas.structure_factors import * +from sas.sascalc.shape2sas.Typing import * + + class StructureFactor: def __init__(self, q: np.ndarray, x_new: np.ndarray, @@ -11,7 +12,7 @@ def __init__(self, q: np.ndarray, z_new: np.ndarray, p_new: np.ndarray, Stype: str, - par: Optional[List[float]]): + par: List[float] | None): self.q = q self.x_new = x_new self.y_new = y_new diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index da909e3c56..813933e79d 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -1,10 +1,12 @@ -from sas.sascalc.shape2sas.Typing import * +from dataclasses import dataclass + +import numpy as np + from sas.sascalc.shape2sas.HelperFunctions import sinc +from sas.sascalc.shape2sas.Models import ModelSystem, SimulationParameters from sas.sascalc.shape2sas.StructureFactor import StructureFactor +from sas.sascalc.shape2sas.Typing import * -from dataclasses import dataclass -from sas.sascalc.shape2sas.Models import ModelSystem, SimulationParameters -import numpy as np @dataclass class TheoreticalScatteringCalculation: diff --git a/src/sas/sascalc/shape2sas/Typing.py b/src/sas/sascalc/shape2sas/Typing.py index aca1808949..e237b49444 100644 --- a/src/sas/sascalc/shape2sas/Typing.py +++ b/src/sas/sascalc/shape2sas/Typing.py @@ -1,7 +1,7 @@ + import numpy as np -from typing import Tuple, List -Vectors = List[List[float]] -Vector2D = Tuple[np.ndarray, np.ndarray] -Vector3D = Tuple[np.ndarray, np.ndarray, np.ndarray] -Vector4D = Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] \ No newline at end of file +Vectors = list[list[float]] +Vector2D = tuple[np.ndarray, np.ndarray] +Vector3D = tuple[np.ndarray, np.ndarray, np.ndarray] +Vector4D = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/UserText.py b/src/sas/sascalc/shape2sas/UserText.py index 34dcb9ce56..5f3b69661b 100644 --- a/src/sas/sascalc/shape2sas/UserText.py +++ b/src/sas/sascalc/shape2sas/UserText.py @@ -1,5 +1,6 @@ from dataclasses import dataclass + @dataclass class UserText: def __init__(self, imports: list[str], params: list[str], constraints: list[str], symbols: tuple[set[str], set[str]]): diff --git a/src/sas/sascalc/shape2sas/models/Cube.py b/src/sas/sascalc/shape2sas/models/Cube.py index f5a657d636..0578098156 100644 --- a/src/sas/sascalc/shape2sas/models/Cube.py +++ b/src/sas/sascalc/shape2sas/models/Cube.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class Cube: def __init__(self, dimensions: List[float]): self.a = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/Cuboid.py b/src/sas/sascalc/shape2sas/models/Cuboid.py index 5e74740e7e..d01bb097e4 100644 --- a/src/sas/sascalc/shape2sas/models/Cuboid.py +++ b/src/sas/sascalc/shape2sas/models/Cuboid.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class Cuboid: def __init__(self, dimensions: List[float]): self.a = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/Cylinder.py b/src/sas/sascalc/shape2sas/models/Cylinder.py index 59ce916e83..ca054283f7 100644 --- a/src/sas/sascalc/shape2sas/models/Cylinder.py +++ b/src/sas/sascalc/shape2sas/models/Cylinder.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class Cylinder: def __init__(self, dimensions: List[float]): self.R = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/CylinderRing.py b/src/sas/sascalc/shape2sas/models/CylinderRing.py index d4906635a0..c332b802e4 100644 --- a/src/sas/sascalc/shape2sas/models/CylinderRing.py +++ b/src/sas/sascalc/shape2sas/models/CylinderRing.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class CylinderRing: def __init__(self, dimensions: List[float]): self.R = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/Disc.py b/src/sas/sascalc/shape2sas/models/Disc.py index 459c484edb..8a0f331638 100644 --- a/src/sas/sascalc/shape2sas/models/Disc.py +++ b/src/sas/sascalc/shape2sas/models/Disc.py @@ -1,4 +1,5 @@ from sas.sascalc.shape2sas.models.EllipticalCylinder import EllipticalCylinder + class Disc(EllipticalCylinder): pass \ No newline at end of file diff --git a/src/sas/sascalc/shape2sas/models/DiscRing.py b/src/sas/sascalc/shape2sas/models/DiscRing.py index dc8c5756ed..050d6c0874 100644 --- a/src/sas/sascalc/shape2sas/models/DiscRing.py +++ b/src/sas/sascalc/shape2sas/models/DiscRing.py @@ -1,4 +1,5 @@ from sas.sascalc.shape2sas.models.CylinderRing import CylinderRing + class DiscRing(CylinderRing): pass diff --git a/src/sas/sascalc/shape2sas/models/Ellipsoid.py b/src/sas/sascalc/shape2sas/models/Ellipsoid.py index f7be308c74..bc2a5c2fe8 100644 --- a/src/sas/sascalc/shape2sas/models/Ellipsoid.py +++ b/src/sas/sascalc/shape2sas/models/Ellipsoid.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class Ellipsoid: def __init__(self, dimensions: List[float]): self.a = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py index f069b84a17..0b499956ae 100644 --- a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py +++ b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class EllipticalCylinder: def __init__(self, dimensions: List[float]): self.a = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/HollowCube.py b/src/sas/sascalc/shape2sas/models/HollowCube.py index 5edafb1f3d..3b49dec8f7 100644 --- a/src/sas/sascalc/shape2sas/models/HollowCube.py +++ b/src/sas/sascalc/shape2sas/models/HollowCube.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class HollowCube: def __init__(self, dimensions: List[float]): self.a = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/HollowSphere.py b/src/sas/sascalc/shape2sas/models/HollowSphere.py index 9794633b66..3d40036a82 100644 --- a/src/sas/sascalc/shape2sas/models/HollowSphere.py +++ b/src/sas/sascalc/shape2sas/models/HollowSphere.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class HollowSphere: def __init__(self, dimensions: List[float]): self.R = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/Sphere.py b/src/sas/sascalc/shape2sas/models/Sphere.py index 291cf2e577..cf5f4ce7e9 100644 --- a/src/sas/sascalc/shape2sas/models/Sphere.py +++ b/src/sas/sascalc/shape2sas/models/Sphere.py @@ -1,5 +1,6 @@ from sas.sascalc.shape2sas.Typing import * + class Sphere: def __init__(self, dimensions: List[float]): self.R = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py index d2ead1be04..02692687d6 100644 --- a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py +++ b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py @@ -1,6 +1,8 @@ -from sas.sascalc.shape2sas.Typing import * from scipy.special import gamma +from sas.sascalc.shape2sas.Typing import * + + class SuperEllipsoid: def __init__(self, dimensions: List[float]): self.R = dimensions[0] diff --git a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py index c8d6720186..d58397ca42 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py +++ b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py @@ -1,7 +1,9 @@ -from sas.sascalc.shape2sas.Typing import * -from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox import numpy as np +from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox +from sas.sascalc.shape2sas.Typing import * + + class Aggregation(StructureDecouplingApprox): def __init__(self, q: np.ndarray, x_new: np.ndarray, diff --git a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py index 3ab9c0073e..8b9f029378 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py +++ b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py @@ -1,7 +1,9 @@ -from sas.sascalc.shape2sas.Typing import * -from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox import numpy as np +from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox +from sas.sascalc.shape2sas.Typing import * + + class HardSphereStructure(StructureDecouplingApprox): def __init__(self, q: np.ndarray, x_new: np.ndarray, diff --git a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py index ba2597716d..e5ff4fff9c 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py +++ b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py @@ -1,8 +1,10 @@ -from sas.sascalc.shape2sas.Typing import * -from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox +from typing import Any import numpy as np -from typing import Any + +from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox +from sas.sascalc.shape2sas.Typing import * + class NoStructure(StructureDecouplingApprox): def __init__(self, q: np.ndarray, diff --git a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py index 16c4b3843e..f11cd4b5bf 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py +++ b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py @@ -1,7 +1,8 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np + from sas.sascalc.shape2sas.HelperFunctions import sinc +from sas.sascalc.shape2sas.Typing import * -import numpy as np class StructureDecouplingApprox: def __init__(self, q: np.ndarray, From b5ea2c8e152d5575f38aaa56e3df26185e390e86 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Fri, 22 Aug 2025 12:51:46 +0200 Subject: [PATCH 31/37] fixed errors introduced by ruff --- src/sas/sascalc/shape2sas/HelperFunctions.py | 28 +++++++++---------- src/sas/sascalc/shape2sas/StructureFactor.py | 4 +-- src/sas/sascalc/shape2sas/models/Cube.py | 2 +- src/sas/sascalc/shape2sas/models/Cuboid.py | 2 +- src/sas/sascalc/shape2sas/models/Cylinder.py | 2 +- .../sascalc/shape2sas/models/CylinderRing.py | 2 +- src/sas/sascalc/shape2sas/models/Ellipsoid.py | 2 +- .../shape2sas/models/EllipticalCylinder.py | 2 +- .../sascalc/shape2sas/models/HollowCube.py | 2 +- .../sascalc/shape2sas/models/HollowSphere.py | 2 +- src/sas/sascalc/shape2sas/models/Sphere.py | 2 +- .../shape2sas/models/SuperEllipsoid.py | 2 +- .../structure_factors/Aggregation.py | 2 +- .../structure_factors/HardSphereStructure.py | 2 +- 14 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/sas/sascalc/shape2sas/HelperFunctions.py b/src/sas/sascalc/shape2sas/HelperFunctions.py index 843524f6d7..9b910357cf 100644 --- a/src/sas/sascalc/shape2sas/HelperFunctions.py +++ b/src/sas/sascalc/shape2sas/HelperFunctions.py @@ -137,14 +137,14 @@ def plot_2D(x_list: np.ndarray, def plot_results(q: np.ndarray, - r_list: List[np.ndarray], - pr_list: List[np.ndarray], - I_list: List[np.ndarray], - Isim_list: List[np.ndarray], - sigma_list: List[np.ndarray], - S_list: List[np.ndarray], - names: List[str], - scales: List[float], + r_list: list[np.ndarray], + pr_list: list[np.ndarray], + I_list: list[np.ndarray], + Isim_list: list[np.ndarray], + sigma_list: list[np.ndarray], + S_list: list[np.ndarray], + names: list[str], + scales: list[float], xscale_log: bool, high_res: bool) -> None: """ @@ -206,11 +206,11 @@ def plot_results(q: np.ndarray, plt.close() -def generate_pdb(x_list: List[np.ndarray], - y_list: List[np.ndarray], - z_list: List[np.ndarray], - p_list: List[np.ndarray], - Model_list: List[str]) -> None: +def generate_pdb(x_list: list[np.ndarray], + y_list: list[np.ndarray], + z_list: list[np.ndarray], + p_list: list[np.ndarray], + Model_list: list[str]) -> None: """ Generates a visualisation file in PDB format with the simulated points (coordinates) and contrasts ONLY FOR VISUALIZATION! @@ -245,7 +245,7 @@ def generate_pdb(x_list: List[np.ndarray], f.write('END') -def check_unique(A_list: List[float]) -> bool: +def check_unique(A_list: list[float]) -> bool: """ if all elements in a list are unique then return True, else return False """ diff --git a/src/sas/sascalc/shape2sas/StructureFactor.py b/src/sas/sascalc/shape2sas/StructureFactor.py index b85340c3c6..1171cd979b 100644 --- a/src/sas/sascalc/shape2sas/StructureFactor.py +++ b/src/sas/sascalc/shape2sas/StructureFactor.py @@ -12,7 +12,7 @@ def __init__(self, q: np.ndarray, z_new: np.ndarray, p_new: np.ndarray, Stype: str, - par: List[float] | None): + par: list[float] | None): self.q = q self.x_new = x_new self.y_new = y_new @@ -44,7 +44,7 @@ def getStructureFactorClass(self): ValueError(f"Structure factor '{self.Stype}' was not found in structureFactor or global scope.") @staticmethod - def getparname(name: str) -> List[str]: + def getparname(name: str) -> list[str]: """Return the name of the parameters""" pars = { 'HS': {'conc': 0.02,'r_hs': 50}, diff --git a/src/sas/sascalc/shape2sas/models/Cube.py b/src/sas/sascalc/shape2sas/models/Cube.py index 0578098156..2171f436a3 100644 --- a/src/sas/sascalc/shape2sas/models/Cube.py +++ b/src/sas/sascalc/shape2sas/models/Cube.py @@ -2,7 +2,7 @@ class Cube: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.a = dimensions[0] def getVolume(self) -> float: diff --git a/src/sas/sascalc/shape2sas/models/Cuboid.py b/src/sas/sascalc/shape2sas/models/Cuboid.py index d01bb097e4..a998535adc 100644 --- a/src/sas/sascalc/shape2sas/models/Cuboid.py +++ b/src/sas/sascalc/shape2sas/models/Cuboid.py @@ -2,7 +2,7 @@ class Cuboid: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.a = dimensions[0] self.b = dimensions[1] self.c = dimensions[2] diff --git a/src/sas/sascalc/shape2sas/models/Cylinder.py b/src/sas/sascalc/shape2sas/models/Cylinder.py index ca054283f7..9d4c847c63 100644 --- a/src/sas/sascalc/shape2sas/models/Cylinder.py +++ b/src/sas/sascalc/shape2sas/models/Cylinder.py @@ -2,7 +2,7 @@ class Cylinder: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.R = dimensions[0] self.l = dimensions[1] diff --git a/src/sas/sascalc/shape2sas/models/CylinderRing.py b/src/sas/sascalc/shape2sas/models/CylinderRing.py index c332b802e4..f9aa0f7cfc 100644 --- a/src/sas/sascalc/shape2sas/models/CylinderRing.py +++ b/src/sas/sascalc/shape2sas/models/CylinderRing.py @@ -2,7 +2,7 @@ class CylinderRing: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.R = dimensions[0] self.r = dimensions[1] self.l = dimensions[2] diff --git a/src/sas/sascalc/shape2sas/models/Ellipsoid.py b/src/sas/sascalc/shape2sas/models/Ellipsoid.py index bc2a5c2fe8..514c854238 100644 --- a/src/sas/sascalc/shape2sas/models/Ellipsoid.py +++ b/src/sas/sascalc/shape2sas/models/Ellipsoid.py @@ -2,7 +2,7 @@ class Ellipsoid: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.a = dimensions[0] self.b = dimensions[1] self.c = dimensions[2] diff --git a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py index 0b499956ae..53683b3da9 100644 --- a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py +++ b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py @@ -2,7 +2,7 @@ class EllipticalCylinder: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.a = dimensions[0] self.b = dimensions[1] self.l = dimensions[2] diff --git a/src/sas/sascalc/shape2sas/models/HollowCube.py b/src/sas/sascalc/shape2sas/models/HollowCube.py index 3b49dec8f7..28f27ccbd5 100644 --- a/src/sas/sascalc/shape2sas/models/HollowCube.py +++ b/src/sas/sascalc/shape2sas/models/HollowCube.py @@ -2,7 +2,7 @@ class HollowCube: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.a = dimensions[0] self.b = dimensions[1] diff --git a/src/sas/sascalc/shape2sas/models/HollowSphere.py b/src/sas/sascalc/shape2sas/models/HollowSphere.py index 3d40036a82..739f2609b9 100644 --- a/src/sas/sascalc/shape2sas/models/HollowSphere.py +++ b/src/sas/sascalc/shape2sas/models/HollowSphere.py @@ -2,7 +2,7 @@ class HollowSphere: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.R = dimensions[0] self.r = dimensions[1] diff --git a/src/sas/sascalc/shape2sas/models/Sphere.py b/src/sas/sascalc/shape2sas/models/Sphere.py index cf5f4ce7e9..aa31a0e1b9 100644 --- a/src/sas/sascalc/shape2sas/models/Sphere.py +++ b/src/sas/sascalc/shape2sas/models/Sphere.py @@ -2,7 +2,7 @@ class Sphere: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.R = dimensions[0] def getVolume(self) -> float: diff --git a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py index 02692687d6..adc8fb4a2d 100644 --- a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py +++ b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py @@ -4,7 +4,7 @@ class SuperEllipsoid: - def __init__(self, dimensions: List[float]): + def __init__(self, dimensions: list[float]): self.R = dimensions[0] self.eps = dimensions[1] self.t = dimensions[2] diff --git a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py index d58397ca42..2675442bdf 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py +++ b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py @@ -10,7 +10,7 @@ def __init__(self, q: np.ndarray, y_new: np.ndarray, z_new: np.ndarray, p_new: np.ndarray, - par: List[float]): + par: list[float]): super(Aggregation, self).__init__(q, x_new, y_new, z_new, p_new) self.q = q self.x_new = x_new diff --git a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py index 8b9f029378..305664a35c 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py +++ b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py @@ -10,7 +10,7 @@ def __init__(self, q: np.ndarray, y_new: np.ndarray, z_new: np.ndarray, p_new: np.ndarray, - par: List[float]): + par: list[float]): super(HardSphereStructure, self).__init__(q, x_new, y_new, z_new, p_new) self.q = q self.x_new = x_new From 1a9fd3a3b08ed58149012b4f8bb41033381308d3 Mon Sep 17 00:00:00 2001 From: krellemeister Date: Fri, 3 Oct 2025 14:23:04 +0200 Subject: [PATCH 32/37] explicit euler convention; bugfixing --- src/sas/sascalc/shape2sas/HelperFunctions.py | 14 +++ src/sas/sascalc/shape2sas/Models.py | 92 +++++--------------- test/sascalculator/utest_sas_gen.py | 31 ++++++- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/src/sas/sascalc/shape2sas/HelperFunctions.py b/src/sas/sascalc/shape2sas/HelperFunctions.py index 9b910357cf..7847987c78 100644 --- a/src/sas/sascalc/shape2sas/HelperFunctions.py +++ b/src/sas/sascalc/shape2sas/HelperFunctions.py @@ -31,6 +31,20 @@ def qMethodsInput(name: str): return inputs[name] +def euler_rotation_matrix(alpha: float, beta: float, gamma: float) -> np.ndarray: + """ + Convert Euler angles to a rotation matrix, following the intrinsic ZYX convention. + """ + cosa, cosb, cosg = np.cos(alpha), np.cos(beta), np.cos(gamma) + sina, sinb, sing = np.sin(alpha), np.sin(beta), np.sin(gamma) + sinasinb, cosasinb = sina*sinb, cosa*sinb + return np.array([ + [cosb*cosg, sinasinb*cosg - cosa*sing, cosasinb*cosg + sina*sing], + [cosb*sing, sinasinb*sing + cosa*cosg, cosasinb*sing - sina*cosg], + [-sinb, sina*cosb, cosa*cosb] + ]) + + def sinc(x) -> np.ndarray: """ function for calculating sinc = sin(x)/x diff --git a/src/sas/sascalc/shape2sas/Models.py b/src/sas/sascalc/shape2sas/Models.py index c4b873eae5..d57ff295c1 100644 --- a/src/sas/sascalc/shape2sas/Models.py +++ b/src/sas/sascalc/shape2sas/Models.py @@ -2,7 +2,7 @@ import numpy as np -from sas.sascalc.shape2sas.HelperFunctions import Qsampling +from sas.sascalc.shape2sas.HelperFunctions import Qsampling, euler_rotation_matrix from sas.sascalc.shape2sas.models import * from sas.sascalc.shape2sas.Typing import * @@ -63,67 +63,25 @@ class ModelSystem: class Rotation: - def __init__( - self, - x_add: np.ndarray, y_add: np.ndarray, z_add: np.ndarray, - alpha: float, beta: float, gam: float, - rotp_x: float, rotp_y: float,rotp_z: float - ): - self.x_add = x_add - self.y_add = y_add - self.z_add = z_add - self.alpha = alpha - self.beta = beta - self.gam = gam - self.rotp_x = rotp_x - self.rotp_y = rotp_y - self.rotp_z = rotp_z - - def onRotatingPoints(self) -> Vector3D: - """Simple Euler rotation""" - self.x_add -= self.rotp_x - self.y_add -= self.rotp_y - self.z_add -= self.rotp_z - - x_rot = (self.x_add * np.cos(self.gam) * np.cos(self.beta) - + self.y_add * (np.cos(self.gam) * np.sin(self.beta) * np.sin(self.alpha) - np.sin(self.gam) * np.cos(self.alpha)) - + self.z_add * (np.cos(self.gam) * np.sin(self.beta) * np.cos(self.alpha) + np.sin(self.gam) * np.sin(self.alpha))) - - y_rot = (self.x_add * np.sin(self.gam) * np.cos(self.beta) - + self.y_add * (np.sin(self.gam) * np.sin(self.beta) * np.sin(self.alpha) + np.cos(self.gam) * np.cos(self.alpha)) - + self.z_add * (np.sin(self.gam) * np.sin(self.beta) * np.cos(self.alpha) - np.cos(self.gam) * np.sin(self.alpha))) - - z_rot = (-self.x_add * np.sin(self.beta) - + self.y_add * np.cos(self.beta) * np.sin(self.alpha) - + self.z_add * np.cos(self.beta) * np.cos(self.alpha)) - - x_rot += self.rotp_x - y_rot += self.rotp_y - z_rot += self.rotp_z - - return x_rot, y_rot, z_rot - - #More advanced rotation functions can be added here - #but GeneratePoints should be changed.... - - -class Translation: - def __init__(self, x_add: np.ndarray, - y_add: np.ndarray, - z_add: np.ndarray, - com_x: float, - com_y: float, - com_z: float): - self.x_add = x_add - self.y_add = y_add - self.z_add = z_add - self.com_x = com_x - self.com_y = com_y - self.com_z = com_z - - def onTranslatingPoints(self) -> Vector3D: - """Translates points""" - return self.x_add + self.com_x, self.y_add + self.com_y, self.z_add + self.com_z + def __init__(self, matrix: np.ndarray, center_mass: np.ndarray): + self.M = matrix # matrix + self.cm = center_mass # center of mass +Translation = np.ndarray + +def transform(coords: np.ndarray[Vector3D], T: Translation, R: Rotation): + """Transform a set of coordinates by a rotation R and translation T""" + assert coords.shape[0] == 3 + assert T.shape == (3,) + assert R.M.shape == (3, 3) + assert R.cm.shape == (3,) + + # The transform is: + # v' = R*(v - R_cm) + R_cm + T + # = R*v - R*R_cm + R_cm + T + # = R*v + (-R*R_cm + R_cm + T) + + Tp = -np.dot(R.M, R.cm) + R.cm + T + return np.dot(R.M, coords) + Tp[:, np.newaxis] class GeneratePoints: @@ -157,11 +115,9 @@ def onTransformingPoints(self, x: np.ndarray, alpha = np.radians(alpha) beta = np.radians(beta) gam = np.radians(gam) - com_x, com_y, com_z = self.com - - x, y, z = Rotation(x, y, z, alpha, beta, gam, rotp_x, rotp_y, rotp_z).onRotatingPoints() - x, y, z = Translation(x, y, z, com_x, com_y, com_z).onTranslatingPoints() - return x, y, z + rotation = Rotation(euler_rotation_matrix(alpha, beta, gam), np.array([rotp_x, rotp_y, rotp_z])) + translation = np.array(self.com) + return transform(np.vstack([x, y, z]), translation, rotation) class GenerateAllPoints: @@ -268,7 +224,7 @@ def onCheckOverlap( """check for overlap with previous subunits. if overlap, the point is removed""" - if sum(rotation) != 0: + if any(r != 0 for r in rotation): ## effective coordinates, shifted by (x_com,y_com,z_com) x_eff, y_eff, z_eff = Translation(x, y, z, -com[0], -com[1], -com[2]).onTranslatingPoints() diff --git a/test/sascalculator/utest_sas_gen.py b/test/sascalculator/utest_sas_gen.py index bd10005e0d..542058f777 100644 --- a/test/sascalculator/utest_sas_gen.py +++ b/test/sascalculator/utest_sas_gen.py @@ -8,6 +8,7 @@ import warnings import numpy as np +import scipy.stats as stats from scipy.spatial.transform import Rotation from sas.sascalc.calculator import sas_gen @@ -315,7 +316,35 @@ def test_calculator_elements(self): for val in np.abs(errs): self.assertLessEqual(val, 1e-3) - + def test_euler_angle_consistency(self): + """ + Test that the euler angle implementation in Models.py is consistent with the scipy Rotation module + """ + from sas.sascalc.shape2sas.HelperFunctions import euler_rotation_matrix + def rotation(theta, phi, psi): # from sasmodels/explore/realspace.py + def Ry(a): + R = [[+np.cos(a), 0, +np.sin(a)], + [0, 1, 0], + [-np.sin(a), 0, +np.cos(a)]] + return np.array(R) + + def Rz(a): + R = [[+np.cos(a), -np.sin(a), 0], + [+np.sin(a), +np.cos(a), 0], + [0, 0, 1]] + return np.array(R) + return Rz(phi) @ Ry(theta) @ Rz(psi) + + np.random.seed(seed=1984) + angles = stats.uniform(0, 2*np.pi).rvs([100, 3]) + print(angles) + for alpha, beta, gamma in angles: + R_s2s = euler_rotation_matrix(alpha, beta, gamma) + R_scipy_XYZ = Rotation.from_euler('ZYX', [gamma, beta, alpha]).as_matrix() + R_sasview = rotation(alpha, beta, gamma) + R_scipy_zyz = Rotation.from_euler('ZYZ', [beta, alpha, gamma]).as_matrix() + self.assertTrue(np.allclose(R_s2s, R_scipy_XYZ)) + self.assertTrue(np.allclose(R_sasview, R_scipy_zyz)) if __name__ == '__main__': unittest.main() From ce7d819a587b833ac003dd65f19d3cfbe4ee501d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:29:19 +0000 Subject: [PATCH 33/37] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- .../Calculators/Shape2SAS/Constraints.py | 10 ++-- .../Calculators/Shape2SAS/DesignWindow.py | 16 +++--- .../Shape2SAS/Tables/subunitTable.py | 4 +- .../Calculators/Shape2SAS/ViewerModel.py | 6 +-- .../shape2sas/ExperimentalScattering.py | 14 ++--- src/sas/sascalc/shape2sas/HelperFunctions.py | 52 +++++++++---------- src/sas/sascalc/shape2sas/Models.py | 28 +++++----- src/sas/sascalc/shape2sas/PluginGenerator.py | 24 ++++----- src/sas/sascalc/shape2sas/Shape2SAS.py | 2 +- src/sas/sascalc/shape2sas/StructureFactor.py | 12 ++--- .../shape2sas/TheoreticalScattering.py | 44 ++++++++-------- src/sas/sascalc/shape2sas/Typing.py | 2 +- src/sas/sascalc/shape2sas/UserText.py | 2 +- src/sas/sascalc/shape2sas/models/Cube.py | 8 +-- src/sas/sascalc/shape2sas/models/Cuboid.py | 8 +-- src/sas/sascalc/shape2sas/models/Cylinder.py | 6 +-- .../sascalc/shape2sas/models/CylinderRing.py | 10 ++-- src/sas/sascalc/shape2sas/models/Disc.py | 2 +- src/sas/sascalc/shape2sas/models/Ellipsoid.py | 4 +- .../shape2sas/models/EllipticalCylinder.py | 6 +-- .../sascalc/shape2sas/models/HollowCube.py | 26 +++++----- .../sascalc/shape2sas/models/HollowSphere.py | 10 ++-- src/sas/sascalc/shape2sas/models/Sphere.py | 6 +-- .../shape2sas/models/SuperEllipsoid.py | 12 ++--- src/sas/sascalc/shape2sas/models/__init__.py | 2 +- .../structure_factors/Aggregation.py | 10 ++-- .../structure_factors/HardSphereStructure.py | 18 +++---- .../structure_factors/NoStructure.py | 10 ++-- .../StructureDecouplingApprox.py | 14 ++--- .../shape2sas/structure_factors/__init__.py | 2 +- 30 files changed, 185 insertions(+), 185 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py index 25f2c0f619..1dd35e9c6b 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Constraints.py @@ -112,7 +112,7 @@ def merge_text(current_text: str, parameter_text: str): return get_default(parameter_text) new_lines = parameter_text.split("\n") - # fit_param string is formatted as: + # fit_param string is formatted as: # [ # # header # ['name1', 'unit1', ...], @@ -129,7 +129,7 @@ def merge_text(current_text: str, parameter_text: str): if name in old_names: entry = old_lines[old_names.index(name)+2] new_lines[i+2] = entry + ',' if entry[-1] != ',' else entry - + # remove old lines from the current text and insert the new ones in the middle current_text = "\n".join(current_text_lines[:start+1] + new_lines[2:-1] + current_text_lines[start+end:]) return current_text @@ -167,7 +167,7 @@ def expand_center_of_mass_pars(constraint: ast.Assign) -> list[ast.Assign]: """Expand center of mass parameters to include all components.""" # check if this is a COM assignment we need to expand - if (len(constraint.targets) != 1 or + if (len(constraint.targets) != 1 or not isinstance(constraint.targets[0], ast.Name) or not isinstance(constraint.value, ast.Name)): return constraint @@ -267,7 +267,7 @@ def extract_symbols(constraints: list[ast.AST]) -> tuple[list[str], list[str]]: return lhs, rhs, lineno - def validate_params(params: ast.AST): + def validate_params(params: ast.AST): if params is None: self.log_embedded_error("No parameters found in constraints text.") raise ValueError("No parameters found in constraints text.") @@ -302,7 +302,7 @@ def validate_imports(imports: list[ast.ImportFrom | ast.Import]): def mark_named_parameters(checkedPars: list[list[bool]], modelPars: list[str], symbols: set[str]): """Mark parameters in the modelPars as checked if they are in symbols_lhs.""" - def in_symbols(par: str): + def in_symbols(par: str): if par in symbols: return True if 'd' + par in symbols: return True return False diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index eaf8f74037..c15b9aba3d 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -647,12 +647,12 @@ def getPluginModel(self): modelProfile = self.getModelProfile(self.ifFitPar, conditionBool=checkedPars, conditionFitPar=parNames) model_str, full_path = generate_plugin( - modelProfile, + modelProfile, [parNames, parVals], usertext, - fitPar, - Npoints, - prPoints, + fitPar, + Npoints, + prPoints, modelName ) @@ -711,10 +711,10 @@ def getSimulatedSAXSData(self): Distr = getPointDistribution(Profile, N) model = ModelSystem( - PointDistribution=Distr, - Stype=Stype, par=par, - polydispersity=polydispersity, - conc=conc, + PointDistribution=Distr, + Stype=Stype, par=par, + polydispersity=polydispersity, + conc=conc, sigma_r=sigma_r ) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/Tables/subunitTable.py b/src/sas/qtgui/Calculators/Shape2SAS/Tables/subunitTable.py index d6dd54d194..2ba6364156 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/Tables/subunitTable.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/Tables/subunitTable.py @@ -512,7 +512,7 @@ def onAdding(self): if row in subunitName.keys(): paintedName = subunitName[row] + f"{to_column_name}" + " = " item = CustomStandardItem( - paintedName, subunitUnits[row], + paintedName, subunitUnits[row], subunitTooltip[row], subunitDefault_value[row], plot_callback=self.updatePlotCallback ) @@ -536,7 +536,7 @@ def onAdding(self): method = MethodType(attr, OptionLayout) name, defaultVal, units, tooltip, _, _ = method() item = CustomStandardItem( - name[row] + f"{to_column_name}" + " = ", units[row], + name[row] + f"{to_column_name}" + " = ", units[row], tooltip[row], defaultVal[row], plot_callback=self.updatePlotCallback ) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py b/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py index b8fdc2030b..6e091d45dc 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py @@ -147,7 +147,7 @@ def initialiseAxis(self): def setAxis(self, x_range: (float, float), y_range: (float, float), z_range: (float, float)): """Set axis for the model with equal aspect ratio""" - + # Calculate the overall range to ensure equal aspect ratio x_min, x_max = x_range y_min, y_max = y_range @@ -156,10 +156,10 @@ def setAxis(self, x_range: (float, float), y_range: (float, float), z_range: (fl y_center = (y_min + y_max) / 2 z_center = (z_min + z_max) / 2 max_range = max(x_max - x_min, y_max - y_min, z_max - z_min) - + # Add some padding half_range = (max_range*1.1) / 2 - + # Set equal ranges for all axes centered on their respective centers self.X_ax.setRange(x_center - half_range, x_center + half_range) self.Y_ax.setRange(y_center - half_range, y_center + half_range) diff --git a/src/sas/sascalc/shape2sas/ExperimentalScattering.py b/src/sas/sascalc/shape2sas/ExperimentalScattering.py index 65a019d590..12a56e97b8 100644 --- a/src/sas/sascalc/shape2sas/ExperimentalScattering.py +++ b/src/sas/sascalc/shape2sas/ExperimentalScattering.py @@ -26,10 +26,10 @@ class SimulatedScattering: I_err: np.ndarray class IExperimental: - def __init__(self, - q: np.ndarray, - I0: np.ndarray, - I: np.ndarray, + def __init__(self, + q: np.ndarray, + I0: np.ndarray, + I: np.ndarray, exposure: float): self.q = q self.I0 = I0 @@ -73,7 +73,7 @@ def simulate_data(self) -> Vector2D: N = k * self.q # original expression from Sedlak2017 paper qt = 1.4 # threshold - above this q value, the linear expression do not hold - a = 3.0 # empirical constant + a = 3.0 # empirical constant b = 0.6 # empirical constant idx = np.where(self.q > qt) N[idx] = k * qt * np.exp(-0.5 * ((self.q[idx] - qt) / b)**a) @@ -83,7 +83,7 @@ def simulate_data(self) -> Vector2D: q_arb = 0.3 if q_max <= q_arb: I_sed_arb = I_sed[-2] - else: + else: idx_arb = np.where(self.q > q_arb)[0][0] I_sed_arb = I_sed[idx_arb] @@ -116,4 +116,4 @@ def getSimulatedScattering(scalc: SimulateScattering) -> SimulatedScattering: Isim_class = IExperimental(scalc.q, scalc.I0, scalc.I, scalc.exposure) I_sim, I_err = Isim_class.simulate_data() - return SimulatedScattering(I_sim=I_sim, q=scalc.q, I_err=I_err) \ No newline at end of file + return SimulatedScattering(I_sim=I_sim, q=scalc.q, I_err=I_err) diff --git a/src/sas/sascalc/shape2sas/HelperFunctions.py b/src/sas/sascalc/shape2sas/HelperFunctions.py index 7847987c78..e2b38d9f20 100644 --- a/src/sas/sascalc/shape2sas/HelperFunctions.py +++ b/src/sas/sascalc/shape2sas/HelperFunctions.py @@ -22,7 +22,7 @@ def qMethodsNames(name: str): "User_sampled": Qsampling.onUserSampledQ } return methods[name] - + def qMethodsInput(name: str): inputs = { "Uniform": {"qmin": 0.001, "qmax": 0.5, "Nq": 400}, @@ -50,7 +50,7 @@ def sinc(x) -> np.ndarray: function for calculating sinc = sin(x)/x numpy.sinc is defined as sinc(x) = sin(pi*x)/(pi*x) """ - return np.sinc(x / np.pi) + return np.sinc(x / np.pi) def get_max_dimension(x_list: np.ndarray, y_list: np.ndarray, z_list: np.ndarray) -> float: @@ -76,11 +76,11 @@ def get_max_dimension(x_list: np.ndarray, y_list: np.ndarray, z_list: np.ndarray return max_l -def plot_2D(x_list: np.ndarray, - y_list: np.ndarray, - z_list: np.ndarray, - p_list: np.ndarray, - Models: np.ndarray, +def plot_2D(x_list: np.ndarray, + y_list: np.ndarray, + z_list: np.ndarray, + p_list: np.ndarray, + Models: np.ndarray, high_res: bool) -> None: """ plot 2D-projections of generated points (shapes) using matplotlib: @@ -123,7 +123,7 @@ def plot_2D(x_list: np.ndarray, ax[0].set_title('pointmodel, (x,z), "front"') ## plot, perspective 2 - ax[1].plot(y[idx_pos], z[idx_pos], linestyle='none', marker='.', markersize=markersize) + ax[1].plot(y[idx_pos], z[idx_pos], linestyle='none', marker='.', markersize=markersize) ax[1].plot(y[idx_neg], z[idx_neg], linestyle='none', marker='.', markersize=markersize, color='black') ax[1].plot(y[idx_nul], z[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') ax[1].set_xlim(lim) @@ -133,15 +133,15 @@ def plot_2D(x_list: np.ndarray, ax[1].set_title('pointmodel, (y,z), "side"') ## plot, perspective 3 - ax[2].plot(x[idx_pos], y[idx_pos], linestyle='none', marker='.', markersize=markersize) + ax[2].plot(x[idx_pos], y[idx_pos], linestyle='none', marker='.', markersize=markersize) ax[2].plot(x[idx_neg], y[idx_neg], linestyle='none', marker='.', markersize=markersize, color='black') - ax[2].plot(x[idx_nul], y[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') + ax[2].plot(x[idx_nul], y[idx_nul], linestyle='none', marker='.', markersize=markersize, color='grey') ax[2].set_xlim(lim) ax[2].set_ylim(lim) ax[2].set_xlabel('x') ax[2].set_ylabel('y') ax[2].set_title('pointmodel, (x,y), "bottom"') - + plt.tight_layout() if high_res: plt.savefig('points%s.png' % Model,dpi=600) @@ -150,16 +150,16 @@ def plot_2D(x_list: np.ndarray, plt.close() -def plot_results(q: np.ndarray, - r_list: list[np.ndarray], - pr_list: list[np.ndarray], - I_list: list[np.ndarray], - Isim_list: list[np.ndarray], - sigma_list: list[np.ndarray], - S_list: list[np.ndarray], - names: list[str], - scales: list[float], - xscale_log: bool, +def plot_results(q: np.ndarray, + r_list: list[np.ndarray], + pr_list: list[np.ndarray], + I_list: list[np.ndarray], + Isim_list: list[np.ndarray], + sigma_list: list[np.ndarray], + S_list: list[np.ndarray], + names: list[str], + scales: list[float], + xscale_log: bool, high_res: bool) -> None: """ plot results for all models, using matplotlib: @@ -174,7 +174,7 @@ def plot_results(q: np.ndarray, for (r, pr, I, Isim, sigma, S, model_name, scale) in zip (r_list, pr_list, I_list, Isim_list, sigma_list, S_list, names, scales): ax[0].plot(r,pr,zorder=zo,label='p(r), %s' % model_name) - if scale > 1: + if scale > 1: ax[2].errorbar(q,Isim*scale,yerr=sigma*scale,linestyle='none',marker='.',label=r'$I_\mathrm{sim}(q)$, %s, scaled by %d' % (model_name,scale),zorder=1/zo) else: ax[2].errorbar(q,Isim*scale,yerr=sigma*scale,linestyle='none',marker='.',label=r'$I_\mathrm{sim}(q)$, %s' % model_name,zorder=zo) @@ -220,10 +220,10 @@ def plot_results(q: np.ndarray, plt.close() -def generate_pdb(x_list: list[np.ndarray], - y_list: list[np.ndarray], - z_list: list[np.ndarray], - p_list: list[np.ndarray], +def generate_pdb(x_list: list[np.ndarray], + y_list: list[np.ndarray], + z_list: list[np.ndarray], + p_list: list[np.ndarray], Model_list: list[str]) -> None: """ Generates a visualisation file in PDB format with the simulated points (coordinates) and contrasts diff --git a/src/sas/sascalc/shape2sas/Models.py b/src/sas/sascalc/shape2sas/Models.py index d57ff295c1..0b4f2ef345 100644 --- a/src/sas/sascalc/shape2sas/Models.py +++ b/src/sas/sascalc/shape2sas/Models.py @@ -146,8 +146,8 @@ def setAvailableSubunits(self): "sphere": Sphere, "ball": Sphere, - "hollow_sphere": HollowSphere, - "Hollow sphere": HollowSphere, + "hollow_sphere": HollowSphere, + "Hollow sphere": HollowSphere, "cylinder": Cylinder, @@ -170,7 +170,7 @@ def setAvailableSubunits(self): "disc_ring": DiscRing, "Disc ring": DiscRing, - + "superellipsoid": SuperEllipsoid } @@ -211,14 +211,14 @@ def onAppendingPoints(x_new: np.ndarray, @staticmethod def onCheckOverlap( - x: np.ndarray, - y: np.ndarray, - z: np.ndarray, - p: np.ndarray, - rotation: list[float], + x: np.ndarray, + y: np.ndarray, + z: np.ndarray, + p: np.ndarray, + rotation: list[float], rotation_point: list[float], - com: list[float], - subunitClass: object, + com: list[float], + subunitClass: object, dimensions: list[float] ): """check for overlap with previous subunits. @@ -385,8 +385,8 @@ def getPointDistribution(prof: ModelProfile, Npoints): print(f" Subunit {i}: {subunit} with dimensions {prof.dimensions[i]} at COM {prof.com[i]}") print(f" Rotation: {prof.rotation[i]} at rotation point {prof.rotation_points[i]} with SLD {prof.p_s[i]}") - x_new, y_new, z_new, p_new, volume_total = GenerateAllPoints(Npoints, prof.com, prof.subunits, - prof.dimensions, prof.rotation, prof.rotation_points, + x_new, y_new, z_new, p_new, volume_total = GenerateAllPoints(Npoints, prof.com, prof.subunits, + prof.dimensions, prof.rotation, prof.rotation_points, prof.p_s, prof.exclude_overlap).onGeneratingAllPointsSeparately() - - return ModelPointDistribution(x=x_new, y=y_new, z=z_new, p=p_new, volume_total=volume_total) \ No newline at end of file + + return ModelPointDistribution(x=x_new, y=y_new, z=z_new, p=p_new, volume_total=volume_total) diff --git a/src/sas/sascalc/shape2sas/PluginGenerator.py b/src/sas/sascalc/shape2sas/PluginGenerator.py index ce9624d494..7d05113314 100644 --- a/src/sas/sascalc/shape2sas/PluginGenerator.py +++ b/src/sas/sascalc/shape2sas/PluginGenerator.py @@ -9,12 +9,12 @@ def generate_plugin( - prof: ModelProfile, + prof: ModelProfile, modelPars: list[list[str], list[str | float]], - usertext: UserText, + usertext: UserText, fitPar: list[str], - Npoints: int, - pr_points: int, + Npoints: int, + pr_points: int, file_name: str ) -> tuple[str, Path]: """Generates a theoretical scattering plugin model""" @@ -36,7 +36,7 @@ def get_shape_symbols(symbols: tuple[set[str], set[str]], modelPars: list[list[s for shape in modelPars[0]: # iterate over shape names for symbol in shape[1:]: # skip shape name shape_symbols.add(symbol) - + # filter out user-defined symbols lhs_symbols, rhs_symbols = set(), set() for symbol in symbols[0]: @@ -46,7 +46,7 @@ def get_shape_symbols(symbols: tuple[set[str], set[str]], modelPars: list[list[s for symbol in symbols[1]: if symbol in shape_symbols or symbol[1:] in shape_symbols: rhs_symbols.add(symbol) - + return lhs_symbols, rhs_symbols def format_parameter_list(par: list[list[str | float]]) -> str: @@ -187,12 +187,12 @@ def script_insert_constrained_parameters(symbols: set[str], modelPars: list[list return bool(text), "\n".join(text) # indentation for the function body def generate_model( - prof: ModelProfile, + prof: ModelProfile, modelPars: list[list[str], list[str | float]], - usertext: UserText, + usertext: UserText, fitPar: list[str], - Npoints: int, - pr_points: int, + Npoints: int, + pr_points: int, model_name: str ) -> str: """Generates a theoretical model""" @@ -288,5 +288,5 @@ def Iq({', '.join(fitPar)}): Iq.vectorized = True ''') - - return model_str \ No newline at end of file + + return model_str diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index 5fc00aa79d..d5788d10b7 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -278,7 +278,7 @@ def check_input(input: float, default: float, name: str, i: int): Theo_I = getTheoreticalScattering( TheoreticalScatteringCalculation( - System=model, + System=model, Calculation=Sim_par ) ) diff --git a/src/sas/sascalc/shape2sas/StructureFactor.py b/src/sas/sascalc/shape2sas/StructureFactor.py index 1171cd979b..d069d34991 100644 --- a/src/sas/sascalc/shape2sas/StructureFactor.py +++ b/src/sas/sascalc/shape2sas/StructureFactor.py @@ -6,10 +6,10 @@ class StructureFactor: - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, p_new: np.ndarray, Stype: str, par: list[float] | None): @@ -31,12 +31,12 @@ def setAvailableStructureFactors(self): 'Aggregation': Aggregation, 'None': NoStructure } - + def getStructureFactorClass(self): """Return chosen structure factor""" if self.Stype in self.structureFactor: return self.structureFactor[self.Stype](self.q, self.x_new, self.y_new, self.z_new, self.p_new, self.par) - + else: try: return globals()[self.Stype](self.q, self.x_new, self.y_new, self.z_new, self.p_new, self.par) diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index 813933e79d..bba58871c0 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -29,9 +29,9 @@ class TheoreticalScattering: class WeightedPairDistribution: - def __init__(self, x: np.ndarray, - y: np.ndarray, - z: np.ndarray, + def __init__(self, x: np.ndarray, + y: np.ndarray, + z: np.ndarray, p: np.ndarray): self.x = x self.y = y @@ -46,7 +46,7 @@ def calc_dist(x: np.ndarray) -> np.ndarray: # mesh this array so that you will have all combinations m, n = np.meshgrid(x, x, sparse=True) # get the distance via the norm - dist = abs(m - n) + dist = abs(m - n) return dist def calc_all_dist(self) -> np.ndarray: @@ -59,7 +59,7 @@ def calc_all_dist(self) -> np.ndarray: square_sum = 0 for arr in [self.x, self.y, self.z]: - square_sum += self.calc_dist(arr)**2 #arr will input x_new, then y_new and z_new so you get + square_sum += self.calc_dist(arr)**2 #arr will input x_new, then y_new and z_new so you get #x_new^2 + y_new^2 + z_new^2 d = np.sqrt(square_sum) #then the square root is taken to get avector for the distance # convert from matrix to array @@ -99,7 +99,7 @@ def generate_histogram(dist: np.ndarray, contrast: np.ndarray, r_max: float, Nbi """ - histo, bin_edges = np.histogram(dist, bins=Nbins, weights=contrast, range=(0, r_max)) + histo, bin_edges = np.histogram(dist, bins=Nbins, weights=contrast, range=(0, r_max)) dr = bin_edges[2] - bin_edges[1] r = bin_edges[0:-1] + dr / 2 @@ -115,11 +115,11 @@ def calc_Rg(r: np.ndarray, pr: np.ndarray) -> float: Rg = np.sqrt(abs(sum_pr_r2 / sum_pr) / 2) return Rg - - def calc_hr(self, - dist: np.ndarray, - Nbins: int, - contrast: np.ndarray, + + def calc_hr(self, + dist: np.ndarray, + Nbins: int, + contrast: np.ndarray, polydispersity: float) -> Vector2D: """ calculate h(r) @@ -201,12 +201,12 @@ def calc_pr(self, Nbins: int, polydispersity: float) -> Vector3D: #NOTE: If Nreps is to be added from the original code #Then r_sum, pr_sum and pr_norm_sum should be added here - return r, pr, pr_norm - + return r, pr, pr_norm + @staticmethod def save_pr(Nbins: int, - r: np.ndarray, - pr_norm: np.ndarray, + r: np.ndarray, + pr_norm: np.ndarray, Model: str): """ save p(r) to textfile @@ -231,7 +231,7 @@ def calc_Pq(self, r: np.ndarray, pr: np.ndarray, conc: float, volume_total: floa I0 += pr_i qr = self.q * r_i Pq += pr_i * sinc(qr) - + # normalization, P(0) = 1 if I0 == 0: I0 = 1E-5 @@ -239,13 +239,13 @@ def calc_Pq(self, r: np.ndarray, pr: np.ndarray, conc: float, volume_total: floa I0 = abs(I0) Pq /= I0 - # make I0 scale with volume fraction (concentration) and + # make I0 scale with volume fraction (concentration) and # volume squared and scale so default values gives I(0) of approx unity I0 *= conc * volume_total * 1E-4 return I0, Pq - + def calc_Pq_ausaxs(self, q: np.ndarray, x: np.ndarray, y: np.ndarray, z: np.ndarray, p: np.ndarray) -> np.ndarray: """ calculate form factor, P(q), using ausaxs SANS Debye method @@ -253,8 +253,8 @@ def calc_Pq_ausaxs(self, q: np.ndarray, x: np.ndarray, y: np.ndarray, z: np.ndar from sas.sascalc.calculator.ausaxs.ausaxs_sans_debye import evaluate_sans_debye return evaluate_sans_debye(q, np.array([x, y, z]), p) - def calc_Iq(self, Pq: np.ndarray, - S_eff: np.ndarray, + def calc_Iq(self, Pq: np.ndarray, + S_eff: np.ndarray, sigma_r: float) -> np.ndarray: """ calculates intensity @@ -302,7 +302,7 @@ def getTheoreticalScattering(scalc: TheoreticalScatteringCalculation) -> Theoret Pq = I_theory.calc_Pq_ausaxs(q, x, y, z, p)/I0 I0 = 1 - else: + else: r, pr, _ = WeightedPairDistribution(x, y, z, p).calc_pr(calc.prpoints, sys.polydispersity) I0, Pq = I_theory.calc_Pq(r, pr, sys.conc, prof.volume_total) @@ -325,4 +325,4 @@ def getTheoreticalHistogram(model: ModelSystem, sim_pars: SimulationParameters) y = np.concatenate(prof.y) z = np.concatenate(prof.z) p = np.concatenate(prof.p) - return WeightedPairDistribution(x, y, z, p).calc_pr(sim_pars.prpoints, model.polydispersity) \ No newline at end of file + return WeightedPairDistribution(x, y, z, p).calc_pr(sim_pars.prpoints, model.polydispersity) diff --git a/src/sas/sascalc/shape2sas/Typing.py b/src/sas/sascalc/shape2sas/Typing.py index e237b49444..8d66ec4637 100644 --- a/src/sas/sascalc/shape2sas/Typing.py +++ b/src/sas/sascalc/shape2sas/Typing.py @@ -4,4 +4,4 @@ Vectors = list[list[float]] Vector2D = tuple[np.ndarray, np.ndarray] Vector3D = tuple[np.ndarray, np.ndarray, np.ndarray] -Vector4D = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] \ No newline at end of file +Vector4D = tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] diff --git a/src/sas/sascalc/shape2sas/UserText.py b/src/sas/sascalc/shape2sas/UserText.py index 5f3b69661b..d832121965 100644 --- a/src/sas/sascalc/shape2sas/UserText.py +++ b/src/sas/sascalc/shape2sas/UserText.py @@ -7,4 +7,4 @@ def __init__(self, imports: list[str], params: list[str], constraints: list[str] self.imports = imports self.params = params self.constraints = constraints - self.symbols = symbols \ No newline at end of file + self.symbols = symbols diff --git a/src/sas/sascalc/shape2sas/models/Cube.py b/src/sas/sascalc/shape2sas/models/Cube.py index 2171f436a3..21bf91f13c 100644 --- a/src/sas/sascalc/shape2sas/models/Cube.py +++ b/src/sas/sascalc/shape2sas/models/Cube.py @@ -19,10 +19,10 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: z_add = np.random.uniform(-self.a / 2, self.a / 2, N) return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a cube""" - idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.a/2) | + idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.a/2) | (abs(z_eff) >= self.a/2)) - return idx \ No newline at end of file + return idx diff --git a/src/sas/sascalc/shape2sas/models/Cuboid.py b/src/sas/sascalc/shape2sas/models/Cuboid.py index a998535adc..367898ae11 100644 --- a/src/sas/sascalc/shape2sas/models/Cuboid.py +++ b/src/sas/sascalc/shape2sas/models/Cuboid.py @@ -18,10 +18,10 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: z_add = np.random.uniform(-self.c, self.c, Npoints) return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a Cuboid""" - idx = np.where((abs(x_eff) >= self.a/2) + idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.b/2) | (abs(z_eff) >= self.c/2)) - return idx \ No newline at end of file + return idx diff --git a/src/sas/sascalc/shape2sas/models/Cylinder.py b/src/sas/sascalc/shape2sas/models/Cylinder.py index 9d4c847c63..580078a535 100644 --- a/src/sas/sascalc/shape2sas/models/Cylinder.py +++ b/src/sas/sascalc/shape2sas/models/Cylinder.py @@ -26,10 +26,10 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a cylinder""" d = np.sqrt(x_eff**2+y_eff**2) idx = np.where((d > self.R) | (abs(z_eff) > self.l / 2)) - return idx \ No newline at end of file + return idx diff --git a/src/sas/sascalc/shape2sas/models/CylinderRing.py b/src/sas/sascalc/shape2sas/models/CylinderRing.py index f9aa0f7cfc..0d38c4a2c2 100644 --- a/src/sas/sascalc/shape2sas/models/CylinderRing.py +++ b/src/sas/sascalc/shape2sas/models/CylinderRing.py @@ -16,7 +16,7 @@ def getVolume(self) -> float: if self.r == self.R: return 2 * np.pi * self.R * self.l #surface area of a cylinder - else: + else: return np.pi * (self.R**2 - self.r**2) * self.l def getPointDistribution(self, Npoints: int) -> Vector3D: @@ -43,8 +43,8 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a cylinder ring""" d = np.sqrt(x_eff**2 + y_eff**2) @@ -54,6 +54,6 @@ def checkOverlap(self, x_eff: np.ndarray, if self.r == self.R: idx = np.where((d != self.R) | (abs(z_eff) > self.l / 2)) return idx - else: + else: idx = np.where((d > self.R) | (d < self.r) | (abs(z_eff) > self.l / 2)) - return idx \ No newline at end of file + return idx diff --git a/src/sas/sascalc/shape2sas/models/Disc.py b/src/sas/sascalc/shape2sas/models/Disc.py index 8a0f331638..8426791735 100644 --- a/src/sas/sascalc/shape2sas/models/Disc.py +++ b/src/sas/sascalc/shape2sas/models/Disc.py @@ -2,4 +2,4 @@ class Disc(EllipticalCylinder): - pass \ No newline at end of file + pass diff --git a/src/sas/sascalc/shape2sas/models/Ellipsoid.py b/src/sas/sascalc/shape2sas/models/Ellipsoid.py index 514c854238..a13ff4f575 100644 --- a/src/sas/sascalc/shape2sas/models/Ellipsoid.py +++ b/src/sas/sascalc/shape2sas/models/Ellipsoid.py @@ -28,8 +28,8 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """check for points within a ellipsoid""" d2 = x_eff**2 / self.a**2 + y_eff**2 / self.b**2 + z_eff**2 / self.c**2 diff --git a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py index 53683b3da9..ab188a22ed 100644 --- a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py +++ b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py @@ -28,10 +28,10 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a Elliptical cylinder""" d2 = x_eff**2 / self.a**2 + y_eff**2 / self.b**2 idx = np.where((d2 > 1) | (abs(z_eff) > self.l / 2)) - return idx \ No newline at end of file + return idx diff --git a/src/sas/sascalc/shape2sas/models/HollowCube.py b/src/sas/sascalc/shape2sas/models/HollowCube.py index 28f27ccbd5..0d50557465 100644 --- a/src/sas/sascalc/shape2sas/models/HollowCube.py +++ b/src/sas/sascalc/shape2sas/models/HollowCube.py @@ -15,27 +15,27 @@ def getVolume(self) -> float: if self.a == self.b: return 6 * self.a**2 #surface area of a cube - else: + else: return (self.a - self.b)**3 def getPointDistribution(self, Npoints: int) -> Vector3D: """Returns the point distribution of a hollow cube""" Volume = self.getVolume() - + if self.a == self.b: #The hollow cube is a shell d = self.a / 2 N = int(Npoints / 6) one = np.ones(N) - + #make each side of the cube at a time x_add, y_add, z_add = [], [], [] for sign in [-1, 1]: x_add = np.concatenate((x_add, sign * one * d)) y_add = np.concatenate((y_add, np.random.uniform(-d, d, N))) z_add = np.concatenate((z_add, np.random.uniform(-d, d, N))) - + x_add = np.concatenate((x_add, np.random.uniform(-d, d, N))) y_add = np.concatenate((y_add, sign * one * d)) z_add = np.concatenate((z_add, np.random.uniform(-d, d, N))) @@ -44,7 +44,7 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: y_add = np.concatenate((y_add, np.random.uniform(-d, d, N))) z_add = np.concatenate((z_add, sign * one * d)) return x_add, y_add, z_add - + Volume_max = self.a**3 Vratio = Volume_max / Volume N = int(Vratio * Npoints) @@ -59,21 +59,21 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a hollow cube""" if self.a < self.b: self.a, self.b = self.b, self.a - + if self.a == self.b: idx = np.where((abs(x_eff)!=self.a/2) | (abs(y_eff)!=self.a/2) | (abs(z_eff)!=self.a/2)) return idx - - else: - idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.a/2) | - (abs(z_eff) >= self.a/2) | ((abs(x_eff) <= self.b/2) + + else: + idx = np.where((abs(x_eff) >= self.a/2) | (abs(y_eff) >= self.a/2) | + (abs(z_eff) >= self.a/2) | ((abs(x_eff) <= self.b/2) & (abs(y_eff) <= self.b/2) & (abs(z_eff) <= self.b/2))) - return idx \ No newline at end of file + return idx diff --git a/src/sas/sascalc/shape2sas/models/HollowSphere.py b/src/sas/sascalc/shape2sas/models/HollowSphere.py index 739f2609b9..c55e928009 100644 --- a/src/sas/sascalc/shape2sas/models/HollowSphere.py +++ b/src/sas/sascalc/shape2sas/models/HollowSphere.py @@ -13,7 +13,7 @@ def getVolume(self) -> float: if self.r == self.R: return 4 * np.pi * self.R**2 #surface area of a sphere - else: + else: return (4 / 3) * np.pi * (self.R**3 - self.r**3) def getPointDistribution(self, Npoints: int) -> Vector3D: @@ -44,8 +44,8 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: x_add, y_add, z_add = x[idx], y[idx], z[idx] return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a hollow sphere""" @@ -56,7 +56,7 @@ def checkOverlap(self, x_eff: np.ndarray, if self.r == self.R: idx = np.where(d != self.R) return idx - + else: idx = np.where((d > self.R) | (d < self.r)) - return idx \ No newline at end of file + return idx diff --git a/src/sas/sascalc/shape2sas/models/Sphere.py b/src/sas/sascalc/shape2sas/models/Sphere.py index aa31a0e1b9..610e7ed68e 100644 --- a/src/sas/sascalc/shape2sas/models/Sphere.py +++ b/src/sas/sascalc/shape2sas/models/Sphere.py @@ -26,9 +26,9 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: return x_add, y_add, z_add - def checkOverlap(self, - x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, + x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a sphere""" diff --git a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py index adc8fb4a2d..64ded0f404 100644 --- a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py +++ b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py @@ -19,7 +19,7 @@ def beta(a, b) -> float: def getVolume(self) -> float: """Returns the volume of a superellipsoid""" - return (8 / (3 * self.t * self.s) * self.R**3 * self.eps * + return (8 / (3 * self.t * self.s) * self.R**3 * self.eps * self.beta(1 / self.s, 1 / self.s) * self.beta(2 / self.t, 1 / self.t)) def getPointDistribution(self, Npoints: int) -> Vector3D: @@ -33,19 +33,19 @@ def getPointDistribution(self, Npoints: int) -> Vector3D: y = np.random.uniform(-self.R, self.R, N) z = np.random.uniform(-self.R * self.eps, self.R * self.eps, N) - d = ((np.abs(x)**self.s + np.abs(y)**self.s)**(self.t/ self.s) + d = ((np.abs(x)**self.s + np.abs(y)**self.s)**(self.t/ self.s) + np.abs(z / self.eps)**self.t) idx = np.where(d < np.abs(self.R)**self.t) x_add, y_add, z_add = x[idx], y[idx], z[idx] return x_add, y_add, z_add - def checkOverlap(self, x_eff: np.ndarray, - y_eff: np.ndarray, + def checkOverlap(self, x_eff: np.ndarray, + y_eff: np.ndarray, z_eff: np.ndarray) -> np.ndarray: """Check for points within a superellipsoid""" - d = ((np.abs(x_eff)**self.s + np.abs(y_eff)**self.s)**(self.t / self.s) + d = ((np.abs(x_eff)**self.s + np.abs(y_eff)**self.s)**(self.t / self.s) + np.abs(z_eff / self.eps)**self.t) idx = np.where(d >= np.abs(self.R)**self.t) - return idx \ No newline at end of file + return idx diff --git a/src/sas/sascalc/shape2sas/models/__init__.py b/src/sas/sascalc/shape2sas/models/__init__.py index 4c92b0bada..e82abf8188 100644 --- a/src/sas/sascalc/shape2sas/models/__init__.py +++ b/src/sas/sascalc/shape2sas/models/__init__.py @@ -15,4 +15,4 @@ 'Cube', 'Cuboid', 'Cylinder', 'CylinderRing', 'Disc', 'DiscRing', 'Ellipsoid', 'EllipticalCylinder', 'HollowCube', 'HollowSphere', 'Sphere', 'SuperEllipsoid' -] \ No newline at end of file +] diff --git a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py index 2675442bdf..dd822379eb 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py +++ b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py @@ -5,11 +5,11 @@ class Aggregation(StructureDecouplingApprox): - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray, + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray, par: list[float]): super(Aggregation, self).__init__(q, x_new, y_new, z_new, p_new) self.q = q diff --git a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py index 305664a35c..0fde5419e5 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py +++ b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py @@ -5,11 +5,11 @@ class HardSphereStructure(StructureDecouplingApprox): - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray, + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray, par: list[float]): super(HardSphereStructure, self).__init__(q, x_new, y_new, z_new, p_new) self.q = q @@ -35,14 +35,14 @@ def calc_S_HS(self) -> np.ndarray: """ if self.conc > 0.0: - A = 2 * self.R_HS * self.q + A = 2 * self.R_HS * self.q G = self.calc_G(A, self.conc) S_HS = 1 / (1 + 24 * self.conc * G / A) #percus-yevick approximation for else: #calculating the structure factor S_HS = np.ones(len(self.q)) return S_HS - + @staticmethod def calc_G(A: np.ndarray, eta: float) -> np.ndarray: """ @@ -59,7 +59,7 @@ def calc_G(A: np.ndarray, eta: float) -> np.ndarray: """ a = (1 + 2 * eta)**2 / (1 - eta)**4 - b = -6 * eta * (1 + eta / 2)**2/(1 - eta)**4 + b = -6 * eta * (1 + eta / 2)**2/(1 - eta)**4 c = eta * a / 2 sinA = np.sin(A) cosA = np.cos(A) @@ -73,4 +73,4 @@ def calc_G(A: np.ndarray, eta: float) -> np.ndarray: def structure_eff(self, Pq: np.ndarray) -> np.ndarray: S = self.calc_S_HS() S_eff = self.decoupling_approx(Pq, S) - return S_eff + return S_eff diff --git a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py index e5ff4fff9c..a3e55b8bae 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py +++ b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py @@ -7,11 +7,11 @@ class NoStructure(StructureDecouplingApprox): - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, - p_new: np.ndarray, + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, + p_new: np.ndarray, par: Any): super(NoStructure, self).__init__(q, x_new, y_new, z_new, p_new) self.q = q diff --git a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py index f11cd4b5bf..1c95d1321a 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py +++ b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py @@ -5,10 +5,10 @@ class StructureDecouplingApprox: - def __init__(self, q: np.ndarray, - x_new: np.ndarray, - y_new: np.ndarray, - z_new: np.ndarray, + def __init__(self, q: np.ndarray, + x_new: np.ndarray, + y_new: np.ndarray, + z_new: np.ndarray, p_new: np.ndarray): self.q = q self.x_new = x_new @@ -38,7 +38,7 @@ def calc_A00(self) -> np.ndarray: d_new = self.calc_com_dist() M = len(self.q) A00 = np.zeros(M) - + for i in range(M): qr = self.q[i] * d_new @@ -46,7 +46,7 @@ def calc_A00(self) -> np.ndarray: A00 = A00 / A00[0] # normalise, A00[0] = 1 return A00 - + def decoupling_approx(self, Pq: np.ndarray, S: np.ndarray) -> np.ndarray: """ modify structure factor with the decoupling approximation @@ -94,4 +94,4 @@ def structure_eff(self, Pq: np.ndarray) -> np.ndarray: S_eff = self.decoupling_approx(Pq, S) return S_eff -''' \ No newline at end of file +''' diff --git a/src/sas/sascalc/shape2sas/structure_factors/__init__.py b/src/sas/sascalc/shape2sas/structure_factors/__init__.py index 41d8641349..567435658d 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/__init__.py +++ b/src/sas/sascalc/shape2sas/structure_factors/__init__.py @@ -7,4 +7,4 @@ 'Aggregation', 'HardSphereStructure', 'NoStructure' -] \ No newline at end of file +] From 7acfa36b4d735aaf6244ed171b5b02d786eac8ca Mon Sep 17 00:00:00 2001 From: krellemeister Date: Fri, 3 Oct 2025 14:59:11 +0200 Subject: [PATCH 34/37] removed wildcard imports; enforced usage of new transform method --- .../Calculators/Shape2SAS/ViewerModel.py | 3 +- .../shape2sas/ExperimentalScattering.py | 2 +- src/sas/sascalc/shape2sas/HelperFunctions.py | 2 -- src/sas/sascalc/shape2sas/Models.py | 30 +++++++++++-------- src/sas/sascalc/shape2sas/Shape2SAS.py | 8 +++-- src/sas/sascalc/shape2sas/StructureFactor.py | 3 +- .../shape2sas/TheoreticalScattering.py | 2 +- src/sas/sascalc/shape2sas/Typing.py | 1 - src/sas/sascalc/shape2sas/models/Cube.py | 3 +- src/sas/sascalc/shape2sas/models/Cuboid.py | 3 +- src/sas/sascalc/shape2sas/models/Cylinder.py | 3 +- .../sascalc/shape2sas/models/CylinderRing.py | 3 +- src/sas/sascalc/shape2sas/models/Ellipsoid.py | 3 +- .../shape2sas/models/EllipticalCylinder.py | 3 +- .../sascalc/shape2sas/models/HollowCube.py | 3 +- .../sascalc/shape2sas/models/HollowSphere.py | 3 +- src/sas/sascalc/shape2sas/models/Sphere.py | 3 +- .../shape2sas/models/SuperEllipsoid.py | 3 +- .../structure_factors/Aggregation.py | 1 - .../structure_factors/HardSphereStructure.py | 1 - .../structure_factors/NoStructure.py | 1 - .../StructureDecouplingApprox.py | 1 - 22 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py b/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py index 6e091d45dc..5d8d3dc4d9 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/ViewerModel.py @@ -10,7 +10,8 @@ # Local Perspectives from sas.qtgui.Calculators.Shape2SAS.ViewerAllOptions import ViewerButtons, ViewerModelRadius -from sas.sascalc.shape2sas.Shape2SAS import ModelPointDistribution, TheoreticalScattering +from sas.sascalc.shape2sas.Models import ModelPointDistribution +from sas.sascalc.shape2sas.TheoreticalScattering import TheoreticalScattering class ViewerModel(QWidget): diff --git a/src/sas/sascalc/shape2sas/ExperimentalScattering.py b/src/sas/sascalc/shape2sas/ExperimentalScattering.py index 12a56e97b8..ecbaa11654 100644 --- a/src/sas/sascalc/shape2sas/ExperimentalScattering.py +++ b/src/sas/sascalc/shape2sas/ExperimentalScattering.py @@ -2,7 +2,7 @@ import numpy as np -from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.Typing import Vector2D @dataclass diff --git a/src/sas/sascalc/shape2sas/HelperFunctions.py b/src/sas/sascalc/shape2sas/HelperFunctions.py index e2b38d9f20..dca3eeda39 100644 --- a/src/sas/sascalc/shape2sas/HelperFunctions.py +++ b/src/sas/sascalc/shape2sas/HelperFunctions.py @@ -1,8 +1,6 @@ import matplotlib.pyplot as plt import numpy as np -from sas.sascalc.shape2sas.Typing import * - ################################ Shape2SAS helper functions ################################### class Qsampling: diff --git a/src/sas/sascalc/shape2sas/Models.py b/src/sas/sascalc/shape2sas/Models.py index 0b4f2ef345..14c8204508 100644 --- a/src/sas/sascalc/shape2sas/Models.py +++ b/src/sas/sascalc/shape2sas/Models.py @@ -3,8 +3,10 @@ import numpy as np from sas.sascalc.shape2sas.HelperFunctions import Qsampling, euler_rotation_matrix -from sas.sascalc.shape2sas.models import * -from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.models import \ + Cube, Cuboid, Cylinder, CylinderRing, Disc, DiscRing, Ellipsoid, \ + EllipticalCylinder, HollowCube, HollowSphere, Sphere, SuperEllipsoid +from sas.sascalc.shape2sas.Typing import Vectors, Vector3D, Vector4D @dataclass @@ -68,20 +70,22 @@ def __init__(self, matrix: np.ndarray, center_mass: np.ndarray): self.cm = center_mass # center of mass Translation = np.ndarray -def transform(coords: np.ndarray[Vector3D], T: Translation, R: Rotation): +def transform(coords: np.ndarray[Vector3D], translate: Translation = np.array([0, 0, 0]), rotate: Rotation = Rotation(np.eye(3), np.array([0, 0, 0]))): """Transform a set of coordinates by a rotation R and translation T""" + if isinstance(rotate, np.ndarray): + rotate = Rotation(rotate, np.array([0, 0, 0])) assert coords.shape[0] == 3 - assert T.shape == (3,) - assert R.M.shape == (3, 3) - assert R.cm.shape == (3,) + assert translate.shape == (3,) + assert rotate.M.shape == (3, 3) + assert rotate.cm.shape == (3,) # The transform is: # v' = R*(v - R_cm) + R_cm + T # = R*v - R*R_cm + R_cm + T - # = R*v + (-R*R_cm + R_cm + T) + # = R*v + T' - Tp = -np.dot(R.M, R.cm) + R.cm + T - return np.dot(R.M, coords) + Tp[:, np.newaxis] + Tp = -np.dot(rotate.M, rotate.cm) + rotate.cm + translate + return np.dot(rotate.M, coords) + Tp[:, np.newaxis] class GeneratePoints: @@ -226,7 +230,7 @@ def onCheckOverlap( if any(r != 0 for r in rotation): ## effective coordinates, shifted by (x_com,y_com,z_com) - x_eff, y_eff, z_eff = Translation(x, y, z, -com[0], -com[1], -com[2]).onTranslatingPoints() + x_eff, y_eff, z_eff = transform(np.vstack([x, y, z]), translate=np.array([-com[0], -com[1], -com[2]])) #rotate backwards with minus rotation angles alpha, beta, gam = rotation @@ -235,12 +239,12 @@ def onCheckOverlap( beta = np.radians(beta) gam = np.radians(gam) - x_eff, y_eff, z_eff = Rotation(x_eff, y_eff, z_eff, -alpha, -beta, -gam, rotp_x, rotp_y, rotp_z).onRotatingPoints() + rotation = Rotation(euler_rotation_matrix(-alpha, -beta, -gam), np.array([rotp_x, rotp_y, rotp_z])) + x_eff, y_eff, z_eff = transform(np.vstack([x_eff, y_eff, z_eff]), rotate=rotation) else: ## effective coordinates, shifted by (x_com,y_com,z_com) - x_eff, y_eff, z_eff = Translation(x, y, z, -com[0], -com[1], -com[2]).onTranslatingPoints() - + x_eff, y_eff, z_eff = transform(np.vstack([x, y, z]), translate=np.array([-com[0], -com[1], -com[2]])) idx = subunitClass(dimensions).checkOverlap(x_eff, y_eff, z_eff) x_add, y_add, z_add, p_add = x[idx], y[idx], z[idx], p[idx] diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index d5788d10b7..3e956ae82a 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -5,11 +5,13 @@ import numpy as np -from sas.sascalc.shape2sas.ExperimentalScattering import * +from sas.sascalc.shape2sas.ExperimentalScattering import SimulateScattering, getSimulatedScattering from sas.sascalc.shape2sas.HelperFunctions import generate_pdb, plot_2D, plot_results -from sas.sascalc.shape2sas.Models import * +from sas.sascalc.shape2sas.Models import Qsampling, ModelProfile, SimulationParameters, getPointDistribution from sas.sascalc.shape2sas.StructureFactor import StructureFactor -from sas.sascalc.shape2sas.TheoreticalScattering import * +from sas.sascalc.shape2sas.TheoreticalScattering import \ + TheoreticalScatteringCalculation, ModelSystem, ITheoretical, WeightedPairDistribution, \ + getTheoreticalScattering, getTheoreticalHistogram ################################ Shape2SAS batch version ################################ if __name__ == "__main__": diff --git a/src/sas/sascalc/shape2sas/StructureFactor.py b/src/sas/sascalc/shape2sas/StructureFactor.py index d069d34991..2cab24d9cb 100644 --- a/src/sas/sascalc/shape2sas/StructureFactor.py +++ b/src/sas/sascalc/shape2sas/StructureFactor.py @@ -1,8 +1,7 @@ import numpy as np -from sas.sascalc.shape2sas.structure_factors import * -from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.structure_factors import HardSphereStructure, Aggregation, NoStructure class StructureFactor: diff --git a/src/sas/sascalc/shape2sas/TheoreticalScattering.py b/src/sas/sascalc/shape2sas/TheoreticalScattering.py index bba58871c0..962fcc81ec 100644 --- a/src/sas/sascalc/shape2sas/TheoreticalScattering.py +++ b/src/sas/sascalc/shape2sas/TheoreticalScattering.py @@ -5,7 +5,7 @@ from sas.sascalc.shape2sas.HelperFunctions import sinc from sas.sascalc.shape2sas.Models import ModelSystem, SimulationParameters from sas.sascalc.shape2sas.StructureFactor import StructureFactor -from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.Typing import Vector2D, Vector3D @dataclass diff --git a/src/sas/sascalc/shape2sas/Typing.py b/src/sas/sascalc/shape2sas/Typing.py index 8d66ec4637..c2d6f4224e 100644 --- a/src/sas/sascalc/shape2sas/Typing.py +++ b/src/sas/sascalc/shape2sas/Typing.py @@ -1,4 +1,3 @@ - import numpy as np Vectors = list[list[float]] diff --git a/src/sas/sascalc/shape2sas/models/Cube.py b/src/sas/sascalc/shape2sas/models/Cube.py index 21bf91f13c..3afd3bc410 100644 --- a/src/sas/sascalc/shape2sas/models/Cube.py +++ b/src/sas/sascalc/shape2sas/models/Cube.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class Cube: diff --git a/src/sas/sascalc/shape2sas/models/Cuboid.py b/src/sas/sascalc/shape2sas/models/Cuboid.py index 367898ae11..d9beb316f7 100644 --- a/src/sas/sascalc/shape2sas/models/Cuboid.py +++ b/src/sas/sascalc/shape2sas/models/Cuboid.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class Cuboid: diff --git a/src/sas/sascalc/shape2sas/models/Cylinder.py b/src/sas/sascalc/shape2sas/models/Cylinder.py index 580078a535..4b6a07ef66 100644 --- a/src/sas/sascalc/shape2sas/models/Cylinder.py +++ b/src/sas/sascalc/shape2sas/models/Cylinder.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class Cylinder: diff --git a/src/sas/sascalc/shape2sas/models/CylinderRing.py b/src/sas/sascalc/shape2sas/models/CylinderRing.py index 0d38c4a2c2..36935eee56 100644 --- a/src/sas/sascalc/shape2sas/models/CylinderRing.py +++ b/src/sas/sascalc/shape2sas/models/CylinderRing.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class CylinderRing: diff --git a/src/sas/sascalc/shape2sas/models/Ellipsoid.py b/src/sas/sascalc/shape2sas/models/Ellipsoid.py index a13ff4f575..8e5368fe01 100644 --- a/src/sas/sascalc/shape2sas/models/Ellipsoid.py +++ b/src/sas/sascalc/shape2sas/models/Ellipsoid.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class Ellipsoid: diff --git a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py index ab188a22ed..dd52d199f3 100644 --- a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py +++ b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class EllipticalCylinder: diff --git a/src/sas/sascalc/shape2sas/models/HollowCube.py b/src/sas/sascalc/shape2sas/models/HollowCube.py index 0d50557465..2a3237d459 100644 --- a/src/sas/sascalc/shape2sas/models/HollowCube.py +++ b/src/sas/sascalc/shape2sas/models/HollowCube.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class HollowCube: diff --git a/src/sas/sascalc/shape2sas/models/HollowSphere.py b/src/sas/sascalc/shape2sas/models/HollowSphere.py index c55e928009..87fada6c34 100644 --- a/src/sas/sascalc/shape2sas/models/HollowSphere.py +++ b/src/sas/sascalc/shape2sas/models/HollowSphere.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class HollowSphere: diff --git a/src/sas/sascalc/shape2sas/models/Sphere.py b/src/sas/sascalc/shape2sas/models/Sphere.py index 610e7ed68e..c92bae0a9a 100644 --- a/src/sas/sascalc/shape2sas/models/Sphere.py +++ b/src/sas/sascalc/shape2sas/models/Sphere.py @@ -1,4 +1,5 @@ -from sas.sascalc.shape2sas.Typing import * +import numpy as np +from sas.sascalc.shape2sas.Typing import Vector3D class Sphere: diff --git a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py index 64ded0f404..08f1acfcb4 100644 --- a/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py +++ b/src/sas/sascalc/shape2sas/models/SuperEllipsoid.py @@ -1,6 +1,7 @@ +import numpy as np from scipy.special import gamma -from sas.sascalc.shape2sas.Typing import * +from sas.sascalc.shape2sas.Typing import Vector3D class SuperEllipsoid: diff --git a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py index dd822379eb..f1e059af64 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py +++ b/src/sas/sascalc/shape2sas/structure_factors/Aggregation.py @@ -1,7 +1,6 @@ import numpy as np from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox -from sas.sascalc.shape2sas.Typing import * class Aggregation(StructureDecouplingApprox): diff --git a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py index 0fde5419e5..d1e496fbcf 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py +++ b/src/sas/sascalc/shape2sas/structure_factors/HardSphereStructure.py @@ -1,7 +1,6 @@ import numpy as np from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox -from sas.sascalc.shape2sas.Typing import * class HardSphereStructure(StructureDecouplingApprox): diff --git a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py index a3e55b8bae..723bc13914 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py +++ b/src/sas/sascalc/shape2sas/structure_factors/NoStructure.py @@ -3,7 +3,6 @@ import numpy as np from sas.sascalc.shape2sas.structure_factors.StructureDecouplingApprox import StructureDecouplingApprox -from sas.sascalc.shape2sas.Typing import * class NoStructure(StructureDecouplingApprox): diff --git a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py index 1c95d1321a..8c2435b3d5 100644 --- a/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py +++ b/src/sas/sascalc/shape2sas/structure_factors/StructureDecouplingApprox.py @@ -1,7 +1,6 @@ import numpy as np from sas.sascalc.shape2sas.HelperFunctions import sinc -from sas.sascalc.shape2sas.Typing import * class StructureDecouplingApprox: From 09fbd6d5f09d48b5898a6795648bcf26525c84ee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:59:56 +0000 Subject: [PATCH 35/37] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- src/sas/sascalc/shape2sas/Models.py | 21 ++++++++++++++----- src/sas/sascalc/shape2sas/Shape2SAS.py | 13 ++++++++---- src/sas/sascalc/shape2sas/StructureFactor.py | 2 +- src/sas/sascalc/shape2sas/models/Cube.py | 1 + src/sas/sascalc/shape2sas/models/Cuboid.py | 1 + src/sas/sascalc/shape2sas/models/Cylinder.py | 1 + .../sascalc/shape2sas/models/CylinderRing.py | 1 + src/sas/sascalc/shape2sas/models/Ellipsoid.py | 1 + .../shape2sas/models/EllipticalCylinder.py | 1 + .../sascalc/shape2sas/models/HollowCube.py | 1 + .../sascalc/shape2sas/models/HollowSphere.py | 1 + src/sas/sascalc/shape2sas/models/Sphere.py | 1 + 12 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/sas/sascalc/shape2sas/Models.py b/src/sas/sascalc/shape2sas/Models.py index 14c8204508..6c264741a0 100644 --- a/src/sas/sascalc/shape2sas/Models.py +++ b/src/sas/sascalc/shape2sas/Models.py @@ -3,10 +3,21 @@ import numpy as np from sas.sascalc.shape2sas.HelperFunctions import Qsampling, euler_rotation_matrix -from sas.sascalc.shape2sas.models import \ - Cube, Cuboid, Cylinder, CylinderRing, Disc, DiscRing, Ellipsoid, \ - EllipticalCylinder, HollowCube, HollowSphere, Sphere, SuperEllipsoid -from sas.sascalc.shape2sas.Typing import Vectors, Vector3D, Vector4D +from sas.sascalc.shape2sas.models import ( + Cube, + Cuboid, + Cylinder, + CylinderRing, + Disc, + DiscRing, + Ellipsoid, + EllipticalCylinder, + HollowCube, + HollowSphere, + Sphere, + SuperEllipsoid, +) +from sas.sascalc.shape2sas.Typing import Vector3D, Vector4D, Vectors @dataclass @@ -72,7 +83,7 @@ def __init__(self, matrix: np.ndarray, center_mass: np.ndarray): def transform(coords: np.ndarray[Vector3D], translate: Translation = np.array([0, 0, 0]), rotate: Rotation = Rotation(np.eye(3), np.array([0, 0, 0]))): """Transform a set of coordinates by a rotation R and translation T""" - if isinstance(rotate, np.ndarray): + if isinstance(rotate, np.ndarray): rotate = Rotation(rotate, np.array([0, 0, 0])) assert coords.shape[0] == 3 assert translate.shape == (3,) diff --git a/src/sas/sascalc/shape2sas/Shape2SAS.py b/src/sas/sascalc/shape2sas/Shape2SAS.py index 3e956ae82a..4ce0a337c3 100644 --- a/src/sas/sascalc/shape2sas/Shape2SAS.py +++ b/src/sas/sascalc/shape2sas/Shape2SAS.py @@ -7,11 +7,16 @@ from sas.sascalc.shape2sas.ExperimentalScattering import SimulateScattering, getSimulatedScattering from sas.sascalc.shape2sas.HelperFunctions import generate_pdb, plot_2D, plot_results -from sas.sascalc.shape2sas.Models import Qsampling, ModelProfile, SimulationParameters, getPointDistribution +from sas.sascalc.shape2sas.Models import ModelProfile, Qsampling, SimulationParameters, getPointDistribution from sas.sascalc.shape2sas.StructureFactor import StructureFactor -from sas.sascalc.shape2sas.TheoreticalScattering import \ - TheoreticalScatteringCalculation, ModelSystem, ITheoretical, WeightedPairDistribution, \ - getTheoreticalScattering, getTheoreticalHistogram +from sas.sascalc.shape2sas.TheoreticalScattering import ( + ITheoretical, + ModelSystem, + TheoreticalScatteringCalculation, + WeightedPairDistribution, + getTheoreticalHistogram, + getTheoreticalScattering, +) ################################ Shape2SAS batch version ################################ if __name__ == "__main__": diff --git a/src/sas/sascalc/shape2sas/StructureFactor.py b/src/sas/sascalc/shape2sas/StructureFactor.py index 2cab24d9cb..cb7d49bf28 100644 --- a/src/sas/sascalc/shape2sas/StructureFactor.py +++ b/src/sas/sascalc/shape2sas/StructureFactor.py @@ -1,7 +1,7 @@ import numpy as np -from sas.sascalc.shape2sas.structure_factors import HardSphereStructure, Aggregation, NoStructure +from sas.sascalc.shape2sas.structure_factors import Aggregation, HardSphereStructure, NoStructure class StructureFactor: diff --git a/src/sas/sascalc/shape2sas/models/Cube.py b/src/sas/sascalc/shape2sas/models/Cube.py index 3afd3bc410..69807c0ddc 100644 --- a/src/sas/sascalc/shape2sas/models/Cube.py +++ b/src/sas/sascalc/shape2sas/models/Cube.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D diff --git a/src/sas/sascalc/shape2sas/models/Cuboid.py b/src/sas/sascalc/shape2sas/models/Cuboid.py index d9beb316f7..53fe9adef6 100644 --- a/src/sas/sascalc/shape2sas/models/Cuboid.py +++ b/src/sas/sascalc/shape2sas/models/Cuboid.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D diff --git a/src/sas/sascalc/shape2sas/models/Cylinder.py b/src/sas/sascalc/shape2sas/models/Cylinder.py index 4b6a07ef66..76ee96f95c 100644 --- a/src/sas/sascalc/shape2sas/models/Cylinder.py +++ b/src/sas/sascalc/shape2sas/models/Cylinder.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D diff --git a/src/sas/sascalc/shape2sas/models/CylinderRing.py b/src/sas/sascalc/shape2sas/models/CylinderRing.py index 36935eee56..245992a91f 100644 --- a/src/sas/sascalc/shape2sas/models/CylinderRing.py +++ b/src/sas/sascalc/shape2sas/models/CylinderRing.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D diff --git a/src/sas/sascalc/shape2sas/models/Ellipsoid.py b/src/sas/sascalc/shape2sas/models/Ellipsoid.py index 8e5368fe01..0513650887 100644 --- a/src/sas/sascalc/shape2sas/models/Ellipsoid.py +++ b/src/sas/sascalc/shape2sas/models/Ellipsoid.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D diff --git a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py index dd52d199f3..772f27dfe1 100644 --- a/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py +++ b/src/sas/sascalc/shape2sas/models/EllipticalCylinder.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D diff --git a/src/sas/sascalc/shape2sas/models/HollowCube.py b/src/sas/sascalc/shape2sas/models/HollowCube.py index 2a3237d459..d6006d2a74 100644 --- a/src/sas/sascalc/shape2sas/models/HollowCube.py +++ b/src/sas/sascalc/shape2sas/models/HollowCube.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D diff --git a/src/sas/sascalc/shape2sas/models/HollowSphere.py b/src/sas/sascalc/shape2sas/models/HollowSphere.py index 87fada6c34..a9610bb5db 100644 --- a/src/sas/sascalc/shape2sas/models/HollowSphere.py +++ b/src/sas/sascalc/shape2sas/models/HollowSphere.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D diff --git a/src/sas/sascalc/shape2sas/models/Sphere.py b/src/sas/sascalc/shape2sas/models/Sphere.py index c92bae0a9a..a47c2c05c8 100644 --- a/src/sas/sascalc/shape2sas/models/Sphere.py +++ b/src/sas/sascalc/shape2sas/models/Sphere.py @@ -1,4 +1,5 @@ import numpy as np + from sas.sascalc.shape2sas.Typing import Vector3D From a7dd042a503ab57901298088b56c9a2e0e42a782 Mon Sep 17 00:00:00 2001 From: Krelle Date: Fri, 14 Nov 2025 12:05:10 +0100 Subject: [PATCH 36/37] disabled plugin model button --- src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py index c15b9aba3d..01bdc54932 100644 --- a/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py +++ b/src/sas/qtgui/Calculators/Shape2SAS/DesignWindow.py @@ -91,6 +91,9 @@ def __init__(self, parent=None): self.plugin.setEnabled(False) self.modelTabButtonOptions.horizontalLayout_5.insertWidget(1, self.plugin) + self.plugin.setHidden(True) + self.line2.setHidden(True) + #connect buttons self.modelTabButtonOptions.reset.clicked.connect(self.onSubunitTableReset) self.modelTabButtonOptions.closePage.clicked.connect(self.onClickingClose) From 4b2353d1f5a378e26a048b739da7eb35fcc4c510 Mon Sep 17 00:00:00 2001 From: Krelle Date: Fri, 14 Nov 2025 13:20:42 +0100 Subject: [PATCH 37/37] added missing fittingwidget import --- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index b9fcacb39e..d90c8f4b55 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -39,6 +39,7 @@ from sas.sascalc.fit import models from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit from sas.system import HELP_SYSTEM +from sas.system.user import find_plugins_dir TAB_MAGNETISM = 4 TAB_POLY = 3