Metadata-Version: 2.4
Name: midas-saxs
Version: 0.1.2
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.
Author-email: Hemant Sharma <hsharma@anl.gov>
License-Expression: BSD-3-Clause
Project-URL: Homepage, https://github.com/marinerhemant/MIDAS
Project-URL: Documentation, https://github.com/marinerhemant/MIDAS/tree/master/packages/midas_saxs
Project-URL: Issues, https://github.com/marinerhemant/MIDAS/issues
Keywords: MIDAS,SAXS,USAXS,small-angle scattering,X-ray,dislocation loop,irradiation,void,form factor,differentiable,PyTorch
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.22
Requires-Dist: torch>=2.0
Requires-Dist: midas-hkls>=0.9.0
Requires-Dist: midas-transforms>=0.10.0
Requires-Dist: midas-distortion>=0.2.0
Provides-Extra: dislocations
Requires-Dist: midas-ddd>=0.1.2; extra == "dislocations"
Provides-Extra: viz
Requires-Dist: matplotlib>=3.5; extra == "viz"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: matplotlib>=3.5; extra == "dev"
Requires-Dist: midas-ddd>=0.1.2; extra == "dev"
Dynamic: license-file

# midas-saxs

Differentiable small-angle X-ray scattering for MIDAS. Two source terms, one
detector:

- **Density contrast** — voids, gas bubbles, precipitates. Sphere / ellipsoid /
  cylinder form factors, lognormal polydispersity, Percus-Yevick `S(Q)`, Guinier
  and Porod analysis. These came from `midas_pdf.saxs`, which now re-exports them
  from here, so MIDAS has one definition of "SAXS form factor".
- **Strain contrast** — dislocation loops, driven by a `midas_ddd` network read
  from ExaDiS. Optional extra: `pip install 'midas-saxs[dislocations]'`.

```python
from midas_saxs import SAXSGeometry, SpherePopulation, simulate_frame

geom = SAXSGeometry(lsd_um=2.0e6, bcy_px=512, bcz_px=512, px_um=75.0,
                    wavelength_A=0.7293, n_pix_y=1024, n_pix_z=1024,
                    beamstop_radius_px=30)
voids = SpherePopulation(radius_A=50.0, number_density_per_A3=1e-8,
                         delta_rho_e_per_A3=-2.45, label="voids")
frame = simulate_frame(geom, particles=[voids], sample_volume_A3=1e15)
```

## Read this before promising anyone a loop measurement

Three numbers, measured with a development demo script that is not part of the
package.

**1. Loops are faint.** A dislocation loop of radius R has relaxation volume
`pi R^2 b`; a void of the same radius displaces `4 pi R^3 / 3`. For R = 5 nm in
Cu that is 26x in volume, so **680x in intensity from volume alone**. The loop's
relaxation volume is that of a sphere of radius 1.69 nm, but it does not scatter
like that sphere: in its own plane its intensity carries a further
`(1 - kappa)^2` (0.36 for `lambda = 100, mu = 75`, making **1890x**), and along
its normal it is zero (point 2).

**2. Aligned loops differ from voids in shape.** A loop's small-angle amplitude
is the distortion term *plus* the Laue term, the scattering from the loop's own
extra or missing atoms (Ehrhart, Trinkaus & Larson, Phys. Rev. B 25 (1982) 834,
Eq. 8a/8b). As q -> 0 it is `dV (1 - kappa) sin^2(theta)`, with theta measured
from the loop normal and `kappa = lambda/(lambda + 2 mu)`: **zero along the
normal**, largest in the loop plane. On the demo (24 loops, normals along
detector z) the azimuthal profile follows `cos^4(azimuth)`, and its minimum is
set by the bin width (I_min/I_max = 3.8e-5 with 48 bins, 2.1e-14 with 144). A
void is isotropic. **Radially averaging a frame destroys the null**, which is
why this package renders 2-D frames and offers `azimuthal_profile` alongside
`radial_average`. Two limits: the minimum reaches zero only when the loop
normal lies in the detector plane and flattens as the normal tilts toward the
beam, and loops with isotropically distributed normals scatter isotropically,
like voids (see Identifiability below).

An earlier version of this README quoted `kappa dV` in the loop plane and `dV`
along the normal, a `1/kappa^2 = 6.25` contrast, and 6.30x from the demo. That
is the distortion term alone, and at small angle it is backwards.

**3. Real voids bury the signature.** Add voids of the same radius at the same
number density — the physically realistic irradiated case — and the azimuthal
contrast collapses to **1.001x**, with the voids outshining the loops by
**5212x** integrated over the frame.

So: loops are detectable at small angle in a clean matrix, and essentially
undetectable alongside a void or bubble population of comparable size. If the
question is defect-type discrimination in an irradiated single crystal,
near-Bragg diffuse (Huang) scattering is usually the better measurement. For a
single prismatic loop (isotropic matrix, Cu 111) the median gain over q
directions is 1.3e6, 1.3e4 and 1.3e2 at `|q|` = 1e-3, 1e-2 and 1e-1 1/A, about
7x below the naive `(G/q)^2`. The spread covers more than three decades, and
small-angle is the stronger signal in 9.8 % of directions at 1e-1 1/A. Method
and the full table are in the `midas_defect.huang` module docstring.

## Identifiability — what a frame determines, and what it only appears to

**Loop number density is not recoverable from a single SAXS frame.** A study in
this package claimed 2.6 % on loop number density and 0.6 % on loop radius, at
1e6 photons/q-point with voids at volume fraction 5.2e-5, from a loop-block
condition number of 639. It was **REFUTED on 2026-09-04** by adversarial review.
Every number in it reproduces, several bit-identically — the arithmetic was
never the problem. The numbers describe a different parameter vector, a
different void loading and a different sample mounting than the claim stated.

