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