axsdb 0.0.2__py3-none-any.whl → 0.0.3__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.
- axsdb/core.py +40 -31
- axsdb/factory.py +3 -2
- axsdb/interpolation.py +803 -0
- axsdb/math.py +503 -0
- axsdb/testing/__init__.py +0 -0
- axsdb/testing/fixtures.py +77 -0
- {axsdb-0.0.2.dist-info → axsdb-0.0.3.dist-info}/METADATA +7 -5
- axsdb-0.0.3.dist-info/RECORD +17 -0
- {axsdb-0.0.2.dist-info → axsdb-0.0.3.dist-info}/WHEEL +1 -1
- axsdb-0.0.2.dist-info/RECORD +0 -13
- {axsdb-0.0.2.dist-info → axsdb-0.0.3.dist-info}/entry_points.txt +0 -0
axsdb/math.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fast interpolation with Numba.
|
|
3
|
+
|
|
4
|
+
This module provides high-performance interpolation functions implemented
|
|
5
|
+
using Numba's ``guvectorize`` decorator. These functions are designed to replace
|
|
6
|
+
xarray's interpolation for specific use cases where performance is critical.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from numba import guvectorize
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Bounds mode constants (used internally by gufunc)
|
|
18
|
+
_BOUNDS_FILL = 0
|
|
19
|
+
_BOUNDS_CLAMP = 1
|
|
20
|
+
_BOUNDS_RAISE = 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _make_interp1d_gufunc():
|
|
24
|
+
"""
|
|
25
|
+
Create the Numba gufunc for 1D linear interpolation.
|
|
26
|
+
|
|
27
|
+
Returns a gufunc with signature ``(n),(n),(m),(),(),()->(m)`` that performs
|
|
28
|
+
linear interpolation.
|
|
29
|
+
|
|
30
|
+
The function is created at module load time to ensure JIT compilation
|
|
31
|
+
happens only once.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@guvectorize(
|
|
35
|
+
[
|
|
36
|
+
"void(float32[:], float32[:], float32[:], int64, float32, float32, float32[:])",
|
|
37
|
+
"void(float64[:], float64[:], float64[:], int64, float64, float64, float64[:])",
|
|
38
|
+
],
|
|
39
|
+
"(n),(n),(m),(),(),()->(m)",
|
|
40
|
+
nopython=True,
|
|
41
|
+
cache=True,
|
|
42
|
+
)
|
|
43
|
+
def _interp1d_gufunc_impl(x, y, xnew, bounds_mode, fill_lower, fill_upper, out):
|
|
44
|
+
"""
|
|
45
|
+
Low-level gufunc for 1D linear interpolation.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
x : ndarray
|
|
50
|
+
X coordinates of the data points (must be sorted in ascending order).
|
|
51
|
+
Shape (n,).
|
|
52
|
+
|
|
53
|
+
y : ndarray
|
|
54
|
+
Y coordinates of the data points.
|
|
55
|
+
Shape (n,).
|
|
56
|
+
|
|
57
|
+
xnew : ndarray
|
|
58
|
+
X coordinates at which to evaluate the interpolation.
|
|
59
|
+
Shape (m,).
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
bounds_mode : int
|
|
63
|
+
Bounds handling mode:
|
|
64
|
+
|
|
65
|
+
* 0 (fill): use ``fill_lower``/``fill_upper`` for out-of-bounds
|
|
66
|
+
points;
|
|
67
|
+
* 1 (clamp): use nearest boundary value;
|
|
68
|
+
* 2 (raise): mark out-of-bounds with NaN for later validation.
|
|
69
|
+
|
|
70
|
+
fill_lower : float
|
|
71
|
+
Fill value for points below ``x[0]`` (only used when bounds_mode=0).
|
|
72
|
+
|
|
73
|
+
fill_upper : float
|
|
74
|
+
Fill value for points above ``x[-1]`` (only used when bounds_mode=0).
|
|
75
|
+
|
|
76
|
+
out : ndarray
|
|
77
|
+
Output array for interpolated values.
|
|
78
|
+
Shape (m,).
|
|
79
|
+
"""
|
|
80
|
+
n = len(x)
|
|
81
|
+
m = len(xnew)
|
|
82
|
+
|
|
83
|
+
x_min = x[0]
|
|
84
|
+
x_max = x[n - 1]
|
|
85
|
+
|
|
86
|
+
for i in range(m):
|
|
87
|
+
xi = xnew[i]
|
|
88
|
+
|
|
89
|
+
# Handle NaN in query point
|
|
90
|
+
if np.isnan(xi):
|
|
91
|
+
out[i] = np.nan
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Handle out-of-bounds: below minimum
|
|
95
|
+
if xi < x_min:
|
|
96
|
+
if bounds_mode == _BOUNDS_FILL:
|
|
97
|
+
out[i] = fill_lower
|
|
98
|
+
elif bounds_mode == _BOUNDS_CLAMP:
|
|
99
|
+
out[i] = y[0]
|
|
100
|
+
else: # bounds_mode == _BOUNDS_RAISE
|
|
101
|
+
out[i] = np.nan # Mark for validation in wrapper
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Handle out-of-bounds: above maximum
|
|
105
|
+
if xi > x_max:
|
|
106
|
+
if bounds_mode == _BOUNDS_FILL:
|
|
107
|
+
out[i] = fill_upper
|
|
108
|
+
elif bounds_mode == _BOUNDS_CLAMP:
|
|
109
|
+
out[i] = y[n - 1]
|
|
110
|
+
else: # bounds_mode == _BOUNDS_RAISE
|
|
111
|
+
out[i] = np.nan # Mark for validation in wrapper
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# Binary search to find the interval [x[left], x[right]]
|
|
115
|
+
left = 0
|
|
116
|
+
right = n - 1
|
|
117
|
+
|
|
118
|
+
while right - left > 1:
|
|
119
|
+
mid = (left + right) // 2
|
|
120
|
+
if x[mid] <= xi:
|
|
121
|
+
left = mid
|
|
122
|
+
else:
|
|
123
|
+
right = mid
|
|
124
|
+
|
|
125
|
+
# Handle exact match at boundary to avoid numerical issues
|
|
126
|
+
if xi == x[left]:
|
|
127
|
+
out[i] = y[left]
|
|
128
|
+
continue
|
|
129
|
+
if xi == x[right]:
|
|
130
|
+
out[i] = y[right]
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# Linear interpolation: y0 + t * (y1 - y0) where t = (xi - x0) / (x1 - x0)
|
|
134
|
+
x0 = x[left]
|
|
135
|
+
x1 = x[right]
|
|
136
|
+
y0 = y[left]
|
|
137
|
+
y1 = y[right]
|
|
138
|
+
|
|
139
|
+
t = (xi - x0) / (x1 - x0)
|
|
140
|
+
out[i] = y0 + t * (y1 - y0)
|
|
141
|
+
|
|
142
|
+
return _interp1d_gufunc_impl
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _make_lerp_gufunc():
|
|
146
|
+
"""
|
|
147
|
+
Create the numba gufunc for lerp with precomputed indices and weights.
|
|
148
|
+
|
|
149
|
+
Returns a gufunc with signature ``(n),(m),(m)->(m)``. The search step
|
|
150
|
+
is skipped entirely; the caller must supply the left-index and weight
|
|
151
|
+
arrays produced by :func:`lerp_indices`.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
@guvectorize(
|
|
155
|
+
[
|
|
156
|
+
"void(float32[:], float32[:], float32[:], float32[:])",
|
|
157
|
+
"void(float64[:], float64[:], float64[:], float64[:])",
|
|
158
|
+
],
|
|
159
|
+
"(n),(m),(m)->(m)",
|
|
160
|
+
nopython=True,
|
|
161
|
+
cache=True,
|
|
162
|
+
)
|
|
163
|
+
def _lerp_gufunc_impl(y, indices, weights, out):
|
|
164
|
+
m = len(indices)
|
|
165
|
+
for i in range(m):
|
|
166
|
+
left = int(indices[i])
|
|
167
|
+
t = weights[i]
|
|
168
|
+
out[i] = y[left] + t * (y[left + 1] - y[left])
|
|
169
|
+
|
|
170
|
+
return _lerp_gufunc_impl
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Create gufuncs at module load time
|
|
174
|
+
_interp1d_gufunc = _make_interp1d_gufunc()
|
|
175
|
+
_lerp_gufunc = _make_lerp_gufunc()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def interp1d(
|
|
179
|
+
x: np.ndarray,
|
|
180
|
+
y: np.ndarray,
|
|
181
|
+
xnew: np.ndarray,
|
|
182
|
+
bounds: Literal["fill", "clamp", "raise"] = "fill",
|
|
183
|
+
fill_value: float | tuple[float, float] = np.nan,
|
|
184
|
+
) -> np.ndarray:
|
|
185
|
+
"""
|
|
186
|
+
Fast 1D linear interpolation.
|
|
187
|
+
|
|
188
|
+
This function provides high-performance linear interpolation that
|
|
189
|
+
broadcasts over leading dimensions. It powers a drop-in replacement for
|
|
190
|
+
cases where xarray's interpolation is too slow.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
x : array-like
|
|
195
|
+
X coordinates of the data points. Must be sorted in ascending order
|
|
196
|
+
along the last axis. Results are undefined for unsorted x.
|
|
197
|
+
Shape (..., n).
|
|
198
|
+
|
|
199
|
+
y : array-like
|
|
200
|
+
Y coordinates of the data points.
|
|
201
|
+
Shape (..., n).
|
|
202
|
+
|
|
203
|
+
xnew : array-like
|
|
204
|
+
X coordinates at which to evaluate the interpolation.
|
|
205
|
+
Shape (..., m).
|
|
206
|
+
|
|
207
|
+
bounds : {"fill", "clamp", "raise"}, default: "fill"
|
|
208
|
+
How to handle out-of-bounds query points:
|
|
209
|
+
|
|
210
|
+
* ``"fill"``: use ``fill_value`` for points outside the data range.
|
|
211
|
+
* ``"clamp"``: use the nearest boundary value (``y[0]`` or ``y[-1]``).
|
|
212
|
+
* ``"raise"``: raise a ValueError if any query point is out of bounds.
|
|
213
|
+
|
|
214
|
+
fill_value : float or tuple of (float, float), default: np.nan
|
|
215
|
+
Value(s) to use for out-of-bounds points when ``bounds="fill"``:
|
|
216
|
+
|
|
217
|
+
* if a single float, use for both lower and upper bounds;
|
|
218
|
+
* if a 2-tuple, use (``fill_lower``, ``fill_upper``).
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
ndarray
|
|
223
|
+
Interpolated values at the query points. The output shape is
|
|
224
|
+
determined by numpy broadcasting rules applied to x, y, and xnew.
|
|
225
|
+
Shape (..., m).
|
|
226
|
+
|
|
227
|
+
Raises
|
|
228
|
+
------
|
|
229
|
+
ValueError
|
|
230
|
+
* If ``bounds="raise"`` and any query point is outside the data range.
|
|
231
|
+
* If ``bounds`` is not one of "fill", "clamp", or "raise".
|
|
232
|
+
* If ``fill_value`` is a tuple with length != 2.
|
|
233
|
+
|
|
234
|
+
Notes
|
|
235
|
+
-----
|
|
236
|
+
* The implementation uses a Numba gufunc with signature ``(n),(n),(m)->(m)``
|
|
237
|
+
for the core interpolation, enabling efficient broadcasting over arbitrary
|
|
238
|
+
leading dimensions.
|
|
239
|
+
* The function assumes ``x`` is sorted in ascending order along the last
|
|
240
|
+
axis. Results are undefined if this assumption is violated.
|
|
241
|
+
* NaN values in ``xnew`` are passed through (output will be NaN).
|
|
242
|
+
|
|
243
|
+
Examples
|
|
244
|
+
--------
|
|
245
|
+
Basic interpolation:
|
|
246
|
+
|
|
247
|
+
>>> x = np.array([0.0, 1.0, 2.0, 3.0])
|
|
248
|
+
>>> y = np.array([0.0, 1.0, 4.0, 9.0])
|
|
249
|
+
>>> xnew = np.array([0.5, 1.5, 2.5])
|
|
250
|
+
>>> interp1d(x, y, xnew)
|
|
251
|
+
array([0.5, 2.5, 6.5])
|
|
252
|
+
|
|
253
|
+
With fill values for out-of-bounds:
|
|
254
|
+
|
|
255
|
+
>>> xnew = np.array([-1.0, 1.5, 5.0])
|
|
256
|
+
>>> interp1d(x, y, xnew, bounds="fill", fill_value=(-999.0, 999.0))
|
|
257
|
+
array([-999. , 2.5, 999. ])
|
|
258
|
+
|
|
259
|
+
Clamping to boundary values:
|
|
260
|
+
|
|
261
|
+
>>> interp1d(x, y, xnew, bounds="clamp")
|
|
262
|
+
array([0. , 2.5, 9. ])
|
|
263
|
+
|
|
264
|
+
Broadcasting over multiple curves:
|
|
265
|
+
|
|
266
|
+
>>> x = np.array([0.0, 1.0, 2.0])
|
|
267
|
+
>>> y = np.array([[0.0, 1.0, 2.0], # Linear
|
|
268
|
+
... [0.0, 1.0, 4.0]]) # Quadratic
|
|
269
|
+
>>> xnew = np.array([0.5, 1.5])
|
|
270
|
+
>>> interp1d(x, y, xnew)
|
|
271
|
+
array([[0.5, 1.5],
|
|
272
|
+
[0.5, 2.5]])
|
|
273
|
+
"""
|
|
274
|
+
# Convert inputs to numpy arrays
|
|
275
|
+
x = np.asarray(x)
|
|
276
|
+
y = np.asarray(y)
|
|
277
|
+
xnew = np.asarray(xnew)
|
|
278
|
+
|
|
279
|
+
# Validate bounds mode
|
|
280
|
+
if bounds == "fill":
|
|
281
|
+
bounds_mode = _BOUNDS_FILL
|
|
282
|
+
elif bounds == "clamp":
|
|
283
|
+
bounds_mode = _BOUNDS_CLAMP
|
|
284
|
+
elif bounds == "raise":
|
|
285
|
+
bounds_mode = _BOUNDS_RAISE
|
|
286
|
+
else:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"Invalid bounds mode: {bounds!r}. Must be one of 'fill', 'clamp', 'raise'."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Parse fill_value
|
|
292
|
+
if isinstance(fill_value, tuple):
|
|
293
|
+
if len(fill_value) != 2:
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"fill_value tuple must have exactly 2 elements, got {len(fill_value)}"
|
|
296
|
+
)
|
|
297
|
+
fill_lower, fill_upper = fill_value
|
|
298
|
+
else:
|
|
299
|
+
fill_lower = fill_upper = fill_value
|
|
300
|
+
|
|
301
|
+
# Ensure float dtype (convert integers to float64)
|
|
302
|
+
if not np.issubdtype(x.dtype, np.floating):
|
|
303
|
+
x = x.astype(np.float64)
|
|
304
|
+
if not np.issubdtype(y.dtype, np.floating):
|
|
305
|
+
y = y.astype(np.float64)
|
|
306
|
+
if not np.issubdtype(xnew.dtype, np.floating):
|
|
307
|
+
xnew = xnew.astype(np.float64)
|
|
308
|
+
|
|
309
|
+
# Promote to common dtype
|
|
310
|
+
common_dtype = np.result_type(x, y, xnew)
|
|
311
|
+
if x.dtype != common_dtype:
|
|
312
|
+
x = x.astype(common_dtype)
|
|
313
|
+
if y.dtype != common_dtype:
|
|
314
|
+
y = y.astype(common_dtype)
|
|
315
|
+
if xnew.dtype != common_dtype:
|
|
316
|
+
xnew = xnew.astype(common_dtype)
|
|
317
|
+
|
|
318
|
+
# Convert fill values to the common dtype
|
|
319
|
+
fill_lower = common_dtype.type(fill_lower)
|
|
320
|
+
fill_upper = common_dtype.type(fill_upper)
|
|
321
|
+
|
|
322
|
+
# Pre-validate bounds="raise" mode
|
|
323
|
+
if bounds == "raise":
|
|
324
|
+
# Get valid (non-NaN) query points
|
|
325
|
+
xnew_flat = xnew.ravel()
|
|
326
|
+
xnew_valid = xnew_flat[~np.isnan(xnew_flat)]
|
|
327
|
+
|
|
328
|
+
if xnew_valid.size > 0:
|
|
329
|
+
# Get overall bounds of x (simplest and most robust approach)
|
|
330
|
+
x_min_val = np.min(x)
|
|
331
|
+
x_max_val = np.max(x)
|
|
332
|
+
|
|
333
|
+
# Check for violations
|
|
334
|
+
min_query = np.min(xnew_valid)
|
|
335
|
+
max_query = np.max(xnew_valid)
|
|
336
|
+
|
|
337
|
+
below = min_query < x_min_val
|
|
338
|
+
above = max_query > x_max_val
|
|
339
|
+
|
|
340
|
+
if below or above:
|
|
341
|
+
# Build error message
|
|
342
|
+
msg_parts = ["Query points out of bounds."]
|
|
343
|
+
if below:
|
|
344
|
+
delta_low = x_min_val - min_query
|
|
345
|
+
msg_parts.append(f"Below lower bound by up to {delta_low:.6g}")
|
|
346
|
+
if above:
|
|
347
|
+
delta_high = max_query - x_max_val
|
|
348
|
+
msg_parts.append(f"Above upper bound by up to {delta_high:.6g}")
|
|
349
|
+
raise ValueError(" ".join(msg_parts))
|
|
350
|
+
|
|
351
|
+
# Call the gufunc
|
|
352
|
+
result = _interp1d_gufunc(x, y, xnew, np.int64(bounds_mode), fill_lower, fill_upper)
|
|
353
|
+
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def lerp_indices(
|
|
358
|
+
x: np.ndarray,
|
|
359
|
+
xnew: np.ndarray,
|
|
360
|
+
bounds: Literal["fill", "clamp", "raise"] = "fill",
|
|
361
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
362
|
+
"""
|
|
363
|
+
Precompute left-indices and interpolation weights for linear interpolation.
|
|
364
|
+
|
|
365
|
+
When the same query points ``xnew`` will be applied to many different
|
|
366
|
+
``y`` arrays sharing the same ``x`` grid, it is far cheaper to run the
|
|
367
|
+
binary search once here and then call :func:`lerp` for each ``y``.
|
|
368
|
+
That function skips the search entirely and executes only the
|
|
369
|
+
``y[left] + t*(y[left+1] - y[left])`` step.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
x : ndarray
|
|
374
|
+
Sorted coordinate grid (1-D).
|
|
375
|
+
Shape (n,).
|
|
376
|
+
|
|
377
|
+
xnew : ndarray
|
|
378
|
+
Query points (1-D).
|
|
379
|
+
Shape (m,).
|
|
380
|
+
|
|
381
|
+
bounds : {"fill", "clamp", "raise"}, default: "fill"
|
|
382
|
+
Out-of-bounds handling, same semantics as :func:`interp1d`.
|
|
383
|
+
|
|
384
|
+
* ``"fill"``: out-of-bounds indices are set to 0 with weight NaN so that
|
|
385
|
+
:func:`lerp` will produce NaN there. The caller can replace those
|
|
386
|
+
NaNs after the fact if a different fill value is needed.
|
|
387
|
+
* ``"clamp"``: out-of-bounds queries are clamped to the nearest boundary
|
|
388
|
+
index with weight 0 (reproducing ``y[0]`` or ``y[-1]``).
|
|
389
|
+
* ``"raise"``: raises immediately if any query is out of bounds.
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
indices : ndarray
|
|
394
|
+
Left-bin indices as floats (required by the gufunc signature).
|
|
395
|
+
Shape (m,), dtype float64.
|
|
396
|
+
|
|
397
|
+
weights : ndarray
|
|
398
|
+
Fractional position within each bin: ``t = (xnew - x[i]) / (x[i+1] - x[i])``.
|
|
399
|
+
Shape (m,), dtype float64.
|
|
400
|
+
|
|
401
|
+
Raises
|
|
402
|
+
------
|
|
403
|
+
ValueError
|
|
404
|
+
If ``bounds="raise"`` and any query point is outside ``[x[0], x[-1]]``.
|
|
405
|
+
"""
|
|
406
|
+
x = np.asarray(x, dtype=np.float64)
|
|
407
|
+
xnew = np.asarray(xnew, dtype=np.float64)
|
|
408
|
+
n = len(x)
|
|
409
|
+
|
|
410
|
+
# searchsorted gives insertion point; left-bin index = insertion - 1
|
|
411
|
+
raw = np.searchsorted(x, xnew, side="right") - 1 # shape (m,)
|
|
412
|
+
|
|
413
|
+
if bounds == "raise":
|
|
414
|
+
# Check for out-of-bounds points
|
|
415
|
+
x_min_val = x[0]
|
|
416
|
+
x_max_val = x[-1]
|
|
417
|
+
min_query = xnew.min()
|
|
418
|
+
max_query = xnew.max()
|
|
419
|
+
|
|
420
|
+
below = min_query < x_min_val
|
|
421
|
+
above = max_query > x_max_val
|
|
422
|
+
|
|
423
|
+
if below or above:
|
|
424
|
+
# Build informative error message
|
|
425
|
+
msg_parts = ["Query points out of bounds."]
|
|
426
|
+
if below:
|
|
427
|
+
delta_low = x_min_val - min_query
|
|
428
|
+
msg_parts.append(f"Below lower bound by up to {delta_low:.6g}")
|
|
429
|
+
if above:
|
|
430
|
+
delta_high = max_query - x_max_val
|
|
431
|
+
msg_parts.append(f"Above upper bound by up to {delta_high:.6g}")
|
|
432
|
+
raise ValueError(" ".join(msg_parts))
|
|
433
|
+
|
|
434
|
+
# Clamp indices to valid bin range [0, n-2]
|
|
435
|
+
indices = np.clip(raw, 0, n - 2)
|
|
436
|
+
|
|
437
|
+
# Compute weights
|
|
438
|
+
x0 = x[indices]
|
|
439
|
+
x1 = x[indices + 1]
|
|
440
|
+
weights = (xnew - x0) / (x1 - x0)
|
|
441
|
+
|
|
442
|
+
if bounds == "clamp":
|
|
443
|
+
# For clamping, we need special handling for boundary points:
|
|
444
|
+
# - Points below x[0]: index=0, weight=0 -> y[0] + 0*(y[1]-y[0]) = y[0]
|
|
445
|
+
# - Points above x[-1]: index=n-2, weight=1 -> y[n-2] + 1*(y[n-1]-y[n-2]) = y[n-1]
|
|
446
|
+
# We use <= and >= (not < and >) to avoid numerical issues with exact
|
|
447
|
+
# boundary matches where floating-point arithmetic might produce tiny
|
|
448
|
+
# non-zero weights.
|
|
449
|
+
weights = np.where(xnew <= x[0], 0.0, weights)
|
|
450
|
+
weights = np.where(xnew >= x[-1], 1.0, weights)
|
|
451
|
+
elif bounds == "fill":
|
|
452
|
+
# Mark out-of-bounds with NaN weight so lerp produces NaN
|
|
453
|
+
oob = (xnew < x[0]) | (xnew > x[-1])
|
|
454
|
+
weights = np.where(oob, np.nan, weights)
|
|
455
|
+
|
|
456
|
+
return indices.astype(np.float64), weights
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def lerp(y: np.ndarray, indices: np.ndarray, weights: np.ndarray) -> np.ndarray:
|
|
460
|
+
"""
|
|
461
|
+
Linear interpolation using precomputed indices and weights.
|
|
462
|
+
|
|
463
|
+
This is the fast inner loop for the case where many ``y`` arrays share
|
|
464
|
+
the same ``x`` grid and query points. The binary search is done once
|
|
465
|
+
via :func:`lerp_indices`; this function executes only the linear
|
|
466
|
+
combination ``y[i] + t * (y[i+1] - y[i])``.
|
|
467
|
+
|
|
468
|
+
Parameters
|
|
469
|
+
----------
|
|
470
|
+
y : ndarray
|
|
471
|
+
Data values. The last axis must correspond to the ``x`` grid used
|
|
472
|
+
in :func:`lerp_indices`. Broadcasting over leading dimensions is
|
|
473
|
+
handled by the underlying gufunc.
|
|
474
|
+
Shape (..., n).
|
|
475
|
+
|
|
476
|
+
indices : ndarray
|
|
477
|
+
Left-bin indices from :func:`lerp_indices`.
|
|
478
|
+
**IMPORTANT**: Indices must be in the range ``[0, n-2]`` where
|
|
479
|
+
``n = y.shape[-1]``. This invariant is enforced by :func:`lerp_indices`
|
|
480
|
+
but is not validated here for performance reasons.
|
|
481
|
+
Shape (m,).
|
|
482
|
+
|
|
483
|
+
weights : ndarray
|
|
484
|
+
Interpolation weights from :func:`lerp_indices`.
|
|
485
|
+
Shape (m,).
|
|
486
|
+
|
|
487
|
+
Returns
|
|
488
|
+
-------
|
|
489
|
+
ndarray
|
|
490
|
+
Interpolated values. NaN weights (from ``bounds="fill"``) propagate
|
|
491
|
+
as NaN in the output.
|
|
492
|
+
Shape (..., m).
|
|
493
|
+
|
|
494
|
+
Notes
|
|
495
|
+
-----
|
|
496
|
+
This function does not perform bounds checking on ``indices`` for
|
|
497
|
+
performance. The caller must ensure indices are valid (in ``[0, n-2]``).
|
|
498
|
+
Using :func:`lerp_indices` guarantees this invariant.
|
|
499
|
+
"""
|
|
500
|
+
y = np.asarray(y, dtype=np.float64)
|
|
501
|
+
indices = np.asarray(indices, dtype=np.float64)
|
|
502
|
+
weights = np.asarray(weights, dtype=np.float64)
|
|
503
|
+
return _lerp_gufunc(y, indices, weights)
|
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import xarray as xr
|
|
3
|
+
|
|
4
|
+
from ..core import CKDAbsorptionDatabase, MonoAbsorptionDatabase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture
|
|
8
|
+
def absorption_database_error_handler_config():
|
|
9
|
+
"""
|
|
10
|
+
Error handler configuration for absorption coefficient interpolation.
|
|
11
|
+
|
|
12
|
+
Notes
|
|
13
|
+
-----
|
|
14
|
+
This configuration is chosen to ignore all interpolation issues (except
|
|
15
|
+
bounds error along the mole fraction dimension) because warnings are
|
|
16
|
+
captured by pytest which will raise.
|
|
17
|
+
Ignoring the bounds on pressure and temperature is safe because
|
|
18
|
+
out-of-bounds values usually correspond to locations in the atmosphere
|
|
19
|
+
that are so high that the contribution to the absorption coefficient
|
|
20
|
+
are negligible at these heights.
|
|
21
|
+
The bounds error for the 'x' (mole fraction) coordinate is considered
|
|
22
|
+
fatal.
|
|
23
|
+
"""
|
|
24
|
+
return {
|
|
25
|
+
"p": {"missing": "raise", "scalar": "raise", "bounds": "ignore"},
|
|
26
|
+
"t": {"missing": "raise", "scalar": "raise", "bounds": "ignore"},
|
|
27
|
+
"x": {"missing": "ignore", "scalar": "ignore", "bounds": "raise"},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def thermoprops_us_standard(shared_datadir):
|
|
33
|
+
"""
|
|
34
|
+
This dataset is created with the following command::
|
|
35
|
+
|
|
36
|
+
joseki.make(
|
|
37
|
+
identifier="afgl_1986-us_standard",
|
|
38
|
+
z=np.linspace(0.0, 120.0, 121) * ureg.km,
|
|
39
|
+
additional_molecules=False,
|
|
40
|
+
)
|
|
41
|
+
"""
|
|
42
|
+
yield xr.load_dataset(shared_datadir / "afgl_1986-us_standard.nc")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _absdb(mode, path):
|
|
46
|
+
if mode == "mono":
|
|
47
|
+
return MonoAbsorptionDatabase.from_directory(
|
|
48
|
+
path / "nanomono_v1", lazy=True, fix=False
|
|
49
|
+
)
|
|
50
|
+
elif mode == "ckd":
|
|
51
|
+
return CKDAbsorptionDatabase.from_directory(
|
|
52
|
+
path / "nanockd_v1", lazy=False, fix=False
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
raise RuntimeError
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.fixture
|
|
59
|
+
def absdb(shared_datadir, request):
|
|
60
|
+
mode = request.param
|
|
61
|
+
_db = _absdb(mode, shared_datadir)
|
|
62
|
+
yield _db
|
|
63
|
+
_db.cache_clear()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def absdb_mono(shared_datadir):
|
|
68
|
+
_db = _absdb("mono", shared_datadir)
|
|
69
|
+
yield _db
|
|
70
|
+
_db.cache_clear()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.fixture
|
|
74
|
+
def absdb_ckd(shared_datadir):
|
|
75
|
+
_db = _absdb("ckd", shared_datadir)
|
|
76
|
+
yield _db
|
|
77
|
+
_db.cache_clear()
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: axsdb
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: An absorption database reader for the Eradiate radiative transfer model.
|
|
5
5
|
Author-email: Vincent Leroy <vincent.leroy@rayference.eu>
|
|
6
6
|
Requires-Python: >=3.9
|
|
7
7
|
Requires-Dist: attrs
|
|
8
8
|
Requires-Dist: cachetools
|
|
9
9
|
Requires-Dist: netcdf4
|
|
10
|
+
Requires-Dist: numba<0.63,>=0.58; sys_platform == 'darwin' and platform_machine == 'x86_64'
|
|
11
|
+
Requires-Dist: numba>=0.58; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
|
10
12
|
Requires-Dist: pint
|
|
11
13
|
Requires-Dist: scipy
|
|
12
14
|
Requires-Dist: typer
|
|
13
|
-
Requires-Dist: xarray
|
|
15
|
+
Requires-Dist: xarray>=2024.7.0
|
|
14
16
|
Description-Content-Type: text/markdown
|
|
15
17
|
|
|
16
18
|
# AxsDB — The Eradiate Absorption Cross-section Database Interface
|
|
@@ -21,10 +23,10 @@ Description-Content-Type: text/markdown
|
|
|
21
23
|
[](https://github.com/astral-sh/uv)
|
|
22
24
|
[](https://github.com/astral-sh/ruff)
|
|
23
25
|
|
|
24
|
-
This library provides an interface to read and
|
|
25
|
-
|
|
26
|
+
This library provides an interface to read and query the absorption databases
|
|
27
|
+
of the [Eradiate radiative transfer model](https://eradiate.eu).
|
|
26
28
|
|
|
27
29
|
## License
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
AxsDB is distributed under the terms of the
|
|
30
32
|
[GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
axsdb/__init__.py,sha256=9-EMYK0zJ6dTDZxRvqxpoAwZYER6xHjhNRROhCDPqEs,666
|
|
2
|
+
axsdb/_version.py,sha256=zpfzURzIlh9ajjpvYv4uvkfd404RwJGzy_drTalmMm0,163
|
|
3
|
+
axsdb/cli.py,sha256=UUtmlrS1Y0y6TrXDZtjDY2xBeqPlH_RRSDdRQjv9_b4,1689
|
|
4
|
+
axsdb/core.py,sha256=qN8sEeg2gY7CRkc-hO01zmr3drkhQIb2prWxiEHhM9U,31371
|
|
5
|
+
axsdb/error.py,sha256=dmdurgFPkZxyutyWnYb85ddt7TV3jFw1etLln14boPQ,5621
|
|
6
|
+
axsdb/factory.py,sha256=NzJ0iC6DERD2SESDg4yBxqbjnwNCa99SxDdHwiiwp78,3731
|
|
7
|
+
axsdb/interpolation.py,sha256=PwvjiW6hId6A_iMm0tr0XXJwB-XZnLhL_lJoKz07hfM,30681
|
|
8
|
+
axsdb/math.py,sha256=dtuUqCyHMUAEnypJO1gEJUc63zTPhW9gXvUTIJydL6s,16448
|
|
9
|
+
axsdb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
axsdb/typing.py,sha256=Aol4ouIrfrUs-HBBgblW6Iv-xJDSpFe1RxWCQj8QDFs,94
|
|
11
|
+
axsdb/units.py,sha256=XxbcC-dXiXd8ZkB3cBtd3NTT8zSEd1g8FGQUr5-nrhQ,1762
|
|
12
|
+
axsdb/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
axsdb/testing/fixtures.py,sha256=WPByh5M_tAtFOA8rdTLSDulaK6lWTPRRVpbgsB1dxoU,2181
|
|
14
|
+
axsdb-0.0.3.dist-info/METADATA,sha256=-DQ8TDRgTvu3m0uqstNL-QFsEc9RbEV-sdxLflUfF70,1644
|
|
15
|
+
axsdb-0.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
axsdb-0.0.3.dist-info/entry_points.txt,sha256=4OnXeexRQs2Bl3T4F_jwijW6hPb6vOsIcQAECTsnHUE,41
|
|
17
|
+
axsdb-0.0.3.dist-info/RECORD,,
|
axsdb-0.0.2.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
axsdb/__init__.py,sha256=9-EMYK0zJ6dTDZxRvqxpoAwZYER6xHjhNRROhCDPqEs,666
|
|
2
|
-
axsdb/_version.py,sha256=zpfzURzIlh9ajjpvYv4uvkfd404RwJGzy_drTalmMm0,163
|
|
3
|
-
axsdb/cli.py,sha256=UUtmlrS1Y0y6TrXDZtjDY2xBeqPlH_RRSDdRQjv9_b4,1689
|
|
4
|
-
axsdb/core.py,sha256=_FXP-gio8yTGvYEuiOjWCizi6OV8r9WdI0UNo9Kqk3U,31343
|
|
5
|
-
axsdb/error.py,sha256=dmdurgFPkZxyutyWnYb85ddt7TV3jFw1etLln14boPQ,5621
|
|
6
|
-
axsdb/factory.py,sha256=Pr8kxQ51hgQY8wBCuMy7sarHT5ORmaRYhFWiu5styhk,3710
|
|
7
|
-
axsdb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
axsdb/typing.py,sha256=Aol4ouIrfrUs-HBBgblW6Iv-xJDSpFe1RxWCQj8QDFs,94
|
|
9
|
-
axsdb/units.py,sha256=XxbcC-dXiXd8ZkB3cBtd3NTT8zSEd1g8FGQUr5-nrhQ,1762
|
|
10
|
-
axsdb-0.0.2.dist-info/METADATA,sha256=CN4Ek0lbULDF88DFdZnXDni-oquWTsGdAkU6L_TQ-JQ,1457
|
|
11
|
-
axsdb-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
-
axsdb-0.0.2.dist-info/entry_points.txt,sha256=4OnXeexRQs2Bl3T4F_jwijW6hPb6vOsIcQAECTsnHUE,41
|
|
13
|
-
axsdb-0.0.2.dist-info/RECORD,,
|
|
File without changes
|