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.
Files changed (233) hide show
  1. quantized/__init__.py +11 -0
  2. quantized/__main__.py +14 -0
  3. quantized/api.py +220 -0
  4. quantized/app.py +206 -0
  5. quantized/calc/__init__.py +8 -0
  6. quantized/calc/_clipfit.py +76 -0
  7. quantized/calc/_natural_neighbor.py +219 -0
  8. quantized/calc/aggregate.py +159 -0
  9. quantized/calc/backgrounds.py +353 -0
  10. quantized/calc/baseline.py +349 -0
  11. quantized/calc/batch_fit.py +148 -0
  12. quantized/calc/constants.py +27 -0
  13. quantized/calc/corrections.py +192 -0
  14. quantized/calc/crystallography.py +400 -0
  15. quantized/calc/diffusion.py +120 -0
  16. quantized/calc/electrical.py +248 -0
  17. quantized/calc/electrochemistry.py +176 -0
  18. quantized/calc/element_data.json +1 -0
  19. quantized/calc/element_data.py +59 -0
  20. quantized/calc/errors.py +246 -0
  21. quantized/calc/figure.py +417 -0
  22. quantized/calc/figure_break.py +152 -0
  23. quantized/calc/figure_categorical.py +156 -0
  24. quantized/calc/figure_corner.py +229 -0
  25. quantized/calc/figure_facets.py +127 -0
  26. quantized/calc/figure_field.py +137 -0
  27. quantized/calc/figure_hitmap.py +116 -0
  28. quantized/calc/figure_labels.py +62 -0
  29. quantized/calc/figure_map.py +287 -0
  30. quantized/calc/figure_overrides.py +159 -0
  31. quantized/calc/figure_page.py +266 -0
  32. quantized/calc/figure_scale.py +125 -0
  33. quantized/calc/figure_statplots.py +167 -0
  34. quantized/calc/figure_styles.py +131 -0
  35. quantized/calc/figure_ternary.py +239 -0
  36. quantized/calc/figure_ticks.py +217 -0
  37. quantized/calc/fit_autoguess.py +156 -0
  38. quantized/calc/fit_bootstrap.py +163 -0
  39. quantized/calc/fit_bumps.py +258 -0
  40. quantized/calc/fit_constraints.py +114 -0
  41. quantized/calc/fit_equation.py +264 -0
  42. quantized/calc/fit_findxy.py +80 -0
  43. quantized/calc/fit_models.py +195 -0
  44. quantized/calc/fit_models_special.py +189 -0
  45. quantized/calc/fit_odr.py +99 -0
  46. quantized/calc/fit_scan.py +243 -0
  47. quantized/calc/fit_stats.py +215 -0
  48. quantized/calc/fitting.py +199 -0
  49. quantized/calc/formula.py +90 -0
  50. quantized/calc/global_curve_fit.py +305 -0
  51. quantized/calc/global_fit.py +181 -0
  52. quantized/calc/interp2d.py +260 -0
  53. quantized/calc/linecut.py +269 -0
  54. quantized/calc/magnetic.py +414 -0
  55. quantized/calc/magnetometry.py +464 -0
  56. quantized/calc/map.py +228 -0
  57. quantized/calc/mcmc.py +177 -0
  58. quantized/calc/optics.py +228 -0
  59. quantized/calc/pawley.py +251 -0
  60. quantized/calc/peak_batch.py +128 -0
  61. quantized/calc/peak_fit.py +259 -0
  62. quantized/calc/peak_integrate.py +104 -0
  63. quantized/calc/peak_multifit.py +260 -0
  64. quantized/calc/peak_track.py +134 -0
  65. quantized/calc/peaks.py +298 -0
  66. quantized/calc/peakshapes.py +85 -0
  67. quantized/calc/plotting.py +147 -0
  68. quantized/calc/processing.py +232 -0
  69. quantized/calc/qspace.py +48 -0
  70. quantized/calc/reductions.py +155 -0
  71. quantized/calc/reductions_fft.py +383 -0
  72. quantized/calc/refl_sld_presets.json +1 -0
  73. quantized/calc/reflectivity.py +80 -0
  74. quantized/calc/registry.py +303 -0
  75. quantized/calc/relaxation.py +119 -0
  76. quantized/calc/report.py +253 -0
  77. quantized/calc/report_emit.py +227 -0
  78. quantized/calc/resample.py +142 -0
  79. quantized/calc/rsm.py +91 -0
  80. quantized/calc/rsm_analyze.py +245 -0
  81. quantized/calc/semiconductor.py +488 -0
  82. quantized/calc/sld.py +131 -0
  83. quantized/calc/sld_formula.py +138 -0
  84. quantized/calc/spectral.py +357 -0
  85. quantized/calc/statplots.py +214 -0
  86. quantized/calc/stats.py +399 -0
  87. quantized/calc/stats_anova2.py +202 -0
  88. quantized/calc/stats_anova_ext.py +338 -0
  89. quantized/calc/stats_dist.py +196 -0
  90. quantized/calc/stats_glm.py +245 -0
  91. quantized/calc/stats_multivar.py +289 -0
  92. quantized/calc/stats_roc.py +157 -0
  93. quantized/calc/stats_survival.py +261 -0
  94. quantized/calc/stats_tests.py +380 -0
  95. quantized/calc/substrates.py +181 -0
  96. quantized/calc/superconductor.py +359 -0
  97. quantized/calc/surface_fit.py +290 -0
  98. quantized/calc/surface_models.py +156 -0
  99. quantized/calc/thermal.py +119 -0
  100. quantized/calc/thin_film.py +425 -0
  101. quantized/calc/unit_convert.py +259 -0
  102. quantized/calc/units.py +80 -0
  103. quantized/calc/vacuum.py +290 -0
  104. quantized/calc/xray.py +169 -0
  105. quantized/cli.py +214 -0
  106. quantized/datastruct.py +153 -0
  107. quantized/io/__init__.py +11 -0
  108. quantized/io/_hdf5_layout.py +308 -0
  109. quantized/io/_jcamp_asdf.py +135 -0
  110. quantized/io/_xrdml_scan.py +291 -0
  111. quantized/io/base.py +82 -0
  112. quantized/io/bruker_brml.py +177 -0
  113. quantized/io/bruker_raw.py +158 -0
  114. quantized/io/cif.py +266 -0
  115. quantized/io/consolidated.py +122 -0
  116. quantized/io/delimited.py +222 -0
  117. quantized/io/excel.py +135 -0
  118. quantized/io/hdf5.py +192 -0
  119. quantized/io/import_filters.py +178 -0
  120. quantized/io/import_preview.py +262 -0
  121. quantized/io/jcamp.py +179 -0
  122. quantized/io/lakeshore.py +163 -0
  123. quantized/io/ncnr.py +278 -0
  124. quantized/io/netcdf.py +195 -0
  125. quantized/io/opus.py +231 -0
  126. quantized/io/origin.py +346 -0
  127. quantized/io/origin_com.py +194 -0
  128. quantized/io/origin_project/__init__.py +221 -0
  129. quantized/io/origin_project/annotation_marks.py +288 -0
  130. quantized/io/origin_project/container.py +262 -0
  131. quantized/io/origin_project/curve_style_color.py +359 -0
  132. quantized/io/origin_project/figure_geometry.py +108 -0
  133. quantized/io/origin_project/figure_layers.py +333 -0
  134. quantized/io/origin_project/figure_text.py +258 -0
  135. quantized/io/origin_project/figures.py +210 -0
  136. quantized/io/origin_project/figures_opju.py +440 -0
  137. quantized/io/origin_project/notes.py +302 -0
  138. quantized/io/origin_project/opj.py +459 -0
  139. quantized/io/origin_project/opj_curves.py +297 -0
  140. quantized/io/origin_project/opj_shapes.py +148 -0
  141. quantized/io/origin_project/opju.py +146 -0
  142. quantized/io/origin_project/opju_axis_real_form.py +418 -0
  143. quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
  144. quantized/io/origin_project/opju_codec.py +370 -0
  145. quantized/io/origin_project/opju_curves.py +497 -0
  146. quantized/io/origin_project/opju_curves_allcols.py +258 -0
  147. quantized/io/origin_project/opju_figure_curves.py +302 -0
  148. quantized/io/origin_project/opju_figure_text.py +245 -0
  149. quantized/io/origin_project/opju_reports.py +129 -0
  150. quantized/io/origin_project/origin_richtext.py +145 -0
  151. quantized/io/origin_project/preview.py +132 -0
  152. quantized/io/origin_project/templates.py +314 -0
  153. quantized/io/origin_project/tree.py +379 -0
  154. quantized/io/origin_project/tree_opju.py +228 -0
  155. quantized/io/origin_project/windows.py +238 -0
  156. quantized/io/origin_project/windows_opju.py +393 -0
  157. quantized/io/origin_project/writer.py +156 -0
  158. quantized/io/origin_project/writer_blocks.py +282 -0
  159. quantized/io/qd.py +380 -0
  160. quantized/io/refl1d.py +132 -0
  161. quantized/io/registry.py +210 -0
  162. quantized/io/report_export.py +347 -0
  163. quantized/io/rigaku.py +100 -0
  164. quantized/io/sims.py +398 -0
  165. quantized/io/spc.py +311 -0
  166. quantized/io/xrd_csv.py +308 -0
  167. quantized/io/xrdml.py +394 -0
  168. quantized/jobs.py +173 -0
  169. quantized/plugins/__init__.py +50 -0
  170. quantized/plugins/contract.py +111 -0
  171. quantized/plugins/loader.py +394 -0
  172. quantized/plugins/steps.py +90 -0
  173. quantized/routes/__init__.py +7 -0
  174. quantized/routes/_bookcache.py +62 -0
  175. quantized/routes/_export_common.py +27 -0
  176. quantized/routes/_payload.py +58 -0
  177. quantized/routes/_uploadcache.py +59 -0
  178. quantized/routes/aggregate.py +46 -0
  179. quantized/routes/baseline.py +210 -0
  180. quantized/routes/books.py +117 -0
  181. quantized/routes/calc.py +56 -0
  182. quantized/routes/corrections.py +78 -0
  183. quantized/routes/crystallography.py +80 -0
  184. quantized/routes/diffusion.py +58 -0
  185. quantized/routes/electrical.py +101 -0
  186. quantized/routes/electrochemistry.py +83 -0
  187. quantized/routes/export.py +280 -0
  188. quantized/routes/export_facets.py +83 -0
  189. quantized/routes/export_figures.py +471 -0
  190. quantized/routes/export_page.py +125 -0
  191. quantized/routes/fitting.py +379 -0
  192. quantized/routes/fitting_bumps.py +97 -0
  193. quantized/routes/import_template.py +97 -0
  194. quantized/routes/import_wizard.py +150 -0
  195. quantized/routes/jobs_api.py +59 -0
  196. quantized/routes/magnetic.py +135 -0
  197. quantized/routes/magnetometry.py +133 -0
  198. quantized/routes/optics.py +98 -0
  199. quantized/routes/parsers.py +281 -0
  200. quantized/routes/peaks.py +184 -0
  201. quantized/routes/plot.py +103 -0
  202. quantized/routes/reductions.py +121 -0
  203. quantized/routes/reference.py +58 -0
  204. quantized/routes/reflectivity.py +91 -0
  205. quantized/routes/report_export.py +119 -0
  206. quantized/routes/rsm.py +136 -0
  207. quantized/routes/samples.py +32 -0
  208. quantized/routes/semiconductor.py +207 -0
  209. quantized/routes/sld.py +42 -0
  210. quantized/routes/spectral.py +54 -0
  211. quantized/routes/statplots.py +99 -0
  212. quantized/routes/stats.py +418 -0
  213. quantized/routes/stats_design.py +321 -0
  214. quantized/routes/substrates.py +46 -0
  215. quantized/routes/superconductor.py +139 -0
  216. quantized/routes/thermal.py +57 -0
  217. quantized/routes/thin_film.py +153 -0
  218. quantized/routes/vacuum.py +113 -0
  219. quantized/routes/xray.py +32 -0
  220. quantized/samples/demo_vsm.csv +42 -0
  221. quantized/server_launch.py +251 -0
  222. quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
  223. quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
  224. quantized/web/assets/index-BHmmCL-x.js +27 -0
  225. quantized/web/assets/index-BiZzN7J6.css +1 -0
  226. quantized/web/index.html +13 -0
  227. quantized/web/loading.html +69 -0
  228. quantized_lab-0.8.0.dist-info/METADATA +122 -0
  229. quantized_lab-0.8.0.dist-info/RECORD +233 -0
  230. quantized_lab-0.8.0.dist-info/WHEEL +4 -0
  231. quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
  232. quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
  233. 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
+ )