quantized-lab 0.8.0__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.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""FFT-based thickness reductions (PORT_PLAN #19) — see reductions.py.
|
|
2
|
+
|
|
3
|
+
Split from calc/reductions.py to respect the 500-line ceiling. Both public
|
|
4
|
+
functions re-export through ``quantized.calc.reductions``.
|
|
5
|
+
|
|
6
|
+
- ``fft_thickness`` — ``+bosonPlotter/peakTools.m`` ``fftThickness/doFFT``
|
|
7
|
+
- ``reflectivity_fft`` — ``peakTools.m`` ``reflectivityFFT/doReflFFT``
|
|
8
|
+
(including the superlattice harmonic analysis)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from numpy.typing import NDArray
|
|
18
|
+
from scipy.interpolate import PchipInterpolator
|
|
19
|
+
|
|
20
|
+
__all__ = ["fft_thickness", "reflectivity_fft"]
|
|
21
|
+
|
|
22
|
+
_FloatArray = NDArray[np.float64]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _window(name: str, n: int) -> _FloatArray:
|
|
26
|
+
"""MATLAB peakTools window formulas (symmetric, N-1 denominator)."""
|
|
27
|
+
k = np.arange(n, dtype=float)
|
|
28
|
+
if name == "hann":
|
|
29
|
+
return np.asarray(0.5 * (1.0 - np.cos(2.0 * math.pi * k / (n - 1))), dtype=float)
|
|
30
|
+
if name == "blackman":
|
|
31
|
+
return np.asarray(
|
|
32
|
+
0.42
|
|
33
|
+
- 0.5 * np.cos(2.0 * math.pi * k / (n - 1))
|
|
34
|
+
+ 0.08 * np.cos(4.0 * math.pi * k / (n - 1)),
|
|
35
|
+
dtype=float,
|
|
36
|
+
)
|
|
37
|
+
if name == "none":
|
|
38
|
+
return np.ones(n, dtype=float)
|
|
39
|
+
raise ValueError(f"unknown window {name!r} (expected 'hann', 'blackman' or 'none')")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _next_pow2_len(n: int) -> int:
|
|
43
|
+
"""MATLAB ``2^nextpow2(4*N)`` — smallest power of two >= 4*N."""
|
|
44
|
+
return 1 << (4 * n - 1).bit_length()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _uniform_q_fft(
|
|
48
|
+
q: _FloatArray, signal: _FloatArray, window: str
|
|
49
|
+
) -> tuple[_FloatArray, _FloatArray, _FloatArray]:
|
|
50
|
+
"""Shared tail of both FFT reductions: pchip onto a uniform Q grid is done
|
|
51
|
+
by the callers (they differ in detrending); this applies the window and
|
|
52
|
+
the 4x zero-padded FFT, returning (F_half, thickness_nm_axis, w) —
|
|
53
|
+
thickness axis in nm from ``2*pi*k / (N_fft*dQ)`` Angstroms."""
|
|
54
|
+
n = signal.size
|
|
55
|
+
w = _window(window, n)
|
|
56
|
+
windowed = signal * w
|
|
57
|
+
n_fft = _next_pow2_len(n)
|
|
58
|
+
f = np.asarray(np.abs(np.fft.fft(windowed, n_fft))[: n_fft // 2], dtype=float)
|
|
59
|
+
dq = float(q[1] - q[0])
|
|
60
|
+
thickness_a = 2.0 * math.pi * np.arange(n_fft // 2, dtype=float) / (n_fft * dq)
|
|
61
|
+
return f, np.asarray(thickness_a / 10.0, dtype=float), w
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fft_thickness(
|
|
65
|
+
two_theta_deg: Any,
|
|
66
|
+
intensity: Any,
|
|
67
|
+
wavelength_a: float,
|
|
68
|
+
*,
|
|
69
|
+
two_theta_min: float | None = None,
|
|
70
|
+
two_theta_max: float | None = None,
|
|
71
|
+
window: str = "hann",
|
|
72
|
+
max_thickness_nm: float = 200.0,
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
"""Film thickness from Laue-fringe periodicity via FFT (XRD).
|
|
75
|
+
|
|
76
|
+
2-theta -> Q, pchip-resample to a uniform Q grid, mean-subtract, window,
|
|
77
|
+
4x zero-padded FFT; the strongest peak in bins 4..(t <= max) is the
|
|
78
|
+
thickness, with the FFT-peak FWHM/2 as the uncertainty.
|
|
79
|
+
"""
|
|
80
|
+
if wavelength_a <= 0 or not math.isfinite(wavelength_a):
|
|
81
|
+
raise ValueError("wavelength_a must be positive and finite")
|
|
82
|
+
x_all = np.asarray(two_theta_deg, dtype=float).ravel()
|
|
83
|
+
y_all = np.asarray(intensity, dtype=float).ravel()
|
|
84
|
+
if x_all.size != y_all.size:
|
|
85
|
+
raise ValueError("two_theta and intensity must have the same length")
|
|
86
|
+
|
|
87
|
+
lo = float(x_all.min()) if two_theta_min is None else float(two_theta_min)
|
|
88
|
+
hi = float(x_all.max()) if two_theta_max is None else float(two_theta_max)
|
|
89
|
+
if lo >= hi:
|
|
90
|
+
raise ValueError("two_theta_min must be less than two_theta_max")
|
|
91
|
+
mask = (x_all >= lo) & (x_all <= hi)
|
|
92
|
+
if int(mask.sum()) < 10:
|
|
93
|
+
raise ValueError("too few data points in selected range (need >= 10)")
|
|
94
|
+
tt_sel = x_all[mask]
|
|
95
|
+
i_sel = y_all[mask]
|
|
96
|
+
|
|
97
|
+
q = np.asarray(
|
|
98
|
+
(4.0 * math.pi / wavelength_a) * np.sin(tt_sel / 2.0 * math.pi / 180.0), dtype=float
|
|
99
|
+
)
|
|
100
|
+
order = np.argsort(q, kind="stable") # interp1 sorts sample points internally
|
|
101
|
+
q_sorted = q[order]
|
|
102
|
+
i_sorted = i_sel[order]
|
|
103
|
+
|
|
104
|
+
n_pts = q_sorted.size
|
|
105
|
+
q_uniform = np.linspace(float(q_sorted[0]), float(q_sorted[-1]), n_pts)
|
|
106
|
+
i_uniform = np.asarray(PchipInterpolator(q_sorted, i_sorted)(q_uniform), dtype=float)
|
|
107
|
+
i_uniform = np.asarray(i_uniform - np.mean(i_uniform), dtype=float)
|
|
108
|
+
|
|
109
|
+
f, thickness_nm, _ = _uniform_q_fft(q_uniform, i_uniform, window)
|
|
110
|
+
|
|
111
|
+
# MATLAB searches F(4:searchMax) (1-based) — skip the DC/low bins.
|
|
112
|
+
search_min0 = 3 # 0-based index of MATLAB bin 4
|
|
113
|
+
below = np.nonzero(thickness_nm <= max_thickness_nm)[0]
|
|
114
|
+
search_max = int(below[-1]) + 1 if below.size else 0 # exclusive, = MATLAB count
|
|
115
|
+
if search_max < search_min0 + 2:
|
|
116
|
+
search_max = f.size
|
|
117
|
+
seg = f[search_min0:search_max]
|
|
118
|
+
peak_rel = int(np.argmax(seg))
|
|
119
|
+
peak_val = float(seg[peak_rel])
|
|
120
|
+
peak_idx = peak_rel + search_min0
|
|
121
|
+
t_nm = float(thickness_nm[peak_idx])
|
|
122
|
+
|
|
123
|
+
# Uncertainty from the FFT-peak FWHM (searched over the FULL half-spectrum
|
|
124
|
+
# like MATLAB, not just the restricted range).
|
|
125
|
+
half_max = peak_val / 2.0
|
|
126
|
+
left_below = np.nonzero(f[: peak_idx + 1] < half_max)[0]
|
|
127
|
+
right_below = np.nonzero(f[peak_idx:] < half_max)[0]
|
|
128
|
+
if left_below.size and right_below.size:
|
|
129
|
+
left_idx = int(left_below[-1])
|
|
130
|
+
right_idx = peak_idx + int(right_below[0])
|
|
131
|
+
fwhm_bins = right_idx - left_idx
|
|
132
|
+
hw = math.ceil(fwhm_bins / 2)
|
|
133
|
+
dt_nm = float(
|
|
134
|
+
thickness_nm[min(peak_idx + hw, thickness_nm.size - 1)]
|
|
135
|
+
- thickness_nm[max(peak_idx - hw, 0)]
|
|
136
|
+
)
|
|
137
|
+
uncertainty_nm = dt_nm / 2.0
|
|
138
|
+
else:
|
|
139
|
+
uncertainty_nm = float("nan")
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"thickness_nm": t_nm,
|
|
143
|
+
"uncertainty_nm": uncertainty_nm,
|
|
144
|
+
"wavelength_a": wavelength_a,
|
|
145
|
+
"two_theta_range": [lo, hi],
|
|
146
|
+
"fft_magnitude": f[:search_max].tolist(),
|
|
147
|
+
"thickness_axis": thickness_nm[:search_max].tolist(),
|
|
148
|
+
"n_points": int(mask.sum()),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
_PREPROCESS_MODES = ("logR", "logRQ4", "R", "RQ4")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def reflectivity_fft(
|
|
156
|
+
x: Any,
|
|
157
|
+
reflectivity: Any,
|
|
158
|
+
*,
|
|
159
|
+
is_neutron: bool = False,
|
|
160
|
+
wavelength_a: float | None = None,
|
|
161
|
+
x_min: float | None = None,
|
|
162
|
+
x_max: float | None = None,
|
|
163
|
+
window: str = "hann",
|
|
164
|
+
preprocess: str = "logR",
|
|
165
|
+
max_thickness_nm: float = 500.0,
|
|
166
|
+
peak_prominence_threshold: float = 0.05,
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Film thickness(es) from Kiessig-fringe periodicity via FFT.
|
|
169
|
+
|
|
170
|
+
Supports neutron NR (``x`` already Q in 1/Angstrom) and XRR (``x`` in
|
|
171
|
+
degrees 2-theta, converted with ``wavelength_a``). Preprocess modes
|
|
172
|
+
``logR`` / ``logRQ4`` / ``R`` / ``RQ4`` (Q^4 = Fresnel correction),
|
|
173
|
+
linear detrend, window, 4x zero-padded FFT, multi-peak detection with a
|
|
174
|
+
prominence filter, and the MATLAB superlattice heuristics (harmonic
|
|
175
|
+
scoring at 8% tolerance, satellite counting, suppressed-order sublayer
|
|
176
|
+
split).
|
|
177
|
+
"""
|
|
178
|
+
if preprocess not in _PREPROCESS_MODES:
|
|
179
|
+
raise ValueError(f"preprocess must be one of {_PREPROCESS_MODES} (got {preprocess!r})")
|
|
180
|
+
if not 0.0 < peak_prominence_threshold <= 1.0:
|
|
181
|
+
raise ValueError("peak_prominence_threshold must be in (0, 1]")
|
|
182
|
+
x_all = np.asarray(x, dtype=float).ravel()
|
|
183
|
+
r_all = np.asarray(reflectivity, dtype=float).ravel()
|
|
184
|
+
if x_all.size != r_all.size:
|
|
185
|
+
raise ValueError("x and reflectivity must have the same length")
|
|
186
|
+
|
|
187
|
+
lo = float(x_all.min()) if x_min is None else float(x_min)
|
|
188
|
+
hi = float(x_all.max()) if x_max is None else float(x_max)
|
|
189
|
+
if lo >= hi:
|
|
190
|
+
raise ValueError("x_min must be less than x_max")
|
|
191
|
+
mask = (x_all >= lo) & (x_all <= hi)
|
|
192
|
+
if int(mask.sum()) < 10:
|
|
193
|
+
raise ValueError("too few data points in selected range (need >= 10)")
|
|
194
|
+
x_sel = x_all[mask]
|
|
195
|
+
r_sel = r_all[mask]
|
|
196
|
+
|
|
197
|
+
if is_neutron:
|
|
198
|
+
q = np.asarray(x_sel, dtype=float)
|
|
199
|
+
else:
|
|
200
|
+
if wavelength_a is None or not math.isfinite(wavelength_a) or wavelength_a <= 0:
|
|
201
|
+
raise ValueError("wavelength_a is required (positive, finite) for XRR mode")
|
|
202
|
+
q = np.asarray(
|
|
203
|
+
(4.0 * math.pi / wavelength_a) * np.sin(x_sel / 2.0 * math.pi / 180.0), dtype=float
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
use_q4 = "Q4" in preprocess
|
|
207
|
+
use_log = preprocess.startswith("log")
|
|
208
|
+
r_proc = np.asarray(r_sel, dtype=float)
|
|
209
|
+
if use_q4:
|
|
210
|
+
q_safe = np.asarray(np.maximum(q, 1e-6), dtype=float)
|
|
211
|
+
r_proc = np.asarray(r_proc * q_safe**4, dtype=float)
|
|
212
|
+
if use_log:
|
|
213
|
+
r_proc = np.asarray(np.log10(np.maximum(r_proc, 1e-30)), dtype=float)
|
|
214
|
+
|
|
215
|
+
order = np.argsort(q, kind="stable")
|
|
216
|
+
q_sorted = q[order]
|
|
217
|
+
r_sorted = r_proc[order]
|
|
218
|
+
n_pts = q_sorted.size
|
|
219
|
+
q_uniform = np.linspace(float(q_sorted[0]), float(q_sorted[-1]), n_pts)
|
|
220
|
+
r_uniform = np.asarray(PchipInterpolator(q_sorted, r_sorted)(q_uniform), dtype=float)
|
|
221
|
+
|
|
222
|
+
trend = np.polyfit(q_uniform, r_uniform, 1)
|
|
223
|
+
r_uniform = np.asarray(r_uniform - np.polyval(trend, q_uniform), dtype=float)
|
|
224
|
+
|
|
225
|
+
f, thickness_nm, _ = _uniform_q_fft(q_uniform, r_uniform, window)
|
|
226
|
+
|
|
227
|
+
search_min0 = 3 # MATLAB bin 4, 0-based
|
|
228
|
+
below = np.nonzero(thickness_nm <= max_thickness_nm)[0]
|
|
229
|
+
search_max = int(below[-1]) + 1 if below.size else 0
|
|
230
|
+
if search_max < search_min0 + 2:
|
|
231
|
+
search_max = f.size
|
|
232
|
+
f_search = np.asarray(f[search_min0:search_max], dtype=float)
|
|
233
|
+
t_search = np.asarray(thickness_nm[search_min0:search_max], dtype=float)
|
|
234
|
+
|
|
235
|
+
# Local maxima (strict on both sides), MATLAB fallback to the global max.
|
|
236
|
+
n_s = f_search.size
|
|
237
|
+
interior = np.arange(1, n_s - 1)
|
|
238
|
+
is_max = (f_search[interior] > f_search[interior - 1]) & (
|
|
239
|
+
f_search[interior] > f_search[interior + 1]
|
|
240
|
+
)
|
|
241
|
+
max_idx = interior[is_max]
|
|
242
|
+
if max_idx.size == 0:
|
|
243
|
+
max_idx = np.asarray([int(np.argmax(f_search))])
|
|
244
|
+
|
|
245
|
+
pk_amps = f_search[max_idx]
|
|
246
|
+
pk_thick = t_search[max_idx]
|
|
247
|
+
|
|
248
|
+
# Prominence: height above the higher of the two flanking minima.
|
|
249
|
+
prominences = np.empty(pk_amps.size, dtype=float)
|
|
250
|
+
for j, idx in enumerate(max_idx):
|
|
251
|
+
left_min = float(np.min(f_search[: idx + 1]))
|
|
252
|
+
right_min = float(np.min(f_search[idx:]))
|
|
253
|
+
prominences[j] = pk_amps[j] - max(left_min, right_min)
|
|
254
|
+
prom_thresh = peak_prominence_threshold * float(np.max(prominences))
|
|
255
|
+
keep = prominences > prom_thresh
|
|
256
|
+
pk_amps = pk_amps[keep]
|
|
257
|
+
pk_thick = pk_thick[keep]
|
|
258
|
+
|
|
259
|
+
sort_ord = np.argsort(-pk_amps, kind="stable")
|
|
260
|
+
pk_amps = pk_amps[sort_ord]
|
|
261
|
+
pk_thick = pk_thick[sort_ord]
|
|
262
|
+
if pk_amps.size > 20:
|
|
263
|
+
pk_amps = pk_amps[:20]
|
|
264
|
+
pk_thick = pk_thick[:20]
|
|
265
|
+
|
|
266
|
+
superlattice, labels = _superlattice_analysis(pk_thick)
|
|
267
|
+
|
|
268
|
+
result: dict[str, Any] = {
|
|
269
|
+
"thicknesses_nm": pk_thick.tolist(),
|
|
270
|
+
"amplitudes": pk_amps.tolist(),
|
|
271
|
+
"harmonic_labels": labels,
|
|
272
|
+
"q_range": [float(q.min()), float(q.max())],
|
|
273
|
+
"preprocess": preprocess,
|
|
274
|
+
"fft_magnitude": f_search.tolist(),
|
|
275
|
+
"thickness_axis": t_search.tolist(),
|
|
276
|
+
"is_neutron": is_neutron,
|
|
277
|
+
"superlattice": superlattice,
|
|
278
|
+
}
|
|
279
|
+
if not is_neutron:
|
|
280
|
+
result["wavelength_a"] = wavelength_a
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_HARM_TOL = 0.08 # MATLAB harmTol: 8% relative tolerance for harmonic matching
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _ml_round(x: float) -> int:
|
|
288
|
+
"""MATLAB round(): half away from zero (Python round() is half-to-even)."""
|
|
289
|
+
return int(math.floor(x + 0.5)) if x >= 0 else int(math.ceil(x - 0.5))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _superlattice_analysis(
|
|
293
|
+
pk_thick: _FloatArray,
|
|
294
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
295
|
+
"""Port of the doReflFFT superlattice block: try the 5 smallest peak
|
|
296
|
+
thicknesses as bilayer-period candidates, score how many peaks sit on
|
|
297
|
+
integer harmonics, and accept at >= 3 matches."""
|
|
298
|
+
n_pk = pk_thick.size
|
|
299
|
+
labels = [""] * n_pk
|
|
300
|
+
|
|
301
|
+
detected = False
|
|
302
|
+
lambda_nm = float("nan")
|
|
303
|
+
total_nm = float("nan")
|
|
304
|
+
n_repeats: float = float("nan")
|
|
305
|
+
sub_a_nm = float("nan")
|
|
306
|
+
sub_b_nm = float("nan")
|
|
307
|
+
suppressed: list[int] = []
|
|
308
|
+
|
|
309
|
+
if n_pk >= 2:
|
|
310
|
+
t_asc = np.sort(pk_thick)
|
|
311
|
+
best_score = 0
|
|
312
|
+
best_lambda = float("nan")
|
|
313
|
+
for cand in t_asc[: min(5, n_pk)]:
|
|
314
|
+
score = 0
|
|
315
|
+
for t in pk_thick:
|
|
316
|
+
ratio = t / cand
|
|
317
|
+
nr = _ml_round(ratio)
|
|
318
|
+
if nr >= 1 and abs(ratio - nr) / nr < _HARM_TOL:
|
|
319
|
+
score += 1
|
|
320
|
+
if score > best_score:
|
|
321
|
+
best_score = score
|
|
322
|
+
best_lambda = float(cand)
|
|
323
|
+
|
|
324
|
+
if best_score >= 3:
|
|
325
|
+
detected = True
|
|
326
|
+
lambda_nm = best_lambda
|
|
327
|
+
|
|
328
|
+
n_max = 1
|
|
329
|
+
for t in pk_thick:
|
|
330
|
+
ratio = t / lambda_nm
|
|
331
|
+
nr = _ml_round(ratio)
|
|
332
|
+
if nr >= 1 and abs(ratio - nr) / nr < _HARM_TOL and nr > n_max:
|
|
333
|
+
n_max = nr
|
|
334
|
+
|
|
335
|
+
# Satellites between Lambda and 2*Lambda imply extra repeats.
|
|
336
|
+
n_sub = 0
|
|
337
|
+
for t in pk_thick:
|
|
338
|
+
if 1.15 * lambda_nm < t < 1.85 * lambda_nm:
|
|
339
|
+
ratio = t / lambda_nm
|
|
340
|
+
nr = _ml_round(ratio)
|
|
341
|
+
if not (nr == 2 and abs(ratio - 2) / 2 < _HARM_TOL):
|
|
342
|
+
n_sub += 1
|
|
343
|
+
|
|
344
|
+
n_repeats = n_sub + 2 if n_sub > 0 else n_max
|
|
345
|
+
total_nm = n_repeats * lambda_nm
|
|
346
|
+
|
|
347
|
+
for order in range(2, min(6, max(n_max, 3)) + 1):
|
|
348
|
+
expected = order * lambda_nm
|
|
349
|
+
if not any(abs(t - expected) / expected < _HARM_TOL for t in pk_thick):
|
|
350
|
+
suppressed.append(order)
|
|
351
|
+
|
|
352
|
+
if suppressed:
|
|
353
|
+
sub_a_nm = lambda_nm / suppressed[0]
|
|
354
|
+
sub_b_nm = lambda_nm - sub_a_nm
|
|
355
|
+
|
|
356
|
+
bilayer_assigned = False
|
|
357
|
+
for j, t in enumerate(pk_thick):
|
|
358
|
+
ratio = t / lambda_nm
|
|
359
|
+
nr = _ml_round(ratio)
|
|
360
|
+
is_harm = nr >= 1 and abs(ratio - nr) / nr < _HARM_TOL
|
|
361
|
+
if is_harm and nr == 1 and not bilayer_assigned:
|
|
362
|
+
labels[j] = "Bilayer Λ"
|
|
363
|
+
bilayer_assigned = True
|
|
364
|
+
elif is_harm and nr >= 2:
|
|
365
|
+
labels[j] = f"SL order {nr}"
|
|
366
|
+
elif 1.15 * lambda_nm < t < 1.85 * lambda_nm:
|
|
367
|
+
nr2 = _ml_round(t / lambda_nm)
|
|
368
|
+
if not (nr2 == 2 and abs(t / lambda_nm - 2) / 2 < _HARM_TOL):
|
|
369
|
+
labels[j] = "Satellite"
|
|
370
|
+
# MATLAB leaves the order-2-within-tol case unlabeled here
|
|
371
|
+
else:
|
|
372
|
+
labels[j] = "Independent"
|
|
373
|
+
|
|
374
|
+
superlattice = {
|
|
375
|
+
"detected": detected,
|
|
376
|
+
"bilayer_period_nm": lambda_nm,
|
|
377
|
+
"total_thickness_nm": total_nm,
|
|
378
|
+
"n_repeats": n_repeats,
|
|
379
|
+
"sublayer_a_nm": sub_a_nm,
|
|
380
|
+
"sublayer_b_nm": sub_b_nm,
|
|
381
|
+
"suppressed_orders": suppressed,
|
|
382
|
+
}
|
|
383
|
+
return superlattice, labels
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"name":"Silicon","formula":"Si","sldX":2.007E-5,"sldN":2.073E-6,"sldImag":0,"density":2.33},{"name":"Silicon Oxide","formula":"SiO2","sldX":1.888E-5,"sldN":3.47E-6,"sldImag":0,"density":2.2},{"name":"Sapphire","formula":"Al2O3","sldX":2.451E-5,"sldN":5.726E-6,"sldImag":0,"density":3.97},{"name":"Glass (borosilicate)","formula":"BK7","sldX":1.693E-5,"sldN":3.96E-6,"sldImag":0,"density":2.51},{"name":"Quartz","formula":"SiO2","sldX":1.888E-5,"sldN":4.18E-6,"sldImag":0,"density":2.65},{"name":"Gold","formula":"Au","sldX":0.0001245,"sldN":4.46E-6,"sldImag":4.42E-7,"density":19.32},{"name":"Silver","formula":"Ag","sldX":7.44E-5,"sldN":3.47E-6,"sldImag":0,"density":10.5},{"name":"Platinum","formula":"Pt","sldX":0.0001155,"sldN":6.35E-6,"sldImag":2.28E-7,"density":21.45},{"name":"Copper","formula":"Cu","sldX":6.43E-5,"sldN":6.53E-6,"sldImag":8.7E-8,"density":8.96},{"name":"Aluminum","formula":"Al","sldX":2.2E-5,"sldN":2.078E-6,"sldImag":0,"density":2.7},{"name":"Titanium","formula":"Ti","sldX":3.08E-5,"sldN":-1.95E-6,"sldImag":0,"density":4.51},{"name":"Chromium","formula":"Cr","sldX":5.48E-5,"sldN":3.027E-6,"sldImag":0,"density":7.19},{"name":"Tantalum","formula":"Ta","sldX":8.38E-5,"sldN":3.83E-6,"sldImag":0,"density":16.69},{"name":"Palladium","formula":"Pd","sldX":8.02E-5,"sldN":4.01E-6,"sldImag":0,"density":12.02},{"name":"Iron","formula":"Fe","sldX":5.94E-5,"sldN":8.024E-6,"sldImag":0,"density":7.87},{"name":"Cobalt","formula":"Co","sldX":5.89E-5,"sldN":2.261E-6,"sldImag":0,"density":8.9},{"name":"Nickel","formula":"Ni","sldX":6.4E-5,"sldN":9.408E-6,"sldImag":0,"density":8.91},{"name":"Permalloy","formula":"Ni80Fe20","sldX":6.31E-5,"sldN":9.12E-6,"sldImag":0,"density":8.72},{"name":"Magnetite","formula":"Fe3O4","sldX":4.23E-5,"sldN":6.95E-6,"sldImag":0,"density":5.17},{"name":"Alumina","formula":"Al2O3","sldX":2.45E-5,"sldN":5.726E-6,"sldImag":0,"density":3.97},{"name":"Titanium Oxide","formula":"TiO2","sldX":3.1E-5,"sldN":2.632E-6,"sldImag":0,"density":4.23},{"name":"Hafnium Oxide","formula":"HfO2","sldX":4.7E-5,"sldN":5.16E-6,"sldImag":0,"density":9.68},{"name":"Air / Vacuum","formula":"","sldX":0,"sldN":0,"sldImag":0,"density":0},{"name":"Water (H2O)","formula":"H2O","sldX":9.43E-6,"sldN":-5.6E-7,"sldImag":0,"density":1},{"name":"Heavy Water (D2O)","formula":"D2O","sldX":9.43E-6,"sldN":6.335E-6,"sldImag":0,"density":1.11},{"name":"Polystyrene","formula":"PS","sldX":9.6E-6,"sldN":1.412E-6,"sldImag":0,"density":1.05},{"name":"PMMA","formula":"PMMA","sldX":1.093E-5,"sldN":1.065E-6,"sldImag":0,"density":1.18},{"name":"Polyethylene","formula":"PE","sldX":8.61E-6,"sldN":-2.8E-7,"sldImag":0,"density":0.92},{"name":"Silicon Nitride","formula":"Si3N4","sldX":2.215E-5,"sldN":3.27E-6,"sldImag":0,"density":3.17},{"name":"Titanium Nitride","formula":"TiN","sldX":3.32E-5,"sldN":-4.8E-7,"sldImag":0,"density":5.22}]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Specular reflectivity (Parratt recursion). Port of fitting.parrattRefl.
|
|
2
|
+
|
|
3
|
+
Pure calc layer. Computes X-ray/neutron specular reflectivity R(Q) from a layer
|
|
4
|
+
stack via the Parratt recursion, with optional Névot-Croce roughness and Gaussian
|
|
5
|
+
resolution smearing. Internally complex (Fresnel); the returned R = |r|^2 is real.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import ArrayLike, NDArray
|
|
12
|
+
|
|
13
|
+
__all__ = ["parratt_refl"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parratt_refl(
|
|
17
|
+
q: ArrayLike,
|
|
18
|
+
layers: ArrayLike,
|
|
19
|
+
*,
|
|
20
|
+
roughness: bool = True,
|
|
21
|
+
scale: float = 1.0,
|
|
22
|
+
background: float = 0.0,
|
|
23
|
+
resolution: ArrayLike | None = None,
|
|
24
|
+
) -> NDArray[np.float64]:
|
|
25
|
+
"""Specular reflectivity R(Q) from a layer stack. Port of parrattRefl.
|
|
26
|
+
|
|
27
|
+
``layers`` is (M, 4): [thickness Å, SLD_real Å⁻², SLD_imag Å⁻², roughness Å],
|
|
28
|
+
layer 0 = incident medium, last = substrate. ``resolution`` is None, a scalar
|
|
29
|
+
dQ/Q, or a per-point σ_Q vector (Gaussian-smeared over 21 samples, ±3σ).
|
|
30
|
+
"""
|
|
31
|
+
lay = np.asarray(layers, dtype=float)
|
|
32
|
+
n_layers = lay.shape[0]
|
|
33
|
+
if n_layers < 2:
|
|
34
|
+
raise ValueError("need at least 2 layers (incident medium + substrate)")
|
|
35
|
+
q2 = np.asarray(q, dtype=float).ravel()
|
|
36
|
+
n = q2.size
|
|
37
|
+
|
|
38
|
+
if resolution is not None and np.any(np.asarray(resolution, dtype=float) > 0):
|
|
39
|
+
res = np.asarray(resolution, dtype=float)
|
|
40
|
+
if res.size == 1:
|
|
41
|
+
dq = q2 * float(res)
|
|
42
|
+
elif res.size == n:
|
|
43
|
+
dq = res.ravel()
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("resolution must be empty, a scalar dQ/Q, or an N-vector σ_Q")
|
|
46
|
+
n_over, n_sigma = 21, 3
|
|
47
|
+
offsets = np.linspace(-n_sigma, n_sigma, n_over)
|
|
48
|
+
zero_res = dq <= 0
|
|
49
|
+
dq_safe = dq.copy()
|
|
50
|
+
dq_safe[zero_res] = 1.0
|
|
51
|
+
q_full = np.maximum(q2[:, None] + dq_safe[:, None] * offsets[None, :], 1e-6)
|
|
52
|
+
r_flat = parratt_refl(
|
|
53
|
+
q_full.ravel(), lay, roughness=roughness, scale=scale, background=background
|
|
54
|
+
)
|
|
55
|
+
r_mat = r_flat.reshape(n, n_over)
|
|
56
|
+
w = np.exp(-0.5 * ((q_full - q2[:, None]) / dq_safe[:, None]) ** 2)
|
|
57
|
+
r_out = np.sum(w * r_mat, axis=1) / np.sum(w, axis=1)
|
|
58
|
+
if np.any(zero_res):
|
|
59
|
+
r_out[zero_res] = r_mat[zero_res, n_over // 2]
|
|
60
|
+
return np.asarray(r_out, dtype=float)
|
|
61
|
+
|
|
62
|
+
d = lay[:, 0]
|
|
63
|
+
sigma = lay[:, 3]
|
|
64
|
+
sld = lay[:, 1] + 1j * lay[:, 2]
|
|
65
|
+
kz = np.sqrt((q2[:, None] / 2) ** 2 - 4 * np.pi * sld[None, :]) # (N, M) complex
|
|
66
|
+
r = np.zeros(n, dtype=complex)
|
|
67
|
+
for j in range(n_layers - 1, 0, -1):
|
|
68
|
+
kz_above = kz[:, j - 1]
|
|
69
|
+
kz_below = kz[:, j]
|
|
70
|
+
fj = (kz_above - kz_below) / (kz_above + kz_below)
|
|
71
|
+
if roughness and sigma[j] > 0:
|
|
72
|
+
fj = fj * np.exp(-2 * kz_above * kz_below * sigma[j] ** 2)
|
|
73
|
+
if j < n_layers - 1 and d[j] > 0:
|
|
74
|
+
phase = np.exp(2j * kz_below * d[j])
|
|
75
|
+
else:
|
|
76
|
+
phase = np.ones(n, dtype=complex)
|
|
77
|
+
r = (fj + r * phase) / (1 + fj * r * phase)
|
|
78
|
+
|
|
79
|
+
refl = scale * np.abs(r) ** 2 + background
|
|
80
|
+
return np.asarray(np.maximum(refl, 0.0), dtype=float)
|