midas-saxs 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.
- midas_saxs-0.1.0/LICENSE +31 -0
- midas_saxs-0.1.0/PKG-INFO +130 -0
- midas_saxs-0.1.0/README.md +97 -0
- midas_saxs-0.1.0/midas_saxs/__init__.py +105 -0
- midas_saxs-0.1.0/midas_saxs/core_shell.py +122 -0
- midas_saxs-0.1.0/midas_saxs/detector.py +234 -0
- midas_saxs-0.1.0/midas_saxs/form_factors.py +212 -0
- midas_saxs-0.1.0/midas_saxs/geometry.py +208 -0
- midas_saxs-0.1.0/midas_saxs/model.py +165 -0
- midas_saxs-0.1.0/midas_saxs/particles.py +145 -0
- midas_saxs-0.1.0/midas_saxs/strain_source.py +198 -0
- midas_saxs-0.1.0/midas_saxs/wide_band.py +265 -0
- midas_saxs-0.1.0/midas_saxs.egg-info/PKG-INFO +130 -0
- midas_saxs-0.1.0/midas_saxs.egg-info/SOURCES.txt +18 -0
- midas_saxs-0.1.0/midas_saxs.egg-info/dependency_links.txt +1 -0
- midas_saxs-0.1.0/midas_saxs.egg-info/requires.txt +16 -0
- midas_saxs-0.1.0/midas_saxs.egg-info/top_level.txt +1 -0
- midas_saxs-0.1.0/pyproject.toml +64 -0
- midas_saxs-0.1.0/setup.cfg +4 -0
- midas_saxs-0.1.0/tests/test_saxs_forward.py +474 -0
midas_saxs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, UChicago Argonne, LLC, operator of Argonne National
|
|
4
|
+
Laboratory, and the midas-diffract authors.
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
24
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
25
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
26
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
27
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
28
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
29
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
30
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
31
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: midas-saxs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Differentiable small-angle X-ray scattering for MIDAS: particle form factors and polydispersity, plus a 2-D detector-image forward model for dislocation loops and voids driven by a discrete-dislocation network.
|
|
5
|
+
Author-email: Hemant Sharma <hsharma@anl.gov>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/marinerhemant/MIDAS
|
|
8
|
+
Project-URL: Documentation, https://github.com/marinerhemant/MIDAS/tree/master/packages/midas_saxs
|
|
9
|
+
Project-URL: Issues, https://github.com/marinerhemant/MIDAS/issues
|
|
10
|
+
Keywords: MIDAS,SAXS,USAXS,small-angle scattering,X-ray,dislocation loop,irradiation,void,form factor,differentiable,PyTorch
|
|
11
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy>=1.22
|
|
20
|
+
Requires-Dist: torch>=2.0
|
|
21
|
+
Requires-Dist: midas-hkls>=0.9.0
|
|
22
|
+
Requires-Dist: midas-transforms>=0.10.0
|
|
23
|
+
Requires-Dist: midas-distortion>=0.2.0
|
|
24
|
+
Provides-Extra: dislocations
|
|
25
|
+
Requires-Dist: midas-ddd>=0.1.0; extra == "dislocations"
|
|
26
|
+
Provides-Extra: viz
|
|
27
|
+
Requires-Dist: matplotlib>=3.5; extra == "viz"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
|
+
Requires-Dist: matplotlib>=3.5; extra == "dev"
|
|
31
|
+
Requires-Dist: midas-ddd>=0.1.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# midas-saxs
|
|
35
|
+
|
|
36
|
+
Differentiable small-angle X-ray scattering for MIDAS. Two source terms, one
|
|
37
|
+
detector:
|
|
38
|
+
|
|
39
|
+
- **Density contrast** — voids, gas bubbles, precipitates. Sphere / ellipsoid /
|
|
40
|
+
cylinder form factors, lognormal polydispersity, Percus-Yevick `S(Q)`, Guinier
|
|
41
|
+
and Porod analysis. These came from `midas_pdf.saxs`, which now re-exports them
|
|
42
|
+
from here, so MIDAS has one definition of "SAXS form factor".
|
|
43
|
+
- **Strain contrast** — dislocation loops, driven by a `midas_ddd` network read
|
|
44
|
+
from ExaDiS. Optional extra: `pip install 'midas-saxs[dislocations]'`.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from midas_saxs import SAXSGeometry, SpherePopulation, simulate_frame
|
|
48
|
+
|
|
49
|
+
geom = SAXSGeometry(lsd_um=2.0e6, bcy_px=512, bcz_px=512, px_um=75.0,
|
|
50
|
+
wavelength_A=0.7293, n_pix_y=1024, n_pix_z=1024,
|
|
51
|
+
beamstop_radius_px=30)
|
|
52
|
+
voids = SpherePopulation(radius_A=50.0, number_density_per_A3=1e-8,
|
|
53
|
+
delta_rho_e_per_A3=-2.45, label="voids")
|
|
54
|
+
frame = simulate_frame(geom, particles=[voids], sample_volume_A3=1e15)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`dev/paper/loop_vs_void_demo.py` renders the five-panel figure that motivates the
|
|
58
|
+
whole package.
|
|
59
|
+
|
|
60
|
+
## Read this before promising anyone a loop measurement
|
|
61
|
+
|
|
62
|
+
Three numbers, all reproducible from the demo script.
|
|
63
|
+
|
|
64
|
+
**1. Loops are faint.** A dislocation loop of radius R displaces only its
|
|
65
|
+
relaxation volume `pi R^2 b`; a void of the same radius displaces
|
|
66
|
+
`4 pi R^3 / 3`. For R = 5 nm in Cu that is 26x in volume and **680x in forward
|
|
67
|
+
intensity**. The loop scatters like a *sphere of radius 1.7 nm*.
|
|
68
|
+
|
|
69
|
+
**2. What separates a loop from a void is shape, not size.** The loop's
|
|
70
|
+
small-angle amplitude is direction-dependent — `kappa dV` in its own plane,
|
|
71
|
+
`dV` along its normal, with `kappa = lambda/(lambda + 2 mu)`. Intensity is
|
|
72
|
+
therefore modulated by `1 - kappa^2` (0.84 for `lambda = 100, mu = 75`). A void
|
|
73
|
+
is isotropic. **Radially averaging a frame destroys exactly this**, which is why
|
|
74
|
+
this package renders 2-D frames and offers `azimuthal_profile` alongside
|
|
75
|
+
`radial_average`.
|
|
76
|
+
|
|
77
|
+
**3. Real voids bury the signature.** Measured on the demo: 24 aligned loops
|
|
78
|
+
alone give an azimuthal contrast of **6.30x** (the closed form predicts
|
|
79
|
+
`1/kappa^2 = 6.25`). Add equal-radius voids at the same number density — the
|
|
80
|
+
physically realistic irradiated case — and the contrast collapses to **1.002x**,
|
|
81
|
+
with the voids outshining the loops by **1089x** integrated over the frame.
|
|
82
|
+
|
|
83
|
+
So: loops are detectable at small angle in a clean matrix, and essentially
|
|
84
|
+
undetectable alongside a void or bubble population of comparable size. If the
|
|
85
|
+
question is defect-type discrimination in an irradiated material, near-Bragg
|
|
86
|
+
diffuse (Huang) scattering is stronger than the small-angle signal by
|
|
87
|
+
`(G/q)^2` ~ 1e4 to 1e7 and is the better measurement.
|
|
88
|
+
|
|
89
|
+
## Scope
|
|
90
|
+
|
|
91
|
+
**Closed dislocation loops only.** A cut surface exists only for a closed
|
|
92
|
+
circuit. Open lines — the *deformation* population — enclose no area, carry no
|
|
93
|
+
relaxation volume, and contribute nothing as `q -> 0`; their small-angle
|
|
94
|
+
signature is a weak transverse streak that is not modelled. `simulate_frame`
|
|
95
|
+
reports how much line length it ignored rather than returning a number that
|
|
96
|
+
looks complete.
|
|
97
|
+
|
|
98
|
+
## Units
|
|
99
|
+
|
|
100
|
+
| Quantity | Unit |
|
|
101
|
+
|---|---|
|
|
102
|
+
| distances, geometry | micrometers |
|
|
103
|
+
| wavelength, particle radii, electron density | angstroms |
|
|
104
|
+
| `q` from `midas_saxs.geometry` | **inverse angstroms** |
|
|
105
|
+
| `q` into the `midas_ddd` kernel | **inverse micrometers** |
|
|
106
|
+
|
|
107
|
+
The 1e4 between the last two is easy to lose, so `inv_A_to_inv_um` /
|
|
108
|
+
`inv_um_to_inv_A` exist and `strain_source` uses them rather than a literal.
|
|
109
|
+
|
|
110
|
+
## Detector geometry
|
|
111
|
+
|
|
112
|
+
`pixel_to_q` goes through `midas_transforms.apply_tilt_distortion` — the same
|
|
113
|
+
tilt (`R_z R_y R_x`) and 15-coefficient distortion model the HEDM side uses.
|
|
114
|
+
That is deliberate: a SAXS geometry and an FF geometry calibrated from the same
|
|
115
|
+
detector must agree about where q sits. Transmission SAXS has no omega, so lab q
|
|
116
|
+
is sample q.
|
|
117
|
+
|
|
118
|
+
## Migration note
|
|
119
|
+
|
|
120
|
+
`form_factors`, `model`, `wide_band` and `core_shell` moved here from
|
|
121
|
+
`midas_pdf.saxs`. Both the package-level path (`from midas_pdf.saxs import
|
|
122
|
+
SAXSModel`) and the deep path (`from midas_pdf.saxs.form_factors import ...`)
|
|
123
|
+
still resolve, to the same objects — asserted by
|
|
124
|
+
`test_midas_pdf_reexports_the_same_objects`. `midas_pdf.saxs` keeps the genuinely
|
|
125
|
+
PDF-coupled part: joint SAXS + PDF refinement and its Bayesian variants.
|
|
126
|
+
|
|
127
|
+
One convention worth knowing: `sphere_form_factor_squared` returns `V^2 |F|^2`,
|
|
128
|
+
**not** the normalised `|F|^2`. `SpherePopulation.intensity` multiplies it by
|
|
129
|
+
`n * delta_rho^2` and nothing else, which is only correct because the volume is
|
|
130
|
+
already in there.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# midas-saxs
|
|
2
|
+
|
|
3
|
+
Differentiable small-angle X-ray scattering for MIDAS. Two source terms, one
|
|
4
|
+
detector:
|
|
5
|
+
|
|
6
|
+
- **Density contrast** — voids, gas bubbles, precipitates. Sphere / ellipsoid /
|
|
7
|
+
cylinder form factors, lognormal polydispersity, Percus-Yevick `S(Q)`, Guinier
|
|
8
|
+
and Porod analysis. These came from `midas_pdf.saxs`, which now re-exports them
|
|
9
|
+
from here, so MIDAS has one definition of "SAXS form factor".
|
|
10
|
+
- **Strain contrast** — dislocation loops, driven by a `midas_ddd` network read
|
|
11
|
+
from ExaDiS. Optional extra: `pip install 'midas-saxs[dislocations]'`.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from midas_saxs import SAXSGeometry, SpherePopulation, simulate_frame
|
|
15
|
+
|
|
16
|
+
geom = SAXSGeometry(lsd_um=2.0e6, bcy_px=512, bcz_px=512, px_um=75.0,
|
|
17
|
+
wavelength_A=0.7293, n_pix_y=1024, n_pix_z=1024,
|
|
18
|
+
beamstop_radius_px=30)
|
|
19
|
+
voids = SpherePopulation(radius_A=50.0, number_density_per_A3=1e-8,
|
|
20
|
+
delta_rho_e_per_A3=-2.45, label="voids")
|
|
21
|
+
frame = simulate_frame(geom, particles=[voids], sample_volume_A3=1e15)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`dev/paper/loop_vs_void_demo.py` renders the five-panel figure that motivates the
|
|
25
|
+
whole package.
|
|
26
|
+
|
|
27
|
+
## Read this before promising anyone a loop measurement
|
|
28
|
+
|
|
29
|
+
Three numbers, all reproducible from the demo script.
|
|
30
|
+
|
|
31
|
+
**1. Loops are faint.** A dislocation loop of radius R displaces only its
|
|
32
|
+
relaxation volume `pi R^2 b`; a void of the same radius displaces
|
|
33
|
+
`4 pi R^3 / 3`. For R = 5 nm in Cu that is 26x in volume and **680x in forward
|
|
34
|
+
intensity**. The loop scatters like a *sphere of radius 1.7 nm*.
|
|
35
|
+
|
|
36
|
+
**2. What separates a loop from a void is shape, not size.** The loop's
|
|
37
|
+
small-angle amplitude is direction-dependent — `kappa dV` in its own plane,
|
|
38
|
+
`dV` along its normal, with `kappa = lambda/(lambda + 2 mu)`. Intensity is
|
|
39
|
+
therefore modulated by `1 - kappa^2` (0.84 for `lambda = 100, mu = 75`). A void
|
|
40
|
+
is isotropic. **Radially averaging a frame destroys exactly this**, which is why
|
|
41
|
+
this package renders 2-D frames and offers `azimuthal_profile` alongside
|
|
42
|
+
`radial_average`.
|
|
43
|
+
|
|
44
|
+
**3. Real voids bury the signature.** Measured on the demo: 24 aligned loops
|
|
45
|
+
alone give an azimuthal contrast of **6.30x** (the closed form predicts
|
|
46
|
+
`1/kappa^2 = 6.25`). Add equal-radius voids at the same number density — the
|
|
47
|
+
physically realistic irradiated case — and the contrast collapses to **1.002x**,
|
|
48
|
+
with the voids outshining the loops by **1089x** integrated over the frame.
|
|
49
|
+
|
|
50
|
+
So: loops are detectable at small angle in a clean matrix, and essentially
|
|
51
|
+
undetectable alongside a void or bubble population of comparable size. If the
|
|
52
|
+
question is defect-type discrimination in an irradiated material, near-Bragg
|
|
53
|
+
diffuse (Huang) scattering is stronger than the small-angle signal by
|
|
54
|
+
`(G/q)^2` ~ 1e4 to 1e7 and is the better measurement.
|
|
55
|
+
|
|
56
|
+
## Scope
|
|
57
|
+
|
|
58
|
+
**Closed dislocation loops only.** A cut surface exists only for a closed
|
|
59
|
+
circuit. Open lines — the *deformation* population — enclose no area, carry no
|
|
60
|
+
relaxation volume, and contribute nothing as `q -> 0`; their small-angle
|
|
61
|
+
signature is a weak transverse streak that is not modelled. `simulate_frame`
|
|
62
|
+
reports how much line length it ignored rather than returning a number that
|
|
63
|
+
looks complete.
|
|
64
|
+
|
|
65
|
+
## Units
|
|
66
|
+
|
|
67
|
+
| Quantity | Unit |
|
|
68
|
+
|---|---|
|
|
69
|
+
| distances, geometry | micrometers |
|
|
70
|
+
| wavelength, particle radii, electron density | angstroms |
|
|
71
|
+
| `q` from `midas_saxs.geometry` | **inverse angstroms** |
|
|
72
|
+
| `q` into the `midas_ddd` kernel | **inverse micrometers** |
|
|
73
|
+
|
|
74
|
+
The 1e4 between the last two is easy to lose, so `inv_A_to_inv_um` /
|
|
75
|
+
`inv_um_to_inv_A` exist and `strain_source` uses them rather than a literal.
|
|
76
|
+
|
|
77
|
+
## Detector geometry
|
|
78
|
+
|
|
79
|
+
`pixel_to_q` goes through `midas_transforms.apply_tilt_distortion` — the same
|
|
80
|
+
tilt (`R_z R_y R_x`) and 15-coefficient distortion model the HEDM side uses.
|
|
81
|
+
That is deliberate: a SAXS geometry and an FF geometry calibrated from the same
|
|
82
|
+
detector must agree about where q sits. Transmission SAXS has no omega, so lab q
|
|
83
|
+
is sample q.
|
|
84
|
+
|
|
85
|
+
## Migration note
|
|
86
|
+
|
|
87
|
+
`form_factors`, `model`, `wide_band` and `core_shell` moved here from
|
|
88
|
+
`midas_pdf.saxs`. Both the package-level path (`from midas_pdf.saxs import
|
|
89
|
+
SAXSModel`) and the deep path (`from midas_pdf.saxs.form_factors import ...`)
|
|
90
|
+
still resolve, to the same objects — asserted by
|
|
91
|
+
`test_midas_pdf_reexports_the_same_objects`. `midas_pdf.saxs` keeps the genuinely
|
|
92
|
+
PDF-coupled part: joint SAXS + PDF refinement and its Bayesian variants.
|
|
93
|
+
|
|
94
|
+
One convention worth knowing: `sphere_form_factor_squared` returns `V^2 |F|^2`,
|
|
95
|
+
**not** the normalised `|F|^2`. `SpherePopulation.intensity` multiplies it by
|
|
96
|
+
`n * delta_rho^2` and nothing else, which is only correct because the volume is
|
|
97
|
+
already in there.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""midas-saxs — differentiable small-angle X-ray scattering for MIDAS.
|
|
2
|
+
|
|
3
|
+
Two source terms, one detector.
|
|
4
|
+
|
|
5
|
+
**Density contrast** (:mod:`midas_saxs.particles`, :mod:`midas_saxs.form_factors`,
|
|
6
|
+
:mod:`midas_saxs.model`, :mod:`midas_saxs.core_shell`, :mod:`midas_saxs.wide_band`)
|
|
7
|
+
— voids, bubbles, precipitates; sphere / ellipsoid / cylinder form factors,
|
|
8
|
+
lognormal polydispersity, Percus-Yevick S(Q), Guinier and Porod analysis. These
|
|
9
|
+
came from ``midas_pdf.saxs``, which now re-exports them from here, so there is
|
|
10
|
+
one definition of "SAXS form factor" in MIDAS.
|
|
11
|
+
|
|
12
|
+
**Strain contrast** (:mod:`midas_saxs.strain_source`) — dislocation loops, driven
|
|
13
|
+
by a :mod:`midas_ddd` network read from ExaDiS. Optional extra; plain particle
|
|
14
|
+
SAXS does not pull a dislocation-dynamics stack.
|
|
15
|
+
|
|
16
|
+
pip install 'midas-saxs[dislocations]'
|
|
17
|
+
|
|
18
|
+
Quick start
|
|
19
|
+
-----------
|
|
20
|
+
from midas_saxs import SAXSGeometry, SpherePopulation, simulate_frame
|
|
21
|
+
|
|
22
|
+
geom = SAXSGeometry(lsd_um=2.0e6, bcy_px=512, bcz_px=512, px_um=75.0,
|
|
23
|
+
wavelength_A=0.7293, n_pix_y=1024, n_pix_z=1024,
|
|
24
|
+
beamstop_radius_px=30)
|
|
25
|
+
voids = SpherePopulation(radius_A=50.0, number_density_per_A3=1e-32,
|
|
26
|
+
delta_rho_e_per_A3=-2.2, label="voids")
|
|
27
|
+
frame = simulate_frame(geom, particles=[voids], sample_volume_A3=1e15)
|
|
28
|
+
|
|
29
|
+
The one number to keep in mind
|
|
30
|
+
------------------------------
|
|
31
|
+
A dislocation loop of radius R displaces only its relaxation volume
|
|
32
|
+
``pi R^2 b``, while a void of the same radius displaces ``4 pi R^3 / 3``. At
|
|
33
|
+
R = 5 nm in Cu that is a factor 27 in volume and ~700 in forward intensity. If
|
|
34
|
+
an irradiated sample has voids or bubbles, they dominate the small-angle image,
|
|
35
|
+
and a dislocation-only simulation will not resemble the measurement.
|
|
36
|
+
|
|
37
|
+
What distinguishes them is not magnitude but **shape**: voids are isotropic,
|
|
38
|
+
loops are not (``kappa dV`` in-plane versus ``dV`` along the normal, with
|
|
39
|
+
``kappa = lambda/(lambda + 2 mu)``). That lives on the 2-D frame and is destroyed
|
|
40
|
+
by radial averaging — see :func:`midas_saxs.detector.azimuthal_profile`.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
|
44
|
+
|
|
45
|
+
from .core_shell import (
|
|
46
|
+
core_shell_sphere_form_factor_squared,
|
|
47
|
+
multi_shell_sphere_form_factor_squared,
|
|
48
|
+
)
|
|
49
|
+
from .detector import (
|
|
50
|
+
Frame,
|
|
51
|
+
azimuthal_profile,
|
|
52
|
+
radial_average,
|
|
53
|
+
simulate_frame,
|
|
54
|
+
)
|
|
55
|
+
from .form_factors import (
|
|
56
|
+
cylinder_form_factor_squared,
|
|
57
|
+
ellipsoid_form_factor_squared,
|
|
58
|
+
percus_yevick_S,
|
|
59
|
+
sphere_form_factor_squared,
|
|
60
|
+
)
|
|
61
|
+
from .geometry import (
|
|
62
|
+
SAXSGeometry,
|
|
63
|
+
inv_A_to_inv_um,
|
|
64
|
+
inv_um_to_inv_A,
|
|
65
|
+
pixel_to_q,
|
|
66
|
+
q_magnitude_grid,
|
|
67
|
+
two_theta_to_q,
|
|
68
|
+
)
|
|
69
|
+
from .model import SAXSModel, lognormal_quadrature_nodes
|
|
70
|
+
from .particles import (
|
|
71
|
+
SpherePopulation,
|
|
72
|
+
loop_equivalent_sphere_radius_A,
|
|
73
|
+
void_intensity,
|
|
74
|
+
)
|
|
75
|
+
from .wide_band import (
|
|
76
|
+
GuinierFit,
|
|
77
|
+
PorodFit,
|
|
78
|
+
guinier_fit,
|
|
79
|
+
kratky_plot,
|
|
80
|
+
porod_fit,
|
|
81
|
+
porod_invariant,
|
|
82
|
+
worm_like_chain_form_factor_squared,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# `midas_saxs.strain_source` is NOT imported here: it needs midas_ddd, which is
|
|
86
|
+
# an optional extra. Import it explicitly, or go through `simulate_frame`, which
|
|
87
|
+
# only reaches for it when a network is actually passed.
|
|
88
|
+
|
|
89
|
+
__all__ = [
|
|
90
|
+
# geometry
|
|
91
|
+
"SAXSGeometry", "pixel_to_q", "q_magnitude_grid", "two_theta_to_q",
|
|
92
|
+
"inv_A_to_inv_um", "inv_um_to_inv_A",
|
|
93
|
+
# detector
|
|
94
|
+
"Frame", "simulate_frame", "radial_average", "azimuthal_profile",
|
|
95
|
+
# particles
|
|
96
|
+
"SpherePopulation", "void_intensity", "loop_equivalent_sphere_radius_A",
|
|
97
|
+
# form factors (migrated from midas_pdf.saxs)
|
|
98
|
+
"sphere_form_factor_squared", "ellipsoid_form_factor_squared",
|
|
99
|
+
"cylinder_form_factor_squared", "percus_yevick_S",
|
|
100
|
+
"core_shell_sphere_form_factor_squared", "multi_shell_sphere_form_factor_squared",
|
|
101
|
+
"SAXSModel", "lognormal_quadrature_nodes",
|
|
102
|
+
"GuinierFit", "guinier_fit", "PorodFit", "porod_fit", "porod_invariant",
|
|
103
|
+
"kratky_plot", "worm_like_chain_form_factor_squared",
|
|
104
|
+
"__version__",
|
|
105
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Core-shell and multi-shell SAXS form factors.
|
|
2
|
+
|
|
3
|
+
The single-density sphere / ellipsoid / cylinder in
|
|
4
|
+
:mod:`midas_saxs.form_factors` model a uniform-density particle.
|
|
5
|
+
Many real nanoparticles are structured:
|
|
6
|
+
|
|
7
|
+
* Coated nanoparticles: metallic core + oxide / ligand shell.
|
|
8
|
+
* Reverse micelles: solvent core + amphiphile shell.
|
|
9
|
+
* Multi-shell nanocrystals: layered lattice + interface + surface region.
|
|
10
|
+
|
|
11
|
+
For these, the SAXS form factor is a difference of nested-sphere
|
|
12
|
+
amplitudes weighted by the contrast (electron-density difference)
|
|
13
|
+
in each layer.
|
|
14
|
+
|
|
15
|
+
For a two-shell (core + shell) spherical particle:
|
|
16
|
+
|
|
17
|
+
F(Q) = 4π (ρ_core − ρ_shell) V_core · j₁(Q R_core) / (Q R_core)
|
|
18
|
+
+ 4π (ρ_shell − ρ_solvent) V_total · j₁(Q R_total) / (Q R_total)
|
|
19
|
+
|
|
20
|
+
where ``j₁`` is the spherical Bessel of order 1 and V is the volume of
|
|
21
|
+
each region. This generalises to N shells recursively.
|
|
22
|
+
|
|
23
|
+
All routines here return **|F(Q)|²** and are torch-differentiable in the
|
|
24
|
+
shell radii and contrast ratios.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Optional, Sequence
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
import torch
|
|
32
|
+
|
|
33
|
+
_FOUR_PI = 4.0 * float(np.pi)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sphere_amplitude(q: torch.Tensor, R: torch.Tensor) -> torch.Tensor:
|
|
37
|
+
"""Complex spherical-shell amplitude at Q = 0 is V (the volume).
|
|
38
|
+
|
|
39
|
+
Returns ``F(Q, R) = V · 3 [sin(QR) − QR cos(QR)] / (QR)³``
|
|
40
|
+
with a stable Q → 0 limit.
|
|
41
|
+
"""
|
|
42
|
+
V = _FOUR_PI * R ** 3 / 3.0
|
|
43
|
+
x = q * R
|
|
44
|
+
small = x.abs() < 1e-3
|
|
45
|
+
# small-x: 1 − x²/10 + x⁴/280
|
|
46
|
+
shape_small = 1.0 - x ** 2 / 10.0 + x ** 4 / 280.0
|
|
47
|
+
x_safe = x.clamp(min=1e-9)
|
|
48
|
+
shape_general = 3.0 * (torch.sin(x_safe) - x_safe * torch.cos(x_safe)) / x_safe ** 3
|
|
49
|
+
return V * torch.where(small, shape_small, shape_general)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def core_shell_sphere_form_factor_squared(
|
|
53
|
+
q: torch.Tensor,
|
|
54
|
+
R_core_A: float | torch.Tensor,
|
|
55
|
+
R_total_A: float | torch.Tensor,
|
|
56
|
+
contrast_core: float | torch.Tensor = 1.0,
|
|
57
|
+
contrast_shell: float | torch.Tensor = 0.5,
|
|
58
|
+
) -> torch.Tensor:
|
|
59
|
+
"""|F(Q)|² for a core-shell spherical particle.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
R_core_A : radius of the core (Å).
|
|
64
|
+
R_total_A : outer radius (core + shell, Å); shell thickness =
|
|
65
|
+
R_total − R_core.
|
|
66
|
+
contrast_core : electron-density difference (core − solvent), arbitrary
|
|
67
|
+
units. Only the ratio (core − shell)/(shell − solvent) matters
|
|
68
|
+
for the shape of I(Q); the absolute scale drops into the caller's
|
|
69
|
+
overall scale factor.
|
|
70
|
+
contrast_shell : electron-density difference (shell − solvent).
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
|F(Q)|² of the same units as (contrast · ų)².
|
|
75
|
+
"""
|
|
76
|
+
q_t = torch.as_tensor(q, dtype=torch.float64)
|
|
77
|
+
R_c = torch.as_tensor(R_core_A, dtype=torch.float64)
|
|
78
|
+
R_t = torch.as_tensor(R_total_A, dtype=torch.float64)
|
|
79
|
+
delta_core = torch.as_tensor(contrast_core, dtype=torch.float64) \
|
|
80
|
+
- torch.as_tensor(contrast_shell, dtype=torch.float64)
|
|
81
|
+
delta_shell = torch.as_tensor(contrast_shell, dtype=torch.float64)
|
|
82
|
+
F_core = _sphere_amplitude(q_t, R_c)
|
|
83
|
+
F_total = _sphere_amplitude(q_t, R_t)
|
|
84
|
+
F = delta_core * F_core + delta_shell * F_total
|
|
85
|
+
return F ** 2
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def multi_shell_sphere_form_factor_squared(
|
|
89
|
+
q: torch.Tensor,
|
|
90
|
+
radii_A: Sequence[float | torch.Tensor],
|
|
91
|
+
contrasts: Sequence[float | torch.Tensor],
|
|
92
|
+
) -> torch.Tensor:
|
|
93
|
+
"""|F(Q)|² for a multi-shell spherical particle.
|
|
94
|
+
|
|
95
|
+
``radii_A``: list of *outer* radii of each shell (monotonically
|
|
96
|
+
increasing). E.g. for a 3-shell particle, ``[R_1, R_2, R_3]``.
|
|
97
|
+
|
|
98
|
+
``contrasts``: contrast of *each* shell against the solvent (same
|
|
99
|
+
length as ``radii_A``). The interfacial contrast between successive
|
|
100
|
+
shells drives the SAXS signal.
|
|
101
|
+
"""
|
|
102
|
+
q_t = torch.as_tensor(q, dtype=torch.float64)
|
|
103
|
+
if len(radii_A) != len(contrasts):
|
|
104
|
+
raise ValueError("len(radii_A) must equal len(contrasts)")
|
|
105
|
+
if len(radii_A) < 1:
|
|
106
|
+
raise ValueError("need at least one shell")
|
|
107
|
+
# F = sum_i (ρ_i − ρ_{i+1}) F_i, with ρ_{N+1} = solvent ≡ 0
|
|
108
|
+
F = torch.zeros_like(q_t)
|
|
109
|
+
for i in range(len(radii_A)):
|
|
110
|
+
rho_here = torch.as_tensor(contrasts[i], dtype=torch.float64)
|
|
111
|
+
rho_next = (torch.as_tensor(contrasts[i + 1], dtype=torch.float64)
|
|
112
|
+
if i + 1 < len(contrasts) else torch.tensor(0.0, dtype=torch.float64))
|
|
113
|
+
delta = rho_here - rho_next
|
|
114
|
+
R = torch.as_tensor(radii_A[i], dtype=torch.float64)
|
|
115
|
+
F = F + delta * _sphere_amplitude(q_t, R)
|
|
116
|
+
return F ** 2
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
"core_shell_sphere_form_factor_squared",
|
|
121
|
+
"multi_shell_sphere_form_factor_squared",
|
|
122
|
+
]
|