diff --git a/sasmodels/models/compact_polydisperse_cluster.c b/sasmodels/models/compact_polydisperse_cluster.c new file mode 100644 index 00000000..0f5d4050 --- /dev/null +++ b/sasmodels/models/compact_polydisperse_cluster.c @@ -0,0 +1,144 @@ +/* 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 + - sas_gammaln(Z+1.0); + + return exp(dum); +} + +/* 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_aggreg) +{ + double PI43 = 4.1887892; + double RS = 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_aggreg * (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 = square(VS * sas_3j1x_x(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_COMPACT(double Q, double RL, double SIGL, double d, double n_aggreg) +{ + return P_POLY(Q, RL, SIGL, d, n_aggreg); +} + +/* SasView required function */ +double Iq(double Q, double radius_effective, double volfraction, + double radius_cluster, double radius_cluster_sigma_relative, double n_aggreg) +{ + (void)volfraction; + 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 new file mode 100644 index 00000000..d06b810e --- /dev/null +++ b/sasmodels/models/compact_polydisperse_cluster.py @@ -0,0 +1,89 @@ +r""" +Definition +---------- +Structure factor for a polydisperse spherical cluster with internal correlations. + +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, +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-average aggregation number to be a fit parameter. + +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 +---------- +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 +radius_cluster : mean radius of large clusters +radius_cluster_sigma_relative : relative polydispersity of radius_cluster +n_aggreg : 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 = "compact_polydisperse_cluster" +title = "Compact Polydisperse Cluster Structure Factor" +description = """ +Structure factor for a compact polydisperse cluster +with internal correlations. +""" + +category = "structure-factor" +structure_factor = True +single = False + +# 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"], + ["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"] + +def random(): + import random + + return { + "radius_effective": random.uniform(2.5, 15.0), + "volfraction": random.uniform(0.01, 0.3), + "radius_cluster": random.uniform(50, 200), + "radius_cluster_sigma_relative": random.uniform(0.01, 0.3), + "n_aggreg": random.uniform(10, 100), + } + +def test(): + print("compact_polydisperse_cluster model loaded successfully.") diff --git a/sasmodels/models/fractal_aggregate.c b/sasmodels/models/fractal_aggregate.c new file mode 100644 index 00000000..08233bf9 --- /dev/null +++ b/sasmodels/models/fractal_aggregate.c @@ -0,0 +1,59 @@ +/* + * Fractal structure factor S(q) + * Normalization: S(0) = N + * + * Parameters: + * q : scattering vector + * radius_effective : effective scatterer radius (half center-to-center distance) + * volfraction : unused (required for P@S parameter order) + * fractal_dim : fractal dimension + * n_aggreg : 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 radius_effective, double volfraction, + double fractal_dim, double n_aggreg) +{ + (void)volfraction; + return fractal_sq_N(q, radius_effective, fractal_dim, n_aggreg); +} diff --git a/sasmodels/models/fractal_aggregate.py b/sasmodels/models/fractal_aggregate.py new file mode 100644 index 00000000..f9979bc3 --- /dev/null +++ b/sasmodels/models/fractal_aggregate.py @@ -0,0 +1,100 @@ +# 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 anisotropic particle shapes. + +It uses 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}} + +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 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 +---------- +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) +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 +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" +structure_factor = True + +# 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"], + ["fractal_dim", "", 2.0, [1.0, 3.0], "", "Fractal dimension"], + ["n_aggreg", "", 50.0, [1.0, np.inf], "", "Number of particles"], +] + +# Kernel source +source = ["fractal_aggregate.c"] + +def random(): + import random + + return { + "radius_effective": random.uniform(2.5, 25.0), + "volfraction": random.uniform(0.01, 0.3), + "fractal_dim": random.uniform(1.1, 2.9), + "n_aggreg": random.uniform(5.0, 300.0), + } + +def test(): + print("fractal_aggregate plugin loaded correctly.") diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.c b/sasmodels/models/fractal_aggregate_discrete_chain.c new file mode 100644 index 00000000..30dda6f8 --- /dev/null +++ b/sasmodels/models/fractal_aggregate_discrete_chain.c @@ -0,0 +1,81 @@ +/* + * 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 : center-to-center distance between scatterers (= 2 * radius_effective) + * 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 radius_effective, double volfraction, + double fractal_dim, double n_aggreg) +{ + (void)volfraction; + return fractal_sq_N(q, 2.0 * radius_effective, fractal_dim, n_aggreg); +} diff --git a/sasmodels/models/fractal_aggregate_discrete_chain.py b/sasmodels/models/fractal_aggregate_discrete_chain.py new file mode 100644 index 00000000..39262718 --- /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 anisotropic 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`. + +Parameters +---------- +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 +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 +structure factors for analysis of small-angle scattering data from +desired or undesired aggregates. Applied Crystallography, 53(4), 991-1005. + + +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) +""" + +# Sasmodels plugin wrapper for fractal S(q) with discrete-chain baseline. + +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" +structure_factor = True + +# 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"], + ["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"] + +def random(): + import random + + return { + "radius_effective": random.uniform(5.0, 40.0), + "volfraction": random.uniform(0.01, 0.3), + "fractal_dim": random.uniform(1.1, 2.9), + "n_aggreg": random.uniform(5.0, 300.0), + } + +def test(): + print("fractal_aggregate_discrete_chain plugin loaded correctly.") diff --git a/sasmodels/models/free_rotating_chain.c b/sasmodels/models/free_rotating_chain.c new file mode 100644 index 00000000..e5141dd2 --- /dev/null +++ b/sasmodels/models/free_rotating_chain.c @@ -0,0 +1,62 @@ +/* + * 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. + * + * Sasmodels interface: + * Q - momentum transfer + * radius_effective - effective scatterer radius (half center-to-center distance) + * volfraction - unused (required for P@S parameter order) + * n_aggreg - real number of rotating points (RN) + */ + +double Iq(double Q, double radius_effective, double volfraction, double n_aggreg) +{ + int N_SPH, N_SPH1; + double W; + double ARG; + double SN, SN1; + double x; + double intensity; + + double RN = n_aggreg; + double d = 2.0 * radius_effective; + + (void)volfraction; + + /* 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 = sas_sinx_x(x); + + /* FOR N */ + SN = ((double)N_SPH) / (1.0 - ARG) + - ((double)N_SPH) / 2.0 + - (1.0 - pow(ARG, N_SPH)) / square(1.0 - ARG) * 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)) / square(1.0 - ARG) * ARG; + + SN1 = SN1 * 2.0 / (double)N_SPH1; + + /* linear interpolation */ + intensity = (1.0 - W) * SN + W * SN1; + + return intensity; +} diff --git a/sasmodels/models/free_rotating_chain.py b/sasmodels/models/free_rotating_chain.py new file mode 100644 index 00000000..a9da3bcf --- /dev/null +++ b/sasmodels/models/free_rotating_chain.py @@ -0,0 +1,92 @@ +r""" +Definition +---------- + +Free‑rotating chain structure factor: random flight. + +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. + +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. + +Parameters +---------- +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_aggreg : aggregation number :math:`RN`. + +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" +structure_factor = True +single = False + +# 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_aggreg", "", 20.0, [1.0, np.inf], "", "Aggregation number"], +] + +# Kernel source +source = ["free_rotating_chain.c"] + +def random(): + import random + + return { + "radius_effective": random.uniform(5.0, 25.0), + "volfraction": random.uniform(0.01, 0.3), + "n_aggreg": random.uniform(1.0, 100.0), + } + +def test(): + print("Structure factor of free‑rotating chain (free_rotating_chain)") diff --git a/sasmodels/models/linear_aggregate.c b/sasmodels/models/linear_aggregate.c new file mode 100644 index 00000000..c3214987 --- /dev/null +++ b/sasmodels/models/linear_aggregate.c @@ -0,0 +1,84 @@ +/* + * linear_aggregate + * + * Structure factor for a linear aggregate (Debye sum). + * + * Parameters: + * Q - momentum transfer + * radius_effective - effective scatterer radius (half center-to-center distance) + * volfraction - unused (required for P@S parameter order) + * n_aggreg - effective number of points (may be non-integer) + * + * Returns: + * linear_aggregate(Q) + * + * Notes: + * Exits with warning if n_aggreg > 100 + */ +static double sq_linN(double Q, int n_aggreg, double sep) +{ + int k; + double SUM, SN; + + double d = sep; + + if (n_aggreg <= 1) + return 1.0; + +/* + if (n_aggreg > 100) { + fprintf(stderr, + "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_aggreg - 1; k++) { + SUM += (double)(n_aggreg - k) * sas_sinx_x(Q * d * (double)k); + } + + SN = 1.0 + 2.0 * SUM / (double)n_aggreg; + + return SN; +} + +/* + * linear_aggregate (non-integer N) + * + * Linear-chain structure factor with non-integer effective length + * handled by linear interpolation. + * + * Parameters: + * Q - momentum transfer + * radius_effective - effective scatterer radius (half center-to-center distance) + * volfraction - unused + * n_aggreg - real (non-integer) number of points + * + * Returns: + * linear_aggregate(Q) + */ +double Iq(double Q, double radius_effective, double volfraction, double n_aggreg) +{ + int N; + double w; + double intensity; + + (void)volfraction; + + if (n_aggreg <= 1.0) + return 1.0; + + N = (int)n_aggreg; + w = n_aggreg - (double)N; + + double sep = 2.0 * radius_effective; + + intensity = + (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 new file mode 100644 index 00000000..1987714e --- /dev/null +++ b/sasmodels/models/linear_aggregate.py @@ -0,0 +1,90 @@ +r""" +Definition +---------- + +Linear aggregate structure factor. + +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 +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. + +Parameters +---------- +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_aggreg : aggregation number. + +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 + +name = "linear_aggregate" +title = "Linear aggregate structure factor" +description = """\ +Linear aggregate structure factor +""" +category = "structure-factor" +structure_factor = True + +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_aggreg", "", 50.0, [0.0, 100.0], "", "Aggregation number"], +] + +valid = "n_aggreg <= 100" + +source = ["linear_aggregate.c"] + +def random(): + import random + + return { + "radius_effective": random.uniform(5, 25), + "volfraction": random.uniform(0.01, 0.3), + "n_aggreg": random.uniform(1, 100), + } + +def test(): + print("Structure 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..6b2e60a8 --- /dev/null +++ b/sasmodels/models/stabilized_power_law.py @@ -0,0 +1,70 @@ +r""" +Definition +---------- + +Stabilized power-law structure factor. + +WARNING: Should not be used in combination with very anisotropic particle shapes. + +It uses the equation: + +.. math:: + + S(q) = 1.0 + \mathrm{power_law_scale}\,(0.01/q)^{\mathrm{power}} + +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 +here) 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 +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 = """\ +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 + +# ["name", "units", default, [lower, upper], "type","description"], +parameters = [ + ["radius_effective", "Ang", 50.0, [0.0, inf], "", + "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"], + ["power_law_scale", "", 100, [0, inf], "", "scale of power law"], + ["power", "", 2, [0, 6], "", "exponent of power law "], +] + + +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 + power_law_scale * (0.01 / q) ** power