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,219 @@
|
|
|
1
|
+
"""Sibson natural-neighbour interpolation (pure; scipy.spatial only, no GPL).
|
|
2
|
+
|
|
3
|
+
MATLAB ``scatteredInterpolant`` 'natural' (and 'cubic', which it aliases to
|
|
4
|
+
'natural') use Sibson's natural-neighbour interpolation. scipy has no
|
|
5
|
+
equivalent, so this implements it directly from the Delaunay triangulation via
|
|
6
|
+
the local Bowyer-Watson "stolen area" construction:
|
|
7
|
+
|
|
8
|
+
Inserting a query point ``q`` into the Voronoi diagram of the data carves a
|
|
9
|
+
new cell out of the existing ones. The Sibson coordinate of data point ``p``
|
|
10
|
+
is the area ``q``'s new cell steals from ``p``'s old cell, normalised by the
|
|
11
|
+
total new-cell area. The interpolated value is the weighted sum
|
|
12
|
+
``sum_i lambda_i * z_i``.
|
|
13
|
+
|
|
14
|
+
Sibson coordinates are *geometrically* unique (they depend only on the point
|
|
15
|
+
configuration, not on any tie-breaking), so this matches MATLAB's 'natural' to
|
|
16
|
+
~1e-9 at non-degenerate interior points. The interpolant has **linear
|
|
17
|
+
precision** — it reproduces any affine ``z = a*x + b*y + c`` exactly — which the
|
|
18
|
+
tests assert to machine precision.
|
|
19
|
+
|
|
20
|
+
The work per query is *local* (only the few triangles whose circumcircle
|
|
21
|
+
contains ``q``, found by a BFS over Delaunay adjacency), so cost scales with the
|
|
22
|
+
query count, not N^2. The in-circle test reduces to a squared-distance compare
|
|
23
|
+
against precomputed circumradii; ``find_simplex`` is vectorised over all queries.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
from numpy.typing import NDArray
|
|
30
|
+
from scipy.spatial import Delaunay
|
|
31
|
+
from scipy.spatial._qhull import QhullError
|
|
32
|
+
|
|
33
|
+
__all__ = ["sibson_interpolate"]
|
|
34
|
+
|
|
35
|
+
_COINCIDE_TOL2 = 1e-18 # (distance)^2 below which a query is "at" a data node
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _circumcenters(
|
|
39
|
+
a: NDArray[np.float64], b: NDArray[np.float64], c: NDArray[np.float64]
|
|
40
|
+
) -> NDArray[np.float64]:
|
|
41
|
+
"""Vectorised circumcentres for triangle vertex arrays (each ``(T, 2)``)."""
|
|
42
|
+
ax, ay = a[:, 0], a[:, 1]
|
|
43
|
+
bx, by = b[:, 0], b[:, 1]
|
|
44
|
+
cx, cy = c[:, 0], c[:, 1]
|
|
45
|
+
d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
|
|
46
|
+
a2 = ax * ax + ay * ay
|
|
47
|
+
b2 = bx * bx + by * by
|
|
48
|
+
c2 = cx * cx + cy * cy
|
|
49
|
+
ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d
|
|
50
|
+
uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d
|
|
51
|
+
return np.column_stack([ux, uy])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _circumcenter1(
|
|
55
|
+
a: NDArray[np.float64], b: NDArray[np.float64], q: NDArray[np.float64]
|
|
56
|
+
) -> NDArray[np.float64]:
|
|
57
|
+
"""Circumcentre of a single triangle (a, b, q) — for new (inserted) triangles."""
|
|
58
|
+
ax, ay = float(a[0]), float(a[1])
|
|
59
|
+
bx, by = float(b[0]), float(b[1])
|
|
60
|
+
qx, qy = float(q[0]), float(q[1])
|
|
61
|
+
d = 2.0 * (ax * (by - qy) + bx * (qy - ay) + qx * (ay - by))
|
|
62
|
+
a2 = ax * ax + ay * ay
|
|
63
|
+
b2 = bx * bx + by * by
|
|
64
|
+
q2 = qx * qx + qy * qy
|
|
65
|
+
ux = (a2 * (by - qy) + b2 * (qy - ay) + q2 * (ay - by)) / d
|
|
66
|
+
uy = (a2 * (qx - bx) + b2 * (ax - qx) + q2 * (bx - ax)) / d
|
|
67
|
+
return np.array([ux, uy], dtype=float)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _poly_area(pts: NDArray[np.float64]) -> float:
|
|
71
|
+
"""Area of a convex polygon given its (unordered) vertices.
|
|
72
|
+
|
|
73
|
+
The stolen region is convex, so ordering vertices by angle about their
|
|
74
|
+
centroid recovers the boundary; the shoelace formula then gives the area.
|
|
75
|
+
"""
|
|
76
|
+
if pts.shape[0] < 3:
|
|
77
|
+
return 0.0
|
|
78
|
+
cen = pts.mean(axis=0)
|
|
79
|
+
ang = np.arctan2(pts[:, 1] - cen[1], pts[:, 0] - cen[0])
|
|
80
|
+
p = pts[np.argsort(ang)]
|
|
81
|
+
x, y = p[:, 0], p[:, 1]
|
|
82
|
+
return 0.5 * abs(float(np.dot(x, np.roll(y, -1)) - np.dot(y, np.roll(x, -1))))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _linear_bary(
|
|
86
|
+
q: NDArray[np.float64],
|
|
87
|
+
simplex: NDArray[np.intp],
|
|
88
|
+
points: NDArray[np.float64],
|
|
89
|
+
values: NDArray[np.float64],
|
|
90
|
+
) -> float:
|
|
91
|
+
"""Barycentric (piecewise-linear) value in one triangle — degenerate fallback."""
|
|
92
|
+
a, b, c = points[simplex[0]], points[simplex[1]], points[simplex[2]]
|
|
93
|
+
mat = np.array([[a[0] - c[0], b[0] - c[0]], [a[1] - c[1], b[1] - c[1]]], dtype=float)
|
|
94
|
+
try:
|
|
95
|
+
lam = np.linalg.solve(mat, q - c)
|
|
96
|
+
except np.linalg.LinAlgError:
|
|
97
|
+
return float("nan")
|
|
98
|
+
l0, l1 = float(lam[0]), float(lam[1])
|
|
99
|
+
l2 = 1.0 - l0 - l1
|
|
100
|
+
return (
|
|
101
|
+
l0 * float(values[simplex[0]])
|
|
102
|
+
+ l1 * float(values[simplex[1]])
|
|
103
|
+
+ l2 * float(values[simplex[2]])
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _sibson_one(
|
|
108
|
+
q: NDArray[np.float64],
|
|
109
|
+
s0: int,
|
|
110
|
+
simplices: NDArray[np.intp],
|
|
111
|
+
neighbors: NDArray[np.intp],
|
|
112
|
+
points: NDArray[np.float64],
|
|
113
|
+
values: NDArray[np.float64],
|
|
114
|
+
centers: NDArray[np.float64],
|
|
115
|
+
radii2: NDArray[np.float64],
|
|
116
|
+
) -> float:
|
|
117
|
+
"""Sibson-interpolated value at one query point (NaN outside the hull)."""
|
|
118
|
+
if s0 < 0:
|
|
119
|
+
return float("nan") # outside convex hull → extrapolation 'none'
|
|
120
|
+
|
|
121
|
+
# Query coincident with a data node → return that node's value exactly
|
|
122
|
+
# (Sibson's new cell degenerates to zero area here, as at any data site).
|
|
123
|
+
for vi in simplices[s0]:
|
|
124
|
+
dx0 = q[0] - points[vi, 0]
|
|
125
|
+
dy0 = q[1] - points[vi, 1]
|
|
126
|
+
if dx0 * dx0 + dy0 * dy0 <= _COINCIDE_TOL2:
|
|
127
|
+
return float(values[vi])
|
|
128
|
+
|
|
129
|
+
# Cavity = triangles whose circumcircle contains q (squared-distance test).
|
|
130
|
+
cavity: set[int] = set()
|
|
131
|
+
stack = [s0]
|
|
132
|
+
while stack:
|
|
133
|
+
t = stack.pop()
|
|
134
|
+
if t < 0 or t in cavity:
|
|
135
|
+
continue
|
|
136
|
+
if t != s0:
|
|
137
|
+
dx = q[0] - centers[t, 0]
|
|
138
|
+
dy = q[1] - centers[t, 1]
|
|
139
|
+
if dx * dx + dy * dy >= radii2[t]:
|
|
140
|
+
continue
|
|
141
|
+
cavity.add(t)
|
|
142
|
+
for nb in neighbors[t]:
|
|
143
|
+
if nb >= 0 and nb not in cavity:
|
|
144
|
+
stack.append(int(nb))
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# New Voronoi vertices: circumcentre of (edge, q) per cavity boundary edge.
|
|
148
|
+
cc_new: dict[tuple[int, int], NDArray[np.float64]] = {}
|
|
149
|
+
for t in cavity:
|
|
150
|
+
tv = simplices[t]
|
|
151
|
+
nbrs = neighbors[t]
|
|
152
|
+
for i in range(3):
|
|
153
|
+
if int(nbrs[i]) in cavity:
|
|
154
|
+
continue
|
|
155
|
+
a_idx = int(tv[(i + 1) % 3])
|
|
156
|
+
b_idx = int(tv[(i + 2) % 3])
|
|
157
|
+
cc_new[(a_idx, b_idx)] = _circumcenter1(points[a_idx], points[b_idx], q)
|
|
158
|
+
|
|
159
|
+
neighbours: set[int] = set()
|
|
160
|
+
for t in cavity:
|
|
161
|
+
neighbours.update(int(v) for v in simplices[t])
|
|
162
|
+
|
|
163
|
+
total = 0.0
|
|
164
|
+
weighted = 0.0
|
|
165
|
+
for p_idx in neighbours:
|
|
166
|
+
verts: list[NDArray[np.float64]] = [
|
|
167
|
+
centers[t] for t in cavity if p_idx in simplices[t]
|
|
168
|
+
]
|
|
169
|
+
for (a_idx, b_idx), cc in cc_new.items():
|
|
170
|
+
if p_idx in (a_idx, b_idx):
|
|
171
|
+
verts.append(cc)
|
|
172
|
+
if len(verts) < 3:
|
|
173
|
+
continue
|
|
174
|
+
stolen = _poly_area(np.asarray(verts, dtype=float))
|
|
175
|
+
total += stolen
|
|
176
|
+
weighted += stolen * float(values[p_idx])
|
|
177
|
+
except ZeroDivisionError:
|
|
178
|
+
total = 0.0 # collinear new triangle → fall through to the linear fallback
|
|
179
|
+
|
|
180
|
+
if total <= 0.0: # degenerate (q on a circumcircle / collinear cavity)
|
|
181
|
+
return _linear_bary(q, simplices[s0], points, values)
|
|
182
|
+
return weighted / total
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def sibson_interpolate(
|
|
186
|
+
xv: NDArray[np.float64],
|
|
187
|
+
yv: NDArray[np.float64],
|
|
188
|
+
zv: NDArray[np.float64],
|
|
189
|
+
xqv: NDArray[np.float64],
|
|
190
|
+
yqv: NDArray[np.float64],
|
|
191
|
+
) -> NDArray[np.float64]:
|
|
192
|
+
"""Sibson natural-neighbour interpolation of scattered (xv, yv, zv) at queries.
|
|
193
|
+
|
|
194
|
+
Returns NaN outside the convex hull (matching MATLAB ``Extrapolation='none'``)
|
|
195
|
+
and for collinear/degenerate inputs that cannot be triangulated.
|
|
196
|
+
"""
|
|
197
|
+
pts = np.column_stack([xv, yv])
|
|
198
|
+
try:
|
|
199
|
+
tri = Delaunay(pts)
|
|
200
|
+
except QhullError:
|
|
201
|
+
return np.full(xqv.size, np.nan, dtype=float)
|
|
202
|
+
|
|
203
|
+
simplices = tri.simplices
|
|
204
|
+
neighbors = tri.neighbors
|
|
205
|
+
a = pts[simplices[:, 0]]
|
|
206
|
+
b = pts[simplices[:, 1]]
|
|
207
|
+
c = pts[simplices[:, 2]]
|
|
208
|
+
centers = _circumcenters(a, b, c)
|
|
209
|
+
radii2 = np.asarray(((a - centers) ** 2).sum(axis=1), dtype=float)
|
|
210
|
+
|
|
211
|
+
values = np.asarray(zv, dtype=float)
|
|
212
|
+
queries = np.column_stack([xqv.ravel(), yqv.ravel()])
|
|
213
|
+
loc = tri.find_simplex(queries) # vectorised hull/containment query
|
|
214
|
+
out = np.empty(queries.shape[0], dtype=float)
|
|
215
|
+
for k in range(queries.shape[0]):
|
|
216
|
+
out[k] = _sibson_one(
|
|
217
|
+
queries[k], int(loc[k]), simplices, neighbors, pts, values, centers, radii2
|
|
218
|
+
)
|
|
219
|
+
return out
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Multi-dataset aggregation utilities (confidence / spread bands).
|
|
2
|
+
|
|
3
|
+
Pure calc layer: operates on :class:`DataStruct` inputs, no fastapi/pydantic
|
|
4
|
+
imports. Distinct from ``calc.stats`` (single-vector statistics) — these
|
|
5
|
+
functions combine *several* datasets onto a shared grid.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from scipy.interpolate import PchipInterpolator
|
|
14
|
+
|
|
15
|
+
from ..datastruct import DataStruct
|
|
16
|
+
from .resample import _interp_column, _sanitize_xy
|
|
17
|
+
|
|
18
|
+
__all__ = ["confidence_band", "dataset_algebra"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def confidence_band(
|
|
22
|
+
datasets: list[DataStruct],
|
|
23
|
+
*,
|
|
24
|
+
method: str = "mean",
|
|
25
|
+
channel: int = 0,
|
|
26
|
+
n_points: int = 0,
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""Pointwise confidence/spread band across N datasets. Port of confidenceBand.
|
|
29
|
+
|
|
30
|
+
Each dataset is pchip-interpolated onto a shared grid spanning the
|
|
31
|
+
*overlapping* x-range (``max`` of the per-set minima to ``min`` of the
|
|
32
|
+
maxima), then reduced column-wise:
|
|
33
|
+
|
|
34
|
+
- ``method='mean'`` : center = nanmean, band = center +/- sample std (ddof=1)
|
|
35
|
+
- ``method='median'``: center = nanmedian, band = [p25, p75], spread = IQR/2
|
|
36
|
+
|
|
37
|
+
``channel`` is 0-based (MATLAB ``Channel`` is 1-based) and clamped to the last
|
|
38
|
+
channel. Percentiles use the Hazen plotting position ``(i-0.5)/n`` to match
|
|
39
|
+
MATLAB ``prctile``. ``n_points=0`` uses the longest input length.
|
|
40
|
+
"""
|
|
41
|
+
if method not in ("mean", "median"):
|
|
42
|
+
raise ValueError("method must be mean/median")
|
|
43
|
+
n_sets = len(datasets)
|
|
44
|
+
if n_sets < 2:
|
|
45
|
+
raise ValueError(f"need at least 2 datasets, got {n_sets}")
|
|
46
|
+
|
|
47
|
+
x_min, x_max, max_len = -np.inf, np.inf, 0
|
|
48
|
+
for ds in datasets:
|
|
49
|
+
xi = np.asarray(ds.time, dtype=float)
|
|
50
|
+
if not np.isfinite(xi).any():
|
|
51
|
+
continue # a fully non-finite x axis contributes no overlap
|
|
52
|
+
x_min = max(x_min, float(np.nanmin(xi)))
|
|
53
|
+
x_max = min(x_max, float(np.nanmax(xi)))
|
|
54
|
+
max_len = max(max_len, int(xi.size))
|
|
55
|
+
if not np.isfinite([x_min, x_max]).all() or x_min >= x_max:
|
|
56
|
+
raise ValueError("datasets have no overlapping x-range")
|
|
57
|
+
|
|
58
|
+
n_pts = n_points if n_points > 0 else max_len
|
|
59
|
+
x_common = np.linspace(x_min, x_max, n_pts)
|
|
60
|
+
y_matrix = np.full((n_pts, n_sets), np.nan)
|
|
61
|
+
for i, ds in enumerate(datasets):
|
|
62
|
+
xi = np.asarray(ds.time, dtype=float)
|
|
63
|
+
vals = np.asarray(ds.values, dtype=float)
|
|
64
|
+
ch = min(channel, vals.shape[1] - 1)
|
|
65
|
+
# Sanitize first (drop non-finite, sort, average duplicate x): pchip
|
|
66
|
+
# rejects NaN and non-strictly-increasing x. No-op on clean data.
|
|
67
|
+
xs, ys = _sanitize_xy(xi, vals[:, ch])
|
|
68
|
+
if xs.size < 2:
|
|
69
|
+
continue # fewer than 2 finite points — leave this set's column NaN
|
|
70
|
+
interp = PchipInterpolator(xs, ys, extrapolate=False)
|
|
71
|
+
y_matrix[:, i] = interp(x_common)
|
|
72
|
+
|
|
73
|
+
if method == "mean":
|
|
74
|
+
center = np.nanmean(y_matrix, axis=1)
|
|
75
|
+
spread = np.nanstd(y_matrix, axis=1, ddof=1)
|
|
76
|
+
upper = center + spread
|
|
77
|
+
lower = center - spread
|
|
78
|
+
else: # median
|
|
79
|
+
center = np.nanmedian(y_matrix, axis=1)
|
|
80
|
+
q25 = np.nanpercentile(y_matrix, 25, axis=1, method="hazen")
|
|
81
|
+
q75 = np.nanpercentile(y_matrix, 75, axis=1, method="hazen")
|
|
82
|
+
upper, lower = q75, q25
|
|
83
|
+
spread = (q75 - q25) / 2.0
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"x": x_common,
|
|
87
|
+
"center": center,
|
|
88
|
+
"upper": upper,
|
|
89
|
+
"lower": lower,
|
|
90
|
+
"spread": spread,
|
|
91
|
+
"method": method,
|
|
92
|
+
"nSets": n_sets,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
_ALGEBRA_OPS = ("A+B", "A-B", "A*B", "A/B", "(A-B)/(A+B)")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _safe_label(ds: DataStruct, ch: int) -> str:
|
|
100
|
+
return ds.labels[ch] if ch < len(ds.labels) else f"ch{ch + 1}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _safe_unit(ds: DataStruct, ch: int) -> str:
|
|
104
|
+
return ds.units[ch] if ch < len(ds.units) else ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def dataset_algebra(
|
|
108
|
+
ds_a: DataStruct,
|
|
109
|
+
ds_b: DataStruct,
|
|
110
|
+
operation: str,
|
|
111
|
+
*,
|
|
112
|
+
interp_method: str = "pchip",
|
|
113
|
+
channel_a: int = 0,
|
|
114
|
+
channel_b: int = 0,
|
|
115
|
+
) -> DataStruct:
|
|
116
|
+
"""Combine two datasets pointwise on A's x-grid. Port of utilities.datasetAlgebra.
|
|
117
|
+
|
|
118
|
+
B is interpolated onto A's time axis (``interp_method`` = pchip/linear/spline,
|
|
119
|
+
NaN outside B's range) and combined via ``operation`` (``A+B``, ``A-B``,
|
|
120
|
+
``A*B``, ``A/B``, ``(A-B)/(A+B)``). Division/asymmetry guard zeros with NaN.
|
|
121
|
+
``channel_a``/``channel_b`` are 0-based and clamped to the last channel.
|
|
122
|
+
"""
|
|
123
|
+
if operation not in _ALGEBRA_OPS:
|
|
124
|
+
raise ValueError(f"operation must be one of {_ALGEBRA_OPS}")
|
|
125
|
+
xa = np.asarray(ds_a.time, dtype=float).ravel()
|
|
126
|
+
xb = np.asarray(ds_b.time, dtype=float).ravel()
|
|
127
|
+
va = np.asarray(ds_a.values, dtype=float)
|
|
128
|
+
vb = np.asarray(ds_b.values, dtype=float)
|
|
129
|
+
ch_a = min(channel_a, va.shape[1] - 1)
|
|
130
|
+
ch_b = min(channel_b, vb.shape[1] - 1)
|
|
131
|
+
ya = va[:, ch_a]
|
|
132
|
+
yb = _interp_column(xb, vb[:, ch_b], xa, interp_method, False)
|
|
133
|
+
|
|
134
|
+
la, lb = _safe_label(ds_a, ch_a), _safe_label(ds_b, ch_b)
|
|
135
|
+
unit_a = _safe_unit(ds_a, ch_a)
|
|
136
|
+
if operation == "A+B":
|
|
137
|
+
y_result, label, unit = ya + yb, f"{la} + {lb}", unit_a
|
|
138
|
+
elif operation == "A-B":
|
|
139
|
+
y_result, label, unit = ya - yb, f"{la} - {lb}", unit_a
|
|
140
|
+
elif operation == "A*B":
|
|
141
|
+
y_result, label, unit = ya * yb, f"{la} × {lb}", f"{unit_a}²"
|
|
142
|
+
elif operation == "A/B":
|
|
143
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
144
|
+
y_result = ya / yb
|
|
145
|
+
y_result[yb == 0] = np.nan
|
|
146
|
+
label, unit = f"{la} / {lb}", "ratio"
|
|
147
|
+
else: # (A-B)/(A+B)
|
|
148
|
+
denom = ya + yb
|
|
149
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
150
|
+
y_result = (ya - yb) / denom
|
|
151
|
+
y_result[denom == 0] = np.nan
|
|
152
|
+
label = f"({la} - {lb}) / ({la} + {lb})"
|
|
153
|
+
unit = "asymmetry"
|
|
154
|
+
|
|
155
|
+
meta: dict[str, Any] = {}
|
|
156
|
+
if "source" in ds_a.metadata:
|
|
157
|
+
meta["source"] = ds_a.metadata["source"]
|
|
158
|
+
meta["operation"] = operation
|
|
159
|
+
return DataStruct.create(xa, y_result, labels=[label], units=[unit], metadata=meta)
|