ffsi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ffsi-0.1.0/LICENSE +28 -0
- ffsi-0.1.0/PKG-INFO +84 -0
- ffsi-0.1.0/README.md +50 -0
- ffsi-0.1.0/ffsi/__init__.py +16 -0
- ffsi-0.1.0/ffsi/api.py +191 -0
- ffsi-0.1.0/ffsi/array_module.py +42 -0
- ffsi-0.1.0/ffsi/crazy_distributions.py +34 -0
- ffsi-0.1.0/ffsi/optimize_galahad.py +252 -0
- ffsi-0.1.0/ffsi/optimize_galahad_bounded.py +292 -0
- ffsi-0.1.0/ffsi/plotting.py +190 -0
- ffsi-0.1.0/ffsi/sensitivity_analysis.py +180 -0
- ffsi-0.1.0/ffsi/utils.py +47 -0
- ffsi-0.1.0/ffsi.egg-info/PKG-INFO +84 -0
- ffsi-0.1.0/ffsi.egg-info/SOURCES.txt +17 -0
- ffsi-0.1.0/ffsi.egg-info/dependency_links.txt +1 -0
- ffsi-0.1.0/ffsi.egg-info/requires.txt +4 -0
- ffsi-0.1.0/ffsi.egg-info/top_level.txt +1 -0
- ffsi-0.1.0/pyproject.toml +48 -0
- ffsi-0.1.0/setup.cfg +4 -0
ffsi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, The Science and Technology Facilities Council (STFC)
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
ffsi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ffsi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A package for the free-form inversion of Small Angle Scattering problems arising from X-ray and Neutron sources
|
|
5
|
+
Author-email: Jaroslav Fowkes <jaroslav.fowkes@maths.ox.ac.uk>
|
|
6
|
+
Maintainer-email: Jaroslav Fowkes <jaroslav.fowkes@maths.ox.ac.uk>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://github.com/ralna/free-form-sas-inversion/
|
|
9
|
+
Project-URL: Download, https://github.com/ralna/free-form-sas-inversion/releases/
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/ralna/free-form-sas-inversion/issues/
|
|
11
|
+
Project-URL: Documentation, https://ralna.github.io/free-form-sas-inversion/
|
|
12
|
+
Project-URL: Source Code, https://github.com/ralna/free-form-sas-inversion/
|
|
13
|
+
Keywords: physics,optimization
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Environment :: GPU
|
|
17
|
+
Classifier: Environment :: GPU :: NVIDIA CUDA
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: Operating System :: Unix
|
|
20
|
+
Classifier: Operating System :: MacOS
|
|
21
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
22
|
+
Classifier: Programming Language :: Python
|
|
23
|
+
Classifier: Programming Language :: Python :: 3
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: setuptools
|
|
30
|
+
Requires-Dist: numpy
|
|
31
|
+
Requires-Dist: scipy
|
|
32
|
+
Requires-Dist: galahad-optrove
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# Free-Form SAS Inversion for SAXS/SANS
|
|
36
|
+
A package for the *free-form* inversion of Small Angle Scattering (SAS) problems arising from X-ray and Neutron sources.
|
|
37
|
+
|
|
38
|
+
This is a much improved algorithm and implementation over that proposed in the paper:
|
|
39
|
+
https://journals.iucr.org/j/issues/2022/04/00/jl5041/index.html
|
|
40
|
+
|
|
41
|
+
## SAS Inversion
|
|
42
|
+
Small Angle Scattering is used to probe and analyse the nanoscale structure of materials.
|
|
43
|
+
An excellent introduction to (fixed-form) SAS inversion can be found in:
|
|
44
|
+
https://journals.iucr.org/j/issues/2021/06/00/gj5274/gj5274.pdf
|
|
45
|
+
|
|
46
|
+
## Free-Form SAS Inversion
|
|
47
|
+
Free-form SAS inversion seeks to determine the distributions of structural properties of nanoparticles (e.g. radius, length),
|
|
48
|
+
but cruically (unlike conventional SAS inversion) does not make any assumptions about the forms these distributions take (hence *free-form*).
|
|
49
|
+
|
|
50
|
+
A Small Angle Scattering experiment records the intensity, i.e. squared amplitude, $I(q)$ of the scattered wave as a function of the scattering vector $q$.
|
|
51
|
+
The scattering vector $q$ is itself a function of the small angle $\theta$ and is given by
|
|
52
|
+
```math
|
|
53
|
+
q = \frac{4 \pi}{\lambda} \sin\left(\frac{\theta}{2}\right)
|
|
54
|
+
```
|
|
55
|
+
where $\lambda$ is the wavelength of the radiation source. For small angles, $\sin(\theta) \approx \theta$ and the scattering vector reduces to
|
|
56
|
+
```math
|
|
57
|
+
q \approx \frac{2 \pi}{\lambda}\theta
|
|
58
|
+
```
|
|
59
|
+
i.e. essentially the small angle normalised by the wavelength of the radiation.
|
|
60
|
+
|
|
61
|
+
### Form Factors (aka Green's Functions)
|
|
62
|
+
The form factor $F(q)$ describes the intensity $I(q)$ of scattering from a *single* nanoparticle over a full range of q and constitutes its SAS fingerprint.
|
|
63
|
+
For example, the form factor $F(q)$ for a sphere of radius $r$ is given via $j_1(z)$ the spherical Bessel function of the first kind as
|
|
64
|
+
```math
|
|
65
|
+
F(q) = \left[ V(r)\Delta\rho \dfrac{3j_1(qr)}{qr} \right]^2
|
|
66
|
+
```
|
|
67
|
+
where $V$ is the sphere volume and $\Delta\rho$ the scattering length density difference.
|
|
68
|
+
|
|
69
|
+
This repository currently contains the form factors for:
|
|
70
|
+
|
|
71
|
+
- [Sphere (1D)](https://www.sasview.org/docs/user/models/sphere.html)
|
|
72
|
+
- [Cylinder (1D and 2D)](https://www.sasview.org/docs/user/models/cylinder.html)
|
|
73
|
+
- [Ellipsoid (1D and 2D)](https://www.sasview.org/docs/user/models/ellipsoid.html)
|
|
74
|
+
|
|
75
|
+
More form-factors will be added as the project progresses (as the code in this repository is vectorized and GPU accelerated, we are unable to directly use the form factors from SASView).
|
|
76
|
+
|
|
77
|
+
### Polydispersity
|
|
78
|
+
In a real SAS experiment, we are likely to see a population of nanoparticles that possess size and/or orientational distributions, this is called *polydispersity*.
|
|
79
|
+
The resultant intensity $I(q)$ is then averaged over the distributions.
|
|
80
|
+
For example, for spheres with different radii,
|
|
81
|
+
```math
|
|
82
|
+
I(q) = \int F(q,r) w(r) dr
|
|
83
|
+
```
|
|
84
|
+
where $w(r)$ is the distribution over the radius.
|
ffsi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Free-Form SAS Inversion for SAXS/SANS
|
|
2
|
+
A package for the *free-form* inversion of Small Angle Scattering (SAS) problems arising from X-ray and Neutron sources.
|
|
3
|
+
|
|
4
|
+
This is a much improved algorithm and implementation over that proposed in the paper:
|
|
5
|
+
https://journals.iucr.org/j/issues/2022/04/00/jl5041/index.html
|
|
6
|
+
|
|
7
|
+
## SAS Inversion
|
|
8
|
+
Small Angle Scattering is used to probe and analyse the nanoscale structure of materials.
|
|
9
|
+
An excellent introduction to (fixed-form) SAS inversion can be found in:
|
|
10
|
+
https://journals.iucr.org/j/issues/2021/06/00/gj5274/gj5274.pdf
|
|
11
|
+
|
|
12
|
+
## Free-Form SAS Inversion
|
|
13
|
+
Free-form SAS inversion seeks to determine the distributions of structural properties of nanoparticles (e.g. radius, length),
|
|
14
|
+
but cruically (unlike conventional SAS inversion) does not make any assumptions about the forms these distributions take (hence *free-form*).
|
|
15
|
+
|
|
16
|
+
A Small Angle Scattering experiment records the intensity, i.e. squared amplitude, $I(q)$ of the scattered wave as a function of the scattering vector $q$.
|
|
17
|
+
The scattering vector $q$ is itself a function of the small angle $\theta$ and is given by
|
|
18
|
+
```math
|
|
19
|
+
q = \frac{4 \pi}{\lambda} \sin\left(\frac{\theta}{2}\right)
|
|
20
|
+
```
|
|
21
|
+
where $\lambda$ is the wavelength of the radiation source. For small angles, $\sin(\theta) \approx \theta$ and the scattering vector reduces to
|
|
22
|
+
```math
|
|
23
|
+
q \approx \frac{2 \pi}{\lambda}\theta
|
|
24
|
+
```
|
|
25
|
+
i.e. essentially the small angle normalised by the wavelength of the radiation.
|
|
26
|
+
|
|
27
|
+
### Form Factors (aka Green's Functions)
|
|
28
|
+
The form factor $F(q)$ describes the intensity $I(q)$ of scattering from a *single* nanoparticle over a full range of q and constitutes its SAS fingerprint.
|
|
29
|
+
For example, the form factor $F(q)$ for a sphere of radius $r$ is given via $j_1(z)$ the spherical Bessel function of the first kind as
|
|
30
|
+
```math
|
|
31
|
+
F(q) = \left[ V(r)\Delta\rho \dfrac{3j_1(qr)}{qr} \right]^2
|
|
32
|
+
```
|
|
33
|
+
where $V$ is the sphere volume and $\Delta\rho$ the scattering length density difference.
|
|
34
|
+
|
|
35
|
+
This repository currently contains the form factors for:
|
|
36
|
+
|
|
37
|
+
- [Sphere (1D)](https://www.sasview.org/docs/user/models/sphere.html)
|
|
38
|
+
- [Cylinder (1D and 2D)](https://www.sasview.org/docs/user/models/cylinder.html)
|
|
39
|
+
- [Ellipsoid (1D and 2D)](https://www.sasview.org/docs/user/models/ellipsoid.html)
|
|
40
|
+
|
|
41
|
+
More form-factors will be added as the project progresses (as the code in this repository is vectorized and GPU accelerated, we are unable to directly use the form factors from SASView).
|
|
42
|
+
|
|
43
|
+
### Polydispersity
|
|
44
|
+
In a real SAS experiment, we are likely to see a population of nanoparticles that possess size and/or orientational distributions, this is called *polydispersity*.
|
|
45
|
+
The resultant intensity $I(q)$ is then averaged over the distributions.
|
|
46
|
+
For example, for spheres with different radii,
|
|
47
|
+
```math
|
|
48
|
+
I(q) = \int F(q,r) w(r) dr
|
|
49
|
+
```
|
|
50
|
+
where $w(r)$ is the distribution over the radius.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Free-Form SAS Inversion
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 The Science and Technology Facilities Council (STFC)
|
|
5
|
+
Author: Jaroslav Fowkes (STFC)
|
|
6
|
+
"""
|
|
7
|
+
CUPY_INSTALLED = False
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import cupy as _cupy
|
|
11
|
+
CUPY_INSTALLED = True
|
|
12
|
+
print('INFO: CuPy is installed, GPU computation is available')
|
|
13
|
+
except Exception as e:
|
|
14
|
+
print('WARNING: CuPy is not installed, cannot use GPU computation')
|
|
15
|
+
print(e)
|
|
16
|
+
print('WARNING: continuing with CPU computation only')
|
ffsi-0.1.0/ffsi/api.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API for free-form SAS inversion.
|
|
3
|
+
"""
|
|
4
|
+
import importlib.util
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
from inspect import getmembers, isabstract, isclass
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ffsi.array_module import get_array_module, to_device, from_device
|
|
12
|
+
from ffsi.models.basemodel import SASModel
|
|
13
|
+
from ffsi.optimize_galahad import optimize
|
|
14
|
+
from ffsi.utils import contract_tensor, xi_to_scale
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Model names available through invert(), for error messages only
|
|
18
|
+
_MODEL_NAMES = ("sphere", "cylinder", "cylinder2d", "ellipsoid", "ellipsoid2d")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_model(model):
|
|
22
|
+
"""
|
|
23
|
+
Resolve `model` (a case-insensitive name or a `SASModel` subclass) to
|
|
24
|
+
`(class, name)`.
|
|
25
|
+
|
|
26
|
+
The model class is imported lazily from its own module `ffsi.models.<name>`
|
|
27
|
+
(module name == class name lowercased), so only the requested model is
|
|
28
|
+
loaded rather than every model up front.
|
|
29
|
+
"""
|
|
30
|
+
name = (model if isinstance(model, str) else model.__name__).lower()
|
|
31
|
+
|
|
32
|
+
# return a proper error
|
|
33
|
+
if importlib.util.find_spec(f"ffsi.models.{name}") is None:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Unknown model '{}', available models: {}".format(
|
|
36
|
+
name, ", ".join(_MODEL_NAMES)
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
module = import_module(f"ffsi.models.{name}")
|
|
41
|
+
|
|
42
|
+
classes = getmembers(
|
|
43
|
+
module,
|
|
44
|
+
lambda m: (
|
|
45
|
+
isclass(m)
|
|
46
|
+
and not isabstract(m)
|
|
47
|
+
and issubclass(m, SASModel)
|
|
48
|
+
and m is not SASModel
|
|
49
|
+
and m.__module__ == module.__name__
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
if not classes:
|
|
53
|
+
raise ValueError(f"Module 'ffsi.models.{name}' defines no SASModel subclass")
|
|
54
|
+
|
|
55
|
+
return classes[0][1], name
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ParamDistribution:
|
|
60
|
+
"""Fitted distribution of one model parameter."""
|
|
61
|
+
|
|
62
|
+
name: str # model parameter name, e.g. 'r', 'l', 'rp', 're'
|
|
63
|
+
grid: np.ndarray # bin centers
|
|
64
|
+
weights: np.ndarray # weights
|
|
65
|
+
|
|
66
|
+
volume_weights: np.ndarray = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class InversionResult:
|
|
71
|
+
"""Output of `invert()`: I_opt = xi * Gw + background."""
|
|
72
|
+
|
|
73
|
+
model: str # model name
|
|
74
|
+
xi: float # raw scale factor
|
|
75
|
+
background: float
|
|
76
|
+
distributions: list = field(default_factory=list)
|
|
77
|
+
theory: np.ndarray = None # I_opt on the input q
|
|
78
|
+
residuals: np.ndarray = None # (theory - intensity) / intensity_std
|
|
79
|
+
chi2: float = None # sum(residuals**2) / residuals.size
|
|
80
|
+
average_volume: float = None # <V> under the optimal weights
|
|
81
|
+
drho: float = None # contrast: sld - sld_solvent
|
|
82
|
+
scale: float = None # volume fraction: xi * <V> * 1e4
|
|
83
|
+
|
|
84
|
+
def distribution(self, name):
|
|
85
|
+
"""The fitted `ParamDistribution` for parameter `name`."""
|
|
86
|
+
for dist in self.distributions:
|
|
87
|
+
if dist.name == name:
|
|
88
|
+
return dist
|
|
89
|
+
raise KeyError("No distribution for parameter '{}', have: {}".format(name, ", ".join(d.name for d in self.distributions)))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _build_grid(spec, xp):
|
|
93
|
+
"""Bin centers (on backend `xp`) from a (min, max, nbins) triple"""
|
|
94
|
+
if isinstance(spec, np.ndarray) or (np.ndim(spec) == 1 and len(spec) > 3):
|
|
95
|
+
return xp.asarray(spec, dtype=float)
|
|
96
|
+
lo, hi, nbins = spec
|
|
97
|
+
return xp.linspace(float(lo), float(hi), int(nbins))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def invert(model, q, intensity, intensity_std, grids, *, sld, sld_solvent, sigma=None):
|
|
101
|
+
"""
|
|
102
|
+
Free-form inversion of 1D SAS data.
|
|
103
|
+
|
|
104
|
+
:param model: model name ('sphere', 'cylinder', 'ellipsoid';
|
|
105
|
+
:param q: scattering vectors
|
|
106
|
+
:param intensity: measured intensity `I(q)`
|
|
107
|
+
:param intensity_std: intensity standard deviations `dI(q)`
|
|
108
|
+
:param grids: `dict` keyed by the model's parameter names; each value
|
|
109
|
+
is a `(min, max, nbins)` triple or a prebuilt 1D array of bin centers
|
|
110
|
+
:param sld: scattering length density of the particle,
|
|
111
|
+
with `sld_solvent` it gives the contrast `drho = sld - sld_solvent`
|
|
112
|
+
:param sld_solvent: scattering length density of the solvent, in 1e-6 A^-2
|
|
113
|
+
:param sigma: smoothness regularization weight (`None` disables it)
|
|
114
|
+
:return: an `InversionResult`; `scale` is the volume fraction `xi * <V> * 1e4`
|
|
115
|
+
|
|
116
|
+
Computation runs on the GPU automatically whenever CuPy is installed; inputs
|
|
117
|
+
may be plain numpy arrays (they are moved onto the GPU here), so callers need
|
|
118
|
+
no CuPy dependency of their own. Without CuPy it runs on the CPU.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# resolve a name or a SASModel subclass to (class, name)
|
|
122
|
+
model_class, model_name = _resolve_model(model)
|
|
123
|
+
param_names = list(model_class.param_names_scattering_intensity)
|
|
124
|
+
|
|
125
|
+
# contrast: drho = sld - sld_solvent
|
|
126
|
+
drho = float(sld) - float(sld_solvent)
|
|
127
|
+
|
|
128
|
+
# move host inputs onto the compute backend (GPU when CuPy is available)
|
|
129
|
+
# array-module dispatch below and everything downstream run on that backend
|
|
130
|
+
q, intensity, intensity_std = to_device(q, intensity, intensity_std)
|
|
131
|
+
|
|
132
|
+
# xp resolves to cupy when the inputs are on the GPU, else numpy
|
|
133
|
+
xp = get_array_module(q, intensity, intensity_std)
|
|
134
|
+
q = xp.ascontiguousarray(q, dtype=float)
|
|
135
|
+
intensity = xp.asarray(intensity, dtype=float)
|
|
136
|
+
intensity_std = xp.asarray(intensity_std, dtype=float)
|
|
137
|
+
# build grids on the same backend
|
|
138
|
+
param_list = [_build_grid(grids[name], xp) for name in param_names]
|
|
139
|
+
|
|
140
|
+
# scattering intensity (Green's tensor) and inversion
|
|
141
|
+
G = model_class.compute_scattering_intensity([q], param_list, drho)
|
|
142
|
+
xi, background, w_opt_list = optimize(G, intensity, intensity_std, sigma=sigma)
|
|
143
|
+
xi, background = float(xi), float(background)
|
|
144
|
+
# GALAHAD returns numpy weights; move them onto G's backend to reconstruct
|
|
145
|
+
w_list = [xp.asarray(w) for w in w_opt_list]
|
|
146
|
+
|
|
147
|
+
# fitted intensity, residuals and chi-squared
|
|
148
|
+
theory = xi * contract_tensor(G, w_list, skip_axes=[0]) + background
|
|
149
|
+
residuals = (theory - intensity) / intensity_std
|
|
150
|
+
chi2 = float(xp.sum(residuals**2) / residuals.size)
|
|
151
|
+
|
|
152
|
+
# average volume
|
|
153
|
+
volume_params = [param_list[param_names.index(name)]
|
|
154
|
+
for name in model_class.param_names_average_volume]
|
|
155
|
+
volume_weights_list = [w_list[param_names.index(name)]
|
|
156
|
+
for name in model_class.param_names_average_volume]
|
|
157
|
+
average_volume = float(model_class.compute_average_volume(volume_params, volume_weights_list))
|
|
158
|
+
|
|
159
|
+
# convert xi to SasView scale
|
|
160
|
+
scale = xi_to_scale(xi, average_volume)
|
|
161
|
+
|
|
162
|
+
# package results as host numpy as plotters and the GUI
|
|
163
|
+
# cannot take cupy arrays
|
|
164
|
+
distributions = []
|
|
165
|
+
for name, grid, weights in zip(param_names, param_list, w_list):
|
|
166
|
+
volume_weights = None
|
|
167
|
+
if len(param_names) == 1:
|
|
168
|
+
volume = model_class.compute_volume(param_list)
|
|
169
|
+
weighted = weights * volume
|
|
170
|
+
volume_weights = from_device(weighted / xp.sum(weighted))
|
|
171
|
+
distributions.append(
|
|
172
|
+
ParamDistribution(
|
|
173
|
+
name=name,
|
|
174
|
+
grid=from_device(grid),
|
|
175
|
+
weights=from_device(weights),
|
|
176
|
+
volume_weights=volume_weights,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return InversionResult(
|
|
181
|
+
model=model_name,
|
|
182
|
+
xi=xi,
|
|
183
|
+
background=background,
|
|
184
|
+
distributions=distributions,
|
|
185
|
+
theory=from_device(theory),
|
|
186
|
+
residuals=from_device(residuals),
|
|
187
|
+
chi2=chi2,
|
|
188
|
+
average_volume=average_volume,
|
|
189
|
+
drho=drho,
|
|
190
|
+
scale=scale,
|
|
191
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Get array module that works when CuPy is not installed
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 The Science and Technology Facilities Council (STFC)
|
|
5
|
+
Author: Jaroslav Fowkes (STFC)
|
|
6
|
+
"""
|
|
7
|
+
import numpy as _numpy
|
|
8
|
+
import scipy as _scipy
|
|
9
|
+
|
|
10
|
+
from ffsi import CUPY_INSTALLED
|
|
11
|
+
|
|
12
|
+
def get_array_module(*args):
|
|
13
|
+
if CUPY_INSTALLED:
|
|
14
|
+
import cupy as cp
|
|
15
|
+
return cp.get_array_module(*args)
|
|
16
|
+
else:
|
|
17
|
+
return _numpy
|
|
18
|
+
|
|
19
|
+
def get_science_module(*args):
|
|
20
|
+
if CUPY_INSTALLED:
|
|
21
|
+
import cupyx.scipy as cps
|
|
22
|
+
return cps.get_array_module(*args)
|
|
23
|
+
else:
|
|
24
|
+
return _scipy
|
|
25
|
+
|
|
26
|
+
def to_device(*arrays):
|
|
27
|
+
"""
|
|
28
|
+
Bring arrays from the host to the device.
|
|
29
|
+
"""
|
|
30
|
+
if CUPY_INSTALLED:
|
|
31
|
+
import cupy as cp
|
|
32
|
+
return (cp.asarray(a) for a in arrays)
|
|
33
|
+
return arrays
|
|
34
|
+
|
|
35
|
+
def from_device(array):
|
|
36
|
+
"""
|
|
37
|
+
Bring an array from the device to the host.
|
|
38
|
+
"""
|
|
39
|
+
if CUPY_INSTALLED:
|
|
40
|
+
import cupy as cp
|
|
41
|
+
return cp.asnumpy(array)
|
|
42
|
+
return array
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Crazy distributions generator
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 The Science and Technology Facilities Council (STFC)
|
|
5
|
+
Author: Jaroslav Fowkes (STFC)
|
|
6
|
+
"""
|
|
7
|
+
from ffsi.array_module import get_array_module
|
|
8
|
+
|
|
9
|
+
def crazy_distribution(x, gaussians, noise_level, fade_start, fade_end, seed=0):
|
|
10
|
+
|
|
11
|
+
# use CPU or GPU as appropriate
|
|
12
|
+
xp = get_array_module(x)
|
|
13
|
+
|
|
14
|
+
# create
|
|
15
|
+
w_true = xp.zeros(x.shape)
|
|
16
|
+
|
|
17
|
+
# add Gaussians
|
|
18
|
+
for factor, mean, stddev in gaussians:
|
|
19
|
+
w_true += factor * xp.exp(-((x - mean) / stddev) ** 2)
|
|
20
|
+
|
|
21
|
+
# add noise
|
|
22
|
+
xp.random.seed(seed)
|
|
23
|
+
w_true += noise_level * xp.random.rand(*x.shape) * xp.random.rand(*x.shape)
|
|
24
|
+
|
|
25
|
+
# fade both ends to make it look nicer
|
|
26
|
+
if len(x) >= 3:
|
|
27
|
+
w_true[0:fade_start] = 0.
|
|
28
|
+
w_true[fade_start:fade_end] *= xp.linspace(0, 1, fade_end - fade_start)
|
|
29
|
+
w_true[-fade_start:] = 0.
|
|
30
|
+
w_true[-fade_end:-fade_start] *= xp.linspace(1, 0, fade_end - fade_start)
|
|
31
|
+
|
|
32
|
+
# normalize to 1
|
|
33
|
+
w_true /= xp.sum(w_true)
|
|
34
|
+
return w_true
|