Six ways it fails, each measured by a refuter rather than read from the study's
own output:

| | effect |
|---|---|
| Signed `dV` restored to the parameter vector (it had been dropped *because* it made the Fisher matrix rank-deficient — that rank deficiency was the finding) | cond **2.77e16**; the data fix only the combination `n*dV^2`; SE[log n_loop] **unbounded** |
| Void loading raised to this package's own "realistic irradiation case" (equal radius, equal number density) — the study ran voids 100x **rarer** than loops | SE[n_loop] **252 %** |
| A true Watson habit distribution instead of `watson_normals`, which is a delta cone at a single polar angle and cannot reach the isotropic control | **4.7 %** at the planted S = 0.75, **699 %** at uniform |
| Sample rotated: the q-grid had put the habit axis in the detector plane, the single most favourable choice | in-plane 2.57 %, 10 deg off the beam 52 %, **along the beam 1.4e4-2.7e4 %**; about **7 %** of habit-axis orientations fail outright |
| Absolute intensity scale freed (it was hard-fixed, i.e. flux x volume x transmission x efficiency known exactly) | an exact null vector appears; only the **ratio** `n_loop/n_void` is identifiable. With a 1 % calibration prior 2.77 %, with 10 % 10.3 % |
| Mild model error: log-normal loop sizes at sigma_ln = 0.2, or a true Watson habit at the same S | bias **-28 %** and **-24 %** on `n_loop`, at 9-11 sigma, with chi2/dof of 0.61 and 0.02 — i.e. entirely inside "the fit looks fine" |

At the claim's own operating point voids already supply **97.9 %** of the
photons and the whole result rides on a **3.54 %** azimuthal modulation. Over
99 % of the claimed information is azimuthal anisotropy, not scattering-curve
shape.

**A condition number cannot be a confirm criterion here.** A Fisher matrix
scales linearly with photon budget, so its condition number is *exactly*
scale-invariant: measured at 639.5832 to seven digits across 1e0, 1e3, 1e6, 1e9
and 1e12 photons/q-point while SE[log n_loop] ran 2580 % to 0.0026 %. Across
four decades of void loading it moved 639.6 to 640.2 while SE went 2.58 % to
101 %, straight through the refute threshold with the criterion passing
untouched.

**What survived.** Loop **radius** at 0.6 % is robust — it held under every
lens, including the ones that destroyed the number density, and is unaffected
by freeing the absolute scale. State it with its condition: `q_max * R >~ 3`.

**What this means for the code.** Nothing here is wrong. Three independent
lenses confirmed the Fourier kernel, the Laue correction, the Fisher assembly
and the Schur profiling. The forward model is sound; the claim was wrong about
what it measured. If you build a Fisher or CRB analysis on this package,
include the degenerate directions (signed `dV`, a loop-vs-plate shape
parameter), use a habit distribution that can actually reach isotropy, put the
void population at its realistic loading, and report the sample mounting --
because the answer depends on all four.

## Scope

**Three dislocation populations, treated differently.** Closed loops, the
irradiation population: exact at every q, distortion plus Laue term. Finite lines
(open lines, junction clusters that do not wind): the line-integral amplitude at
every q; an edge or mixed line scatters into a sheet perpendicular to itself, an
isotropic screw not at all, and a line ending inside the medium carries its
ends' unphysical sources off that sheet (the frame warns). Lines that close only
through a periodic DDD cell's boundary: an amplitude only on the cell's
reciprocal lattice, shown between lattice points at a stated resolution (default
two lattice spacings, `periodic_resolution_fwhm_inv_A`) as the `periodic_lines`
component, with a low-q floor of about three lattice spacings below which pixels
are not representative; a bigger simulation cell lowers it. Pure screws in cubic
or isotropic elasticity have no dilatation, and the frame says so when a
component is only float64 roundoff. The line terms are preregistered and under
adversarial verification (2026-09-11), not yet established. An earlier version
of this README said lines contribute nothing at small angle; that holds only as
`q -> 0`. `include_lines=False` reproduces the loops-only frames of 0.1.x and
reports the line length it left out.

## Units

| Quantity | Unit |
|---|---|
| distances, geometry | micrometers |
| wavelength, particle radii, electron density | angstroms |
| `q` from `midas_saxs.geometry` | **inverse angstroms** |
| `q` into the `midas_ddd` kernel | **inverse micrometers** |

The 1e4 between the last two is easy to lose, so `inv_A_to_inv_um` /
`inv_um_to_inv_A` exist and `strain_source` uses them rather than a literal.

## Detector geometry

`pixel_to_q` goes through `midas_transforms.apply_tilt_distortion` — the same
tilt (`R_z R_y R_x`) and 15-coefficient distortion model the HEDM side uses.
That is deliberate: a SAXS geometry and an FF geometry calibrated from the same
detector must agree about where q sits. Transmission SAXS has no omega, so lab q
is sample q.

## Migration note

`form_factors`, `model`, `wide_band` and `core_shell` moved here from
`midas_pdf.saxs`. Both the package-level path (`from midas_pdf.saxs import
SAXSModel`) and the deep path (`from midas_pdf.saxs.form_factors import ...`)
still resolve, to the same objects — asserted by
`test_midas_pdf_reexports_the_same_objects`. `midas_pdf.saxs` keeps the genuinely
PDF-coupled part: joint SAXS + PDF refinement and its Bayesian variants.

One convention worth knowing: `sphere_form_factor_squared` returns `V^2 |F|^2`,
**not** the normalised `|F|^2`. `SpherePopulation.intensity` multiplies it by
`n * delta_rho^2` and nothing else, which is only correct because the volume is
already in there.
