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,148 @@
|
|
|
1
|
+
"""Run the same curve fit across many datasets. Port of fitting.batchFit.
|
|
2
|
+
|
|
3
|
+
Pure calc layer. Fits one model to each dataset and collects the per-dataset
|
|
4
|
+
parameters / errors / fit statistics into a summary for trend analysis (e.g.
|
|
5
|
+
fitted τ vs temperature). Reuses the bounded ``curve_fit``; optional per-dataset
|
|
6
|
+
auto-guess via a registered model name, x-range restriction, and 1/y or 1/y²
|
|
7
|
+
weighting — all matching the MATLAB original.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from numpy.typing import NDArray
|
|
17
|
+
|
|
18
|
+
from quantized.datastruct import DataStruct
|
|
19
|
+
|
|
20
|
+
from .fit_autoguess import auto_guess
|
|
21
|
+
from .fit_models import FIT_MODELS
|
|
22
|
+
from .fitting import curve_fit
|
|
23
|
+
|
|
24
|
+
__all__ = ["batch_fit"]
|
|
25
|
+
|
|
26
|
+
_EPS = float(np.finfo(float).eps)
|
|
27
|
+
ModelFn = Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extract_xy(
|
|
31
|
+
ds: DataStruct | tuple[Any, Any] | list[Any], channel: int
|
|
32
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], dict[str, Any] | None]:
|
|
33
|
+
"""(x, y, metadata) from a DataStruct or an (x, y) pair."""
|
|
34
|
+
if isinstance(ds, DataStruct):
|
|
35
|
+
x = np.asarray(ds.time, dtype=float).ravel()
|
|
36
|
+
values = np.asarray(ds.values, dtype=float)
|
|
37
|
+
if values.ndim == 2:
|
|
38
|
+
ch = min(channel, values.shape[1] - 1)
|
|
39
|
+
y = values[:, ch]
|
|
40
|
+
else:
|
|
41
|
+
y = values.ravel()
|
|
42
|
+
return x, np.asarray(y, dtype=float).ravel(), dict(ds.metadata)
|
|
43
|
+
if isinstance(ds, (tuple, list)) and len(ds) >= 2:
|
|
44
|
+
return (
|
|
45
|
+
np.asarray(ds[0], dtype=float).ravel(),
|
|
46
|
+
np.asarray(ds[1], dtype=float).ravel(),
|
|
47
|
+
None,
|
|
48
|
+
)
|
|
49
|
+
return np.empty(0), np.empty(0), None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def batch_fit(
|
|
53
|
+
datasets: list[Any],
|
|
54
|
+
model_fcn: ModelFn,
|
|
55
|
+
p0: Sequence[float],
|
|
56
|
+
*,
|
|
57
|
+
lower: Sequence[float] | None = None,
|
|
58
|
+
upper: Sequence[float] | None = None,
|
|
59
|
+
fixed: Sequence[bool] | None = None,
|
|
60
|
+
channel: int = 0,
|
|
61
|
+
model_name: str = "",
|
|
62
|
+
meta_field: str = "",
|
|
63
|
+
x_range: Sequence[float] | None = None,
|
|
64
|
+
weights: str = "none",
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
"""Fit ``model_fcn`` to every dataset; return a per-dataset summary.
|
|
67
|
+
|
|
68
|
+
Port of fitting.batchFit. Each dataset is a ``DataStruct`` or an ``(x, y)``
|
|
69
|
+
pair. ``model_name`` (if registered) drives per-dataset auto-guess and the
|
|
70
|
+
output ``paramNames``. ``weights`` ∈ {``none``, ``1/y``, ``1/y2``}. Returns a
|
|
71
|
+
dict with ``params``/``errors`` (N×M), ``R2``/``chiSqRed``/``RMSE``/``AIC``/
|
|
72
|
+
``exitFlags``/``metaValues`` (length N, NaN where a fit was skipped/failed),
|
|
73
|
+
``paramNames``, ``modelName``, ``nDatasets``, ``converged``.
|
|
74
|
+
"""
|
|
75
|
+
if weights not in ("none", "1/y", "1/y2"):
|
|
76
|
+
raise ValueError(f'weights must be "none", "1/y", or "1/y2", got "{weights}"')
|
|
77
|
+
n = len(datasets)
|
|
78
|
+
m = len(p0)
|
|
79
|
+
nan = float("nan")
|
|
80
|
+
params = np.full((n, m), nan)
|
|
81
|
+
errors = np.full((n, m), nan)
|
|
82
|
+
r2 = [nan] * n
|
|
83
|
+
chi_sq_red = [nan] * n
|
|
84
|
+
rmse = [nan] * n
|
|
85
|
+
aic = [nan] * n
|
|
86
|
+
exit_flags = [0] * n
|
|
87
|
+
meta_values = [nan] * n
|
|
88
|
+
|
|
89
|
+
for i in range(n):
|
|
90
|
+
x_data, y_data, meta = _extract_xy(datasets[i], channel)
|
|
91
|
+
if x_data.size < m + 1:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
if x_range is not None and len(x_range) == 2:
|
|
95
|
+
mask = (x_data >= x_range[0]) & (x_data <= x_range[1])
|
|
96
|
+
x_data, y_data = x_data[mask], y_data[mask]
|
|
97
|
+
|
|
98
|
+
w: NDArray[np.float64] | None = None
|
|
99
|
+
if weights == "1/y":
|
|
100
|
+
w = 1.0 / np.maximum(np.abs(y_data), _EPS)
|
|
101
|
+
elif weights == "1/y2":
|
|
102
|
+
w = 1.0 / np.maximum(y_data**2, _EPS)
|
|
103
|
+
|
|
104
|
+
p0i: Sequence[float] = p0
|
|
105
|
+
if model_name:
|
|
106
|
+
try:
|
|
107
|
+
p0i = auto_guess(model_name, x_data, y_data)
|
|
108
|
+
except Exception: # noqa: BLE001 — fall back to the provided p0
|
|
109
|
+
p0i = p0
|
|
110
|
+
|
|
111
|
+
if meta_field and meta is not None:
|
|
112
|
+
val = meta.get(meta_field)
|
|
113
|
+
if isinstance(val, (int, float)) and not isinstance(val, bool):
|
|
114
|
+
meta_values[i] = float(val)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
r = curve_fit(x_data, y_data, model_fcn, p0i, lower=lower, upper=upper,
|
|
118
|
+
fixed=fixed, weights=w)
|
|
119
|
+
except Exception: # noqa: BLE001 — a failed fit leaves the NaN row (like MATLAB)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
params[i, :] = np.asarray(r["params"], dtype=float)
|
|
123
|
+
errors[i, :] = np.asarray(r["errors"], dtype=float)
|
|
124
|
+
r2[i] = float(r["R2"])
|
|
125
|
+
chi_sq_red[i] = float(r["chiSqRed"])
|
|
126
|
+
rmse[i] = float(r["RMSE"])
|
|
127
|
+
aic[i] = float(r["AIC"])
|
|
128
|
+
exit_flags[i] = int(r["exitFlag"])
|
|
129
|
+
|
|
130
|
+
if model_name and model_name in FIT_MODELS:
|
|
131
|
+
param_names = list(FIT_MODELS[model_name]["paramNames"])
|
|
132
|
+
else:
|
|
133
|
+
param_names = [f"p{j + 1}" for j in range(m)]
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"params": params,
|
|
137
|
+
"errors": errors,
|
|
138
|
+
"R2": r2,
|
|
139
|
+
"chiSqRed": chi_sq_red,
|
|
140
|
+
"RMSE": rmse,
|
|
141
|
+
"AIC": aic,
|
|
142
|
+
"exitFlags": exit_flags,
|
|
143
|
+
"paramNames": param_names,
|
|
144
|
+
"modelName": model_name,
|
|
145
|
+
"metaValues": meta_values,
|
|
146
|
+
"nDatasets": n,
|
|
147
|
+
"converged": [flag == 1 for flag in exit_flags],
|
|
148
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Fundamental physical constants (CODATA 2018, SI units). Port of calc.constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
__all__ = ["constants"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def constants() -> dict[str, float]:
|
|
11
|
+
"""Return a dict of fundamental physical constants (CODATA 2018, SI)."""
|
|
12
|
+
return {
|
|
13
|
+
"h": 6.62607015e-34, # Planck constant (J*s)
|
|
14
|
+
"hbar": 1.054571817e-34, # reduced Planck constant (J*s)
|
|
15
|
+
"c": 2.99792458e8, # speed of light (m/s)
|
|
16
|
+
"e": 1.602176634e-19, # elementary charge (C)
|
|
17
|
+
"kB": 1.380649e-23, # Boltzmann constant (J/K)
|
|
18
|
+
"NA": 6.02214076e23, # Avogadro number (1/mol)
|
|
19
|
+
"mu0": 4 * math.pi * 1e-7, # vacuum permeability (H/m)
|
|
20
|
+
"eps0": 8.8541878128e-12, # vacuum permittivity (F/m)
|
|
21
|
+
"muB": 9.2740100783e-24, # Bohr magneton (J/T)
|
|
22
|
+
"r_e": 2.8179403262e-15, # classical electron radius (m)
|
|
23
|
+
"m_e": 9.1093837015e-31, # electron mass (kg)
|
|
24
|
+
"R": 8.314462618, # molar gas constant (J/mol/K)
|
|
25
|
+
"F": 96485.33212, # Faraday constant (C/mol)
|
|
26
|
+
"Phi0": 2.067833848e-15, # magnetic flux quantum (Wb)
|
|
27
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Correction pipeline for a dataset. Port of bosonPlotter.applyCorrections.
|
|
2
|
+
|
|
3
|
+
Pure calc layer. Applies, in order: trim -> x-offset -> beam-footprint scale
|
|
4
|
+
(GOTO #7b) -> background subtraction (+ y-offset, or neutron R-scale; an
|
|
5
|
+
anchor-point baseline (GOTO #2) beats the polynomial/slope forms) -> optional
|
|
6
|
+
reference-background subtraction -> magnetometry unit conversion -> smoothing
|
|
7
|
+
-> normalization -> derivative. Composes the already-ported processing/units
|
|
8
|
+
helpers; operates on a DataStruct + a params dict mirroring the MATLAB
|
|
9
|
+
``params`` struct (the GOTO additions are new keys, absent from MATLAB).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from ..datastruct import DataStruct
|
|
20
|
+
from .backgrounds import anchor_baseline, footprint_factor
|
|
21
|
+
from .processing import (
|
|
22
|
+
cumulative_integral,
|
|
23
|
+
derivative,
|
|
24
|
+
log_derivative,
|
|
25
|
+
normalize,
|
|
26
|
+
smooth_data,
|
|
27
|
+
)
|
|
28
|
+
from .units import convert_units
|
|
29
|
+
|
|
30
|
+
__all__ = ["apply_corrections"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _matlab_round(x: float) -> int:
|
|
34
|
+
return int(math.copysign(math.floor(abs(x) + 0.5), x))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _interp_zero_fill(
|
|
38
|
+
bgx: np.ndarray, bgy: np.ndarray, xnew: np.ndarray, method: str
|
|
39
|
+
) -> np.ndarray:
|
|
40
|
+
"""interp1(bgx, bgy, xnew, method, 0): interpolate with 0 outside the range."""
|
|
41
|
+
if method == "linear":
|
|
42
|
+
return np.asarray(np.interp(xnew, bgx, bgy, left=0.0, right=0.0), dtype=float)
|
|
43
|
+
from scipy.interpolate import CubicSpline, PchipInterpolator
|
|
44
|
+
|
|
45
|
+
order = np.argsort(bgx)
|
|
46
|
+
bx, by = bgx[order], bgy[order]
|
|
47
|
+
if method == "spline":
|
|
48
|
+
out = CubicSpline(bx, by, bc_type="not-a-knot", extrapolate=False)(xnew)
|
|
49
|
+
else: # pchip
|
|
50
|
+
out = PchipInterpolator(bx, by, extrapolate=False)(xnew)
|
|
51
|
+
return np.asarray(np.nan_to_num(out, nan=0.0), dtype=float)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def apply_corrections(
|
|
55
|
+
data: DataStruct,
|
|
56
|
+
params: dict[str, Any],
|
|
57
|
+
*,
|
|
58
|
+
bg_dataset: DataStruct | None = None,
|
|
59
|
+
bg_interp: str = "linear",
|
|
60
|
+
) -> DataStruct:
|
|
61
|
+
"""Apply the correction pipeline to ``data``. Port of bosonPlotter.applyCorrections.
|
|
62
|
+
|
|
63
|
+
``params`` keys (all optional, with sensible defaults): xOff, yOff, bgSlope,
|
|
64
|
+
bgInt, bgPoly, xTrimMin, xTrimMax, isNeutron, isMag, fieldUnit, momentUnit,
|
|
65
|
+
sampleMass, sampleVolume, smoothEnabled, smoothWindow, smoothMethod,
|
|
66
|
+
normMethod, derivativeMode. GOTO additions (new, beyond MATLAB parity):
|
|
67
|
+
bgAnchors + bgAnchorMethod (anchor-point baseline subtraction, #2) and
|
|
68
|
+
footprintW + footprintL + footprintTwoTheta (XRR/NR beam-footprint scale,
|
|
69
|
+
#7b — skips channels labelled ``dq``, like the neutron R-scale). Returns a
|
|
70
|
+
new DataStruct.
|
|
71
|
+
"""
|
|
72
|
+
time = np.asarray(data.time, dtype=float).copy()
|
|
73
|
+
values = np.asarray(data.values, dtype=float).copy()
|
|
74
|
+
labels = list(data.labels)
|
|
75
|
+
|
|
76
|
+
# 1. Trim on x.
|
|
77
|
+
x_min = params.get("xTrimMin", float("nan"))
|
|
78
|
+
x_max = params.get("xTrimMax", float("nan"))
|
|
79
|
+
if not (math.isnan(x_min) and math.isnan(x_max)):
|
|
80
|
+
mask = np.ones(time.size, dtype=bool)
|
|
81
|
+
if not math.isnan(x_min):
|
|
82
|
+
mask &= time >= x_min
|
|
83
|
+
if not math.isnan(x_max):
|
|
84
|
+
mask &= time <= x_max
|
|
85
|
+
time = time[mask]
|
|
86
|
+
values = values[mask, :]
|
|
87
|
+
|
|
88
|
+
# 2. X offset.
|
|
89
|
+
time = time - params.get("xOff", 0.0)
|
|
90
|
+
|
|
91
|
+
# 2b. XRR/NR beam-footprint scale (GOTO #7b): divide by the illuminated
|
|
92
|
+
# fraction below the spill-over angle, unity above. Applied before any
|
|
93
|
+
# background handling (it corrects the RAW measured intensity); skips
|
|
94
|
+
# resolution channels labelled "dq" like the neutron R-scale below.
|
|
95
|
+
fp_w = params.get("footprintW", 0.0)
|
|
96
|
+
fp_l = params.get("footprintL", 0.0)
|
|
97
|
+
if fp_w > 0 and fp_l > 0:
|
|
98
|
+
theta = time / 2.0 if params.get("footprintTwoTheta", False) else time
|
|
99
|
+
factor = footprint_factor(theta, beam_width=fp_w, sample_length=fp_l)
|
|
100
|
+
for k in range(values.shape[1]):
|
|
101
|
+
if labels[k].lower() != "dq":
|
|
102
|
+
values[:, k] = values[:, k] / factor
|
|
103
|
+
|
|
104
|
+
# 3. Neutron R-scale, or background subtraction + y-offset.
|
|
105
|
+
y_off = params.get("yOff", 0.0)
|
|
106
|
+
if params.get("isNeutron", False):
|
|
107
|
+
for k in range(values.shape[1]):
|
|
108
|
+
if labels[k].lower() != "dq":
|
|
109
|
+
values[:, k] = values[:, k] * y_off
|
|
110
|
+
else:
|
|
111
|
+
# An anchor-point baseline (GOTO #2) beats the polynomial/slope forms.
|
|
112
|
+
bg_anchors = params.get("bgAnchors")
|
|
113
|
+
anchor_bg = (
|
|
114
|
+
anchor_baseline(
|
|
115
|
+
time,
|
|
116
|
+
values[:, 0] if values.shape[1] else time,
|
|
117
|
+
bg_anchors,
|
|
118
|
+
method=str(params.get("bgAnchorMethod", "pchip")),
|
|
119
|
+
)
|
|
120
|
+
if bg_anchors is not None and len(bg_anchors) >= 2
|
|
121
|
+
else None
|
|
122
|
+
)
|
|
123
|
+
bg_poly = params.get("bgPoly")
|
|
124
|
+
poly_coeffs = (
|
|
125
|
+
np.asarray(bg_poly, dtype=float)
|
|
126
|
+
if bg_poly is not None and len(bg_poly) > 2
|
|
127
|
+
else None
|
|
128
|
+
)
|
|
129
|
+
for k in range(values.shape[1]):
|
|
130
|
+
if anchor_bg is not None:
|
|
131
|
+
y_bg = anchor_bg
|
|
132
|
+
elif poly_coeffs is not None:
|
|
133
|
+
y_bg = np.polyval(poly_coeffs, time)
|
|
134
|
+
else:
|
|
135
|
+
y_bg = params.get("bgSlope", 0.0) * time + params.get("bgInt", 0.0)
|
|
136
|
+
values[:, k] = values[:, k] - y_bg - y_off
|
|
137
|
+
|
|
138
|
+
# 4. Optional reference-background dataset subtraction.
|
|
139
|
+
if bg_dataset is not None:
|
|
140
|
+
bgx = np.asarray(bg_dataset.time, dtype=float)
|
|
141
|
+
bgy = np.asarray(bg_dataset.values, dtype=float)[:, 0]
|
|
142
|
+
bg_vals = _interp_zero_fill(bgx, bgy, time, bg_interp)
|
|
143
|
+
for k in range(values.shape[1]):
|
|
144
|
+
values[:, k] = values[:, k] - bg_vals
|
|
145
|
+
|
|
146
|
+
# 5. Magnetometry unit conversion.
|
|
147
|
+
if params.get("isMag", False):
|
|
148
|
+
f_unit = params.get("fieldUnit", "")
|
|
149
|
+
if f_unit and f_unit != "Oe (raw)":
|
|
150
|
+
target = f_unit.replace(" (raw)", "")
|
|
151
|
+
time = np.asarray(convert_units(time, "Oe", target)[0], dtype=float)
|
|
152
|
+
m_unit = params.get("momentUnit", "")
|
|
153
|
+
if m_unit == "emu/g" and params.get("sampleMass", 0.0) > 0:
|
|
154
|
+
values = values / params["sampleMass"]
|
|
155
|
+
elif m_unit in ("emu/cm³", "kA/m") and params.get("sampleVolume", 0.0) > 0:
|
|
156
|
+
values = values / params["sampleVolume"]
|
|
157
|
+
elif m_unit == "A·m²":
|
|
158
|
+
values = values * 1e-3
|
|
159
|
+
|
|
160
|
+
# 6. Smoothing.
|
|
161
|
+
if params.get("smoothEnabled", False):
|
|
162
|
+
win = max(1, _matlab_round(params.get("smoothWindow", 5)))
|
|
163
|
+
values = smooth_data(values, method=str(params["smoothMethod"]).lower(), window=win)
|
|
164
|
+
|
|
165
|
+
# 7. Normalization.
|
|
166
|
+
norm = params.get("normMethod", "None")
|
|
167
|
+
if norm == "Range [0,1]":
|
|
168
|
+
values = normalize(values, method="range")
|
|
169
|
+
elif norm == "Peak (max=1)":
|
|
170
|
+
values = normalize(values, method="peak")
|
|
171
|
+
elif norm == "Z-score":
|
|
172
|
+
values = normalize(values, method="zscore")
|
|
173
|
+
elif norm == "Area (integral=1)":
|
|
174
|
+
for k in range(values.shape[1]):
|
|
175
|
+
area = float(np.trapezoid(values[:, k], time))
|
|
176
|
+
if area != 0:
|
|
177
|
+
values[:, k] = values[:, k] / area
|
|
178
|
+
|
|
179
|
+
# 8. Derivative / integral transforms.
|
|
180
|
+
deriv = params.get("derivativeMode", "None")
|
|
181
|
+
if deriv == "dY/dX":
|
|
182
|
+
values = derivative(time, values, order=1)
|
|
183
|
+
elif deriv == "d²Y/dX²":
|
|
184
|
+
values = derivative(time, values, order=2)
|
|
185
|
+
elif deriv == "∫Y dx":
|
|
186
|
+
values = cumulative_integral(time, values)
|
|
187
|
+
elif deriv == "dlog/dlog":
|
|
188
|
+
values = log_derivative(time, values)
|
|
189
|
+
|
|
190
|
+
return DataStruct.create(
|
|
191
|
+
time, values, labels=labels, units=list(data.units), metadata=dict(data.metadata)
|
|
192
|
+
)
|