From 3acb1cd9a7874426e7c11b5cccee6d69a717aa1f Mon Sep 17 00:00:00 2001 From: Wojtek Potrzebowski Date: Mon, 20 Apr 2026 11:32:58 +0200 Subject: [PATCH 01/10] Adding a number of Structure Factors --- .../models/composite_polydisperse_cluster.c | 181 ++++++++++++++++++ .../models/composite_polydisperse_cluster.py | 80 ++++++++ sasmodels/models/fractal_aggregate.c | 59 ++++++ sasmodels/models/fractal_aggregate.py | 85 ++++++++ .../models/fractal_aggregate_discrete_chain.c | 81 ++++++++ .../fractal_aggregate_discrete_chain.py | 112 +++++++++++ sasmodels/models/free_rotating_chain.c | 68 +++++++ sasmodels/models/free_rotating_chain.py | 79 ++++++++ sasmodels/models/linear_aggregate.c | 90 +++++++++ sasmodels/models/linear_aggregate.py | 77 ++++++++ sasmodels/models/stabilized_power_law.py | 73 +++++++ 11 files changed, 985 insertions(+) create mode 100644 sasmodels/models/composite_polydisperse_cluster.c create mode 100644 sasmodels/models/composite_polydisperse_cluster.py create mode 100644 sasmodels/models/fractal_aggregate.c create mode 100644 sasmodels/models/fractal_aggregate.py create mode 100644 sasmodels/models/fractal_aggregate_discrete_chain.c create mode 100644 sasmodels/models/fractal_aggregate_discrete_chain.py create mode 100644 sasmodels/models/free_rotating_chain.c create mode 100644 sasmodels/models/free_rotating_chain.py create mode 100644 sasmodels/models/linear_aggregate.c create mode 100644 sasmodels/models/linear_aggregate.py create mode 100644 sasmodels/models/stabilized_power_law.py diff --git a/sasmodels/models/composite_polydisperse_cluster.c b/sasmodels/models/composite_polydisperse_cluster.c new file mode 100644 index 00000000..15844653 --- /dev/null +++ b/sasmodels/models/composite_polydisperse_cluster.c @@ -0,0 +1,181 @@ +#include +#include + +/* GAMMLN ------------------------------------------------------------ */ +double GAMMLN(double xx) +{ + static double cof[6] = { + 76.18009173, -86.50532033, 24.01409822, + -1.231739516, 0.00120858003, -0.0000536382 + }; + double stp = 2.50662827465; + double half = 0.5, one = 1.0, fpf = 5.5; + double x = xx - one; + double tmp = x + fpf; + tmp = (x + half) * log(fabs(tmp)) - tmp; + + double ser = one; + for(int j=0;j<6;j++){ + x += one; + ser += cof[j]/x; + } + return tmp + log(stp * ser); +} + +/* SCHULZ ------------------------------------------------------------ */ +double SCHULZ(double R, double RA, double Z) +{ + double dum = + (Z+1.0)*log(fabs((Z+1.0)/RA)) + + Z*log(fabs(R)) + - (Z+1.0)*R/RA + - GAMMLN(Z+1.0); + + return exp(dum); +} + +/* FI ---------------------------------------------------------------- */ +double FI(double X) +{ + if(X > 0.05) + return 3.0*(sin(X)-X*cos(X)) / (X*X*X); + else + return 1.0 - 0.1*X*X; +} + +/* S_HS -------------------------------------------------------------- */ +double S_HS(double Q, double RHS, double ETA) +{ + double ALN = pow(1.0 - ETA, 4.0); + double AL = pow(1.0 + 2.0*ETA, 2.0) / ALN; + double BE = -6.0 * ETA * pow(1.0 + 0.5*ETA, 2.0) / ALN; + double GA = 0.5 * ETA * AL; + + double AR = 2.0 * RHS * Q; + double GG; + + if (AR < 0.4) + { + GG = AL*(1.0/3.0 - AR*AR/30.0) + + BE*(1.0/4.0 - AR*AR/36.0) + + GA*(1.0/6.0 - AR*AR/48.0); + } + else + { + double SA = sin(AR); + double CA = cos(AR); + + GG = AL*(SA - AR*CA)/pow(AR,3.0) + + BE*(2.0*AR*SA + (2.0 - AR*AR)*CA - 2.0)/pow(AR,4.0) + + GA*(-pow(AR,4.0)*CA + + 4.0*((3.0*AR*AR - 6.0)*CA + + (pow(AR,3.0) - 6.0*AR)*SA + 6.0)) / pow(AR,6.0); + } + + return 1.0 / (1.0 + 24.0*ETA*GG); +} +/* PSCHULZ ----------------------------------------------------------- */ +double PSCHULZ(double Q, double RAV, double SIGMA) +{ + double Z = 1.0/(SIGMA*SIGMA) - 1.0; + double pi = acos(-1.0); + double FIPI3 = 4.0*pi/3.0; + + double ALP = (Z+1.0)/(Q*RAV); + double ALP1 = (Q*RAV)/(Z+1.0); + double ATTA = atan(2.0*ALP1); + + double PSI1 = (Z+1.0)*ATTA; + double PSI2 = (Z+2.0)*ATTA; + double PSI3 = (Z+3.0)*ATTA; + + double ALP2 = ALP*ALP; + double ALP2_4 = 1.0/(4.0 + ALP2); + + double C1 = pow(ALP2*ALP2_4,0.5*Z+0.5)*pow(ALP/(Z+1.0),6.0); + double T1 = pow(ALP/(Z+1.0),6.0) - C1*cos(PSI1); + + double C2 = pow(ALP2*ALP2_4,0.5*Z+1.5)*pow(ALP/(Z+1.0),4.0); + double T2 = (Z+2.0)/(Z+1.0)*( pow(ALP/(Z+1.0),4.0) + C2*cos(PSI3) ); + + double C3 = pow(ALP2*ALP2_4,0.5*Z+1.0)*pow(ALP/(Z+1.0),5.0); + double T3 = -2.0*C3*sin(PSI2); + + double V2 = pow(FIPI3,2.0) *(Z+6.0)*(Z+5.0)*(Z+4.0)* + (Z+3.0)*(Z+2.0)/pow(Z+1.0,5.0); + + return (8.0*pi*pi*(T1+T2+T3))/V2; +} + +/* P_POLY ------------------------------------------------------------ */ +double P_POLY(double Q, double RL, double SIGL, double d, double N_agg) +{ + double PI43 = 4.1887892; + double RS = 0.5 * d; + double RDIF = RL - RS; + + double Z = 1.0/(SIGL*SIGL) - 1.0; + + double SVL = PI43*pow(RDIF,3.0)*(Z+3.0)*(Z+2.0)/((Z+1.0)*(Z+1.0)); + double SVL2 = PI43*PI43 * pow(RDIF,6.0) + * (Z+6.0)*(Z+5.0)*(Z+4.0)*(Z+3.0)*(Z+2.0) + / pow(Z+1.0,5.0); + + double VS = PI43*pow(RS,3.0); + double ETA = N_agg * (SVL * VS)/SVL2; + double ALPP = ETA/VS; + + + + int NPOI = 100; + double RMAX = RS + 6.0*RL*SIGL; + double DELR = RMAX/NPOI; + + double SSQ = 0.0; + double SSQN= 0.0; + + for(int i=1;i<=NPOI;i++) + { + double R = RS + (i-0.5)*DELR; + double RD = R - RS; + double D = SCHULZ(RD, RDIF, Z); + + double RH = R - 2.0*RS; + if(RH < 0) RH = 0.0; + + double FR = (pow(RD,3.0) - pow(RH,3.0))/pow(RD,3.0); + if(FR < 0.0) FR = 0.0; + if(FR > 1.0) FR = 1.0; + + double ETAEFF = ETA*(FR*0.5 + (1.0-FR)); + + SSQ += D * pow(RD,3.0) * S_HS(Q,RS,ETAEFF); + SSQN += D * pow(RD,3.0) + 1.0e-10; + } + + SSQ = SSQ/SSQN; + + double PS = VS*VS * FI(Q*RS)*FI(Q*RS); + double PLAV = PSCHULZ(Q, RDIF, SIGL); + + double RN = SVL * ALPP; + double RNX = sqrt(SVL2)*ALPP; + + return (RN*PS*SSQ + PS*RNX*(RNX-1.0)*PLAV)/(RN*PS); +} + +/* ----------- SASVIEW ENTRY POINT ----------- */ +double SQ_COMP(double Q, double RL, double SIGL, double d, double N_agg) +{ + return P_POLY(Q, RL, SIGL, d, N_agg); +} + +/* SasView required function */ +double Iq(double Q, double R_clust, double sig_rel_R, double dist_points, double N_agg) +{ + double intensity; + + intensity = SQ_COMP(Q, R_clust, sig_rel_R, dist_points, N_agg); + + return intensity; +} \ No newline at end of file diff --git a/sasmodels/models/composite_polydisperse_cluster.py b/sasmodels/models/composite_polydisperse_cluster.py new file mode 100644 index 00000000..6ecf1c02 --- /dev/null +++ b/sasmodels/models/composite_polydisperse_cluster.py @@ -0,0 +1,80 @@ +r""" +Definition +---------- +Structure factor for a polydisperse spherical cluster with internal correlations.. + +WARNING: Should not be used in combination with very anisotrpic particle shapes. + +Polydispersity described by Schulz distribution +and expressions taken from Pedersen, J. S., Møller, T. L., Raak, N., & Corredig, +M. (2022). A model on an absolute scale for the small-angle X-ray scattering +from bovine casein micelles. Soft Matter, 18(45), 8613-8625. + +The implemented expressions are described in +Pedersen, J. S., Møller, T. L., & Corredig, M. (2026). Scattering from 'Babinet 'particles (or not…): +spherical particles made up of spheres and spherical particles with s +pherical voids. Applied Crystallography, 59(1). + +Modifications have been done for adapting to monodisperse distance between +points in cluster and for allowing weight averrage aggregation number to be a fit paramter. + +See also +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. + + +Parameters: +R_clust : mean radius of large clusters +sig_rel_R : relative polydispersity of R_cluster +dist_points : minimum distance between scatterers +N_agg : weight average aggregation number + +References +---------- + +# Pedersen, J. S., Møller, T. L., & Corredig, M. (2026). Scattering from 'Babinet 'particles (or not…): +spherical particles made up of spheres and spherical particles with s +pherical voids. Applied Crystallography, 59(1). + +Authorship and Verification +---------------------------- + +* **Author:** Jan Skov Pedersen +* **Last Modified by:** Jan Skov Pedersen, April 12, 2026 +* **Last Reviewed by:** **Date:** +""" + + +import numpy as np + +name = "composite_polydisperse_cluster" +title = "Composite Polydisperse Cluster Structure Factor" +description = """ +Structure factor for a polydisperse spherical polydisperse cluster +with internal correlations. +""" + +category = "structure-factor" + +# Must match C kernel signature: Iq(Q, RL, SIGL, RS, ETA) +parameters = [ + ["R_clust", "Ang", 40.0, [0.0, np.inf], "", "Average cluster radius"], + ["sig_rel_R", "", 0.4, [0.0, 1.0], "", "Relative size polydispersity"], + ["dist_points", "Ang", 20.0, [0.0, np.inf], "", "Minimum distance between scatterers"], + ["N_agg", "", 50, [10, 100], "", "Weight average aggregation number"] +] + +source = ["composite_polydisperse_cluster.c"] + +def random(): + import random + return { + "R_clust": random.uniform(50, 200), + "sig_rel_R": random.uniform(0.01, 0.3), + "dist_points": random.uniform(5, 30), + "N_agg": random.uniform(10, 1000) + } + +def test(): + print("composite_polydisperse_cluster model loaded successfully.") \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate.c b/sasmodels/models/fractal_aggregate.c new file mode 100644 index 00000000..43862050 --- /dev/null +++ b/sasmodels/models/fractal_aggregate.c @@ -0,0 +1,59 @@ +#include + +/* + * Fractal structure factor S(q) + * Normalization: S(0) = N + * + * Parameters: + * q : scattering vector + * dist_points : distance between scatterers + * D_ract : fractal dimension + * N_agg : number of particles in cluster + */ + +static double fractal_sq_N(double q, double r, double D, double N) +{ + const double eps = 1e-12; + + /* Special cases */ + if (N <= 1.0) return 1.0; /* no clustering */ + if (D <= 0.0) return 1.0; + + /* q ? 0 limit */ + if (fabs(q) < eps) { + return N; + } + + const double Dm1 = D - 1.0; + + /* Avoid singularity at D = 1 */ + if (fabs(Dm1) < 1e-8) { + /* Limit D ? 1: + S(q) = 1 + (N-1)/N * atan(q*r*N)/(q*r) + */ + double x = q * r; + double y = x * N; /* since N^(1/D) ? N when D?1 */ + return 1.0 + (N-1.0)/N * atan(y)/(x); + } + + /* General case */ + double x = q * r; + double Nd = pow(N, 1.0/D); + double y = x * Nd; /* = q*r*N^(1/D) */ + + double term = + sin(Dm1 * atan(y)) + * pow(x, -D) + * pow(1.0 + 1.0/(y*y), -0.5*Dm1); + + double prefactor = (N - 1.0) / (N * Dm1); + + return 1.0 + prefactor * term; +} + +/* ========== Sasmodels interface ========== */ +double Iq(double q, double dist_points, double D_fract, double N_agg) +{ + double r=0.5 * dist_points; + return fractal_sq_N(q, r, D_fract, N_agg); +} \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate.py b/sasmodels/models/fractal_aggregate.py new file mode 100644 index 00000000..3e3aa1b8 --- /dev/null +++ b/sasmodels/models/fractal_aggregate.py @@ -0,0 +1,85 @@ +# fractal_aggregate.py +# Sasmodels plugin wrapper for the fractal structure factor +# +# Matches the format and syntax of free_rotating_chain.py +r""" +Definition +---------- +This model calculates the structure factor of a fractal-like aggregates. + +WARNING: Should not be used in combination with very anisotrpic particle shapes. + +It uses the following equation: + +.. math:: + + S(q) &= 1 + \frac{D_f\ \Gamma\!(D_f-1)}{[1+1/(q \xi)^2\ ]^{(D_f -1)/2}} + \frac{\sin[(D_f-1) \tan^{-1}(q \xi) ]}{(q R_0)^{D_f}} + +where $\xi$ is the correlation length representing the cluster size and $D_f$ +is the fractal dimension, representing the self similarity of the structure. + +The expresion has been reformulated so that the aggregation number $N_{agg}$ is a fit paramter in stead of $\xi$ +Parameters +---------- +d : distance between adjacent scatterers +D_fract : fractal dimension +N_agg : number of particles in the fractal cluster + +See also +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. + + + +Validation +---------- + +Translated FORTRAN code + +References +---------- + +# Teixeira, J. (1988). Small-angle scattering by fractal systems. Applied Crystallography, 21(6), 781-785. + +Authorship and Verification +---------------------------- + +* **Author:** Jan Skov Pedersen +* **Last Modified by:** Jan Skov Pedersen, April 12, 2026 +* **Last Reviewed by:** Reviewer Name Here **Date:** +""" + + + +import numpy as np + +name = "fractal_aggregate" +title = "Fractal aggregate structure factor" +description = """ +Fractal structure factor S(q) +""" + +category = "structure-factor" + +# Must match C kernel signature: Iq(q, dist_points, D_fract, N_agg) +parameters = [ + ["dist_points", "Ang", 20.0, [0.0, np.inf], "", "distance between scatterers"], + ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], + ["N_agg", "", 50.0, [1.0, np.inf], "", "Number of particles"], +] + +# Kernel source +source = ["fractal_aggregate.c"] + +def random(): + import random + return { + "dist_popints": random.uniform(5.0, 50.0), + "D_fract": random.uniform(1.1, 2.9), + "N_agg": random.uniform(5.0, 300.0), + } + +def test(): + print("fractal_aggregate plugin loaded correctly.") \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.c b/sasmodels/models/fractal_aggregate_discrete_chain.c new file mode 100644 index 00000000..704a35ef --- /dev/null +++ b/sasmodels/models/fractal_aggregate_discrete_chain.c @@ -0,0 +1,81 @@ +#include + +/* + * Fractal structure factor S(q) + * + * Discrete?chain baseline using d directly. + * + * High?q limit: + * S(q) ? 1 + 2*sin(q d)/(q d) + 2*(sin(q d)/(q d))^2 + * + * Low?q crossover constant: C ? 4.2 + * + * Normalization: S(0) = N + * + * Parameters: + * q : scattering vector + * d : distance between scatterers + * D : fractal dimension + * N : number of particles + */ + +static double fractal_sq_N(double q, double d, double D, double N) +{ + const double eps = 1e-12; + const double C = 5.2; + + /* Trivial cases */ + if (N <= 1.0) return 1.0; + if (D <= 0.0) return 1.0; + + /* q ? 0 limit */ + if (fabs(q) < eps) { + return N; + } + + /* ---------- discrete baseline term ---------- */ + + double xd = q * d; + double A; + + if (fabs(xd) < 1e-8) + A = 1.0; + else + A = sin(xd)/xd; + + double f = 1.0 - exp(-pow(xd / C, 4.0)); + + double S_base = 1.0 + (2.0*A + 2.0*A*A) * f; + + /* ---------- fractal correction ---------- */ + + const double Dm1 = D - 1.0; + + /* D ? 1 limit */ + if (fabs(Dm1) < 1e-8) { + double x = q * (d/2.0); /* effective radius equivalent */ + double y = x * N; + return S_base + (N - 1.0)/N * atan(y)/x; + } + + /* General fractal contribution */ + double x = q * (d/2.0); /* consistent with original scaling */ + double Nd = pow(N, 1.0/D); + double y = x * Nd; + + double term = + sin(Dm1 * atan(y)) * + pow(x, -D) * + pow(1.0 + 1.0/(y*y), -0.5*Dm1); + + double prefactor = (N - 1.0)/(N * Dm1); + + return S_base + prefactor * term; +} + +/* ---------------- sasmodels interface ---------------- */ + +double Iq(double q, double dist_points, double D_fract, double N_agg) +{ + return fractal_sq_N(q, dist_points, D_fract, N_agg); +} \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.py b/sasmodels/models/fractal_aggregate_discrete_chain.py new file mode 100644 index 00000000..6bdcd511 --- /dev/null +++ b/sasmodels/models/fractal_aggregate_discrete_chain.py @@ -0,0 +1,112 @@ +r""" +Fractal Structure Factor S(q) +============================= + +Fractal structure factor :math:`S(q)` using a discrete-chain high-q baseline +and a low-q crossover. + +WARNING: Should not be used in combination with very anisotrpic particle shapes. + +This model calculates the structure factor of fractal-like aggregates +according to the following equation: + +.. math:: + + S(q) = 1 + + \frac{D_f\,\Gamma(D_f - 1)} + {\left[1 + 1/(q \xi)^2\right]^{(D_f - 1)/2}} + \frac{\sin\!\left[(D_f - 1)\tan^{-1}(q \xi)\right]} + {(q R_0)^{D_f}} + +Here, :math:`\xi` is the correlation length representing the cluster size, +and :math:`D_f` is the fractal dimension characterizing the internal +self-similarity of the structure. + +The expression has been reformulated so that the aggregation number +:math:`N_{\mathrm{agg}}` is used as a fit parameter instead of :math:`\xi`. + +High-q Baseline Modification +---------------------------- + +The high-q limit is modified to simulate local point correlations, as in a +random-flight model. +The term ``1`` in the expression for :math:`S(q)` is replaced by: + +.. math:: + + S(q) = 1 + + ( 2\frac{\sin(q d)}{q d} + + 2(\frac{\sin(q d)}{q d})^2 ) + \left( 1 - e^{-(q d / C)^4} \right) + +The low-q crossover constant is :math:`C = 5.2`. + +See also +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. + + +Parameters +---------- + +``d`` + Distance between adjacent scatterers. + +``D_fract`` + Fractal dimension :math:`D_f`. + +``N_agg`` + Number of particles in the fractal cluster. + +Validation +---------- + +Translated from original FORTRAN code. + +References +---------- + +* Teixeira, J. (1988). *Small-angle scattering by fractal systems*. + **Journal of Applied Crystallography**, 21(6), 781–785. + +Authorship and Verification +--------------------------- + +* **Author:** Jan Skov Pedersen +* **Last Modified by:** Jan Skov Pedersen, April 12, 2026 +* **Last Reviewed by:** Reviewer Name Here (Date) +""" + +# fractal_aggregate_discrete_chain.py +# Sasmodels plugin wrapper for fractal S(q) using inter-scatterer distance d. + +import numpy as np + +name = "fractal_aggregate_discrete_chain" +title = "Fractal aggregate with discrete-chain baseline" +description = """ +Fractal Structure Factor S(q) with local correlation between points +""" + +category = "structure-factor" + +# Must match C signature: Iq(q, d, D, N) +parameters = [ + ["dist_points", "Ang", 20.0, [0.0, np.inf], "", "Inter-scatterer distance"], + ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], + ["N_agg", "", 50.0, [1.0, np.inf], "", "Number of particles in cluster"] +] + +source = ["fractal_aggregate_discrete_chain.c"] + +def random(): + import random + return { + "d_points": random.uniform(10.0, 80.0), + "D_fract": random.uniform(1.1, 2.9), + "N_agg": random.uniform(5.0, 300.0) + } + +def test(): + print("fractal_aggregate_discrete_chain plugin loaded correctly.") \ No newline at end of file diff --git a/sasmodels/models/free_rotating_chain.c b/sasmodels/models/free_rotating_chain.c new file mode 100644 index 00000000..208b16d9 --- /dev/null +++ b/sasmodels/models/free_rotating_chain.c @@ -0,0 +1,68 @@ +#include + +/* + * free_rotating_chain + * + * Computes SQ free rotating points: + * + * Burchard, W., & Kajiwara, K. (1970). The statistics of stiff chain molecules I. + * The particle scattering factor. + * Proceedings of the Royal Society of London. A. Mathematical and Physical Sciences, + * 316(1525), 185-199. + * + * Inputs: + * Q - momentum transfer + * RN - real number of rotating points + * d - separation between points (fit parameter) + * + * Output: + * Returns free_rotating_chain(Q, RN, d) + * + * Notes: + * This routine is a direct translation of the original Fortran code by Jan Skov Pedersen. + * All variables are treated as real (double precision). + */ + +double Iq(double Q, double N_agg, double dist_points) +{ + int N_SPH, N_SPH1; + double W; + double ARG; + double SN, SN1; + double x; + double intensity; + + double RN = N_agg; + double d = dist_points; + + /* integer and fractional parts of RN */ + N_SPH = (int)fabs(RN); + W = fabs(RN) - (double)N_SPH; + + /* scaled argument Q*d */ + x = Q * d; + + /* sinc kernel */ + ARG = sin(x) / x; + + /* FOR N */ + SN = ((double)N_SPH) / (1.0 - ARG) + - ((double)N_SPH) / 2.0 + - (1.0 - pow(ARG, N_SPH)) / pow(1.0 - ARG, 2.0) * ARG; + + SN = SN * 2.0 / (double)N_SPH; + + /* FOR N+1 */ + N_SPH1 = N_SPH + 1; + + SN1 = ((double)N_SPH1) / (1.0 - ARG) + - ((double)N_SPH1) / 2.0 + - (1.0 - pow(ARG, N_SPH1)) / pow(1.0 - ARG, 2.0) * ARG; + + SN1 = SN1 * 2.0 / (double)N_SPH1; + + /* linear interpolation */ + intensity = (1.0 - W) * SN + W * SN1; + + return intensity; +} \ No newline at end of file diff --git a/sasmodels/models/free_rotating_chain.py b/sasmodels/models/free_rotating_chain.py new file mode 100644 index 00000000..f66d1b5a --- /dev/null +++ b/sasmodels/models/free_rotating_chain.py @@ -0,0 +1,79 @@ +r""" +Definition +---------- + +Free‑rotating chain structure factor: Eandom flight. + +WARNING: Should not be used in combination with very anisotrpic particle shapes. + +This model calculates the structure factor of a free‑rotating chain +of scattering points, following the treatment of Burchard and Kajiwara. + +Equations are given in +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. + +The model is more specifically eq. (23,24) in the paper. + +A non‑integer effective number of rotating points is handled by linear +interpolation between integer chain lengths. + +The model evaluates: + +.. math:: + + S(Q) = (1-w)\,S_N(Q) + w\,S_{N+1}(Q) + +where: + +- :math:`N = \lfloor RN \rfloor` +- :math:`w = RN - N` + +and :math:`S_N(Q)` is the Debye sum for a free‑rotating chain. + +References +---------- + +Burchard, W., & Kajiwara, K. (1970). +The statistics of stiff chain molecules I. +The particle scattering factor. +*Proceedings of the Royal Society of London A* **316**, 185–199. + +Authorship and Verification +---------------------------- + +* **Author:** Jan Skov Pedersen +* **Converted to sasmodels kernel by:** — +* **Last Reviewed by:** — **Date:** April 10, 2026 +""" + +import numpy as np + +# Model identification +name = "free_rotating_chain" +title = "Free-rotating chain structure factor" +description = """ +Structure factor of a free-rotating chain (random flight) +""" + +category = "structure-factor" + +# Parameters must match the kernel signature: Iq(Q, RN, d) +parameters = [ + ["N_agg", "", 20.0, [1.0, np.inf], "", "Aggregation number"], + ["dist_points", "Ang", 20.0, [0.0, np.inf], "", "Separation between points"], +] + +# Kernel source +source = ["free_rotating_chain.c"] + +def random(): + import random + return { + "N_agg": random.uniform(1.0, 100.0), + "dist_points": random.uniform(10.0, 50.0), + } + +def test(): + print("Structure factor of free‑rotating chain (free_rotating_chain)") \ No newline at end of file diff --git a/sasmodels/models/linear_aggregate.c b/sasmodels/models/linear_aggregate.c new file mode 100644 index 00000000..c4e9f702 --- /dev/null +++ b/sasmodels/models/linear_aggregate.c @@ -0,0 +1,90 @@ +#include +#include +#include + +/* + * sinx(x) = sin(x)/x with safe x -> 0 limit + */ +static inline double sinx(double x) +{ + if (fabs(x) < 1e-8) + return 1.0; + return sin(x) / x; +} + +/* + * linear_aggregate + * + * Structure factor for a linear aggregate (Debye sum). + * + * Parameters: + * Q - momentum transfer + * N_agg - number of points (integer) + * dist_points - separation between points (fit parameter) + * + * Returns: + * linear_aggregate(Q) + * + * Notes: + * Exits with warning if N_agg > 100 + */ +static double sq_linN(double Q, int N_agg, double dist_points) +{ + int k; + double SUM, SN; + + double d = dist_points; + + if (N_agg <= 1) + return 1.0; + + if (N_agg > 100) { + fprintf(stderr, + "WARNING (linear_aggregate): N_agg = %d exceeds maximum allowed value (100).\n", + N_agg); + exit(EXIT_FAILURE); + } + + SUM = 0.0; + + for (k = 1; k <= N_agg - 1; k++) { + SUM += (double)(N_agg - k) * sinx(Q * d * (double)k); + } + + SN = 1.0 + 2.0 * SUM / (double)N_agg; + + return SN; +} + +/* + * linear_aggregate (non-integer N) + * + * Linear-chain structure factor with non-integer effective length + * handled by linear interpolation. + * + * Parameters: + * Q - momentum transfer + * an - real (non-integer) number of points + * d - separation between points (fit parameter) + * + * Returns: + * linear_aggregate(Q) + */ +double Iq(double Q, double an, double d) +{ + int N; + double w; + double intensity; + + if (an <= 1.0) + return 1.0; + + N = (int)an; + w = an - (double)N; + + intensity = + (1.0 - w) * sq_linN(Q, N, d) + + w * sq_linN(Q, N + 1, d); + + return intensity; +} \ No newline at end of file diff --git a/sasmodels/models/linear_aggregate.py b/sasmodels/models/linear_aggregate.py new file mode 100644 index 00000000..d89993f7 --- /dev/null +++ b/sasmodels/models/linear_aggregate.py @@ -0,0 +1,77 @@ +r""" +Definition +---------- + +Linear aggregate structure factor. + +WARNING: Should not be used in combination with very anisotrpic particle shapes. + +Equations are given in +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. + +The model is more specifically eq. (21,22) in the paper. + +A non‑integer effective number of points is handled by linear +interpolation between integer chain lengths. + +The model evaluates: + +.. math:: + + S(Q) = (1-w)\,S_N(Q) + w\,S_{N+1}(Q) + +where: + +- :math:`N = \lfloor RN \rfloor` +- :math:`w = RN - N` + +and :math:`S_N(Q)` is the Debye sum for the aggregate. + +Validation +---------- + +Translated FORTRAN code + +References +---------- + +# Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. + +Authorship and Verification +---------------------------- + +* **Author:** Jan Skov Pedersen +* **Last Modified by:** Jan Skov Pedersen, April 12, 2026 +* **Last Reviewed by:** Reviewer Name Here **Date:** +""" + +import numpy as np +from numpy import inf + +name = "linear_aggregate" +title = "Linear aggregate structure factor" +description = """\ +Linear aggregate structure factor +""" +category = "structure-factor" + +parameters = [ + ['N_agg', '', 50.0, [0, np.inf], '', 'Aggregation number'], + ['dist_points', 'Ang', 20.0, [0, np.inf], '', 'distance between scatterers'] +] + +source = ["linear_aggregate.c"] + +def random(): + import random + return { + 'N_agg': random.uniform(1, 100), + 'dist_points': random.uniform(10, 50) + } + +def test(): + print('Steucture factor of linear aggregate') diff --git a/sasmodels/models/stabilized_power_law.py b/sasmodels/models/stabilized_power_law.py new file mode 100644 index 00000000..f68a3973 --- /dev/null +++ b/sasmodels/models/stabilized_power_law.py @@ -0,0 +1,73 @@ +r""" +Definition +---------- + +Stabilized power_law structure factor. + +WARNING: Should not be used in combination with very anisotrpic particle shapes. + +It uses rhe equation: + +.. math:: + + S(q) = 1.0 + amp (0.01/q)^pow + +where *amp* is the scale of the power law and *pow* is the exponent. + +See also +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. + +Validation +---------- + + +References +---------- + +#. Jan Skov Pedersen + +Authorship and Verification +---------------------------- + +* **Author:** Jan Skov Pedersen + +* **Last Modified by:** Jan Skov Pedersen, April 12, 2026 +* **Last Reviewed by:** Reviewer Name Here **Date:** Date Here +""" + +from numpy import inf + +name = "stabilized_power_law" +title = "Stabilized power-law structure factor" +description = """\ +I(q) = 1 + amp *(0.01/q)^pow + amp: scale of power law + pow: exponent of power law +""" +category = "structure-factor" +structure_factor = False + +# ["name", "units", default, [lower, upper], "type","description"], +parameters = [["amp", "", 100, [0, inf], "", "scale of power law"], + ["pow", "", 2, [0, 6], "", "exponent of power law "] + ] + +def Iq(q, amp=100, pow=2): + """ + Parameters: + q: input scattering vectors, units 1/Ang + amp: amplitude of power law , default value=2 + pow: exponent of power law , default value=2 + Returns: + S(q): 1D scattering intensity at q, units none + """ + iq = 1.0 + amp * (0.01/q) ** pow + return iq + +# include tests for your model +# tests = [ +# [{'m': 2.0, 'b' : 1.0}, [q1, q2], [expected_Iq1, expected_Iq2]], +# [{'m': 3.0, 'b' : 5.0}, [q3, q4], [expected_Iq3, expected_Iq4]], +# ] From 3feed25760c7e4e21b1254b4f28ded774504a9ab Mon Sep 17 00:00:00 2001 From: Wojtek Potrzebowski Date: Mon, 20 Apr 2026 11:43:38 +0200 Subject: [PATCH 02/10] Fixing ruff issue --- .../models/composite_polydisperse_cluster.py | 24 +++++++++---------- sasmodels/models/fractal_aggregate.py | 14 +++++------ .../fractal_aggregate_discrete_chain.py | 22 ++++++++--------- sasmodels/models/free_rotating_chain.py | 10 ++++---- sasmodels/models/linear_aggregate.py | 17 +++++++------ sasmodels/models/stabilized_power_law.py | 8 +++---- 6 files changed, 47 insertions(+), 48 deletions(-) diff --git a/sasmodels/models/composite_polydisperse_cluster.py b/sasmodels/models/composite_polydisperse_cluster.py index 6ecf1c02..81cc800b 100644 --- a/sasmodels/models/composite_polydisperse_cluster.py +++ b/sasmodels/models/composite_polydisperse_cluster.py @@ -3,15 +3,15 @@ ---------- Structure factor for a polydisperse spherical cluster with internal correlations.. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotrpic particle shapes. Polydispersity described by Schulz distribution -and expressions taken from Pedersen, J. S., Møller, T. L., Raak, N., & Corredig, -M. (2022). A model on an absolute scale for the small-angle X-ray scattering +and expressions taken from Pedersen, J. S., Møller, T. L., Raak, N., & Corredig, +M. (2022). A model on an absolute scale for the small-angle X-ray scattering from bovine casein micelles. Soft Matter, 18(45), 8613-8625. -The implemented expressions are described in -Pedersen, J. S., Møller, T. L., & Corredig, M. (2026). Scattering from 'Babinet 'particles (or not…): +The implemented expressions are described in +Pedersen, J. S., Møller, T. L., & Corredig, M. (2026). Scattering from 'Babinet 'particles (or not…): spherical particles made up of spheres and spherical particles with s pherical voids. Applied Crystallography, 59(1). @@ -19,21 +19,21 @@ points in cluster and for allowing weight averrage aggregation number to be a fit paramter. See also -Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of -structure factors for analysis of small-angle scattering data from +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. Parameters: R_clust : mean radius of large clusters sig_rel_R : relative polydispersity of R_cluster -dist_points : minimum distance between scatterers +dist_points : minimum distance between scatterers N_agg : weight average aggregation number References ---------- -# Pedersen, J. S., Møller, T. L., & Corredig, M. (2026). Scattering from 'Babinet 'particles (or not…): +# Pedersen, J. S., Møller, T. L., & Corredig, M. (2026). Scattering from 'Babinet 'particles (or not…): spherical particles made up of spheres and spherical particles with s pherical voids. Applied Crystallography, 59(1). @@ -42,7 +42,7 @@ * **Author:** Jan Skov Pedersen * **Last Modified by:** Jan Skov Pedersen, April 12, 2026 -* **Last Reviewed by:** **Date:** +* **Last Reviewed by:** **Date:** """ @@ -52,7 +52,7 @@ title = "Composite Polydisperse Cluster Structure Factor" description = """ Structure factor for a polydisperse spherical polydisperse cluster -with internal correlations. +with internal correlations. """ category = "structure-factor" @@ -77,4 +77,4 @@ def random(): } def test(): - print("composite_polydisperse_cluster model loaded successfully.") \ No newline at end of file + print("composite_polydisperse_cluster model loaded successfully.") diff --git a/sasmodels/models/fractal_aggregate.py b/sasmodels/models/fractal_aggregate.py index 3e3aa1b8..099c029c 100644 --- a/sasmodels/models/fractal_aggregate.py +++ b/sasmodels/models/fractal_aggregate.py @@ -7,8 +7,8 @@ ---------- This model calculates the structure factor of a fractal-like aggregates. -WARNING: Should not be used in combination with very anisotrpic particle shapes. - +WARNING: Should not be used in combination with very anisotrpic particle shapes. + It uses the following equation: .. math:: @@ -19,7 +19,7 @@ where $\xi$ is the correlation length representing the cluster size and $D_f$ is the fractal dimension, representing the self similarity of the structure. -The expresion has been reformulated so that the aggregation number $N_{agg}$ is a fit paramter in stead of $\xi$ +The expresion has been reformulated so that the aggregation number $N_{agg}$ is a fit paramter in stead of $\xi$ Parameters ---------- d : distance between adjacent scatterers @@ -27,8 +27,8 @@ N_agg : number of particles in the fractal cluster See also -Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of -structure factors for analysis of small-angle scattering data from +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. @@ -48,7 +48,7 @@ * **Author:** Jan Skov Pedersen * **Last Modified by:** Jan Skov Pedersen, April 12, 2026 -* **Last Reviewed by:** Reviewer Name Here **Date:** +* **Last Reviewed by:** Reviewer Name Here **Date:** """ @@ -82,4 +82,4 @@ def random(): } def test(): - print("fractal_aggregate plugin loaded correctly.") \ No newline at end of file + print("fractal_aggregate plugin loaded correctly.") diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.py b/sasmodels/models/fractal_aggregate_discrete_chain.py index 6bdcd511..1cc8a4ff 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.py +++ b/sasmodels/models/fractal_aggregate_discrete_chain.py @@ -5,7 +5,7 @@ Fractal structure factor :math:`S(q)` using a discrete-chain high-q baseline and a low-q crossover. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotrpic particle shapes. This model calculates the structure factor of fractal-like aggregates according to the following equation: @@ -29,7 +29,7 @@ ---------------------------- The high-q limit is modified to simulate local point correlations, as in a -random-flight model. +random-flight model. The term ``1`` in the expression for :math:`S(q)` is replaced by: .. math:: @@ -42,21 +42,21 @@ The low-q crossover constant is :math:`C = 5.2`. See also -Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of -structure factors for analysis of small-angle scattering data from +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. Parameters ---------- -``d`` +``d`` Distance between adjacent scatterers. -``D_fract`` +``D_fract`` Fractal dimension :math:`D_f`. -``N_agg`` +``N_agg`` Number of particles in the fractal cluster. Validation @@ -67,14 +67,14 @@ References ---------- -* Teixeira, J. (1988). *Small-angle scattering by fractal systems*. +* Teixeira, J. (1988). *Small-angle scattering by fractal systems*. **Journal of Applied Crystallography**, 21(6), 781–785. Authorship and Verification --------------------------- -* **Author:** Jan Skov Pedersen -* **Last Modified by:** Jan Skov Pedersen, April 12, 2026 +* **Author:** Jan Skov Pedersen +* **Last Modified by:** Jan Skov Pedersen, April 12, 2026 * **Last Reviewed by:** Reviewer Name Here (Date) """ @@ -109,4 +109,4 @@ def random(): } def test(): - print("fractal_aggregate_discrete_chain plugin loaded correctly.") \ No newline at end of file + print("fractal_aggregate_discrete_chain plugin loaded correctly.") diff --git a/sasmodels/models/free_rotating_chain.py b/sasmodels/models/free_rotating_chain.py index f66d1b5a..bca9f4ee 100644 --- a/sasmodels/models/free_rotating_chain.py +++ b/sasmodels/models/free_rotating_chain.py @@ -4,14 +4,14 @@ Free‑rotating chain structure factor: Eandom flight. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotrpic particle shapes. This model calculates the structure factor of a free‑rotating chain of scattering points, following the treatment of Burchard and Kajiwara. -Equations are given in -Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of -structure factors for analysis of small-angle scattering data from +Equations are given in +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. The model is more specifically eq. (23,24) in the paper. @@ -76,4 +76,4 @@ def random(): } def test(): - print("Structure factor of free‑rotating chain (free_rotating_chain)") \ No newline at end of file + print("Structure factor of free‑rotating chain (free_rotating_chain)") diff --git a/sasmodels/models/linear_aggregate.py b/sasmodels/models/linear_aggregate.py index d89993f7..a7094634 100644 --- a/sasmodels/models/linear_aggregate.py +++ b/sasmodels/models/linear_aggregate.py @@ -2,13 +2,13 @@ Definition ---------- -Linear aggregate structure factor. +Linear aggregate structure factor. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotrpic particle shapes. -Equations are given in -Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of -structure factors for analysis of small-angle scattering data from +Equations are given in +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. The model is more specifically eq. (21,22) in the paper. @@ -37,8 +37,8 @@ References ---------- -# Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of -structure factors for analysis of small-angle scattering data from +# Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. Authorship and Verification @@ -46,11 +46,10 @@ * **Author:** Jan Skov Pedersen * **Last Modified by:** Jan Skov Pedersen, April 12, 2026 -* **Last Reviewed by:** Reviewer Name Here **Date:** +* **Last Reviewed by:** Reviewer Name Here **Date:** """ import numpy as np -from numpy import inf name = "linear_aggregate" title = "Linear aggregate structure factor" diff --git a/sasmodels/models/stabilized_power_law.py b/sasmodels/models/stabilized_power_law.py index f68a3973..40f20672 100644 --- a/sasmodels/models/stabilized_power_law.py +++ b/sasmodels/models/stabilized_power_law.py @@ -4,7 +4,7 @@ Stabilized power_law structure factor. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotrpic particle shapes. It uses rhe equation: @@ -15,8 +15,8 @@ where *amp* is the scale of the power law and *pow* is the exponent. See also -Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of -structure factors for analysis of small-angle scattering data from +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. Validation @@ -44,7 +44,7 @@ description = """\ I(q) = 1 + amp *(0.01/q)^pow amp: scale of power law - pow: exponent of power law + pow: exponent of power law """ category = "structure-factor" structure_factor = False From 08bdc2e28bbbca338367991eb0d7709d8667d1f7 Mon Sep 17 00:00:00 2001 From: Wojtek Potrzebowski Date: Mon, 20 Apr 2026 21:43:09 +0200 Subject: [PATCH 03/10] Fixing names --- ...erse_cluster.c => compact_polydisperse_cluster.c} | 6 +++--- ...se_cluster.py => compact_polydisperse_cluster.py} | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) rename sasmodels/models/{composite_polydisperse_cluster.c => compact_polydisperse_cluster.c} (96%) rename sasmodels/models/{composite_polydisperse_cluster.py => compact_polydisperse_cluster.py} (87%) diff --git a/sasmodels/models/composite_polydisperse_cluster.c b/sasmodels/models/compact_polydisperse_cluster.c similarity index 96% rename from sasmodels/models/composite_polydisperse_cluster.c rename to sasmodels/models/compact_polydisperse_cluster.c index 15844653..2856e7ad 100644 --- a/sasmodels/models/composite_polydisperse_cluster.c +++ b/sasmodels/models/compact_polydisperse_cluster.c @@ -165,7 +165,7 @@ double P_POLY(double Q, double RL, double SIGL, double d, double N_agg) } /* ----------- SASVIEW ENTRY POINT ----------- */ -double SQ_COMP(double Q, double RL, double SIGL, double d, double N_agg) +double SQ_COMPACT(double Q, double RL, double SIGL, double d, double N_agg) { return P_POLY(Q, RL, SIGL, d, N_agg); } @@ -175,7 +175,7 @@ double Iq(double Q, double R_clust, double sig_rel_R, double dist_points, double { double intensity; - intensity = SQ_COMP(Q, R_clust, sig_rel_R, dist_points, N_agg); + intensity = SQ_COMPACT(Q, R_clust, sig_rel_R, dist_points, N_agg); return intensity; -} \ No newline at end of file +} diff --git a/sasmodels/models/composite_polydisperse_cluster.py b/sasmodels/models/compact_polydisperse_cluster.py similarity index 87% rename from sasmodels/models/composite_polydisperse_cluster.py rename to sasmodels/models/compact_polydisperse_cluster.py index 81cc800b..e42fa451 100644 --- a/sasmodels/models/composite_polydisperse_cluster.py +++ b/sasmodels/models/compact_polydisperse_cluster.py @@ -48,16 +48,16 @@ import numpy as np -name = "composite_polydisperse_cluster" -title = "Composite Polydisperse Cluster Structure Factor" +name = "compact_polydisperse_cluster" +title = "Compact Polydisperse Cluster Structure Factor" description = """ -Structure factor for a polydisperse spherical polydisperse cluster +Structure factor for a compact polydisperse cluster with internal correlations. """ category = "structure-factor" -# Must match C kernel signature: Iq(Q, RL, SIGL, RS, ETA) +# Must match C kernel signature: Iq(Q, R_clust, sig_rel_R, dist_points, N_agg) parameters = [ ["R_clust", "Ang", 40.0, [0.0, np.inf], "", "Average cluster radius"], ["sig_rel_R", "", 0.4, [0.0, 1.0], "", "Relative size polydispersity"], @@ -65,7 +65,7 @@ ["N_agg", "", 50, [10, 100], "", "Weight average aggregation number"] ] -source = ["composite_polydisperse_cluster.c"] +source = ["compact_polydisperse_cluster.c"] def random(): import random @@ -77,4 +77,4 @@ def random(): } def test(): - print("composite_polydisperse_cluster model loaded successfully.") + print("compact_polydisperse_cluster model loaded successfully.") From 7613e5fa22b500417a15156249590b9880863db7 Mon Sep 17 00:00:00 2001 From: Wojtek Potrzebowski Date: Mon, 20 Apr 2026 21:56:32 +0200 Subject: [PATCH 04/10] Fixing math formating --- sasmodels/models/fractal_aggregate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sasmodels/models/fractal_aggregate.py b/sasmodels/models/fractal_aggregate.py index 099c029c..ab8db190 100644 --- a/sasmodels/models/fractal_aggregate.py +++ b/sasmodels/models/fractal_aggregate.py @@ -13,8 +13,8 @@ .. math:: - S(q) &= 1 + \frac{D_f\ \Gamma\!(D_f-1)}{[1+1/(q \xi)^2\ ]^{(D_f -1)/2}} - \frac{\sin[(D_f-1) \tan^{-1}(q \xi) ]}{(q R_0)^{D_f}} + S(q) = 1 + \frac{D_f\, \Gamma(D_f-1)}{\left[1+1/(q\xi)^2\right]^{(D_f-1)/2}} + \frac{\sin\left[(D_f-1)\tan^{-1}(q\xi)\right]}{(q R_0)^{D_f}} where $\xi$ is the correlation length representing the cluster size and $D_f$ is the fractal dimension, representing the self similarity of the structure. From 63c857fe015fdeed4cd0d016544c233798145923 Mon Sep 17 00:00:00 2001 From: Wojtek Potrzebowski Date: Wed, 22 Apr 2026 19:10:38 +0200 Subject: [PATCH 05/10] Fixing params names --- .../models/compact_polydisperse_cluster.c | 10 ++-- .../models/compact_polydisperse_cluster.py | 40 +++++++++------ sasmodels/models/fractal_aggregate.c | 13 +++-- sasmodels/models/fractal_aggregate.py | 33 +++++++++--- .../models/fractal_aggregate_discrete_chain.c | 6 ++- .../fractal_aggregate_discrete_chain.py | 40 ++++++++------- sasmodels/models/free_rotating_chain.c | 29 +++++------ sasmodels/models/free_rotating_chain.py | 21 ++++++-- sasmodels/models/linear_aggregate.c | 28 +++++----- sasmodels/models/linear_aggregate.py | 23 ++++++--- sasmodels/models/stabilized_power_law.py | 51 +++++++++---------- 11 files changed, 174 insertions(+), 120 deletions(-) diff --git a/sasmodels/models/compact_polydisperse_cluster.c b/sasmodels/models/compact_polydisperse_cluster.c index 2856e7ad..0bbb61ac 100644 --- a/sasmodels/models/compact_polydisperse_cluster.c +++ b/sasmodels/models/compact_polydisperse_cluster.c @@ -171,11 +171,9 @@ double SQ_COMPACT(double Q, double RL, double SIGL, double d, double N_agg) } /* SasView required function */ -double Iq(double Q, double R_clust, double sig_rel_R, double dist_points, double N_agg) +double Iq(double Q, double radius_effective, double volfraction, + double R_clust, double sig_rel_R, double N_agg) { - double intensity; - - intensity = SQ_COMPACT(Q, R_clust, sig_rel_R, dist_points, N_agg); - - return intensity; + (void)volfraction; + return SQ_COMPACT(Q, R_clust, sig_rel_R, radius_effective, N_agg); } diff --git a/sasmodels/models/compact_polydisperse_cluster.py b/sasmodels/models/compact_polydisperse_cluster.py index e42fa451..65d700a7 100644 --- a/sasmodels/models/compact_polydisperse_cluster.py +++ b/sasmodels/models/compact_polydisperse_cluster.py @@ -1,9 +1,9 @@ r""" Definition ---------- -Structure factor for a polydisperse spherical cluster with internal correlations.. +Structure factor for a polydisperse spherical cluster with internal correlations. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotropic particle shapes. Polydispersity described by Schulz distribution and expressions taken from Pedersen, J. S., Møller, T. L., Raak, N., & Corredig, @@ -16,7 +16,7 @@ pherical voids. Applied Crystallography, 59(1). Modifications have been done for adapting to monodisperse distance between -points in cluster and for allowing weight averrage aggregation number to be a fit paramter. +points in cluster and for allowing weight-average aggregation number to be a fit parameter. See also Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of @@ -24,11 +24,15 @@ desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. -Parameters: -R_clust : mean radius of large clusters +Parameters +---------- +radius_effective : minimum distance between scatterers (Å); first parameter for + :math:`P@S` wiring (see sasmodels structure-factor conventions). +volfraction : unused in this :math:`S(q)`; required as second parameter for + :math:`P@S` products. +R_clust : mean radius of large clusters sig_rel_R : relative polydispersity of R_cluster -dist_points : minimum distance between scatterers -N_agg : weight average aggregation number +N_agg : weight-average aggregation number References ---------- @@ -56,24 +60,30 @@ """ category = "structure-factor" +structure_factor = True -# Must match C kernel signature: Iq(Q, R_clust, sig_rel_R, dist_points, N_agg) +# Must match C: Iq(Q, radius_effective, volfraction, R_clust, sig_rel_R, N_agg) parameters = [ - ["R_clust", "Ang", 40.0, [0.0, np.inf], "", "Average cluster radius"], - ["sig_rel_R", "", 0.4, [0.0, 1.0], "", "Relative size polydispersity"], - ["dist_points", "Ang", 20.0, [0.0, np.inf], "", "Minimum distance between scatterers"], - ["N_agg", "", 50, [10, 100], "", "Weight average aggregation number"] + ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", + "Minimum distance between scatterers"], + ["volfraction", "", 0.2, [0.0, 1.0], "", + "unused in S(q); required for P@S products"], + ["R_clust", "Ang", 40.0, [0.0, np.inf], "", "Average cluster radius"], + ["sig_rel_R", "", 0.4, [0.0, 1.0], "", "Relative size polydispersity"], + ["N_agg", "", 50.0, [10.0, 100.0], "", "Weight-average aggregation number"], ] source = ["compact_polydisperse_cluster.c"] def random(): import random + return { - "R_clust": random.uniform(50, 200), + "radius_effective": random.uniform(5, 30), + "volfraction": random.uniform(0.01, 0.3), + "R_clust": random.uniform(50, 200), "sig_rel_R": random.uniform(0.01, 0.3), - "dist_points": random.uniform(5, 30), - "N_agg": random.uniform(10, 1000) + "N_agg": random.uniform(10, 100), } def test(): diff --git a/sasmodels/models/fractal_aggregate.c b/sasmodels/models/fractal_aggregate.c index 43862050..9a43ace5 100644 --- a/sasmodels/models/fractal_aggregate.c +++ b/sasmodels/models/fractal_aggregate.c @@ -6,9 +6,10 @@ * * Parameters: * q : scattering vector - * dist_points : distance between scatterers - * D_ract : fractal dimension - * N_agg : number of particles in cluster + * radius_effective : distance between scatterers + * volfraction : unused (required for P@S parameter order) + * D_fract : fractal dimension + * N_agg : number of particles in cluster */ static double fractal_sq_N(double q, double r, double D, double N) @@ -52,8 +53,10 @@ static double fractal_sq_N(double q, double r, double D, double N) } /* ========== Sasmodels interface ========== */ -double Iq(double q, double dist_points, double D_fract, double N_agg) +double Iq(double q, double radius_effective, double volfraction, + double D_fract, double N_agg) { - double r=0.5 * dist_points; + (void)volfraction; + double r = 0.5 * radius_effective; return fractal_sq_N(q, r, D_fract, N_agg); } \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate.py b/sasmodels/models/fractal_aggregate.py index ab8db190..04374d02 100644 --- a/sasmodels/models/fractal_aggregate.py +++ b/sasmodels/models/fractal_aggregate.py @@ -7,7 +7,7 @@ ---------- This model calculates the structure factor of a fractal-like aggregates. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotropic particle shapes. It uses the following equation: @@ -19,10 +19,21 @@ where $\xi$ is the correlation length representing the cluster size and $D_f$ is the fractal dimension, representing the self similarity of the structure. -The expresion has been reformulated so that the aggregation number $N_{agg}$ is a fit paramter in stead of $\xi$ +The expression has been reformulated so that the aggregation number $N_{agg}$ +is a fit parameter instead of $\xi$. + +When combined with a polydisperse form factor, simple $P(q)\,S(q)$ with +$S(q)$ evaluated at an effective radius differs from averaging +$P(q,r)\,S(q,r)$ over $r$. For polydisperse primary particles, the +$\beta$-/decoupling approximation (*structure_factor_mode = 1*) is usually +more appropriate. + Parameters ---------- -d : distance between adjacent scatterers +radius_effective : distance between adjacent scatterers (Å); use *unconstrained* + *radius_effective_mode* to fit independently of the form-factor radius. +volfraction : required for $P@S$ products; **not used** in this $S(q)$ + (see ``sasmodels.product`` until optional structure-factor volfraction exists). D_fract : fractal dimension N_agg : number of particles in the fractal cluster @@ -62,12 +73,16 @@ """ category = "structure-factor" +structure_factor = True -# Must match C kernel signature: Iq(q, dist_points, D_fract, N_agg) +# Must match C kernel: Iq(q, radius_effective, volfraction, D_fract, N_agg) parameters = [ - ["dist_points", "Ang", 20.0, [0.0, np.inf], "", "distance between scatterers"], - ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], - ["N_agg", "", 50.0, [1.0, np.inf], "", "Number of particles"], + ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", + "distance between adjacent scatterers"], + ["volfraction", "", 0.2, [0.0, 1.0], "", + "unused in S(q); required as second parameter for P@S products"], + ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], + ["N_agg", "", 50.0, [1.0, np.inf], "", "Number of particles"], ] # Kernel source @@ -75,8 +90,10 @@ def random(): import random + return { - "dist_popints": random.uniform(5.0, 50.0), + "radius_effective": random.uniform(5.0, 50.0), + "volfraction": random.uniform(0.01, 0.3), "D_fract": random.uniform(1.1, 2.9), "N_agg": random.uniform(5.0, 300.0), } diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.c b/sasmodels/models/fractal_aggregate_discrete_chain.c index 704a35ef..38b9208a 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.c +++ b/sasmodels/models/fractal_aggregate_discrete_chain.c @@ -75,7 +75,9 @@ static double fractal_sq_N(double q, double d, double D, double N) /* ---------------- sasmodels interface ---------------- */ -double Iq(double q, double dist_points, double D_fract, double N_agg) +double Iq(double q, double radius_effective, double volfraction, + double D_fract, double N_agg) { - return fractal_sq_N(q, dist_points, D_fract, N_agg); + (void)volfraction; + return fractal_sq_N(q, radius_effective, D_fract, N_agg); } \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.py b/sasmodels/models/fractal_aggregate_discrete_chain.py index 1cc8a4ff..44feef1e 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.py +++ b/sasmodels/models/fractal_aggregate_discrete_chain.py @@ -5,7 +5,7 @@ Fractal structure factor :math:`S(q)` using a discrete-chain high-q baseline and a low-q crossover. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotropic particle shapes. This model calculates the structure factor of fractal-like aggregates according to the following equation: @@ -41,23 +41,20 @@ The low-q crossover constant is :math:`C = 5.2`. -See also -Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of -structure factors for analysis of small-angle scattering data from -desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. - - Parameters ---------- -``d`` - Distance between adjacent scatterers. +radius_effective : inter-scatterer distance :math:`d` (Å); see also + *radius_effective_mode* when combining with a form factor. +volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products. +``D_fract`` : fractal dimension :math:`D_f`. +``N_agg`` : number of particles in the fractal cluster. -``D_fract`` - Fractal dimension :math:`D_f`. +See also +Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. -``N_agg`` - Number of particles in the fractal cluster. Validation ---------- @@ -90,22 +87,27 @@ """ category = "structure-factor" +structure_factor = True -# Must match C signature: Iq(q, d, D, N) +# Must match C: Iq(q, radius_effective, volfraction, D_fract, N_agg) parameters = [ - ["dist_points", "Ang", 20.0, [0.0, np.inf], "", "Inter-scatterer distance"], - ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], - ["N_agg", "", 50.0, [1.0, np.inf], "", "Number of particles in cluster"] + ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", "Inter-scatterer distance"], + ["volfraction", "", 0.2, [0.0, 1.0], "", + "unused in S(q); required for P@S products"], + ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], + ["N_agg", "", 50.0, [1.0, np.inf], "", "Number of particles in cluster"], ] source = ["fractal_aggregate_discrete_chain.c"] def random(): import random + return { - "d_points": random.uniform(10.0, 80.0), + "radius_effective": random.uniform(10.0, 80.0), + "volfraction": random.uniform(0.01, 0.3), "D_fract": random.uniform(1.1, 2.9), - "N_agg": random.uniform(5.0, 300.0) + "N_agg": random.uniform(5.0, 300.0), } def test(): diff --git a/sasmodels/models/free_rotating_chain.c b/sasmodels/models/free_rotating_chain.c index 208b16d9..300f380d 100644 --- a/sasmodels/models/free_rotating_chain.c +++ b/sasmodels/models/free_rotating_chain.c @@ -10,20 +10,14 @@ * Proceedings of the Royal Society of London. A. Mathematical and Physical Sciences, * 316(1525), 185-199. * - * Inputs: + * Sasmodels interface: * Q - momentum transfer - * RN - real number of rotating points - * d - separation between points (fit parameter) - * - * Output: - * Returns free_rotating_chain(Q, RN, d) - * - * Notes: - * This routine is a direct translation of the original Fortran code by Jan Skov Pedersen. - * All variables are treated as real (double precision). + * radius_effective - separation d between points + * volfraction - unused (required for P@S parameter order) + * N_agg - real number of rotating points (RN) */ -double Iq(double Q, double N_agg, double dist_points) +double Iq(double Q, double radius_effective, double volfraction, double N_agg) { int N_SPH, N_SPH1; double W; @@ -32,8 +26,10 @@ double Iq(double Q, double N_agg, double dist_points) double x; double intensity; - double RN = N_agg; - double d = dist_points; + double RN = N_agg; + double d = radius_effective; + + (void)volfraction; /* integer and fractional parts of RN */ N_SPH = (int)fabs(RN); @@ -43,7 +39,10 @@ double Iq(double Q, double N_agg, double dist_points) x = Q * d; /* sinc kernel */ - ARG = sin(x) / x; + if (fabs(x) < 1e-12) + ARG = 1.0; + else + ARG = sin(x) / x; /* FOR N */ SN = ((double)N_SPH) / (1.0 - ARG) @@ -65,4 +64,4 @@ double Iq(double Q, double N_agg, double dist_points) intensity = (1.0 - W) * SN + W * SN1; return intensity; -} \ No newline at end of file +} diff --git a/sasmodels/models/free_rotating_chain.py b/sasmodels/models/free_rotating_chain.py index bca9f4ee..77ec4c48 100644 --- a/sasmodels/models/free_rotating_chain.py +++ b/sasmodels/models/free_rotating_chain.py @@ -2,9 +2,9 @@ Definition ---------- -Free‑rotating chain structure factor: Eandom flight. +Free‑rotating chain structure factor: random flight. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotropic particle shapes. This model calculates the structure factor of a free‑rotating chain of scattering points, following the treatment of Burchard and Kajiwara. @@ -32,6 +32,12 @@ and :math:`S_N(Q)` is the Debye sum for a free‑rotating chain. +Parameters +---------- +radius_effective : separation between chain points :math:`d` (Å). +volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products. +N_agg : aggregation number :math:`RN`. + References ---------- @@ -58,11 +64,14 @@ """ category = "structure-factor" +structure_factor = True -# Parameters must match the kernel signature: Iq(Q, RN, d) +# Must match C: Iq(Q, radius_effective, volfraction, N_agg) parameters = [ + ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", "Separation between points"], + ["volfraction", "", 0.2, [0.0, 1.0], "", + "unused in S(q); required for P@S products"], ["N_agg", "", 20.0, [1.0, np.inf], "", "Aggregation number"], - ["dist_points", "Ang", 20.0, [0.0, np.inf], "", "Separation between points"], ] # Kernel source @@ -70,9 +79,11 @@ def random(): import random + return { + "radius_effective": random.uniform(10.0, 50.0), + "volfraction": random.uniform(0.01, 0.3), "N_agg": random.uniform(1.0, 100.0), - "dist_points": random.uniform(10.0, 50.0), } def test(): diff --git a/sasmodels/models/linear_aggregate.c b/sasmodels/models/linear_aggregate.c index c4e9f702..ad61e217 100644 --- a/sasmodels/models/linear_aggregate.c +++ b/sasmodels/models/linear_aggregate.c @@ -19,8 +19,9 @@ static inline double sinx(double x) * * Parameters: * Q - momentum transfer - * N_agg - number of points (integer) - * dist_points - separation between points (fit parameter) + * radius_effective - separation between points (fit parameter) + * volfraction - unused (required for P@S parameter order) + * N_agg - effective number of points (may be non-integer) * * Returns: * linear_aggregate(Q) @@ -28,12 +29,12 @@ static inline double sinx(double x) * Notes: * Exits with warning if N_agg > 100 */ -static double sq_linN(double Q, int N_agg, double dist_points) +static double sq_linN(double Q, int N_agg, double sep) { int k; double SUM, SN; - double d = dist_points; + double d = sep; if (N_agg <= 1) return 1.0; @@ -64,27 +65,30 @@ static double sq_linN(double Q, int N_agg, double dist_points) * * Parameters: * Q - momentum transfer - * an - real (non-integer) number of points - * d - separation between points (fit parameter) + * radius_effective - separation between points + * volfraction - unused + * N_agg - real (non-integer) number of points * * Returns: * linear_aggregate(Q) */ -double Iq(double Q, double an, double d) +double Iq(double Q, double radius_effective, double volfraction, double N_agg) { int N; double w; double intensity; - if (an <= 1.0) + (void)volfraction; + + if (N_agg <= 1.0) return 1.0; - N = (int)an; - w = an - (double)N; + N = (int)N_agg; + w = N_agg - (double)N; intensity = - (1.0 - w) * sq_linN(Q, N, d) - + w * sq_linN(Q, N + 1, d); + (1.0 - w) * sq_linN(Q, N, radius_effective) + + w * sq_linN(Q, N + 1, radius_effective); return intensity; } \ No newline at end of file diff --git a/sasmodels/models/linear_aggregate.py b/sasmodels/models/linear_aggregate.py index a7094634..48e9023a 100644 --- a/sasmodels/models/linear_aggregate.py +++ b/sasmodels/models/linear_aggregate.py @@ -4,7 +4,7 @@ Linear aggregate structure factor. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotropic particle shapes. Equations are given in Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of @@ -29,6 +29,12 @@ and :math:`S_N(Q)` is the Debye sum for the aggregate. +Parameters +---------- +radius_effective : distance between scatterers on the chain (Å). +volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products. +N_agg : aggregation number. + Validation ---------- @@ -57,20 +63,25 @@ Linear aggregate structure factor """ category = "structure-factor" +structure_factor = True parameters = [ - ['N_agg', '', 50.0, [0, np.inf], '', 'Aggregation number'], - ['dist_points', 'Ang', 20.0, [0, np.inf], '', 'distance between scatterers'] + ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", "distance between scatterers"], + ["volfraction", "", 0.2, [0.0, 1.0], "", + "unused in S(q); required for P@S products"], + ["N_agg", "", 50.0, [0.0, np.inf], "", "Aggregation number"], ] source = ["linear_aggregate.c"] def random(): import random + return { - 'N_agg': random.uniform(1, 100), - 'dist_points': random.uniform(10, 50) + "radius_effective": random.uniform(10, 50), + "volfraction": random.uniform(0.01, 0.3), + "N_agg": random.uniform(1, 100), } def test(): - print('Steucture factor of linear aggregate') + print("Structure factor of linear aggregate") diff --git a/sasmodels/models/stabilized_power_law.py b/sasmodels/models/stabilized_power_law.py index 40f20672..0374d9f2 100644 --- a/sasmodels/models/stabilized_power_law.py +++ b/sasmodels/models/stabilized_power_law.py @@ -2,18 +2,22 @@ Definition ---------- -Stabilized power_law structure factor. +Stabilized power-law structure factor. -WARNING: Should not be used in combination with very anisotrpic particle shapes. +WARNING: Should not be used in combination with very anisotropic particle shapes. -It uses rhe equation: +It uses the equation: .. math:: - S(q) = 1.0 + amp (0.01/q)^pow + S(q) = 1.0 + \mathrm{amp}\,(0.01/q)^{\mathrm{pow}} where *amp* is the scale of the power law and *pow* is the exponent. +The first two parameters follow sasmodels structure-factor conventions for +:math:`P@S` products: *radius_effective* and *volfraction* are **not** used in +the formula above. + See also Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of structure factors for analysis of small-angle scattering data from @@ -42,32 +46,25 @@ name = "stabilized_power_law" title = "Stabilized power-law structure factor" description = """\ -I(q) = 1 + amp *(0.01/q)^pow +S(q) = 1 + amp *(0.01/q)^pow amp: scale of power law pow: exponent of power law """ category = "structure-factor" -structure_factor = False +structure_factor = True # ["name", "units", default, [lower, upper], "type","description"], -parameters = [["amp", "", 100, [0, inf], "", "scale of power law"], - ["pow", "", 2, [0, 6], "", "exponent of power law "] - ] - -def Iq(q, amp=100, pow=2): - """ - Parameters: - q: input scattering vectors, units 1/Ang - amp: amplitude of power law , default value=2 - pow: exponent of power law , default value=2 - Returns: - S(q): 1D scattering intensity at q, units none - """ - iq = 1.0 + amp * (0.01/q) ** pow - return iq - -# include tests for your model -# tests = [ -# [{'m': 2.0, 'b' : 1.0}, [q1, q2], [expected_Iq1, expected_Iq2]], -# [{'m': 3.0, 'b' : 5.0}, [q3, q4], [expected_Iq3, expected_Iq4]], -# ] +parameters = [ + ["radius_effective", "Ang", 50.0, [0.0, inf], "", + "unused in S(q); required for P@S products"], + ["volfraction", "", 0.2, [0.0, 1.0], "", + "unused in S(q); required for P@S products"], + ["amp", "", 100, [0, inf], "", "scale of power law"], + ["pow", "", 2, [0, 6], "", "exponent of power law "], +] + + +def Iq(q, radius_effective, volfraction, amp, pow): + """Return S(q); *radius_effective* and *volfraction* are unused.""" + _ = (radius_effective, volfraction) + return 1.0 + amp * (0.01 / q) ** pow From c1dd9eb64506263d319150418c18db1e91910857 Mon Sep 17 00:00:00 2001 From: Wojtek Potrzebowski Date: Wed, 22 Apr 2026 19:24:52 +0200 Subject: [PATCH 06/10] Fixing doc issues --- sasmodels/models/compact_polydisperse_cluster.py | 6 ++---- sasmodels/models/fractal_aggregate.py | 6 ++---- sasmodels/models/fractal_aggregate_discrete_chain.py | 10 ++++------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/sasmodels/models/compact_polydisperse_cluster.py b/sasmodels/models/compact_polydisperse_cluster.py index 65d700a7..c88d71c8 100644 --- a/sasmodels/models/compact_polydisperse_cluster.py +++ b/sasmodels/models/compact_polydisperse_cluster.py @@ -26,10 +26,8 @@ Parameters ---------- -radius_effective : minimum distance between scatterers (Å); first parameter for - :math:`P@S` wiring (see sasmodels structure-factor conventions). -volfraction : unused in this :math:`S(q)`; required as second parameter for - :math:`P@S` products. +radius_effective : minimum distance between scatterers (Å); first parameter for :math:`P@S` wiring (see sasmodels structure-factor conventions) +volfraction : unused in this :math:`S(q)`; required as second parameter for :math:`P@S` products R_clust : mean radius of large clusters sig_rel_R : relative polydispersity of R_cluster N_agg : weight-average aggregation number diff --git a/sasmodels/models/fractal_aggregate.py b/sasmodels/models/fractal_aggregate.py index 04374d02..8ffcd5ca 100644 --- a/sasmodels/models/fractal_aggregate.py +++ b/sasmodels/models/fractal_aggregate.py @@ -30,10 +30,8 @@ Parameters ---------- -radius_effective : distance between adjacent scatterers (Å); use *unconstrained* - *radius_effective_mode* to fit independently of the form-factor radius. -volfraction : required for $P@S$ products; **not used** in this $S(q)$ - (see ``sasmodels.product`` until optional structure-factor volfraction exists). +radius_effective : distance between adjacent scatterers (Å); use unconstrained *radius_effective_mode* to fit independently of the form-factor radius +volfraction : required for $P@S$ products; **not used** in this $S(q)$ (see ``sasmodels.product`` until optional structure-factor volfraction exists) D_fract : fractal dimension N_agg : number of particles in the fractal cluster diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.py b/sasmodels/models/fractal_aggregate_discrete_chain.py index 44feef1e..3bfbf3c9 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.py +++ b/sasmodels/models/fractal_aggregate_discrete_chain.py @@ -43,12 +43,10 @@ Parameters ---------- - -radius_effective : inter-scatterer distance :math:`d` (Å); see also - *radius_effective_mode* when combining with a form factor. -volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products. -``D_fract`` : fractal dimension :math:`D_f`. -``N_agg`` : number of particles in the fractal cluster. +radius_effective : inter-scatterer distance :math:`d` (Å); see *radius_effective_mode* when combining with a form factor +volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products +D_fract : fractal dimension :math:`D_f` +N_agg : number of particles in the fractal cluster See also Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of From 2c0bbb1771ce075318e72bada88a529c47ffb76a Mon Sep 17 00:00:00 2001 From: Wojtek Potrzebowski Date: Wed, 8 Jul 2026 15:56:03 +0200 Subject: [PATCH 07/10] Fixed the radius_effective convention across all six PR models. radius_effective is now treated as the effective scatterer radius (half the center-to-center distance), matching sasmodels structure-factor conventions. --- sasmodels/models/compact_polydisperse_cluster.c | 2 +- sasmodels/models/compact_polydisperse_cluster.py | 8 ++++---- sasmodels/models/fractal_aggregate.c | 5 ++--- sasmodels/models/fractal_aggregate.py | 8 ++++---- sasmodels/models/fractal_aggregate_discrete_chain.c | 4 ++-- sasmodels/models/fractal_aggregate_discrete_chain.py | 10 +++++----- sasmodels/models/free_rotating_chain.c | 4 ++-- sasmodels/models/free_rotating_chain.py | 7 ++++--- sasmodels/models/linear_aggregate.c | 10 ++++++---- sasmodels/models/linear_aggregate.py | 7 ++++--- sasmodels/models/stabilized_power_law.py | 6 +++--- 11 files changed, 37 insertions(+), 34 deletions(-) diff --git a/sasmodels/models/compact_polydisperse_cluster.c b/sasmodels/models/compact_polydisperse_cluster.c index 0bbb61ac..e7b3a2ca 100644 --- a/sasmodels/models/compact_polydisperse_cluster.c +++ b/sasmodels/models/compact_polydisperse_cluster.c @@ -111,7 +111,7 @@ double PSCHULZ(double Q, double RAV, double SIGMA) double P_POLY(double Q, double RL, double SIGL, double d, double N_agg) { double PI43 = 4.1887892; - double RS = 0.5 * d; + double RS = d; double RDIF = RL - RS; double Z = 1.0/(SIGL*SIGL) - 1.0; diff --git a/sasmodels/models/compact_polydisperse_cluster.py b/sasmodels/models/compact_polydisperse_cluster.py index c88d71c8..cef56ae9 100644 --- a/sasmodels/models/compact_polydisperse_cluster.py +++ b/sasmodels/models/compact_polydisperse_cluster.py @@ -26,7 +26,7 @@ Parameters ---------- -radius_effective : minimum distance between scatterers (Å); first parameter for :math:`P@S` wiring (see sasmodels structure-factor conventions) +radius_effective : effective scatterer radius (Å), half the minimum center-to-center distance; first parameter for :math:`P@S` wiring (see sasmodels structure-factor conventions) volfraction : unused in this :math:`S(q)`; required as second parameter for :math:`P@S` products R_clust : mean radius of large clusters sig_rel_R : relative polydispersity of R_cluster @@ -62,8 +62,8 @@ # Must match C: Iq(Q, radius_effective, volfraction, R_clust, sig_rel_R, N_agg) parameters = [ - ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", - "Minimum distance between scatterers"], + ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", + "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], ["R_clust", "Ang", 40.0, [0.0, np.inf], "", "Average cluster radius"], @@ -77,7 +77,7 @@ def random(): import random return { - "radius_effective": random.uniform(5, 30), + "radius_effective": random.uniform(2.5, 15.0), "volfraction": random.uniform(0.01, 0.3), "R_clust": random.uniform(50, 200), "sig_rel_R": random.uniform(0.01, 0.3), diff --git a/sasmodels/models/fractal_aggregate.c b/sasmodels/models/fractal_aggregate.c index 9a43ace5..4cecaab3 100644 --- a/sasmodels/models/fractal_aggregate.c +++ b/sasmodels/models/fractal_aggregate.c @@ -6,7 +6,7 @@ * * Parameters: * q : scattering vector - * radius_effective : distance between scatterers + * radius_effective : effective scatterer radius (half center-to-center distance) * volfraction : unused (required for P@S parameter order) * D_fract : fractal dimension * N_agg : number of particles in cluster @@ -57,6 +57,5 @@ double Iq(double q, double radius_effective, double volfraction, double D_fract, double N_agg) { (void)volfraction; - double r = 0.5 * radius_effective; - return fractal_sq_N(q, r, D_fract, N_agg); + return fractal_sq_N(q, radius_effective, D_fract, N_agg); } \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate.py b/sasmodels/models/fractal_aggregate.py index 8ffcd5ca..1e4a9791 100644 --- a/sasmodels/models/fractal_aggregate.py +++ b/sasmodels/models/fractal_aggregate.py @@ -30,7 +30,7 @@ Parameters ---------- -radius_effective : distance between adjacent scatterers (Å); use unconstrained *radius_effective_mode* to fit independently of the form-factor radius +radius_effective : effective scatterer radius (Å), half the center-to-center distance; use unconstrained *radius_effective_mode* to fit independently of the form-factor radius volfraction : required for $P@S$ products; **not used** in this $S(q)$ (see ``sasmodels.product`` until optional structure-factor volfraction exists) D_fract : fractal dimension N_agg : number of particles in the fractal cluster @@ -75,8 +75,8 @@ # Must match C kernel: Iq(q, radius_effective, volfraction, D_fract, N_agg) parameters = [ - ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", - "distance between adjacent scatterers"], + ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", + "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required as second parameter for P@S products"], ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], @@ -90,7 +90,7 @@ def random(): import random return { - "radius_effective": random.uniform(5.0, 50.0), + "radius_effective": random.uniform(2.5, 25.0), "volfraction": random.uniform(0.01, 0.3), "D_fract": random.uniform(1.1, 2.9), "N_agg": random.uniform(5.0, 300.0), diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.c b/sasmodels/models/fractal_aggregate_discrete_chain.c index 38b9208a..8de9f12d 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.c +++ b/sasmodels/models/fractal_aggregate_discrete_chain.c @@ -14,7 +14,7 @@ * * Parameters: * q : scattering vector - * d : distance between scatterers + * d : center-to-center distance between scatterers (= 2 * radius_effective) * D : fractal dimension * N : number of particles */ @@ -79,5 +79,5 @@ double Iq(double q, double radius_effective, double volfraction, double D_fract, double N_agg) { (void)volfraction; - return fractal_sq_N(q, radius_effective, D_fract, N_agg); + return fractal_sq_N(q, 2.0 * radius_effective, D_fract, N_agg); } \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.py b/sasmodels/models/fractal_aggregate_discrete_chain.py index 3bfbf3c9..a123eccd 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.py +++ b/sasmodels/models/fractal_aggregate_discrete_chain.py @@ -43,7 +43,7 @@ Parameters ---------- -radius_effective : inter-scatterer distance :math:`d` (Å); see *radius_effective_mode* when combining with a form factor +radius_effective : effective scatterer radius (Å), half the center-to-center distance :math:`d`; see *radius_effective_mode* when combining with a form factor volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products D_fract : fractal dimension :math:`D_f` N_agg : number of particles in the fractal cluster @@ -73,8 +73,7 @@ * **Last Reviewed by:** Reviewer Name Here (Date) """ -# fractal_aggregate_discrete_chain.py -# Sasmodels plugin wrapper for fractal S(q) using inter-scatterer distance d. +# Sasmodels plugin wrapper for fractal S(q) with discrete-chain baseline. import numpy as np @@ -89,7 +88,8 @@ # Must match C: Iq(q, radius_effective, volfraction, D_fract, N_agg) parameters = [ - ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", "Inter-scatterer distance"], + ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", + "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], @@ -102,7 +102,7 @@ def random(): import random return { - "radius_effective": random.uniform(10.0, 80.0), + "radius_effective": random.uniform(5.0, 40.0), "volfraction": random.uniform(0.01, 0.3), "D_fract": random.uniform(1.1, 2.9), "N_agg": random.uniform(5.0, 300.0), diff --git a/sasmodels/models/free_rotating_chain.c b/sasmodels/models/free_rotating_chain.c index 300f380d..14b397df 100644 --- a/sasmodels/models/free_rotating_chain.c +++ b/sasmodels/models/free_rotating_chain.c @@ -12,7 +12,7 @@ * * Sasmodels interface: * Q - momentum transfer - * radius_effective - separation d between points + * radius_effective - effective scatterer radius (half center-to-center distance) * volfraction - unused (required for P@S parameter order) * N_agg - real number of rotating points (RN) */ @@ -27,7 +27,7 @@ double Iq(double Q, double radius_effective, double volfraction, double N_agg) double intensity; double RN = N_agg; - double d = radius_effective; + double d = 2.0 * radius_effective; (void)volfraction; diff --git a/sasmodels/models/free_rotating_chain.py b/sasmodels/models/free_rotating_chain.py index 77ec4c48..23c33d3c 100644 --- a/sasmodels/models/free_rotating_chain.py +++ b/sasmodels/models/free_rotating_chain.py @@ -34,7 +34,7 @@ Parameters ---------- -radius_effective : separation between chain points :math:`d` (Å). +radius_effective : effective scatterer radius (Å), half the center-to-center separation :math:`d` between chain points. volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products. N_agg : aggregation number :math:`RN`. @@ -68,7 +68,8 @@ # Must match C: Iq(Q, radius_effective, volfraction, N_agg) parameters = [ - ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", "Separation between points"], + ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", + "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], ["N_agg", "", 20.0, [1.0, np.inf], "", "Aggregation number"], @@ -81,7 +82,7 @@ def random(): import random return { - "radius_effective": random.uniform(10.0, 50.0), + "radius_effective": random.uniform(5.0, 25.0), "volfraction": random.uniform(0.01, 0.3), "N_agg": random.uniform(1.0, 100.0), } diff --git a/sasmodels/models/linear_aggregate.c b/sasmodels/models/linear_aggregate.c index ad61e217..163bcb6e 100644 --- a/sasmodels/models/linear_aggregate.c +++ b/sasmodels/models/linear_aggregate.c @@ -19,7 +19,7 @@ static inline double sinx(double x) * * Parameters: * Q - momentum transfer - * radius_effective - separation between points (fit parameter) + * radius_effective - effective scatterer radius (half center-to-center distance) * volfraction - unused (required for P@S parameter order) * N_agg - effective number of points (may be non-integer) * @@ -65,7 +65,7 @@ static double sq_linN(double Q, int N_agg, double sep) * * Parameters: * Q - momentum transfer - * radius_effective - separation between points + * radius_effective - effective scatterer radius (half center-to-center distance) * volfraction - unused * N_agg - real (non-integer) number of points * @@ -86,9 +86,11 @@ double Iq(double Q, double radius_effective, double volfraction, double N_agg) N = (int)N_agg; w = N_agg - (double)N; + double sep = 2.0 * radius_effective; + intensity = - (1.0 - w) * sq_linN(Q, N, radius_effective) - + w * sq_linN(Q, N + 1, radius_effective); + (1.0 - w) * sq_linN(Q, N, sep) + + w * sq_linN(Q, N + 1, sep); return intensity; } \ No newline at end of file diff --git a/sasmodels/models/linear_aggregate.py b/sasmodels/models/linear_aggregate.py index 48e9023a..3ebc2daf 100644 --- a/sasmodels/models/linear_aggregate.py +++ b/sasmodels/models/linear_aggregate.py @@ -31,7 +31,7 @@ Parameters ---------- -radius_effective : distance between scatterers on the chain (Å). +radius_effective : effective scatterer radius (Å), half the center-to-center distance between scatterers on the chain. volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products. N_agg : aggregation number. @@ -66,7 +66,8 @@ structure_factor = True parameters = [ - ["radius_effective", "Ang", 20.0, [0.0, np.inf], "", "distance between scatterers"], + ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", + "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], ["N_agg", "", 50.0, [0.0, np.inf], "", "Aggregation number"], @@ -78,7 +79,7 @@ def random(): import random return { - "radius_effective": random.uniform(10, 50), + "radius_effective": random.uniform(5, 25), "volfraction": random.uniform(0.01, 0.3), "N_agg": random.uniform(1, 100), } diff --git a/sasmodels/models/stabilized_power_law.py b/sasmodels/models/stabilized_power_law.py index 0374d9f2..9911f4e2 100644 --- a/sasmodels/models/stabilized_power_law.py +++ b/sasmodels/models/stabilized_power_law.py @@ -15,8 +15,8 @@ where *amp* is the scale of the power law and *pow* is the exponent. The first two parameters follow sasmodels structure-factor conventions for -:math:`P@S` products: *radius_effective* and *volfraction* are **not** used in -the formula above. +:math:`P@S` products: *radius_effective* (effective scatterer radius, unused +here) and *volfraction* are **not** used in the formula above. See also Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of @@ -56,7 +56,7 @@ # ["name", "units", default, [lower, upper], "type","description"], parameters = [ ["radius_effective", "Ang", 50.0, [0.0, inf], "", - "unused in S(q); required for P@S products"], + "effective scatterer radius; unused in S(q), required for P@S products"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], ["amp", "", 100, [0, inf], "", "scale of power law"], From 23c468da2a134e6a3f23b2a7ca485dd7c562d475 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 10 Jul 2026 09:50:55 -0400 Subject: [PATCH 08/10] fix OpenCL problems on mac; use sas library functions --- .../models/compact_polydisperse_cluster.c | 36 ++----------------- .../models/compact_polydisperse_cluster.py | 3 +- sasmodels/models/fractal_aggregate.c | 2 -- .../models/fractal_aggregate_discrete_chain.c | 2 -- sasmodels/models/free_rotating_chain.c | 11 ++---- sasmodels/models/free_rotating_chain.py | 1 + sasmodels/models/linear_aggregate.c | 18 ++-------- sasmodels/models/linear_aggregate.py | 4 ++- 8 files changed, 14 insertions(+), 63 deletions(-) diff --git a/sasmodels/models/compact_polydisperse_cluster.c b/sasmodels/models/compact_polydisperse_cluster.c index e7b3a2ca..f8b0067d 100644 --- a/sasmodels/models/compact_polydisperse_cluster.c +++ b/sasmodels/models/compact_polydisperse_cluster.c @@ -1,26 +1,3 @@ -#include -#include - -/* GAMMLN ------------------------------------------------------------ */ -double GAMMLN(double xx) -{ - static double cof[6] = { - 76.18009173, -86.50532033, 24.01409822, - -1.231739516, 0.00120858003, -0.0000536382 - }; - double stp = 2.50662827465; - double half = 0.5, one = 1.0, fpf = 5.5; - double x = xx - one; - double tmp = x + fpf; - tmp = (x + half) * log(fabs(tmp)) - tmp; - - double ser = one; - for(int j=0;j<6;j++){ - x += one; - ser += cof[j]/x; - } - return tmp + log(stp * ser); -} /* SCHULZ ------------------------------------------------------------ */ double SCHULZ(double R, double RA, double Z) @@ -29,20 +6,11 @@ double SCHULZ(double R, double RA, double Z) (Z+1.0)*log(fabs((Z+1.0)/RA)) + Z*log(fabs(R)) - (Z+1.0)*R/RA - - GAMMLN(Z+1.0); + - sas_gammaln(Z+1.0); return exp(dum); } -/* FI ---------------------------------------------------------------- */ -double FI(double X) -{ - if(X > 0.05) - return 3.0*(sin(X)-X*cos(X)) / (X*X*X); - else - return 1.0 - 0.1*X*X; -} - /* S_HS -------------------------------------------------------------- */ double S_HS(double Q, double RHS, double ETA) { @@ -155,7 +123,7 @@ double P_POLY(double Q, double RL, double SIGL, double d, double N_agg) SSQ = SSQ/SSQN; - double PS = VS*VS * FI(Q*RS)*FI(Q*RS); + double PS = square(VS * sas_3j1x_x(Q*RS)); double PLAV = PSCHULZ(Q, RDIF, SIGL); double RN = SVL * ALPP; diff --git a/sasmodels/models/compact_polydisperse_cluster.py b/sasmodels/models/compact_polydisperse_cluster.py index cef56ae9..d026210d 100644 --- a/sasmodels/models/compact_polydisperse_cluster.py +++ b/sasmodels/models/compact_polydisperse_cluster.py @@ -59,6 +59,7 @@ category = "structure-factor" structure_factor = True +single = False # Must match C: Iq(Q, radius_effective, volfraction, R_clust, sig_rel_R, N_agg) parameters = [ @@ -71,7 +72,7 @@ ["N_agg", "", 50.0, [10.0, 100.0], "", "Weight-average aggregation number"], ] -source = ["compact_polydisperse_cluster.c"] +source = ["lib/sas_gammainc.c", "lib/sas_3j1x_x.c", "compact_polydisperse_cluster.c"] def random(): import random diff --git a/sasmodels/models/fractal_aggregate.c b/sasmodels/models/fractal_aggregate.c index 4cecaab3..d6c25eae 100644 --- a/sasmodels/models/fractal_aggregate.c +++ b/sasmodels/models/fractal_aggregate.c @@ -1,5 +1,3 @@ -#include - /* * Fractal structure factor S(q) * Normalization: S(0) = N diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.c b/sasmodels/models/fractal_aggregate_discrete_chain.c index 8de9f12d..382cc2f2 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.c +++ b/sasmodels/models/fractal_aggregate_discrete_chain.c @@ -1,5 +1,3 @@ -#include - /* * Fractal structure factor S(q) * diff --git a/sasmodels/models/free_rotating_chain.c b/sasmodels/models/free_rotating_chain.c index 14b397df..ccfe62a2 100644 --- a/sasmodels/models/free_rotating_chain.c +++ b/sasmodels/models/free_rotating_chain.c @@ -1,5 +1,3 @@ -#include - /* * free_rotating_chain * @@ -39,15 +37,12 @@ double Iq(double Q, double radius_effective, double volfraction, double N_agg) x = Q * d; /* sinc kernel */ - if (fabs(x) < 1e-12) - ARG = 1.0; - else - ARG = sin(x) / x; + ARG = sas_sinx_x(x); /* FOR N */ SN = ((double)N_SPH) / (1.0 - ARG) - ((double)N_SPH) / 2.0 - - (1.0 - pow(ARG, N_SPH)) / pow(1.0 - ARG, 2.0) * ARG; + - (1.0 - pow(ARG, N_SPH)) / square(1.0 - ARG) * ARG; SN = SN * 2.0 / (double)N_SPH; @@ -56,7 +51,7 @@ double Iq(double Q, double radius_effective, double volfraction, double N_agg) SN1 = ((double)N_SPH1) / (1.0 - ARG) - ((double)N_SPH1) / 2.0 - - (1.0 - pow(ARG, N_SPH1)) / pow(1.0 - ARG, 2.0) * ARG; + - (1.0 - pow(ARG, N_SPH1)) / square(1.0 - ARG) * ARG; SN1 = SN1 * 2.0 / (double)N_SPH1; diff --git a/sasmodels/models/free_rotating_chain.py b/sasmodels/models/free_rotating_chain.py index 23c33d3c..948f2037 100644 --- a/sasmodels/models/free_rotating_chain.py +++ b/sasmodels/models/free_rotating_chain.py @@ -65,6 +65,7 @@ category = "structure-factor" structure_factor = True +single = False # Must match C: Iq(Q, radius_effective, volfraction, N_agg) parameters = [ diff --git a/sasmodels/models/linear_aggregate.c b/sasmodels/models/linear_aggregate.c index 163bcb6e..063744eb 100644 --- a/sasmodels/models/linear_aggregate.c +++ b/sasmodels/models/linear_aggregate.c @@ -1,17 +1,3 @@ -#include -#include -#include - -/* - * sinx(x) = sin(x)/x with safe x -> 0 limit - */ -static inline double sinx(double x) -{ - if (fabs(x) < 1e-8) - return 1.0; - return sin(x) / x; -} - /* * linear_aggregate * @@ -39,17 +25,19 @@ static double sq_linN(double Q, int N_agg, double sep) if (N_agg <= 1) return 1.0; +/* if (N_agg > 100) { fprintf(stderr, "WARNING (linear_aggregate): N_agg = %d exceeds maximum allowed value (100).\n", N_agg); exit(EXIT_FAILURE); } +*/ SUM = 0.0; for (k = 1; k <= N_agg - 1; k++) { - SUM += (double)(N_agg - k) * sinx(Q * d * (double)k); + SUM += (double)(N_agg - k) * sas_sinx_x(Q * d * (double)k); } SN = 1.0 + 2.0 * SUM / (double)N_agg; diff --git a/sasmodels/models/linear_aggregate.py b/sasmodels/models/linear_aggregate.py index 3ebc2daf..35a83546 100644 --- a/sasmodels/models/linear_aggregate.py +++ b/sasmodels/models/linear_aggregate.py @@ -70,9 +70,11 @@ "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], - ["N_agg", "", 50.0, [0.0, np.inf], "", "Aggregation number"], + ["N_agg", "", 50.0, [0.0, 100.0], "", "Aggregation number"], ] +valid = "N_agg <= 100" + source = ["linear_aggregate.c"] def random(): From 1fae7ada70b10f8e008ae99b83b4b4cb327202e7 Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 10 Jul 2026 10:09:52 -0400 Subject: [PATCH 09/10] rename parameters to sasmodels conventions --- .../models/compact_polydisperse_cluster.c | 12 ++++---- .../models/compact_polydisperse_cluster.py | 20 ++++++------- sasmodels/models/fractal_aggregate.c | 8 ++--- sasmodels/models/fractal_aggregate.py | 14 ++++----- .../models/fractal_aggregate_discrete_chain.c | 4 +-- .../fractal_aggregate_discrete_chain.py | 14 ++++----- sasmodels/models/free_rotating_chain.c | 6 ++-- sasmodels/models/free_rotating_chain.py | 8 ++--- sasmodels/models/linear_aggregate.c | 30 +++++++++---------- sasmodels/models/linear_aggregate.py | 8 ++--- sasmodels/models/stabilized_power_law.py | 18 +++++------ 11 files changed, 71 insertions(+), 71 deletions(-) diff --git a/sasmodels/models/compact_polydisperse_cluster.c b/sasmodels/models/compact_polydisperse_cluster.c index f8b0067d..9f26515c 100644 --- a/sasmodels/models/compact_polydisperse_cluster.c +++ b/sasmodels/models/compact_polydisperse_cluster.c @@ -76,7 +76,7 @@ double PSCHULZ(double Q, double RAV, double SIGMA) } /* P_POLY ------------------------------------------------------------ */ -double P_POLY(double Q, double RL, double SIGL, double d, double N_agg) +double P_POLY(double Q, double RL, double SIGL, double d, double n_aggreg) { double PI43 = 4.1887892; double RS = d; @@ -90,7 +90,7 @@ double P_POLY(double Q, double RL, double SIGL, double d, double N_agg) / pow(Z+1.0,5.0); double VS = PI43*pow(RS,3.0); - double ETA = N_agg * (SVL * VS)/SVL2; + double ETA = n_aggreg * (SVL * VS)/SVL2; double ALPP = ETA/VS; @@ -133,15 +133,15 @@ double P_POLY(double Q, double RL, double SIGL, double d, double N_agg) } /* ----------- SASVIEW ENTRY POINT ----------- */ -double SQ_COMPACT(double Q, double RL, double SIGL, double d, double N_agg) +double SQ_COMPACT(double Q, double RL, double SIGL, double d, double n_aggreg) { - return P_POLY(Q, RL, SIGL, d, N_agg); + return P_POLY(Q, RL, SIGL, d, n_aggreg); } /* SasView required function */ double Iq(double Q, double radius_effective, double volfraction, - double R_clust, double sig_rel_R, double N_agg) + double radius_cluster, double radius_cluster_sigma_relative, double n_aggreg) { (void)volfraction; - return SQ_COMPACT(Q, R_clust, sig_rel_R, radius_effective, N_agg); + return SQ_COMPACT(Q, radius_cluster, radius_cluster_sigma_relative, radius_effective, n_aggreg); } diff --git a/sasmodels/models/compact_polydisperse_cluster.py b/sasmodels/models/compact_polydisperse_cluster.py index d026210d..d06b810e 100644 --- a/sasmodels/models/compact_polydisperse_cluster.py +++ b/sasmodels/models/compact_polydisperse_cluster.py @@ -28,9 +28,9 @@ ---------- radius_effective : effective scatterer radius (Å), half the minimum center-to-center distance; first parameter for :math:`P@S` wiring (see sasmodels structure-factor conventions) volfraction : unused in this :math:`S(q)`; required as second parameter for :math:`P@S` products -R_clust : mean radius of large clusters -sig_rel_R : relative polydispersity of R_cluster -N_agg : weight-average aggregation number +radius_cluster : mean radius of large clusters +radius_cluster_sigma_relative : relative polydispersity of radius_cluster +n_aggreg : weight-average aggregation number References ---------- @@ -61,15 +61,15 @@ structure_factor = True single = False -# Must match C: Iq(Q, radius_effective, volfraction, R_clust, sig_rel_R, N_agg) +# Must match C: Iq(Q, radius_effective, volfraction, radius_cluster, radius_cluster_sigma_relative, n_aggreg) parameters = [ ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], - ["R_clust", "Ang", 40.0, [0.0, np.inf], "", "Average cluster radius"], - ["sig_rel_R", "", 0.4, [0.0, 1.0], "", "Relative size polydispersity"], - ["N_agg", "", 50.0, [10.0, 100.0], "", "Weight-average aggregation number"], + ["radius_cluster", "Ang", 40.0, [0.0, np.inf], "", "Average cluster radius"], + ["radius_cluster_sigma_relative", "", 0.4, [0.0, 1.0], "", "Relative size polydispersity"], + ["n_aggreg", "", 50.0, [10.0, 100.0], "", "Weight-average aggregation number"], ] source = ["lib/sas_gammainc.c", "lib/sas_3j1x_x.c", "compact_polydisperse_cluster.c"] @@ -80,9 +80,9 @@ def random(): return { "radius_effective": random.uniform(2.5, 15.0), "volfraction": random.uniform(0.01, 0.3), - "R_clust": random.uniform(50, 200), - "sig_rel_R": random.uniform(0.01, 0.3), - "N_agg": random.uniform(10, 100), + "radius_cluster": random.uniform(50, 200), + "radius_cluster_sigma_relative": random.uniform(0.01, 0.3), + "n_aggreg": random.uniform(10, 100), } def test(): diff --git a/sasmodels/models/fractal_aggregate.c b/sasmodels/models/fractal_aggregate.c index d6c25eae..c82442a1 100644 --- a/sasmodels/models/fractal_aggregate.c +++ b/sasmodels/models/fractal_aggregate.c @@ -6,8 +6,8 @@ * q : scattering vector * radius_effective : effective scatterer radius (half center-to-center distance) * volfraction : unused (required for P@S parameter order) - * D_fract : fractal dimension - * N_agg : number of particles in cluster + * fractal_dim : fractal dimension + * n_aggreg : number of particles in cluster */ static double fractal_sq_N(double q, double r, double D, double N) @@ -52,8 +52,8 @@ static double fractal_sq_N(double q, double r, double D, double N) /* ========== Sasmodels interface ========== */ double Iq(double q, double radius_effective, double volfraction, - double D_fract, double N_agg) + double fractal_dim, double n_aggreg) { (void)volfraction; - return fractal_sq_N(q, radius_effective, D_fract, N_agg); + return fractal_sq_N(q, radius_effective, fractal_dim, n_aggreg); } \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate.py b/sasmodels/models/fractal_aggregate.py index 1e4a9791..f9979bc3 100644 --- a/sasmodels/models/fractal_aggregate.py +++ b/sasmodels/models/fractal_aggregate.py @@ -32,8 +32,8 @@ ---------- radius_effective : effective scatterer radius (Å), half the center-to-center distance; use unconstrained *radius_effective_mode* to fit independently of the form-factor radius volfraction : required for $P@S$ products; **not used** in this $S(q)$ (see ``sasmodels.product`` until optional structure-factor volfraction exists) -D_fract : fractal dimension -N_agg : number of particles in the fractal cluster +fractal_dim : fractal dimension +n_aggreg : number of particles in the fractal cluster See also Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of @@ -73,14 +73,14 @@ category = "structure-factor" structure_factor = True -# Must match C kernel: Iq(q, radius_effective, volfraction, D_fract, N_agg) +# Must match C kernel: Iq(q, radius_effective, volfraction, fractal_dim, n_aggreg) parameters = [ ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required as second parameter for P@S products"], - ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], - ["N_agg", "", 50.0, [1.0, np.inf], "", "Number of particles"], + ["fractal_dim", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], + ["n_aggreg", "", 50.0, [1.0, np.inf], "", "Number of particles"], ] # Kernel source @@ -92,8 +92,8 @@ def random(): return { "radius_effective": random.uniform(2.5, 25.0), "volfraction": random.uniform(0.01, 0.3), - "D_fract": random.uniform(1.1, 2.9), - "N_agg": random.uniform(5.0, 300.0), + "fractal_dim": random.uniform(1.1, 2.9), + "n_aggreg": random.uniform(5.0, 300.0), } def test(): diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.c b/sasmodels/models/fractal_aggregate_discrete_chain.c index 382cc2f2..344bef9f 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.c +++ b/sasmodels/models/fractal_aggregate_discrete_chain.c @@ -74,8 +74,8 @@ static double fractal_sq_N(double q, double d, double D, double N) /* ---------------- sasmodels interface ---------------- */ double Iq(double q, double radius_effective, double volfraction, - double D_fract, double N_agg) + double fractal_dim, double n_aggreg) { (void)volfraction; - return fractal_sq_N(q, 2.0 * radius_effective, D_fract, N_agg); + return fractal_sq_N(q, 2.0 * radius_effective, fractal_dim, n_aggreg); } \ No newline at end of file diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.py b/sasmodels/models/fractal_aggregate_discrete_chain.py index a123eccd..39262718 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.py +++ b/sasmodels/models/fractal_aggregate_discrete_chain.py @@ -45,8 +45,8 @@ ---------- radius_effective : effective scatterer radius (Å), half the center-to-center distance :math:`d`; see *radius_effective_mode* when combining with a form factor volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products -D_fract : fractal dimension :math:`D_f` -N_agg : number of particles in the fractal cluster +fractal_dim : fractal dimension :math:`D_f` +n_aggreg : number of particles in the fractal cluster See also Larsen, A. H., Pedersen, J. S., & Arleth, L. (2020). Assessment of @@ -86,14 +86,14 @@ category = "structure-factor" structure_factor = True -# Must match C: Iq(q, radius_effective, volfraction, D_fract, N_agg) +# Must match C: Iq(q, radius_effective, volfraction, fractal_dim, n_aggreg) parameters = [ ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], - ["D_fract", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], - ["N_agg", "", 50.0, [1.0, np.inf], "", "Number of particles in cluster"], + ["fractal_dim", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], + ["n_aggreg", "", 50.0, [1.0, np.inf], "", "Number of particles in cluster"], ] source = ["fractal_aggregate_discrete_chain.c"] @@ -104,8 +104,8 @@ def random(): return { "radius_effective": random.uniform(5.0, 40.0), "volfraction": random.uniform(0.01, 0.3), - "D_fract": random.uniform(1.1, 2.9), - "N_agg": random.uniform(5.0, 300.0), + "fractal_dim": random.uniform(1.1, 2.9), + "n_aggreg": random.uniform(5.0, 300.0), } def test(): diff --git a/sasmodels/models/free_rotating_chain.c b/sasmodels/models/free_rotating_chain.c index ccfe62a2..e5141dd2 100644 --- a/sasmodels/models/free_rotating_chain.c +++ b/sasmodels/models/free_rotating_chain.c @@ -12,10 +12,10 @@ * Q - momentum transfer * radius_effective - effective scatterer radius (half center-to-center distance) * volfraction - unused (required for P@S parameter order) - * N_agg - real number of rotating points (RN) + * n_aggreg - real number of rotating points (RN) */ -double Iq(double Q, double radius_effective, double volfraction, double N_agg) +double Iq(double Q, double radius_effective, double volfraction, double n_aggreg) { int N_SPH, N_SPH1; double W; @@ -24,7 +24,7 @@ double Iq(double Q, double radius_effective, double volfraction, double N_agg) double x; double intensity; - double RN = N_agg; + double RN = n_aggreg; double d = 2.0 * radius_effective; (void)volfraction; diff --git a/sasmodels/models/free_rotating_chain.py b/sasmodels/models/free_rotating_chain.py index 948f2037..a9da3bcf 100644 --- a/sasmodels/models/free_rotating_chain.py +++ b/sasmodels/models/free_rotating_chain.py @@ -36,7 +36,7 @@ ---------- radius_effective : effective scatterer radius (Å), half the center-to-center separation :math:`d` between chain points. volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products. -N_agg : aggregation number :math:`RN`. +n_aggreg : aggregation number :math:`RN`. References ---------- @@ -67,13 +67,13 @@ structure_factor = True single = False -# Must match C: Iq(Q, radius_effective, volfraction, N_agg) +# Must match C: Iq(Q, radius_effective, volfraction, n_aggreg) parameters = [ ["radius_effective", "Ang", 10.0, [0.0, np.inf], "", "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], - ["N_agg", "", 20.0, [1.0, np.inf], "", "Aggregation number"], + ["n_aggreg", "", 20.0, [1.0, np.inf], "", "Aggregation number"], ] # Kernel source @@ -85,7 +85,7 @@ def random(): return { "radius_effective": random.uniform(5.0, 25.0), "volfraction": random.uniform(0.01, 0.3), - "N_agg": random.uniform(1.0, 100.0), + "n_aggreg": random.uniform(1.0, 100.0), } def test(): diff --git a/sasmodels/models/linear_aggregate.c b/sasmodels/models/linear_aggregate.c index 063744eb..c3214987 100644 --- a/sasmodels/models/linear_aggregate.c +++ b/sasmodels/models/linear_aggregate.c @@ -7,40 +7,40 @@ * Q - momentum transfer * radius_effective - effective scatterer radius (half center-to-center distance) * volfraction - unused (required for P@S parameter order) - * N_agg - effective number of points (may be non-integer) + * n_aggreg - effective number of points (may be non-integer) * * Returns: * linear_aggregate(Q) * * Notes: - * Exits with warning if N_agg > 100 + * Exits with warning if n_aggreg > 100 */ -static double sq_linN(double Q, int N_agg, double sep) +static double sq_linN(double Q, int n_aggreg, double sep) { int k; double SUM, SN; double d = sep; - if (N_agg <= 1) + if (n_aggreg <= 1) return 1.0; /* - if (N_agg > 100) { + if (n_aggreg > 100) { fprintf(stderr, - "WARNING (linear_aggregate): N_agg = %d exceeds maximum allowed value (100).\n", - N_agg); + "WARNING (linear_aggregate): n_aggreg = %d exceeds maximum allowed value (100).\n", + n_aggreg); exit(EXIT_FAILURE); } */ SUM = 0.0; - for (k = 1; k <= N_agg - 1; k++) { - SUM += (double)(N_agg - k) * sas_sinx_x(Q * d * (double)k); + for (k = 1; k <= n_aggreg - 1; k++) { + SUM += (double)(n_aggreg - k) * sas_sinx_x(Q * d * (double)k); } - SN = 1.0 + 2.0 * SUM / (double)N_agg; + SN = 1.0 + 2.0 * SUM / (double)n_aggreg; return SN; } @@ -55,12 +55,12 @@ static double sq_linN(double Q, int N_agg, double sep) * Q - momentum transfer * radius_effective - effective scatterer radius (half center-to-center distance) * volfraction - unused - * N_agg - real (non-integer) number of points + * n_aggreg - real (non-integer) number of points * * Returns: * linear_aggregate(Q) */ -double Iq(double Q, double radius_effective, double volfraction, double N_agg) +double Iq(double Q, double radius_effective, double volfraction, double n_aggreg) { int N; double w; @@ -68,11 +68,11 @@ double Iq(double Q, double radius_effective, double volfraction, double N_agg) (void)volfraction; - if (N_agg <= 1.0) + if (n_aggreg <= 1.0) return 1.0; - N = (int)N_agg; - w = N_agg - (double)N; + N = (int)n_aggreg; + w = n_aggreg - (double)N; double sep = 2.0 * radius_effective; diff --git a/sasmodels/models/linear_aggregate.py b/sasmodels/models/linear_aggregate.py index 35a83546..1987714e 100644 --- a/sasmodels/models/linear_aggregate.py +++ b/sasmodels/models/linear_aggregate.py @@ -33,7 +33,7 @@ ---------- radius_effective : effective scatterer radius (Å), half the center-to-center distance between scatterers on the chain. volfraction : unused in this :math:`S(q)`; required for :math:`P@S` products. -N_agg : aggregation number. +n_aggreg : aggregation number. Validation ---------- @@ -70,10 +70,10 @@ "effective scatterer radius (half center-to-center distance)"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], - ["N_agg", "", 50.0, [0.0, 100.0], "", "Aggregation number"], + ["n_aggreg", "", 50.0, [0.0, 100.0], "", "Aggregation number"], ] -valid = "N_agg <= 100" +valid = "n_aggreg <= 100" source = ["linear_aggregate.c"] @@ -83,7 +83,7 @@ def random(): return { "radius_effective": random.uniform(5, 25), "volfraction": random.uniform(0.01, 0.3), - "N_agg": random.uniform(1, 100), + "n_aggreg": random.uniform(1, 100), } def test(): diff --git a/sasmodels/models/stabilized_power_law.py b/sasmodels/models/stabilized_power_law.py index 9911f4e2..6b2e60a8 100644 --- a/sasmodels/models/stabilized_power_law.py +++ b/sasmodels/models/stabilized_power_law.py @@ -10,9 +10,9 @@ .. math:: - S(q) = 1.0 + \mathrm{amp}\,(0.01/q)^{\mathrm{pow}} + S(q) = 1.0 + \mathrm{power_law_scale}\,(0.01/q)^{\mathrm{power}} -where *amp* is the scale of the power law and *pow* is the exponent. +where *power_law_scale* is the scale of the power law and *power* is the exponent. The first two parameters follow sasmodels structure-factor conventions for :math:`P@S` products: *radius_effective* (effective scatterer radius, unused @@ -46,9 +46,9 @@ name = "stabilized_power_law" title = "Stabilized power-law structure factor" description = """\ -S(q) = 1 + amp *(0.01/q)^pow - amp: scale of power law - pow: exponent of power law +S(q) = 1 + power_law_scale *(0.01/q)^power + power_law_scale: scale of power law + power: exponent of power law """ category = "structure-factor" structure_factor = True @@ -59,12 +59,12 @@ "effective scatterer radius; unused in S(q), required for P@S products"], ["volfraction", "", 0.2, [0.0, 1.0], "", "unused in S(q); required for P@S products"], - ["amp", "", 100, [0, inf], "", "scale of power law"], - ["pow", "", 2, [0, 6], "", "exponent of power law "], + ["power_law_scale", "", 100, [0, inf], "", "scale of power law"], + ["power", "", 2, [0, 6], "", "exponent of power law "], ] -def Iq(q, radius_effective, volfraction, amp, pow): +def Iq(q, radius_effective, volfraction, power_law_scale, power): """Return S(q); *radius_effective* and *volfraction* are unused.""" _ = (radius_effective, volfraction) - return 1.0 + amp * (0.01 / q) ** pow + return 1.0 + power_law_scale * (0.01 / q) ** power From 2f6ea20bbb0e5088d4e6eab1c5be70c02a669e5c Mon Sep 17 00:00:00 2001 From: Paul Kienzle Date: Fri, 10 Jul 2026 10:21:19 -0400 Subject: [PATCH 10/10] whitespace fixes --- sasmodels/models/compact_polydisperse_cluster.c | 9 +++------ sasmodels/models/fractal_aggregate.c | 2 +- sasmodels/models/fractal_aggregate_discrete_chain.c | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/sasmodels/models/compact_polydisperse_cluster.c b/sasmodels/models/compact_polydisperse_cluster.c index 9f26515c..0f5d4050 100644 --- a/sasmodels/models/compact_polydisperse_cluster.c +++ b/sasmodels/models/compact_polydisperse_cluster.c @@ -1,4 +1,3 @@ - /* SCHULZ ------------------------------------------------------------ */ double SCHULZ(double R, double RA, double Z) { @@ -93,8 +92,6 @@ double P_POLY(double Q, double RL, double SIGL, double d, double n_aggreg) double ETA = n_aggreg * (SVL * VS)/SVL2; double ALPP = ETA/VS; - - int NPOI = 100; double RMAX = RS + 6.0*RL*SIGL; double DELR = RMAX/NPOI; @@ -107,10 +104,10 @@ double P_POLY(double Q, double RL, double SIGL, double d, double n_aggreg) double R = RS + (i-0.5)*DELR; double RD = R - RS; double D = SCHULZ(RD, RDIF, Z); - - double RH = R - 2.0*RS; + + double RH = R - 2.0*RS; if(RH < 0) RH = 0.0; - + double FR = (pow(RD,3.0) - pow(RH,3.0))/pow(RD,3.0); if(FR < 0.0) FR = 0.0; if(FR > 1.0) FR = 1.0; diff --git a/sasmodels/models/fractal_aggregate.c b/sasmodels/models/fractal_aggregate.c index c82442a1..08233bf9 100644 --- a/sasmodels/models/fractal_aggregate.c +++ b/sasmodels/models/fractal_aggregate.c @@ -56,4 +56,4 @@ double Iq(double q, double radius_effective, double volfraction, { (void)volfraction; return fractal_sq_N(q, radius_effective, fractal_dim, n_aggreg); -} \ No newline at end of file +} diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.c b/sasmodels/models/fractal_aggregate_discrete_chain.c index 344bef9f..30dda6f8 100644 --- a/sasmodels/models/fractal_aggregate_discrete_chain.c +++ b/sasmodels/models/fractal_aggregate_discrete_chain.c @@ -78,4 +78,4 @@ double Iq(double q, double radius_effective, double volfraction, { (void)volfraction; return fractal_sq_N(q, 2.0 * radius_effective, fractal_dim, n_aggreg); -} \ No newline at end of file +}