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,260 @@
|
|
|
1
|
+
"""Scattered 2-D interpolation + regridding. Ports of MATLAB +utilities.
|
|
2
|
+
|
|
3
|
+
Pure calc layer. ``interpolate2d`` interpolates scattered (x, y, z) onto query
|
|
4
|
+
points; ``regrid2d`` builds a regular grid and calls it.
|
|
5
|
+
|
|
6
|
+
Parity notes:
|
|
7
|
+
* ``linear`` / ``nearest`` delegate to ``scipy.interpolate.griddata`` (Qhull
|
|
8
|
+
Delaunay) and match MATLAB ``scatteredInterpolant`` inside the convex hull.
|
|
9
|
+
* ``idw`` and ``thinplate`` are hand-rolled (inverse-distance weighting and
|
|
10
|
+
thin-plate-spline linear systems) and match MATLAB exactly.
|
|
11
|
+
* ``natural`` (the MATLAB default) and ``cubic`` (which MATLAB aliases to
|
|
12
|
+
``natural``) use Sibson natural-neighbour interpolation, hand-rolled in
|
|
13
|
+
``_natural_neighbor`` from the Delaunay triangulation. Sibson coordinates
|
|
14
|
+
are geometrically unique, so this matches MATLAB's 'natural' to ~1e-9 at
|
|
15
|
+
interior points and reproduces affine fields exactly (linear precision).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
from numpy.typing import ArrayLike, NDArray
|
|
24
|
+
from scipy.interpolate import griddata
|
|
25
|
+
from scipy.spatial import Delaunay
|
|
26
|
+
from scipy.spatial._qhull import QhullError
|
|
27
|
+
|
|
28
|
+
from quantized.calc._natural_neighbor import sibson_interpolate
|
|
29
|
+
|
|
30
|
+
__all__ = ["interpolate2d", "regrid2d"]
|
|
31
|
+
|
|
32
|
+
_SCATTERED = ("linear", "natural", "nearest", "cubic")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _unique_rows(
|
|
36
|
+
xv: NDArray[np.float64], yv: NDArray[np.float64], zv: NDArray[np.float64]
|
|
37
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
38
|
+
"""Drop duplicate (x, y) rows keeping first occurrence (MATLAB unique 'stable')."""
|
|
39
|
+
seen: set[tuple[float, float]] = set()
|
|
40
|
+
keep: list[int] = []
|
|
41
|
+
for i in range(xv.size):
|
|
42
|
+
key = (float(xv[i]), float(yv[i]))
|
|
43
|
+
if key not in seen:
|
|
44
|
+
seen.add(key)
|
|
45
|
+
keep.append(i)
|
|
46
|
+
if len(keep) < xv.size:
|
|
47
|
+
return xv[keep], yv[keep], zv[keep]
|
|
48
|
+
return xv, yv, zv
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _hull_mask(
|
|
52
|
+
xv: NDArray[np.float64],
|
|
53
|
+
yv: NDArray[np.float64],
|
|
54
|
+
qpts: NDArray[np.float64],
|
|
55
|
+
zqv: NDArray[np.float64],
|
|
56
|
+
) -> NDArray[np.float64]:
|
|
57
|
+
"""Set query points outside the data convex hull to NaN (extrapolation='none')."""
|
|
58
|
+
if xv.size < 3:
|
|
59
|
+
return zqv
|
|
60
|
+
try:
|
|
61
|
+
tri = Delaunay(np.column_stack([xv, yv]))
|
|
62
|
+
except QhullError:
|
|
63
|
+
return zqv
|
|
64
|
+
out = zqv.copy()
|
|
65
|
+
out[tri.find_simplex(qpts) < 0] = np.nan
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _interp_scattered(
|
|
70
|
+
xv: NDArray[np.float64],
|
|
71
|
+
yv: NDArray[np.float64],
|
|
72
|
+
zv: NDArray[np.float64],
|
|
73
|
+
xqv: NDArray[np.float64],
|
|
74
|
+
yqv: NDArray[np.float64],
|
|
75
|
+
method: str,
|
|
76
|
+
extrapolation: str,
|
|
77
|
+
) -> NDArray[np.float64]:
|
|
78
|
+
pts = np.column_stack([xv, yv])
|
|
79
|
+
qpts = np.column_stack([xqv, yqv])
|
|
80
|
+
if method in ("natural", "cubic"):
|
|
81
|
+
# Sibson natural-neighbour (MATLAB aliases 'cubic' to 'natural'); already
|
|
82
|
+
# NaN outside the convex hull.
|
|
83
|
+
zqv = sibson_interpolate(xv, yv, zv, xqv, yqv)
|
|
84
|
+
elif method == "nearest":
|
|
85
|
+
zqv = np.asarray(griddata(pts, zv, qpts, method="nearest"), dtype=float)
|
|
86
|
+
if extrapolation == "none":
|
|
87
|
+
zqv = _hull_mask(xv, yv, qpts, zqv)
|
|
88
|
+
return zqv
|
|
89
|
+
else: # linear
|
|
90
|
+
try:
|
|
91
|
+
zqv = np.asarray(griddata(pts, zv, qpts, method="linear"), dtype=float)
|
|
92
|
+
except QhullError:
|
|
93
|
+
# Degenerate (collinear / coincident) cloud — Qhull can't triangulate,
|
|
94
|
+
# so linear interpolation is undefined. Degrade to NaN (as for points
|
|
95
|
+
# outside the convex hull) rather than let QhullError escape as a 500;
|
|
96
|
+
# mirrors the QhullError guards already used by _hull_mask and natural.
|
|
97
|
+
zqv = np.full(qpts.shape[0], np.nan)
|
|
98
|
+
if extrapolation == "nearest":
|
|
99
|
+
nan_mask = np.isnan(zqv)
|
|
100
|
+
if nan_mask.any():
|
|
101
|
+
zqv[nan_mask] = griddata(pts, zv, qpts[nan_mask], method="nearest")
|
|
102
|
+
return zqv
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _interp_idw(
|
|
106
|
+
xv: NDArray[np.float64],
|
|
107
|
+
yv: NDArray[np.float64],
|
|
108
|
+
zv: NDArray[np.float64],
|
|
109
|
+
xqv: NDArray[np.float64],
|
|
110
|
+
yqv: NDArray[np.float64],
|
|
111
|
+
power: float,
|
|
112
|
+
) -> NDArray[np.float64]:
|
|
113
|
+
zqv = np.empty(xqv.size)
|
|
114
|
+
for k in range(xqv.size):
|
|
115
|
+
d = np.sqrt((xqv[k] - xv) ** 2 + (yqv[k] - yv) ** 2)
|
|
116
|
+
exact = np.flatnonzero(d == 0)
|
|
117
|
+
if exact.size:
|
|
118
|
+
zqv[k] = zv[exact[0]]
|
|
119
|
+
continue
|
|
120
|
+
w = 1.0 / (d**power)
|
|
121
|
+
zqv[k] = float(np.sum(w * zv) / np.sum(w))
|
|
122
|
+
return zqv
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _interp_thinplate(
|
|
126
|
+
xv: NDArray[np.float64],
|
|
127
|
+
yv: NDArray[np.float64],
|
|
128
|
+
zv: NDArray[np.float64],
|
|
129
|
+
xqv: NDArray[np.float64],
|
|
130
|
+
yqv: NDArray[np.float64],
|
|
131
|
+
lam: float,
|
|
132
|
+
extrapolation: str,
|
|
133
|
+
) -> NDArray[np.float64]:
|
|
134
|
+
n = xv.size
|
|
135
|
+
r2 = (xv[:, None] - xv[None, :]) ** 2 + (yv[:, None] - yv[None, :]) ** 2
|
|
136
|
+
r = np.sqrt(r2)
|
|
137
|
+
phi = np.zeros((n, n))
|
|
138
|
+
mask = r > 0
|
|
139
|
+
phi[mask] = r2[mask] * np.log(r[mask])
|
|
140
|
+
pmat = np.column_stack([np.ones(n), xv, yv])
|
|
141
|
+
amat = np.block([[phi + lam * np.eye(n), pmat], [pmat.T, np.zeros((3, 3))]])
|
|
142
|
+
try:
|
|
143
|
+
coeff = np.linalg.solve(amat, np.concatenate([zv, np.zeros(3)]))
|
|
144
|
+
except np.linalg.LinAlgError as exc:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
"thin-plate spline: data matrix is singular — points are collinear or "
|
|
147
|
+
"coincident; use a coarser grid or a different interpolation method"
|
|
148
|
+
) from exc
|
|
149
|
+
w, a = coeff[:n], coeff[n : n + 3]
|
|
150
|
+
|
|
151
|
+
zqv = np.empty(xqv.size)
|
|
152
|
+
for qi in range(xqv.size):
|
|
153
|
+
r2q = (xqv[qi] - xv) ** 2 + (yqv[qi] - yv) ** 2
|
|
154
|
+
rq = np.sqrt(r2q)
|
|
155
|
+
phiq = np.zeros(n)
|
|
156
|
+
mk = rq > 0
|
|
157
|
+
phiq[mk] = r2q[mk] * np.log(rq[mk])
|
|
158
|
+
zqv[qi] = float(w @ phiq + a[0] + a[1] * xqv[qi] + a[2] * yqv[qi])
|
|
159
|
+
if extrapolation == "none":
|
|
160
|
+
zqv = _hull_mask(xv, yv, np.column_stack([xqv, yqv]), zqv)
|
|
161
|
+
return zqv
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def interpolate2d(
|
|
165
|
+
x: ArrayLike,
|
|
166
|
+
y: ArrayLike,
|
|
167
|
+
z: ArrayLike,
|
|
168
|
+
xq: ArrayLike,
|
|
169
|
+
yq: ArrayLike,
|
|
170
|
+
*,
|
|
171
|
+
method: str = "natural",
|
|
172
|
+
idw_power: float = 2.0,
|
|
173
|
+
extrapolation: str = "none",
|
|
174
|
+
smoothing: float = 0.0,
|
|
175
|
+
) -> dict[str, Any]:
|
|
176
|
+
"""Interpolate scattered (x, y, z) onto query points. Port of utilities.interpolate2D.
|
|
177
|
+
|
|
178
|
+
Returns ``{"zq", "method", "stats": {"nPoints", "rmse"}}`` (rmse is NaN — the
|
|
179
|
+
expensive leave-one-out estimate is not computed). See module docstring for
|
|
180
|
+
per-method parity. ``extrapolation='none'`` yields NaN outside the convex hull.
|
|
181
|
+
"""
|
|
182
|
+
xv = np.asarray(x, dtype=float).ravel()
|
|
183
|
+
yv = np.asarray(y, dtype=float).ravel()
|
|
184
|
+
zv = np.asarray(z, dtype=float).ravel()
|
|
185
|
+
if not (xv.size == yv.size == zv.size):
|
|
186
|
+
raise ValueError("x, y, and z must have the same number of elements")
|
|
187
|
+
if xv.size < 3:
|
|
188
|
+
raise ValueError("at least 3 data points are required for 2-D interpolation")
|
|
189
|
+
xv, yv, zv = _unique_rows(xv, yv, zv)
|
|
190
|
+
|
|
191
|
+
query_shape = np.asarray(xq, dtype=float).shape
|
|
192
|
+
xqv = np.asarray(xq, dtype=float).ravel()
|
|
193
|
+
yqv = np.asarray(yq, dtype=float).ravel()
|
|
194
|
+
|
|
195
|
+
if method in _SCATTERED:
|
|
196
|
+
zqv = _interp_scattered(xv, yv, zv, xqv, yqv, method, extrapolation)
|
|
197
|
+
elif method == "thinplate":
|
|
198
|
+
zqv = _interp_thinplate(xv, yv, zv, xqv, yqv, smoothing, extrapolation)
|
|
199
|
+
elif method == "idw":
|
|
200
|
+
zqv = _interp_idw(xv, yv, zv, xqv, yqv, idw_power)
|
|
201
|
+
if extrapolation == "none":
|
|
202
|
+
zqv = _hull_mask(xv, yv, np.column_stack([xqv, yqv]), zqv)
|
|
203
|
+
else:
|
|
204
|
+
raise ValueError(f"unknown method {method!r}")
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"zq": zqv.reshape(query_shape),
|
|
208
|
+
"method": method,
|
|
209
|
+
"stats": {"nPoints": int(xv.size), "rmse": float("nan")},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def regrid2d(
|
|
214
|
+
x: ArrayLike,
|
|
215
|
+
y: ArrayLike,
|
|
216
|
+
z: ArrayLike,
|
|
217
|
+
*,
|
|
218
|
+
nx: int = 100,
|
|
219
|
+
ny: int = 100,
|
|
220
|
+
method: str = "natural",
|
|
221
|
+
xlim: tuple[float, float] | None = None,
|
|
222
|
+
ylim: tuple[float, float] | None = None,
|
|
223
|
+
extrapolation: str = "none",
|
|
224
|
+
smoothing: float = 0.0,
|
|
225
|
+
idw_power: float = 2.0,
|
|
226
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
227
|
+
"""Resample scattered data onto a regular grid. Port of utilities.regrid2D.
|
|
228
|
+
|
|
229
|
+
Returns ``(Xq, Yq, Zq)`` meshgrids (shape ``ny x nx``). Limits default to the
|
|
230
|
+
data extent. See ``interpolate2d`` for per-method parity caveats.
|
|
231
|
+
"""
|
|
232
|
+
xv = np.asarray(x, dtype=float).ravel()
|
|
233
|
+
yv = np.asarray(y, dtype=float).ravel()
|
|
234
|
+
zv = np.asarray(z, dtype=float).ravel()
|
|
235
|
+
# Scattered interpolation (scatteredInterpolant/griddata) needs finite points;
|
|
236
|
+
# drop any non-finite (x, y, z) triple so real data with NaN gaps still grids.
|
|
237
|
+
# No-op on clean data, so golden parity holds.
|
|
238
|
+
finite = np.isfinite(xv) & np.isfinite(yv) & np.isfinite(zv)
|
|
239
|
+
if not bool(finite.all()):
|
|
240
|
+
xv, yv, zv = xv[finite], yv[finite], zv[finite]
|
|
241
|
+
xl = (float(xv.min()), float(xv.max())) if xlim is None else (float(xlim[0]), float(xlim[1]))
|
|
242
|
+
yl = (float(yv.min()), float(yv.max())) if ylim is None else (float(ylim[0]), float(ylim[1]))
|
|
243
|
+
if xl[0] >= xl[1]:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
"cannot build a 2-D grid: the x axis has no range "
|
|
246
|
+
f"(min == max == {xl[0]:g}); it is constant or single-valued"
|
|
247
|
+
)
|
|
248
|
+
if yl[0] >= yl[1]:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"cannot build a 2-D grid: the y axis has no range "
|
|
251
|
+
f"(min == max == {yl[0]:g}); it is constant or single-valued"
|
|
252
|
+
)
|
|
253
|
+
x_grid = np.linspace(xl[0], xl[1], nx)
|
|
254
|
+
y_grid = np.linspace(yl[0], yl[1], ny)
|
|
255
|
+
xq, yq = np.meshgrid(x_grid, y_grid)
|
|
256
|
+
result = interpolate2d(
|
|
257
|
+
xv, yv, zv, xq, yq,
|
|
258
|
+
method=method, extrapolation=extrapolation, smoothing=smoothing, idw_power=idw_power,
|
|
259
|
+
)
|
|
260
|
+
return xq, yq, np.asarray(result["zq"], dtype=float)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Line cuts + projections from 2-D RSM DataStructs (ORIGIN_GAP_PLAN #18/#46).
|
|
2
|
+
|
|
3
|
+
Port + extension of MATLAB ``+bosonPlotter/extract2DLineCut.m``. The MATLAB
|
|
4
|
+
reference offered single nearest-row/column cuts (Shift/Ctrl+click) in angular
|
|
5
|
+
or Q space. Extended here:
|
|
6
|
+
|
|
7
|
+
- **width-averaged cuts** (``width > 0`` averages every row/column whose axis
|
|
8
|
+
value falls within ±width/2 — a swath, not one detector line);
|
|
9
|
+
- **arbitrary segment cuts** (:func:`cut_segment` — any angle through the
|
|
10
|
+
cloud, distance-parametrized, with optional perpendicular averaging);
|
|
11
|
+
- **integrated projections** (:func:`projection` — the full-map sum onto
|
|
12
|
+
either axis; the "pixels" direction reproduces MATLAB's integrated 1-D
|
|
13
|
+
fallback ``sum(intensityMap, 1)``).
|
|
14
|
+
|
|
15
|
+
Cuts work on the (frames × pixels) index grid via ``metadata.map_shape``, so
|
|
16
|
+
they are exact detector-line extractions for all three mesh kinds (mesh /
|
|
17
|
+
snapshot / coupled). Segment cuts interpolate the scattered cloud directly
|
|
18
|
+
and need no grid. Every function returns a 1-D ``DataStruct`` ready for the
|
|
19
|
+
library (plot, fit, export like any scan).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
from numpy.typing import NDArray
|
|
28
|
+
|
|
29
|
+
from quantized.calc.interp2d import interpolate2d
|
|
30
|
+
from quantized.datastruct import DataStruct
|
|
31
|
+
|
|
32
|
+
__all__ = ["cut_segment", "line_cut", "projection"]
|
|
33
|
+
|
|
34
|
+
_SPACES = ("angular", "q")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _full_grids(ds: DataStruct) -> dict[str, Any]:
|
|
38
|
+
"""Reshape the scattered 2-D DataStruct to full (N, M) per-point grids."""
|
|
39
|
+
if not ds.metadata.get("is2D"):
|
|
40
|
+
raise ValueError("dataset is not a 2-D map (metadata.is2D not set)")
|
|
41
|
+
shape = ds.metadata.get("map_shape")
|
|
42
|
+
if not shape or len(shape) != 2:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"dataset has no regular (frames x pixels) grid (map_shape missing) — "
|
|
45
|
+
"use cut_segment, which works on the scattered cloud"
|
|
46
|
+
)
|
|
47
|
+
n, m = int(shape[0]), int(shape[1])
|
|
48
|
+
axis1_name = str(ds.metadata.get("axis1_name", "Omega"))
|
|
49
|
+
out: dict[str, Any] = {
|
|
50
|
+
"tt": ds.column("2Theta").reshape(n, m),
|
|
51
|
+
"sec": ds.column(axis1_name).reshape(n, m),
|
|
52
|
+
"i": ds.column("Intensity").reshape(n, m),
|
|
53
|
+
"qx": None,
|
|
54
|
+
"qz": None,
|
|
55
|
+
"sec_name": axis1_name,
|
|
56
|
+
"unit": ds.units[list(ds.labels).index("Intensity")],
|
|
57
|
+
}
|
|
58
|
+
if "Qx" in ds.labels and "Qz" in ds.labels:
|
|
59
|
+
out["qx"] = ds.column("Qx").reshape(n, m)
|
|
60
|
+
out["qz"] = ds.column("Qz").reshape(n, m)
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _require_q(g: dict[str, Any]) -> None:
|
|
65
|
+
if g["qx"] is None:
|
|
66
|
+
raise ValueError("dataset has no Qx/Qz columns — Q-space cut unavailable")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _cut_result(
|
|
70
|
+
x: NDArray[np.float64],
|
|
71
|
+
y: NDArray[np.float64],
|
|
72
|
+
*,
|
|
73
|
+
label: str,
|
|
74
|
+
x_name: str,
|
|
75
|
+
x_unit: str,
|
|
76
|
+
unit: str,
|
|
77
|
+
ds: DataStruct,
|
|
78
|
+
extra: dict[str, Any] | None = None,
|
|
79
|
+
) -> DataStruct:
|
|
80
|
+
"""Assemble the 1-D cut DataStruct (ascending x for the plot stage)."""
|
|
81
|
+
order = np.argsort(x, kind="stable")
|
|
82
|
+
metadata: dict[str, Any] = {
|
|
83
|
+
"source": ds.metadata.get("source", ""),
|
|
84
|
+
"parser_name": "line_cut",
|
|
85
|
+
"x_column_name": x_name,
|
|
86
|
+
"x_column_unit": x_unit,
|
|
87
|
+
"cut_label": label,
|
|
88
|
+
"is2D": False,
|
|
89
|
+
**(extra or {}),
|
|
90
|
+
}
|
|
91
|
+
return DataStruct.create(
|
|
92
|
+
np.asarray(x, dtype=float)[order],
|
|
93
|
+
np.asarray(y, dtype=float)[order],
|
|
94
|
+
labels=["Intensity"],
|
|
95
|
+
units=[unit],
|
|
96
|
+
metadata=metadata,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def line_cut(
|
|
101
|
+
ds: DataStruct,
|
|
102
|
+
*,
|
|
103
|
+
direction: str,
|
|
104
|
+
value: float,
|
|
105
|
+
space: str = "angular",
|
|
106
|
+
width: float = 0.0,
|
|
107
|
+
) -> DataStruct:
|
|
108
|
+
"""Horizontal / vertical cut through the map at a fixed axis value.
|
|
109
|
+
|
|
110
|
+
``direction='h'``: intensity vs the horizontal axis (2Theta, or Qx in
|
|
111
|
+
Q-space) at the frame(s) nearest ``value`` on the vertical axis.
|
|
112
|
+
``direction='v'``: intensity vs the vertical axis (secondary motor, or Qz)
|
|
113
|
+
at the pixel column(s) nearest ``value`` on the horizontal axis.
|
|
114
|
+
|
|
115
|
+
``width=0`` reproduces MATLAB's single nearest-line cut; ``width>0``
|
|
116
|
+
averages every line whose axis value lies within ±width/2 of ``value``
|
|
117
|
+
(falls back to the nearest single line when none do).
|
|
118
|
+
"""
|
|
119
|
+
if direction not in ("h", "v"):
|
|
120
|
+
raise ValueError(f'direction must be "h" or "v", got "{direction}"')
|
|
121
|
+
if space not in _SPACES:
|
|
122
|
+
raise ValueError(f"space must be one of {_SPACES}, got {space!r}")
|
|
123
|
+
if width < 0:
|
|
124
|
+
raise ValueError("width must be >= 0")
|
|
125
|
+
g = _full_grids(ds)
|
|
126
|
+
if space == "q":
|
|
127
|
+
_require_q(g)
|
|
128
|
+
|
|
129
|
+
if direction == "h":
|
|
130
|
+
sel = np.mean(g["sec"] if space == "angular" else g["qz"], axis=1)
|
|
131
|
+
x_grid = g["tt"] if space == "angular" else g["qx"]
|
|
132
|
+
pick = np.abs(sel - value) <= width / 2.0
|
|
133
|
+
if not pick.any():
|
|
134
|
+
pick = np.zeros(sel.size, dtype=bool)
|
|
135
|
+
pick[int(np.argmin(np.abs(sel - value)))] = True
|
|
136
|
+
x = np.mean(x_grid[pick, :], axis=0)
|
|
137
|
+
y = np.mean(g["i"][pick, :], axis=0)
|
|
138
|
+
fixed_name = g["sec_name"] if space == "angular" else "Qz"
|
|
139
|
+
x_name = "2Theta" if space == "angular" else "Qx"
|
|
140
|
+
else:
|
|
141
|
+
sel = np.mean(g["tt"] if space == "angular" else g["qx"], axis=0)
|
|
142
|
+
x_grid = g["sec"] if space == "angular" else g["qz"]
|
|
143
|
+
pick = np.abs(sel - value) <= width / 2.0
|
|
144
|
+
if not pick.any():
|
|
145
|
+
pick = np.zeros(sel.size, dtype=bool)
|
|
146
|
+
pick[int(np.argmin(np.abs(sel - value)))] = True
|
|
147
|
+
x = np.mean(x_grid[:, pick], axis=1)
|
|
148
|
+
y = np.mean(g["i"][:, pick], axis=1)
|
|
149
|
+
fixed_name = "2Theta" if space == "angular" else "Qx"
|
|
150
|
+
x_name = g["sec_name"] if space == "angular" else "Qz"
|
|
151
|
+
|
|
152
|
+
unit_ax = "deg" if space == "angular" else "Ang^-1"
|
|
153
|
+
centre = float(np.mean(sel[pick]))
|
|
154
|
+
tag = "H-cut" if direction == "h" else "V-cut"
|
|
155
|
+
label = f"{tag} {fixed_name}≈{centre:.6g} {unit_ax}"
|
|
156
|
+
if width > 0:
|
|
157
|
+
label += f" ±{width / 2:.4g}"
|
|
158
|
+
return _cut_result(
|
|
159
|
+
x, y, label=label, x_name=x_name, x_unit=unit_ax, unit=g["unit"], ds=ds,
|
|
160
|
+
extra={"cut_direction": direction, "cut_value": centre,
|
|
161
|
+
"cut_width": width, "cut_space": space, "n_lines": int(pick.sum())},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cut_segment(
|
|
166
|
+
ds: DataStruct,
|
|
167
|
+
*,
|
|
168
|
+
p0: tuple[float, float],
|
|
169
|
+
p1: tuple[float, float],
|
|
170
|
+
n: int = 200,
|
|
171
|
+
width: float = 0.0,
|
|
172
|
+
space: str = "angular",
|
|
173
|
+
) -> DataStruct:
|
|
174
|
+
"""Arbitrary straight cut from ``p0`` to ``p1`` through the scattered cloud.
|
|
175
|
+
|
|
176
|
+
Coordinates are ``(2Theta, <axis1>)`` for ``space='angular'`` or
|
|
177
|
+
``(Qx, Qz)`` for ``space='q'``. Samples ``n`` points along the segment by
|
|
178
|
+
linear scattered interpolation; ``width>0`` additionally averages 7
|
|
179
|
+
parallel lines spread over ±width/2 perpendicular to the cut (NaN outside
|
|
180
|
+
the data hull is ignored). x = distance from ``p0``.
|
|
181
|
+
"""
|
|
182
|
+
if space not in _SPACES:
|
|
183
|
+
raise ValueError(f"space must be one of {_SPACES}, got {space!r}")
|
|
184
|
+
if not ds.metadata.get("is2D"):
|
|
185
|
+
raise ValueError("dataset is not a 2-D map (metadata.is2D not set)")
|
|
186
|
+
if n < 2:
|
|
187
|
+
raise ValueError("n must be >= 2")
|
|
188
|
+
if width < 0:
|
|
189
|
+
raise ValueError("width must be >= 0")
|
|
190
|
+
axis1_name = str(ds.metadata.get("axis1_name", "Omega"))
|
|
191
|
+
if space == "q":
|
|
192
|
+
if "Qx" not in ds.labels:
|
|
193
|
+
raise ValueError("dataset has no Qx/Qz columns — Q-space cut unavailable")
|
|
194
|
+
xs, ys = ds.column("Qx"), ds.column("Qz")
|
|
195
|
+
unit_ax = "Ang^-1"
|
|
196
|
+
else:
|
|
197
|
+
xs, ys = ds.column("2Theta"), ds.column(axis1_name)
|
|
198
|
+
unit_ax = "deg"
|
|
199
|
+
iv = ds.column("Intensity")
|
|
200
|
+
|
|
201
|
+
d = np.asarray([p1[0] - p0[0], p1[1] - p0[1]], dtype=float)
|
|
202
|
+
length = float(np.hypot(*d))
|
|
203
|
+
if length <= 0:
|
|
204
|
+
raise ValueError("p0 and p1 must be distinct points")
|
|
205
|
+
d /= length
|
|
206
|
+
perp = np.asarray([-d[1], d[0]])
|
|
207
|
+
t = np.linspace(0.0, length, n)
|
|
208
|
+
base_x = p0[0] + t * d[0]
|
|
209
|
+
base_y = p0[1] + t * d[1]
|
|
210
|
+
|
|
211
|
+
offsets = np.linspace(-width / 2.0, width / 2.0, 7) if width > 0 else np.asarray([0.0])
|
|
212
|
+
xq = (base_x[None, :] + offsets[:, None] * perp[0]).ravel()
|
|
213
|
+
yq = (base_y[None, :] + offsets[:, None] * perp[1]).ravel()
|
|
214
|
+
zq = np.asarray(
|
|
215
|
+
interpolate2d(xs, ys, iv, xq, yq, method="linear")["zq"], dtype=float
|
|
216
|
+
).reshape(offsets.size, n)
|
|
217
|
+
with np.errstate(invalid="ignore"):
|
|
218
|
+
y = np.nanmean(zq, axis=0)
|
|
219
|
+
|
|
220
|
+
unit = ds.units[list(ds.labels).index("Intensity")]
|
|
221
|
+
label = (
|
|
222
|
+
f"Cut ({p0[0]:.5g}, {p0[1]:.5g})→({p1[0]:.5g}, {p1[1]:.5g}) "
|
|
223
|
+
f"[{'Q' if space == 'q' else 'angular'}]"
|
|
224
|
+
)
|
|
225
|
+
if width > 0:
|
|
226
|
+
label += f" ±{width / 2:.4g}"
|
|
227
|
+
return _cut_result(
|
|
228
|
+
t, y, label=label, x_name="Distance", x_unit=unit_ax, unit=unit, ds=ds,
|
|
229
|
+
extra={"cut_p0": list(p0), "cut_p1": list(p1), "cut_width": width,
|
|
230
|
+
"cut_space": space, "cut_samples": int(n)},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def projection(
|
|
235
|
+
ds: DataStruct,
|
|
236
|
+
*,
|
|
237
|
+
axis: str = "pixels",
|
|
238
|
+
space: str = "angular",
|
|
239
|
+
) -> DataStruct:
|
|
240
|
+
"""Integrate the whole map onto one axis.
|
|
241
|
+
|
|
242
|
+
``axis='pixels'``: sum over frames → intensity vs 2Theta (or Qx) — exactly
|
|
243
|
+
MATLAB importXRDML's integrated 1-D fallback for a mesh.
|
|
244
|
+
``axis='frames'``: sum over pixels → intensity vs the secondary motor
|
|
245
|
+
(or Qz) — a rocking-curve-like profile.
|
|
246
|
+
"""
|
|
247
|
+
if axis not in ("pixels", "frames"):
|
|
248
|
+
raise ValueError(f'axis must be "pixels" or "frames", got "{axis}"')
|
|
249
|
+
if space not in _SPACES:
|
|
250
|
+
raise ValueError(f"space must be one of {_SPACES}, got {space!r}")
|
|
251
|
+
g = _full_grids(ds)
|
|
252
|
+
if space == "q":
|
|
253
|
+
_require_q(g)
|
|
254
|
+
|
|
255
|
+
if axis == "pixels":
|
|
256
|
+
x = np.mean(g["tt"] if space == "angular" else g["qx"], axis=0)
|
|
257
|
+
y = np.sum(g["i"], axis=0)
|
|
258
|
+
x_name = "2Theta" if space == "angular" else "Qx"
|
|
259
|
+
else:
|
|
260
|
+
x = np.mean(g["sec"] if space == "angular" else g["qz"], axis=1)
|
|
261
|
+
y = np.sum(g["i"], axis=1)
|
|
262
|
+
x_name = g["sec_name"] if space == "angular" else "Qz"
|
|
263
|
+
|
|
264
|
+
unit_ax = "deg" if space == "angular" else "Ang^-1"
|
|
265
|
+
label = f"Projection Σ{'frames' if axis == 'pixels' else 'pixels'} → I vs {x_name}"
|
|
266
|
+
return _cut_result(
|
|
267
|
+
x, y, label=label, x_name=x_name, x_unit=unit_ax, unit=g["unit"], ds=ds,
|
|
268
|
+
extra={"cut_axis": axis, "cut_space": space},
|
|
269
|
+
)
|