refjax 0.1.1__py3-none-any.whl

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.
refjax/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ __version__ = "0.1.1"
2
+
3
+ from jax import config
4
+ from refjax import model, smear
5
+ from refjax.tools import kernel
6
+
7
+ __all__ = ["__version__", "kernel", "model", "smear"]
8
+
9
+ config.update("jax_enable_x64", True)
refjax/model.py ADDED
@@ -0,0 +1,112 @@
1
+ import jax.numpy as jnp
2
+ from functools import reduce
3
+
4
+ # %% Abeles Matrix formalism
5
+
6
+
7
+ def abeles(params, Q):
8
+ """Reflectivity of a stratified medium by the Abeles matrix formalism.
9
+
10
+ Parameters
11
+ ----------
12
+ params : dict
13
+ Slab model with n layers between a fronting and a backing medium:
14
+
15
+ - "thick": (n,) layer thicknesses, in the length unit of 1/Q (Å for Q in Å⁻¹)
16
+ - "rough": (n + 1,) interfacial roughnesses, one per interface
17
+ - "SLD": (n + 2,) scattering length densities in 10⁻⁶ Å⁻², fronting first, backing last
18
+ - "scale": () multiplies the reflectivity
19
+ - "bkg": () constant background added to the reflectivity
20
+ Q : array, shape (m,)
21
+ Momentum transfer.
22
+
23
+ Returns
24
+ -------
25
+ array, shape (m,)
26
+ The reflectivity at each Q.
27
+ """
28
+ # SLD profile differences
29
+ d_rho = (params["SLD"] - params["SLD"][0]).astype(complex) * 1e-6
30
+ # compute wavevectors for the nth layer
31
+ kn = jnp.sqrt(((Q[:, None] / 2) ** 2) - (4 * jnp.pi * d_rho))
32
+ # Fresnel reflection
33
+ a, b = kn[:, :-1], kn[:, 1:]
34
+ r = (a - b) / (a + b) * jnp.exp(-2 * a * b * params["rough"] ** 2)
35
+ # Compute and multiply characteristic matricies elementwise (n_layers + 1, Q.shape)
36
+ C00 = jnp.exp(1j * a * jnp.array([0, *params["thick"]])).T
37
+ C11 = C00**-1
38
+ C10 = r.T * C00
39
+ C01 = r.T * C11
40
+
41
+ # For some reason manually performing the 2x2 matrix multiplications
42
+ # is a huge speedup (~2x) in jit-ed calls compared to matmul (???)
43
+ # Could be something to do with the very wide broadcast size?
44
+ # The batch dim size is often >> 1k and idk if this is not efficient
45
+ # Perhaps there is an einsum that works better? - there isn't 08/04/24
46
+ # Tested it and seemingly not. Bizzare behavior. Absolute rabbithole.
47
+ # Perhaps constructing the large array (n_layers+1, Q.shape, 2,2) is expensive somehow? - Nope 08/04/24.
48
+ # Hard to imagine why this is - something to do with optimisations in jaxprs at a guess.
49
+
50
+ M00, M10, M01, M11 = C00[0], C10[0], C01[0], C11[0]
51
+ for c00, c10, c01, c11 in zip(C00[1:], C10[1:], C01[1:], C11[1:]):
52
+ a = M00 * c00 + M10 * c01
53
+ b = M00 * c10 + M10 * c11
54
+ c = M01 * c00 + M11 * c01
55
+ d = M01 * c10 + M11 * c11
56
+ M00, M10, M01, M11 = a, b, c, d
57
+ R = M01 / M00
58
+ return params["scale"] * (R * R.conj()).real + params["bkg"]
59
+
60
+
61
+ # %% Spectroscopic ellipsometry
62
+
63
+
64
+ def spec_ellips(params, lam):
65
+ """Ellipsometric angles Psi and Delta of a stratified medium by the transfer-matrix method.
66
+
67
+ Parameters
68
+ ----------
69
+ params : dict
70
+ n layers between an ambient medium and a substrate:
71
+
72
+ - "nbar": (n + 2, lam.shape) complex refractive index n + ik (k >= 0) of each medium,
73
+ ambient first, substrate last
74
+ - "thick": (n,) layer thicknesses, in the same length unit as lam
75
+ - "aoi_rads": () angle of incidence in the ambient, in radians
76
+ lam : array
77
+ Wavelengths.
78
+
79
+ Returns
80
+ -------
81
+ psi, delta : arrays, each of shape lam.shape
82
+ In degrees: tan(psi) exp(i delta) = rp / rs in Woollam's convention, delta in [0, 360).
83
+ Compare delta to data modulo 360.
84
+ """
85
+ nbar = params["nbar"]
86
+ # n cos(theta) in each medium (Snell); principal sqrt gives Im >= 0, the decaying wave for n + ik
87
+ ncos = jnp.sqrt(nbar**2 - (nbar[0] * jnp.sin(params["aoi_rads"])) ** 2)
88
+ cos = ncos / nbar
89
+ ncos22 = ncos[1:]
90
+ ncos11 = ncos[:-1]
91
+ ncos21 = nbar[1:] * cos[:-1]
92
+ ncos12 = nbar[:-1] * cos[1:]
93
+ rs = (ncos11 - ncos22) / (ncos11 + ncos22)
94
+ ts = 2 * ncos11 / (ncos11 + ncos22)
95
+ rp = (ncos21 - ncos12) / (ncos12 + ncos21)
96
+ tp = 2 * ncos11 / (ncos12 + ncos21)
97
+ dz = 1j * params["thick"][:, None] * 2 * jnp.pi * ncos[1:-1] / lam
98
+ one = jnp.ones_like(rs)
99
+ edr = jnp.exp(dz)
100
+ nedr = jnp.exp(-dz)
101
+
102
+ def TMM(r, t):
103
+ ms = jnp.array([[nedr, nedr * r[1:]], [edr * r[1:], edr]]) / t[1:]
104
+ ms0 = jnp.array([[one[0], r[0]], [r[0], one[0]]]) / t[0]
105
+ Ms = reduce(jnp.matmul, ms.transpose(2, 3, 0, 1), ms0.T)
106
+ return Ms[:, 1, 0] / Ms[:, 0, 0]
107
+
108
+ rho = TMM(rp, tp) / TMM(rs, ts)
109
+ # n + ik is the conjugate of Woollam's n - ik, hence the minus sign on delta
110
+ _psi = jnp.degrees(jnp.arctan(jnp.abs(rho)))
111
+ _delta = jnp.degrees(-jnp.angle(rho)) % 360
112
+ return _psi, _delta
refjax/smear.py ADDED
@@ -0,0 +1,34 @@
1
+ import jax
2
+ import jax.numpy as jnp
3
+ from numpy.polynomial.hermite import hermgauss
4
+
5
+ _ORDER = 17
6
+ herm_root, herm_weight = hermgauss(_ORDER)
7
+
8
+ def no_smear(params, Q, FWHM, model, **kwargs):
9
+ return model(params, Q)
10
+
11
+ def constant_gaussian(params, Q, const_smear, model, **kwargs):
12
+ '''
13
+ FWHM = const_smear% * Q
14
+ Gaussian kernel smearing.
15
+ There are a few options here can solve as a convolution or just do gauss-hermite?
16
+ For now treat as pointwise.
17
+ '''
18
+ return pointwise_gaussian(params, Q, Q*const_smear/100, model, **kwargs)
19
+
20
+ def pointwise_gaussian(params, Q, FWHMs, model, **kwargs):
21
+ '''
22
+ Pointwise gaussian smearing kernel.
23
+ Compute the expectation integral using a 17th order Gauss-Hermite quadrature.
24
+ Note that this differs from the approach in refnx which uses a finite width interval so numerics may differ.'''
25
+ Q_eval = 0.5 * FWHMs / jnp.sqrt(jnp.log(2.0)) * herm_root[:, None] + Q
26
+ R = jax.vmap(model, in_axes=(None, 0))(params, Q_eval)
27
+ return herm_weight @ R / jnp.sqrt(jnp.pi)
28
+
29
+ def _custom_kernel(params, Q, kernel, model, **kwargs):
30
+ '''TODO
31
+ Manually compute the expectation interval of R(q) w.r.t k(q)
32
+ Can use simpsons or a Gauss-quad maybe?
33
+ '''
34
+ raise NotImplementedError
refjax/tools.py ADDED
@@ -0,0 +1,25 @@
1
+ import jax
2
+ from refjax.smear import no_smear
3
+
4
+
5
+ def kernel(model, smear=no_smear, smear_params=None, jit=True):
6
+ """Build a reflection kernel from a reflection model and a smearing kernel"""
7
+ if smear_params is None:
8
+ smear_params = {}
9
+
10
+ def kern(params, q, dq=None):
11
+ return smear(params, q, dq, model, **smear_params)
12
+ if jit:
13
+ kern = jax.jit(kern)
14
+ return kern
15
+
16
+
17
+ def _from_refnx(refnx_model):
18
+ """TODO Pull out required parameters from a refnx model"""
19
+ raise NotImplementedError
20
+
21
+ def _validata_params(params):
22
+ """TODO Validata params manually because don't want to do this in a jit"""
23
+ raise NotImplementedError
24
+
25
+
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: refjax
3
+ Version: 0.1.1
4
+ Summary: Lightweight `jit` and `grad` compatible reflectometry in jax.
5
+ Author: Stephanie L. Burg
6
+ Author-email: "Max D. Champneys" <max.champneys@sheffield.ac.uk>
7
+ Maintainer-email: Max Champneys <max.champneys@sheffield.ac.uk>
8
+ License-Expression: MIT
9
+ Project-URL: Repository, https://github.com/MDCHAMP/refjax
10
+ Project-URL: Issues, https://github.com/MDCHAMP/refjax/issues
11
+ Project-URL: Paper, https://doi.org/10.1038/s43246-026-01341-4
12
+ Keywords: Reflectometry,Neutron reflectometry,jax,autograd
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Development Status :: 4 - Beta
18
+ Classifier: Intended Audience :: Science/Research
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy>=2.0
24
+ Requires-Dist: jax>=0.4.26
25
+ Requires-Dist: jaxlib>=0.4.26
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=7.0; extra == "test"
28
+ Requires-Dist: pandas>=2.2.2; extra == "test"
29
+ Requires-Dist: refnx>=0.1.45; extra == "test"
30
+ Dynamic: license-file
31
+
32
+ # Refjax - Lightweight `jit` and `grad` compatible reflectometry in jax
33
+
34
+ This package provides lightweight, low-level access to neutron reflectometry and spectroscopic ellipsometry kernels in jax.
35
+
36
+ The provided kernels are all `jit` and `grad` compatible and easy to integrate with your favourite optimisation or uncertainty quantification library for fast parameterisation of stratified reflectometry models. Several examples for popular libraries are available in the [`examples`](https://github.com/MDCHAMP/refjax/tree/main/examples) folder.
37
+
38
+
39
+ ## Installation
40
+
41
+ From PyPI:
42
+
43
+ ```bash
44
+ pip install refjax
45
+ # or
46
+ uv add refjax
47
+ ```
48
+
49
+ Or from GitHub, pinned to the 0.1.1 release by its tag:
50
+
51
+ ```bash
52
+ pip install "refjax @ git+https://github.com/MDCHAMP/refjax@v0.1.1"
53
+ # or
54
+ uv add git+https://github.com/MDCHAMP/refjax --tag v0.1.1
55
+ ```
56
+
57
+ refjax needs Python >= 3.11, `jax` and `jaxlib` >= 0.4.26 and `numpy` >= 2.0. The test suite runs in CI at these minimum versions and at the latest releases.
58
+
59
+ ### 64-bit precision is on by default
60
+
61
+ `import refjax` switches on jax's 64-bit mode (`jax.config.update("jax_enable_x64", True)`), so refjax computes in float64 and complex128. This setting is global: it applies to all jax code in the same Python process, not only to refjax. To work in 32-bit, switch it off after importing refjax, before creating any arrays: `jax.config.update("jax_enable_x64", False)`. refjax then computes in float32 and complex64; its tests run only in 64-bit.
62
+
63
+ ## Neutron reflectometry
64
+
65
+ ### Design pattern
66
+
67
+ Reflectometry models are created by creating reflectivity kernels with `refjax.kernel`:
68
+
69
+ ```python
70
+ kernel = refjax.kernel(model, smear=refjax.smear.no_smear, smear_params=None, jit=True)
71
+ ```
72
+
73
+ Currently implemented models:
74
+ - `refjax.model.abeles` Abeles matrix formalism for stratified media
75
+
76
+ Currently implemented smearing kernels:
77
+ - `refjax.smear.no_smear` No smearing kernel (default)
78
+ - `refjax.smear.constant_gaussian` Constant gaussian smearing at every Q value (the third argument is the FWHM as a percentage of Q)
79
+ - `refjax.smear.pointwise_gaussian` Pointwise Gaussian approximation to the true smearing kernel (requires FWHM resolution measurements), by 17th-order Gauss-Hermite quadrature
80
+
81
+ Once a kernel has been created, it is called with the model parameters and Q (and FWHM values for smeared kernels).
82
+
83
+ ```python
84
+ R = kernel(params, Q, FWHM) # fast because jit
85
+ ```
86
+
87
+ ### Parameters of `abeles`
88
+
89
+ For a slab model with `n` layers between a fronting and a backing medium, `params` is a dict:
90
+
91
+ | key | shape | meaning |
92
+ |---------|-----------|----------------------------------------------------------------------------|
93
+ | `thick` | `(n,)` | layer thicknesses, in the length unit of 1/Q (Å for Q in Å⁻¹) |
94
+ | `SLD` | `(n + 2,)` | scattering length densities in 10⁻⁶ Å⁻², fronting first, backing last |
95
+ | `rough` | `(n + 1,)` | interfacial roughnesses, one per interface |
96
+ | `scale` | `()` | multiplies the reflectivity |
97
+ | `bkg` | `()` | constant background added to the reflectivity |
98
+
99
+ ### Example
100
+
101
+ ```python
102
+ import jax.numpy as jnp
103
+ import refjax
104
+
105
+ params = {
106
+ "thick": jnp.array([255.0, 17.0]),
107
+ "SLD": jnp.array([0.0, 6.4, 3.47, 2.07]),
108
+ "rough": jnp.array([5.2, 5.0, 2.07]),
109
+ "scale": 1.0,
110
+ "bkg": 2e-7,
111
+ }
112
+ Q = jnp.linspace(0.005, 0.3, 200) # Å⁻¹
113
+ FWHM = 0.05 * Q # resolution FWHM at each Q
114
+ kernel = refjax.kernel(refjax.model.abeles, refjax.smear.pointwise_gaussian)
115
+ R = kernel(params, Q, FWHM)
116
+ ```
117
+
118
+ [`examples/01_basic_usage.py`](https://github.com/MDCHAMP/refjax/blob/main/examples/01_basic_usage.py) walks through this model against the example data in [`examples/.example_NR_data.csv`](https://github.com/MDCHAMP/refjax/blob/main/examples/.example_NR_data.csv); the other scripts in [`examples`](https://github.com/MDCHAMP/refjax/tree/main/examples) show parameter identification with optax and numpyro.
119
+
120
+ ## Spectroscopic ellipsometry
121
+
122
+ `refjax.model.spec_ellips(params, lam)` computes the ellipsometric angles Psi and Delta of a stratified medium (transfer-matrix method) at the wavelengths `lam`. It is `jit` and `grad` compatible and is called directly (not through `refjax.kernel`).
123
+
124
+ For `n` layers between an ambient medium and a substrate, `params` is a dict:
125
+
126
+ | key | shape | meaning |
127
+ |------------|----------------------|---------------------------------------------------------------------------------|
128
+ | `nbar` | `(n + 2, lam.shape)` | complex refractive index `n + ik` (`k >= 0`) of each medium, ambient first, substrate last |
129
+ | `thick` | `(n,)` | layer thicknesses, in the same length unit as `lam` |
130
+ | `aoi_rads` | `()` | angle of incidence in the ambient, in radians |
131
+
132
+ It returns `(psi, delta)` in degrees, each of shape `lam.shape`.
133
+
134
+ Conventions:
135
+ - `tan(psi) exp(i delta) = rp / rs` in Woollam's convention, as used by J.A. Woollam's instruments and CompleteEASE and set out in H. Fujiwara, *Spectroscopic Ellipsometry: Principles and Applications* (Wiley, 2007), [doi:10.1002/9780470060193](https://doi.org/10.1002/9780470060193): the complex index written n - ik, so refjax's n + ik enters conjugated. `delta` is wrapped to `[0, 360)`; compare it to data modulo 360.
136
+ - The complex index is `n + ik` with `k >= 0`. This is the complex conjugate of Woollam's `n - ik`, hence the minus sign on the phase of `rp / rs` in the code. A Woollam-convention index `N = n - ik` is passed as `N.conj()`.
137
+ - `n cos(theta)` in each medium follows from Snell's law with the principal square root, which gives `Im >= 0`: the decaying wave for `n + ik`.
138
+
139
+ ```python
140
+ import jax.numpy as jnp
141
+ import numpy as np
142
+ import refjax
143
+
144
+ lam = jnp.linspace(370.0, 1000.0, 50) # nm
145
+ # ambient, film, SiO2, Si substrate (n + ik), each constant in wavelength here
146
+ nbar = jnp.array([1.0, 1.58, 1.46, 3.9 + 0.02j])[:, None] * jnp.ones_like(lam)
147
+ params = {"nbar": nbar, "thick": jnp.array([17.0, 2.0]), "aoi_rads": np.radians(75.0)}
148
+ psi, delta = refjax.model.spec_ellips(params, lam)
149
+ ```
150
+
151
+ [`examples/06_spectroscopic_ellipsometry.py`](https://github.com/MDCHAMP/refjax/blob/main/examples/06_spectroscopic_ellipsometry.py) models and fits a thick polymer film on Si against a measured spectrum in [`examples/.example_ellips_data.csv`](https://github.com/MDCHAMP/refjax/blob/main/examples/.example_ellips_data.csv) (which carries the substrate's indices). [`test/test_ellips.py`](https://github.com/MDCHAMP/refjax/blob/main/test/test_ellips.py) checks `spec_ellips` against an independent numpy implementation and against that spectrum.
152
+
153
+ ### Changes in 0.1.1
154
+
155
+ The first release on PyPI. Over the 0.1.0 tag: the smearing quadrature is computed in float64 (it was float32, an error of about 2e-8 relative in smeared reflectivity), importing refjax no longer starts jax's backend, `abeles` and `spec_ellips` have docstrings, and the packaging metadata is updated.
156
+
157
+ ### Changes in 0.1.0
158
+
159
+ `spec_ellips` now follows the conventions above. `delta` is wrapped to `[0, 360)`; before 0.1.0 it was unwrapped along the wavelength axis, so it could differ from the 0.1.0 value by multiples of 360. `n cos(theta)` now comes from the principal square root rather than `arcsin` of Snell's law. Code written against the earlier `spec_ellips` should compare `delta` modulo 360.
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ uv run --extra test pytest
165
+ ```
166
+
167
+ ## Citing
168
+
169
+ If you use refjax in published work, please cite:
170
+
171
+ > Max D. Champneys, Andrew J. Parnell and Stephanie L. Burg, "Towards real-time surrogate-free Bayesian inversion for neutron reflectometry", *Communications Materials* (2026). [doi:10.1038/s43246-026-01341-4](https://doi.org/10.1038/s43246-026-01341-4)
172
+
173
+ ```bibtex
174
+ @article{champneys2026refjax,
175
+ author = {Champneys, Max D. and Parnell, Andrew J. and Burg, Stephanie L.},
176
+ title = {Towards real-time surrogate-free {B}ayesian inversion for neutron reflectometry},
177
+ journal = {Communications Materials},
178
+ year = {2026},
179
+ doi = {10.1038/s43246-026-01341-4},
180
+ url = {https://doi.org/10.1038/s43246-026-01341-4}
181
+ }
182
+ ```
183
+
184
+ Please also state the refjax version you used (e.g. 0.1.1). Citation metadata is in [`CITATION.cff`](https://github.com/MDCHAMP/refjax/blob/main/CITATION.cff); GitHub's "Cite this repository" button reads it.
185
+
186
+ Having trouble? Feature request? Raise an issue on the [GitHub issues page](https://github.com/MDCHAMP/refjax/issues).
@@ -0,0 +1,9 @@
1
+ refjax/__init__.py,sha256=UlkcAIZuqzpcWqixVnKhdeuJzpsleSbCZKjdvFfLSqY,204
2
+ refjax/model.py,sha256=UyKwaDqt6orONRcpcjcZVSCzuVKcRhE5aLrp3-jluxw,4372
3
+ refjax/smear.py,sha256=pvnlmm9Mmy4TbfJYfegguSgjkqdzuIbGCqiy-h4pPyk,1254
4
+ refjax/tools.py,sha256=sBCyHxieRQ-nb65nD8wJVaII5YyVmsJwNNe7M9zI6Go,670
5
+ refjax-0.1.1.dist-info/licenses/LICENSE,sha256=yQu-_ub0ZmzUBIAXpzP79YqemMrHyItf4lJJ_pktDzI,1069
6
+ refjax-0.1.1.dist-info/METADATA,sha256=F_nfJRIHUaOwy5gjVWzPjUH_cRyUELgjLen8ofdgjpc,9931
7
+ refjax-0.1.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ refjax-0.1.1.dist-info/top_level.txt,sha256=0TA82h8w_ZeRvpC8adSyM5u-rxyNtPBxOf8CrjBt4oE,7
9
+ refjax-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Max Champneys
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ refjax