interpn 0.2.4__cp313-cp313-win32.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.
- interpn/__init__.py +24 -0
- interpn/_interpn.cp313-win32.pyd +0 -0
- interpn/multicubic_rectilinear.py +221 -0
- interpn/multicubic_regular.py +232 -0
- interpn/multilinear_rectilinear.py +204 -0
- interpn/multilinear_regular.py +215 -0
- interpn/py.typed +0 -0
- interpn/raw.py +34 -0
- interpn/serialization.py +75 -0
- interpn-0.2.4.dist-info/METADATA +143 -0
- interpn-0.2.4.dist-info/RECORD +14 -0
- interpn-0.2.4.dist-info/WHEEL +4 -0
- interpn-0.2.4.dist-info/licenses/LICENSE-APACHE +13 -0
- interpn-0.2.4.dist-info/licenses/LICENSE-MIT +25 -0
interpn/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python bindings to the `interpn` Rust library
|
|
3
|
+
for N-dimensional interpolation and extrapolation.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
|
|
9
|
+
from .multilinear_regular import MultilinearRegular
|
|
10
|
+
from .multilinear_rectilinear import MultilinearRectilinear
|
|
11
|
+
from .multicubic_regular import MulticubicRegular
|
|
12
|
+
from .multicubic_rectilinear import MulticubicRectilinear
|
|
13
|
+
from interpn import raw
|
|
14
|
+
|
|
15
|
+
__version__ = version("interpn")
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"MultilinearRegular",
|
|
20
|
+
"MultilinearRectilinear",
|
|
21
|
+
"MulticubicRegular",
|
|
22
|
+
"MulticubicRectilinear",
|
|
23
|
+
"raw",
|
|
24
|
+
]
|
|
Binary file
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from functools import reduce
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from pydantic import (
|
|
10
|
+
model_validator,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
BaseModel,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .serialization import Array, ArrayF32, ArrayF64
|
|
16
|
+
|
|
17
|
+
from ._interpn import (
|
|
18
|
+
interpn_cubic_rectilinear_f64,
|
|
19
|
+
interpn_cubic_rectilinear_f32,
|
|
20
|
+
check_bounds_rectilinear_f64,
|
|
21
|
+
check_bounds_rectilinear_f32,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MulticubicRectilinear(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
Multicubic interpolation on a rectilinear grid in up to 8 dimensions.
|
|
28
|
+
|
|
29
|
+
This method uses a symmetrized Hermite spline interpolant,
|
|
30
|
+
which provides a continuous value and first derivative.
|
|
31
|
+
Unlike a B-spline, the second derivative is not continuous;
|
|
32
|
+
however, also unlike a B-spline, the first derivatives are
|
|
33
|
+
maintained to more exactly match the data as estimated by
|
|
34
|
+
a central difference.
|
|
35
|
+
|
|
36
|
+
If `linearize_extrapolation` is set, dimensions on which extrapolation is occurring
|
|
37
|
+
(but not other dimensions) are extrapolated linearly from the last
|
|
38
|
+
two grid points on that dimension.
|
|
39
|
+
|
|
40
|
+
All array inputs must be of the same type, either np.float32 or np.float64
|
|
41
|
+
and must be 1D and contiguous and have size at least 4.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Immutable after initialization checks
|
|
45
|
+
model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
|
|
46
|
+
|
|
47
|
+
grids: list[Array]
|
|
48
|
+
vals: Array
|
|
49
|
+
linearize_extrapolation: bool
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def new(cls, grids: list[NDArray], vals: NDArray, linearize_extrapolation: bool = False) -> MulticubicRectilinear:
|
|
53
|
+
"""
|
|
54
|
+
Initialize interpolator and check types and dimensions, casting other arrays
|
|
55
|
+
to the same type as `vals` if they do not match, and flattening and/or
|
|
56
|
+
reallocating into contiguous storage if necessary.
|
|
57
|
+
|
|
58
|
+
This method exists primarily to remove boilerplate introduced by
|
|
59
|
+
mixing pydantic and numpy.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
grids: 1D arrays of grid coordinate values.
|
|
63
|
+
All grids must be monotonically increasing.
|
|
64
|
+
vals: Values at grid points in C-style ordering,
|
|
65
|
+
as obtained from np.meshgrid(..., indexing="ij")
|
|
66
|
+
linearize_extrapolation: Whether to fall back to a linear
|
|
67
|
+
interpolant outside the grid
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A new MultilinearRectilinear interpolator instance.
|
|
71
|
+
"""
|
|
72
|
+
dtype = vals.dtype
|
|
73
|
+
arrtype = ArrayF64 if dtype == np.float64 else ArrayF32
|
|
74
|
+
interpolator = MulticubicRectilinear(
|
|
75
|
+
grids=[arrtype(data=x) for x in grids],
|
|
76
|
+
vals=arrtype(data=vals.flatten()),
|
|
77
|
+
linearize_extrapolation=linearize_extrapolation,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return interpolator
|
|
81
|
+
|
|
82
|
+
@model_validator(mode="after")
|
|
83
|
+
def _validate_model(self):
|
|
84
|
+
"""Check that all inputs are contiguous and of the same data type,
|
|
85
|
+
and that the grid dimensions and values make sense."""
|
|
86
|
+
dims = self.dims()
|
|
87
|
+
ndims = self.ndims()
|
|
88
|
+
assert (
|
|
89
|
+
ndims <= 8 and ndims >= 1
|
|
90
|
+
), "Number of dimensions must be at least 1 and no more than 8"
|
|
91
|
+
assert self.vals.data.size == reduce(
|
|
92
|
+
lambda acc, x: acc * x, dims
|
|
93
|
+
), "Size of value array does not match grid dims"
|
|
94
|
+
assert all(
|
|
95
|
+
[np.all(np.diff(x.data) > 0.0) for x in self.grids]
|
|
96
|
+
), "All grids must be monotonically increasing"
|
|
97
|
+
assert all(
|
|
98
|
+
[x.data.dtype == self.vals.data.dtype for x in self.grids]
|
|
99
|
+
), "All grid inputs must be of the same data type (np.float32 or np.float64)"
|
|
100
|
+
assert (
|
|
101
|
+
all([x.data.data.contiguous for x in self.grids])
|
|
102
|
+
and self.vals.data.data.contiguous
|
|
103
|
+
), "Grid data must be contiguous"
|
|
104
|
+
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def ndims(self) -> int:
|
|
108
|
+
return len(self.grids)
|
|
109
|
+
|
|
110
|
+
def dims(self) -> list[int]:
|
|
111
|
+
return [x.data.size for x in self.grids]
|
|
112
|
+
|
|
113
|
+
def eval(self, obs: list[NDArray], out: Optional[NDArray] = None) -> NDArray:
|
|
114
|
+
"""Evaluate the interpolator at a set of observation points,
|
|
115
|
+
optionally writing the output into a preallocated array.
|
|
116
|
+
|
|
117
|
+
This function does not reallocate inputs, and will error if the
|
|
118
|
+
inputs are not contiguous or are of the wrong data type.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
122
|
+
out: Optional preallocated array for output. Defaults to None.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
TypeError: If data type is not np.float32 or np.float64
|
|
126
|
+
AssertionError: If input data is not contiguous or dimensions do not match
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Array of evaluated values in the same shape and data type as obs[0]
|
|
130
|
+
"""
|
|
131
|
+
# Allocate output if it was not provided,
|
|
132
|
+
# then check data type and contiguousness
|
|
133
|
+
out_inner = out if out is not None else np.zeros_like(obs[0])
|
|
134
|
+
self.eval_unchecked(obs, out_inner)
|
|
135
|
+
|
|
136
|
+
return out_inner
|
|
137
|
+
|
|
138
|
+
def eval_unchecked(
|
|
139
|
+
self, obs: list[NDArray], out: Optional[NDArray] = None
|
|
140
|
+
) -> NDArray:
|
|
141
|
+
"""Evaluate the interpolator at a set of observation points,
|
|
142
|
+
optionally writing the output into a preallocated array,
|
|
143
|
+
and skipping checks on the dimensionality and contiguousness
|
|
144
|
+
of the inputs.
|
|
145
|
+
|
|
146
|
+
This function does not reallocate inputs, and will error in a lower-level
|
|
147
|
+
function if the inputs are not contiguous or are of the wrong data type.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
151
|
+
out: Optional preallocated array for output. Defaults to None.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
TypeError: If data type is not np.float32 or np.float64
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Array of evaluated values in the same shape and data type as obs[0]
|
|
158
|
+
"""
|
|
159
|
+
dtype = self.vals.data.dtype
|
|
160
|
+
out_inner = out if out is not None else np.zeros_like(obs[0])
|
|
161
|
+
|
|
162
|
+
if dtype == np.float64:
|
|
163
|
+
interpn_cubic_rectilinear_f64(
|
|
164
|
+
[x.data for x in self.grids],
|
|
165
|
+
self.vals.data,
|
|
166
|
+
self.linearize_extrapolation,
|
|
167
|
+
obs,
|
|
168
|
+
out_inner,
|
|
169
|
+
)
|
|
170
|
+
elif dtype == np.float32:
|
|
171
|
+
interpn_cubic_rectilinear_f32(
|
|
172
|
+
[x.data for x in self.grids],
|
|
173
|
+
self.vals.data,
|
|
174
|
+
self.linearize_extrapolation,
|
|
175
|
+
obs,
|
|
176
|
+
out_inner,
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
raise TypeError(f"Unexpected data type: {dtype}")
|
|
180
|
+
|
|
181
|
+
return out_inner
|
|
182
|
+
|
|
183
|
+
def check_bounds(self, obs: list[NDArray], atol: float) -> NDArray[np.bool_]:
|
|
184
|
+
"""
|
|
185
|
+
Check if the observation points violated the bounds on each dimension.
|
|
186
|
+
|
|
187
|
+
This performs a (small) allocation for the output.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
191
|
+
atol: Absolute tolerance on bounds.
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
TypeError: If an unexpected data type is encountered
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
An array of flags for each dimension, each True if that dimension's
|
|
198
|
+
bounds were violated.
|
|
199
|
+
"""
|
|
200
|
+
ndims = self.ndims()
|
|
201
|
+
out = np.array([False] * ndims)
|
|
202
|
+
|
|
203
|
+
dtype = self.vals.data.dtype
|
|
204
|
+
if dtype == np.float64:
|
|
205
|
+
check_bounds_rectilinear_f64(
|
|
206
|
+
[x.data for x in self.grids],
|
|
207
|
+
[x.flatten() for x in obs],
|
|
208
|
+
atol,
|
|
209
|
+
out,
|
|
210
|
+
)
|
|
211
|
+
elif dtype == np.float32:
|
|
212
|
+
check_bounds_rectilinear_f32(
|
|
213
|
+
[x.data for x in self.grids],
|
|
214
|
+
[x.flatten() for x in obs],
|
|
215
|
+
atol,
|
|
216
|
+
out,
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
raise TypeError(f"Unexpected data type: {dtype}")
|
|
220
|
+
|
|
221
|
+
return out
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from functools import reduce
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from pydantic import (
|
|
10
|
+
model_validator,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
BaseModel,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .serialization import Array, ArrayF32, ArrayF64
|
|
16
|
+
|
|
17
|
+
from ._interpn import (
|
|
18
|
+
interpn_cubic_regular_f64,
|
|
19
|
+
interpn_cubic_regular_f32,
|
|
20
|
+
check_bounds_regular_f64,
|
|
21
|
+
check_bounds_regular_f32,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MulticubicRegular(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
Multicubic interpolation on a regular grid in up to 8 dimensions.
|
|
28
|
+
|
|
29
|
+
This method uses a symmetrized Hermite spline interpolant,
|
|
30
|
+
which provides a continuous value and first derivative.
|
|
31
|
+
Unlike a B-spline, the second derivative is not continuous;
|
|
32
|
+
however, also unlike a B-spline, the first derivatives are
|
|
33
|
+
maintained to more exactly match the data as estimated by
|
|
34
|
+
a central difference.
|
|
35
|
+
|
|
36
|
+
If `linearize_extrapolation` is set, dimensions on which extrapolation is occurring
|
|
37
|
+
(but not other dimensions) are extrapolated linearly from the last
|
|
38
|
+
two grid points on that dimension.
|
|
39
|
+
|
|
40
|
+
All array inputs must be of the same type, either np.float32 or np.float64
|
|
41
|
+
and must be 1D and contiguous and have size at least 4.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# Immutable after initialization checks
|
|
45
|
+
model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
|
|
46
|
+
|
|
47
|
+
dims: list[int]
|
|
48
|
+
starts: Array
|
|
49
|
+
steps: Array
|
|
50
|
+
vals: Array
|
|
51
|
+
linearize_extrapolation: bool
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def new(
|
|
55
|
+
cls, dims: list[int], starts: NDArray, steps: NDArray, vals: NDArray, linearize_extrapolation: bool = False
|
|
56
|
+
) -> MulticubicRegular:
|
|
57
|
+
"""
|
|
58
|
+
Initialize interpolator and check types and dimensions, casting other arrays
|
|
59
|
+
to the same type as `vals` if they do not match, and flattening and/or
|
|
60
|
+
reallocating into contiguous storage if necessary.
|
|
61
|
+
|
|
62
|
+
This method exists primarily to remove boilerplate introduced by
|
|
63
|
+
mixing pydantic and numpy.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
dims: Number of elements on each dimension of the grid
|
|
67
|
+
starts: Starting point of each dimension of the grid
|
|
68
|
+
steps: Step size on each dimension of the grid
|
|
69
|
+
vals: Values at grid points in C-style ordering,
|
|
70
|
+
as obtained from np.meshgrid(..., indexing="ij")
|
|
71
|
+
linearize_extrapolation: Whether to fall back to a linear
|
|
72
|
+
interpolant outside the grid
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
A new MulticubicRegular interpolator instance.
|
|
76
|
+
"""
|
|
77
|
+
dtype = vals.dtype
|
|
78
|
+
arrtype = ArrayF64 if dtype == np.float64 else ArrayF32
|
|
79
|
+
interpolator = MulticubicRegular(
|
|
80
|
+
dims=dims,
|
|
81
|
+
starts=arrtype(data=starts.flatten()),
|
|
82
|
+
steps=arrtype(data=steps.flatten()),
|
|
83
|
+
vals=arrtype(data=vals.flatten()),
|
|
84
|
+
linearize_extrapolation=linearize_extrapolation,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return interpolator
|
|
88
|
+
|
|
89
|
+
@model_validator(mode="after")
|
|
90
|
+
def _validate_model(self):
|
|
91
|
+
"""Check that all inputs are contiguous and of the same data type,
|
|
92
|
+
and that the grid dimensions and values make sense."""
|
|
93
|
+
ndims = self.ndims()
|
|
94
|
+
assert (
|
|
95
|
+
ndims <= 8 and ndims >= 1
|
|
96
|
+
), "Number of dimensions must be at least 1 and no more than 8"
|
|
97
|
+
assert self.starts.data.size == ndims, "Grid dimension mismatch"
|
|
98
|
+
assert self.steps.data.size == ndims, "Grid dimension mismatch"
|
|
99
|
+
assert self.vals.data.size == reduce(
|
|
100
|
+
lambda acc, x: acc * x, self.dims
|
|
101
|
+
), "Size of value array does not match grid dims"
|
|
102
|
+
assert all(
|
|
103
|
+
[x > 0.0 for x in self.steps.data]
|
|
104
|
+
), "All grid steps must be positive and nonzero"
|
|
105
|
+
assert all(
|
|
106
|
+
[x.data.dtype == self.vals.data.dtype for x in [self.steps, self.vals]]
|
|
107
|
+
), "All grid inputs must be of the same data type (np.float32 or np.float64)"
|
|
108
|
+
assert all(
|
|
109
|
+
[x.data.data.contiguous for x in [self.starts, self.steps, self.vals]]
|
|
110
|
+
), "Grid data must be contiguous"
|
|
111
|
+
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def ndims(self) -> int:
|
|
115
|
+
return len(self.dims)
|
|
116
|
+
|
|
117
|
+
def eval(self, obs: list[NDArray], out: Optional[NDArray] = None) -> NDArray:
|
|
118
|
+
"""Evaluate the interpolator at a set of observation points,
|
|
119
|
+
optionally writing the output into a preallocated array.
|
|
120
|
+
|
|
121
|
+
This function does not reallocate inputs, and will error if the
|
|
122
|
+
inputs are not contiguous or are of the wrong data type.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
126
|
+
out: Optional preallocated array for output. Defaults to None.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
TypeError: If data type is not np.float32 or np.float64
|
|
130
|
+
AssertionError: If input data is not contiguous or dimensions do not match
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Array of evaluated values in the same shape and data type as obs[0]
|
|
134
|
+
"""
|
|
135
|
+
# Allocate output if it was not provided
|
|
136
|
+
out_inner = out if out is not None else np.zeros_like(obs[0])
|
|
137
|
+
self.eval_unchecked(obs, out_inner)
|
|
138
|
+
|
|
139
|
+
return out_inner
|
|
140
|
+
|
|
141
|
+
def eval_unchecked(
|
|
142
|
+
self, obs: list[NDArray], out: Optional[NDArray] = None
|
|
143
|
+
) -> NDArray:
|
|
144
|
+
"""Evaluate the interpolator at a set of observation points,
|
|
145
|
+
optionally writing the output into a preallocated array,
|
|
146
|
+
and skipping checks on the dimensionality and contiguousness
|
|
147
|
+
of the inputs.
|
|
148
|
+
|
|
149
|
+
This function does not reallocate inputs, and will error in a lower-level
|
|
150
|
+
function if the inputs are not contiguous or are of the wrong data type.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
154
|
+
out: Optional preallocated array for output. Defaults to None.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
TypeError: If data type is not np.float32 or np.float64
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Array of evaluated values in the same shape and data type as obs[0]
|
|
161
|
+
"""
|
|
162
|
+
dtype = self.vals.data.dtype
|
|
163
|
+
out_inner = out if out is not None else np.zeros_like(obs[0])
|
|
164
|
+
|
|
165
|
+
if dtype == np.float64:
|
|
166
|
+
interpn_cubic_regular_f64(
|
|
167
|
+
self.dims,
|
|
168
|
+
self.starts.data,
|
|
169
|
+
self.steps.data,
|
|
170
|
+
self.vals.data,
|
|
171
|
+
self.linearize_extrapolation,
|
|
172
|
+
obs,
|
|
173
|
+
out_inner,
|
|
174
|
+
)
|
|
175
|
+
elif dtype == np.float32:
|
|
176
|
+
interpn_cubic_regular_f32(
|
|
177
|
+
self.dims,
|
|
178
|
+
self.starts.data,
|
|
179
|
+
self.steps.data,
|
|
180
|
+
self.vals.data,
|
|
181
|
+
self.linearize_extrapolation,
|
|
182
|
+
obs,
|
|
183
|
+
out_inner,
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
raise TypeError(f"Unexpected data type: {dtype}")
|
|
187
|
+
|
|
188
|
+
return out_inner
|
|
189
|
+
|
|
190
|
+
def check_bounds(self, obs: list[NDArray], atol: float) -> NDArray[np.bool_]:
|
|
191
|
+
"""
|
|
192
|
+
Check if the observation points violated the bounds on each dimension.
|
|
193
|
+
|
|
194
|
+
This performs a (small) allocation for the output.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
198
|
+
atol: Absolute tolerance on bounds.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
TypeError: If an unexpected data type is encountered
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
An array of flags for each dimension, each True if that dimension's
|
|
205
|
+
bounds were violated.
|
|
206
|
+
"""
|
|
207
|
+
ndims = self.ndims()
|
|
208
|
+
out = np.array([False] * ndims)
|
|
209
|
+
|
|
210
|
+
dtype = self.vals.data.dtype
|
|
211
|
+
if dtype == np.float64:
|
|
212
|
+
check_bounds_regular_f64(
|
|
213
|
+
self.dims,
|
|
214
|
+
self.starts.data,
|
|
215
|
+
self.steps.data,
|
|
216
|
+
[x.flatten() for x in obs],
|
|
217
|
+
atol,
|
|
218
|
+
out,
|
|
219
|
+
)
|
|
220
|
+
elif dtype == np.float32:
|
|
221
|
+
check_bounds_regular_f32(
|
|
222
|
+
self.dims,
|
|
223
|
+
self.starts.data,
|
|
224
|
+
self.steps.data,
|
|
225
|
+
[x.flatten() for x in obs],
|
|
226
|
+
atol,
|
|
227
|
+
out,
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
raise TypeError(f"Unexpected data type: {dtype}")
|
|
231
|
+
|
|
232
|
+
return out
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from functools import reduce
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from pydantic import (
|
|
10
|
+
model_validator,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
BaseModel,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .serialization import Array, ArrayF32, ArrayF64
|
|
16
|
+
|
|
17
|
+
from ._interpn import (
|
|
18
|
+
interpn_linear_rectilinear_f64,
|
|
19
|
+
interpn_linear_rectilinear_f32,
|
|
20
|
+
check_bounds_rectilinear_f64,
|
|
21
|
+
check_bounds_rectilinear_f32,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MultilinearRectilinear(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
Multilinear interpolation on a rectilinear grid in up to 8 dimensions.
|
|
28
|
+
|
|
29
|
+
All array inputs must be of the same type, either np.float32 or np.float64
|
|
30
|
+
and must be 1D and contiguous.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Immutable after initialization checks
|
|
34
|
+
model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
|
|
35
|
+
|
|
36
|
+
grids: list[Array]
|
|
37
|
+
vals: Array
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def new(cls, grids: list[NDArray], vals: NDArray) -> MultilinearRectilinear:
|
|
41
|
+
"""
|
|
42
|
+
Initialize interpolator and check types and dimensions, casting other arrays
|
|
43
|
+
to the same type as `vals` if they do not match, and flattening and/or
|
|
44
|
+
reallocating into contiguous storage if necessary.
|
|
45
|
+
|
|
46
|
+
This method exists primarily to remove boilerplate introduced by
|
|
47
|
+
mixing pydantic and numpy.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
grids: 1D arrays of grid coordinate values.
|
|
51
|
+
All grids must be monotonically increasing.
|
|
52
|
+
vals: Values at grid points in C-style ordering,
|
|
53
|
+
as obtained from np.meshgrid(..., indexing="ij")
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A new MultilinearRectilinear interpolator instance.
|
|
57
|
+
"""
|
|
58
|
+
dtype = vals.dtype
|
|
59
|
+
arrtype = ArrayF64 if dtype == np.float64 else ArrayF32
|
|
60
|
+
interpolator = MultilinearRectilinear(
|
|
61
|
+
grids=[arrtype(data=x) for x in grids],
|
|
62
|
+
vals=arrtype(data=vals.flatten()),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return interpolator
|
|
66
|
+
|
|
67
|
+
@model_validator(mode="after")
|
|
68
|
+
def _validate_model(self):
|
|
69
|
+
"""Check that all inputs are contiguous and of the same data type,
|
|
70
|
+
and that the grid dimensions and values make sense."""
|
|
71
|
+
dims = self.dims()
|
|
72
|
+
ndims = self.ndims()
|
|
73
|
+
assert (
|
|
74
|
+
ndims <= 8 and ndims >= 1
|
|
75
|
+
), "Number of dimensions must be at least 1 and no more than 8"
|
|
76
|
+
assert self.vals.data.size == reduce(
|
|
77
|
+
lambda acc, x: acc * x, dims
|
|
78
|
+
), "Size of value array does not match grid dims"
|
|
79
|
+
assert all(
|
|
80
|
+
[np.all(np.diff(x.data) > 0.0) for x in self.grids]
|
|
81
|
+
), "All grids must be monotonically increasing"
|
|
82
|
+
assert all(
|
|
83
|
+
[x.data.dtype == self.vals.data.dtype for x in self.grids]
|
|
84
|
+
), "All grid inputs must be of the same data type (np.float32 or np.float64)"
|
|
85
|
+
assert (
|
|
86
|
+
all([x.data.data.contiguous for x in self.grids])
|
|
87
|
+
and self.vals.data.data.contiguous
|
|
88
|
+
), "Grid data must be contiguous"
|
|
89
|
+
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def ndims(self) -> int:
|
|
93
|
+
return len(self.grids)
|
|
94
|
+
|
|
95
|
+
def dims(self) -> list[int]:
|
|
96
|
+
return [x.data.size for x in self.grids]
|
|
97
|
+
|
|
98
|
+
def eval(self, obs: list[NDArray], out: Optional[NDArray] = None) -> NDArray:
|
|
99
|
+
"""Evaluate the interpolator at a set of observation points,
|
|
100
|
+
optionally writing the output into a preallocated array.
|
|
101
|
+
|
|
102
|
+
This function does not reallocate inputs, and will error if the
|
|
103
|
+
inputs are not contiguous or are of the wrong data type.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
107
|
+
out: Optional preallocated array for output. Defaults to None.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
TypeError: If data type is not np.float32 or np.float64
|
|
111
|
+
AssertionError: If input data is not contiguous or dimensions do not match
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Array of evaluated values in the same shape and data type as obs[0]
|
|
115
|
+
"""
|
|
116
|
+
# Allocate output if it was not provided,
|
|
117
|
+
# then check data type and contiguousness
|
|
118
|
+
out_inner = out if out is not None else np.zeros_like(obs[0])
|
|
119
|
+
self.eval_unchecked(obs, out_inner)
|
|
120
|
+
|
|
121
|
+
return out_inner
|
|
122
|
+
|
|
123
|
+
def eval_unchecked(
|
|
124
|
+
self, obs: list[NDArray], out: Optional[NDArray] = None
|
|
125
|
+
) -> NDArray:
|
|
126
|
+
"""Evaluate the interpolator at a set of observation points,
|
|
127
|
+
optionally writing the output into a preallocated array,
|
|
128
|
+
and skipping checks on the dimensionality and contiguousness
|
|
129
|
+
of the inputs.
|
|
130
|
+
|
|
131
|
+
This function does not reallocate inputs, and will error in a lower-level
|
|
132
|
+
function if the inputs are not contiguous or are of the wrong data type.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
136
|
+
out: Optional preallocated array for output. Defaults to None.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
TypeError: If data type is not np.float32 or np.float64
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Array of evaluated values in the same shape and data type as obs[0]
|
|
143
|
+
"""
|
|
144
|
+
dtype = self.vals.data.dtype
|
|
145
|
+
out_inner = out if out is not None else np.zeros_like(obs[0])
|
|
146
|
+
|
|
147
|
+
if dtype == np.float64:
|
|
148
|
+
interpn_linear_rectilinear_f64(
|
|
149
|
+
[x.data for x in self.grids],
|
|
150
|
+
self.vals.data,
|
|
151
|
+
obs,
|
|
152
|
+
out_inner,
|
|
153
|
+
)
|
|
154
|
+
elif dtype == np.float32:
|
|
155
|
+
interpn_linear_rectilinear_f32(
|
|
156
|
+
[x.data for x in self.grids],
|
|
157
|
+
self.vals.data,
|
|
158
|
+
obs,
|
|
159
|
+
out_inner,
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
raise TypeError(f"Unexpected data type: {dtype}")
|
|
163
|
+
|
|
164
|
+
return out_inner
|
|
165
|
+
|
|
166
|
+
def check_bounds(self, obs: list[NDArray], atol: float) -> NDArray[np.bool_]:
|
|
167
|
+
"""
|
|
168
|
+
Check if the observation points violated the bounds on each dimension.
|
|
169
|
+
|
|
170
|
+
This performs a (small) allocation for the output.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
174
|
+
atol: Absolute tolerance on bounds.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
TypeError: If an unexpected data type is encountered
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
An array of flags for each dimension, each True if that dimension's
|
|
181
|
+
bounds were violated.
|
|
182
|
+
"""
|
|
183
|
+
ndims = self.ndims()
|
|
184
|
+
out = np.array([False] * ndims)
|
|
185
|
+
|
|
186
|
+
dtype = self.vals.data.dtype
|
|
187
|
+
if dtype == np.float64:
|
|
188
|
+
check_bounds_rectilinear_f64(
|
|
189
|
+
[x.data for x in self.grids],
|
|
190
|
+
[x.flatten() for x in obs],
|
|
191
|
+
atol,
|
|
192
|
+
out,
|
|
193
|
+
)
|
|
194
|
+
elif dtype == np.float32:
|
|
195
|
+
check_bounds_rectilinear_f32(
|
|
196
|
+
[x.data for x in self.grids],
|
|
197
|
+
[x.flatten() for x in obs],
|
|
198
|
+
atol,
|
|
199
|
+
out,
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
raise TypeError(f"Unexpected data type: {dtype}")
|
|
203
|
+
|
|
204
|
+
return out
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from functools import reduce
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from pydantic import (
|
|
10
|
+
model_validator,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
BaseModel,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .serialization import Array, ArrayF32, ArrayF64
|
|
16
|
+
|
|
17
|
+
from ._interpn import (
|
|
18
|
+
interpn_linear_regular_f64,
|
|
19
|
+
interpn_linear_regular_f32,
|
|
20
|
+
check_bounds_regular_f64,
|
|
21
|
+
check_bounds_regular_f32,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MultilinearRegular(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
Multilinear interpolation on a regular grid in up to 8 dimensions.
|
|
28
|
+
|
|
29
|
+
All array inputs must be of the same type, either np.float32 or np.float64
|
|
30
|
+
and must be 1D and contiguous.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Immutable after initialization checks
|
|
34
|
+
model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
|
|
35
|
+
|
|
36
|
+
dims: list[int]
|
|
37
|
+
starts: Array
|
|
38
|
+
steps: Array
|
|
39
|
+
vals: Array
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def new(
|
|
43
|
+
cls, dims: list[int], starts: NDArray, steps: NDArray, vals: NDArray
|
|
44
|
+
) -> MultilinearRegular:
|
|
45
|
+
"""
|
|
46
|
+
Initialize interpolator and check types and dimensions, casting other arrays
|
|
47
|
+
to the same type as `vals` if they do not match, and flattening and/or
|
|
48
|
+
reallocating into contiguous storage if necessary.
|
|
49
|
+
|
|
50
|
+
This method exists primarily to remove boilerplate introduced by
|
|
51
|
+
mixing pydantic and numpy.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
dims: Number of elements on each dimension of the grid
|
|
55
|
+
starts: Starting point of each dimension of the grid
|
|
56
|
+
steps: Step size on each dimension of the grid
|
|
57
|
+
vals: Values at grid points in C-style ordering,
|
|
58
|
+
as obtained from np.meshgrid(..., indexing="ij")
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
A new MultilinearRegular interpolator instance.
|
|
62
|
+
"""
|
|
63
|
+
dtype = vals.dtype
|
|
64
|
+
arrtype = ArrayF64 if dtype == np.float64 else ArrayF32
|
|
65
|
+
interpolator = MultilinearRegular(
|
|
66
|
+
dims=dims,
|
|
67
|
+
starts=arrtype(data=starts.flatten()),
|
|
68
|
+
steps=arrtype(data=steps.flatten()),
|
|
69
|
+
vals=arrtype(data=vals.flatten()),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return interpolator
|
|
73
|
+
|
|
74
|
+
@model_validator(mode="after")
|
|
75
|
+
def _validate_model(self):
|
|
76
|
+
"""Check that all inputs are contiguous and of the same data type,
|
|
77
|
+
and that the grid dimensions and values make sense."""
|
|
78
|
+
ndims = self.ndims()
|
|
79
|
+
assert (
|
|
80
|
+
ndims <= 8 and ndims >= 1
|
|
81
|
+
), "Number of dimensions must be at least 1 and no more than 8"
|
|
82
|
+
assert self.starts.data.size == ndims, "Grid dimension mismatch"
|
|
83
|
+
assert self.steps.data.size == ndims, "Grid dimension mismatch"
|
|
84
|
+
assert self.vals.data.size == reduce(
|
|
85
|
+
lambda acc, x: acc * x, self.dims
|
|
86
|
+
), "Size of value array does not match grid dims"
|
|
87
|
+
assert all(
|
|
88
|
+
[x > 0.0 for x in self.steps.data]
|
|
89
|
+
), "All grid steps must be positive and nonzero"
|
|
90
|
+
assert all(
|
|
91
|
+
[x.data.dtype == self.vals.data.dtype for x in [self.steps, self.vals]]
|
|
92
|
+
), "All grid inputs must be of the same data type (np.float32 or np.float64)"
|
|
93
|
+
assert all(
|
|
94
|
+
[x.data.data.contiguous for x in [self.starts, self.steps, self.vals]]
|
|
95
|
+
), "Grid data must be contiguous"
|
|
96
|
+
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def ndims(self) -> int:
|
|
100
|
+
return len(self.dims)
|
|
101
|
+
|
|
102
|
+
def eval(self, obs: list[NDArray], out: Optional[NDArray] = None) -> NDArray:
|
|
103
|
+
"""Evaluate the interpolator at a set of observation points,
|
|
104
|
+
optionally writing the output into a preallocated array.
|
|
105
|
+
|
|
106
|
+
This function does not reallocate inputs, and will error if the
|
|
107
|
+
inputs are not contiguous or are of the wrong data type.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
111
|
+
out: Optional preallocated array for output. Defaults to None.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
TypeError: If data type is not np.float32 or np.float64
|
|
115
|
+
AssertionError: If input data is not contiguous or dimensions do not match
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Array of evaluated values in the same shape and data type as obs[0]
|
|
119
|
+
"""
|
|
120
|
+
# Allocate output if it was not provided
|
|
121
|
+
out_inner = out if out is not None else np.zeros_like(obs[0])
|
|
122
|
+
self.eval_unchecked(obs, out_inner)
|
|
123
|
+
|
|
124
|
+
return out_inner
|
|
125
|
+
|
|
126
|
+
def eval_unchecked(
|
|
127
|
+
self, obs: list[NDArray], out: Optional[NDArray] = None
|
|
128
|
+
) -> NDArray:
|
|
129
|
+
"""Evaluate the interpolator at a set of observation points,
|
|
130
|
+
optionally writing the output into a preallocated array,
|
|
131
|
+
and skipping checks on the dimensionality and contiguousness
|
|
132
|
+
of the inputs.
|
|
133
|
+
|
|
134
|
+
This function does not reallocate inputs, and will error in a lower-level
|
|
135
|
+
function if the inputs are not contiguous or are of the wrong data type.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
139
|
+
out: Optional preallocated array for output. Defaults to None.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
TypeError: If data type is not np.float32 or np.float64
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Array of evaluated values in the same shape and data type as obs[0]
|
|
146
|
+
"""
|
|
147
|
+
dtype = self.vals.data.dtype
|
|
148
|
+
out_inner = out if out is not None else np.zeros_like(obs[0])
|
|
149
|
+
|
|
150
|
+
if dtype == np.float64:
|
|
151
|
+
interpn_linear_regular_f64(
|
|
152
|
+
self.dims,
|
|
153
|
+
self.starts.data,
|
|
154
|
+
self.steps.data,
|
|
155
|
+
self.vals.data,
|
|
156
|
+
obs,
|
|
157
|
+
out_inner,
|
|
158
|
+
)
|
|
159
|
+
elif dtype == np.float32:
|
|
160
|
+
interpn_linear_regular_f32(
|
|
161
|
+
self.dims,
|
|
162
|
+
self.starts.data,
|
|
163
|
+
self.steps.data,
|
|
164
|
+
self.vals.data,
|
|
165
|
+
obs,
|
|
166
|
+
out_inner,
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
raise TypeError(f"Unexpected data type: {dtype}")
|
|
170
|
+
|
|
171
|
+
return out_inner
|
|
172
|
+
|
|
173
|
+
def check_bounds(self, obs: list[NDArray], atol: float) -> NDArray[np.bool_]:
|
|
174
|
+
"""
|
|
175
|
+
Check if the observation points violated the bounds on each dimension.
|
|
176
|
+
|
|
177
|
+
This performs a (small) allocation for the output.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
obs: [x, y, ...] coordinates of observation points.
|
|
181
|
+
atol: Absolute tolerance on bounds.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
TypeError: If an unexpected data type is encountered
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
An array of flags for each dimension, each True if that dimension's
|
|
188
|
+
bounds were violated.
|
|
189
|
+
"""
|
|
190
|
+
ndims = self.ndims()
|
|
191
|
+
out = np.array([False] * ndims)
|
|
192
|
+
|
|
193
|
+
dtype = self.vals.data.dtype
|
|
194
|
+
if dtype == np.float64:
|
|
195
|
+
check_bounds_regular_f64(
|
|
196
|
+
self.dims,
|
|
197
|
+
self.starts.data,
|
|
198
|
+
self.steps.data,
|
|
199
|
+
[x.flatten() for x in obs],
|
|
200
|
+
atol,
|
|
201
|
+
out,
|
|
202
|
+
)
|
|
203
|
+
elif dtype == np.float32:
|
|
204
|
+
check_bounds_regular_f32(
|
|
205
|
+
self.dims,
|
|
206
|
+
self.starts.data,
|
|
207
|
+
self.steps.data,
|
|
208
|
+
[x.flatten() for x in obs],
|
|
209
|
+
atol,
|
|
210
|
+
out,
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
raise TypeError(f"Unexpected data type: {dtype}")
|
|
214
|
+
|
|
215
|
+
return out
|
interpn/py.typed
ADDED
|
File without changes
|
interpn/raw.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Re-exported raw PyO3/Maturin bindings to Rust functions.
|
|
3
|
+
Using these can yield some performance benefit at the expense of ergonomics.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ._interpn import (
|
|
7
|
+
interpn_linear_regular_f64,
|
|
8
|
+
interpn_linear_regular_f32,
|
|
9
|
+
interpn_linear_rectilinear_f64,
|
|
10
|
+
interpn_linear_rectilinear_f32,
|
|
11
|
+
interpn_cubic_regular_f64,
|
|
12
|
+
interpn_cubic_regular_f32,
|
|
13
|
+
interpn_cubic_rectilinear_f64,
|
|
14
|
+
interpn_cubic_rectilinear_f32,
|
|
15
|
+
check_bounds_regular_f64,
|
|
16
|
+
check_bounds_regular_f32,
|
|
17
|
+
check_bounds_rectilinear_f64,
|
|
18
|
+
check_bounds_rectilinear_f32,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"interpn_linear_regular_f64",
|
|
23
|
+
"interpn_linear_regular_f32",
|
|
24
|
+
"interpn_linear_rectilinear_f64",
|
|
25
|
+
"interpn_linear_rectilinear_f32",
|
|
26
|
+
"interpn_cubic_regular_f64",
|
|
27
|
+
"interpn_cubic_regular_f32",
|
|
28
|
+
"interpn_cubic_rectilinear_f64",
|
|
29
|
+
"interpn_cubic_rectilinear_f32",
|
|
30
|
+
"check_bounds_regular_f64",
|
|
31
|
+
"check_bounds_regular_f32",
|
|
32
|
+
"check_bounds_rectilinear_f64",
|
|
33
|
+
"check_bounds_rectilinear_f32",
|
|
34
|
+
]
|
interpn/serialization.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Union, Annotated, Literal, Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from pydantic import (
|
|
10
|
+
field_validator,
|
|
11
|
+
field_serializer,
|
|
12
|
+
ConfigDict,
|
|
13
|
+
BaseModel,
|
|
14
|
+
Field,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ArrayF64(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
Serializable wrapper for NDArray[float64].
|
|
21
|
+
"""
|
|
22
|
+
data: NDArray[np.float64]
|
|
23
|
+
dtype: Literal["float64"] = "float64"
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
|
|
26
|
+
|
|
27
|
+
@field_validator("data", mode="before")
|
|
28
|
+
def _validate_x(data: Any) -> NDArray[np.float64]:
|
|
29
|
+
if isinstance(data, str):
|
|
30
|
+
y = np.ascontiguousarray(np.array(json.loads(data), dtype=np.float64))
|
|
31
|
+
elif isinstance(data, np.ndarray):
|
|
32
|
+
y = np.ascontiguousarray(data.astype(np.float64))
|
|
33
|
+
elif isinstance(data, list):
|
|
34
|
+
y = np.array(data, dtype=np.float64)
|
|
35
|
+
else:
|
|
36
|
+
raise TypeError
|
|
37
|
+
|
|
38
|
+
return y
|
|
39
|
+
|
|
40
|
+
@field_serializer("data", return_type=str)
|
|
41
|
+
def _serialize_x(data: Any) -> str:
|
|
42
|
+
return json.dumps(data.tolist())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ArrayF32(BaseModel):
|
|
46
|
+
"""
|
|
47
|
+
Serializable wrapper for NDArray[float32].
|
|
48
|
+
|
|
49
|
+
The data is represented as a list of float64 on disk and in RAM
|
|
50
|
+
during serialization and deserialization.
|
|
51
|
+
"""
|
|
52
|
+
data: NDArray[np.float32]
|
|
53
|
+
dtype: Literal["float32"] = "float32"
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
|
|
56
|
+
|
|
57
|
+
@field_validator("data", mode="before")
|
|
58
|
+
def _validate_x(data: Any) -> NDArray[np.float32]:
|
|
59
|
+
if isinstance(data, str):
|
|
60
|
+
y = np.ascontiguousarray(np.array(json.loads(data), dtype=np.float32))
|
|
61
|
+
elif isinstance(data, np.ndarray):
|
|
62
|
+
y = np.ascontiguousarray(data.astype(np.float32))
|
|
63
|
+
elif isinstance(data, list):
|
|
64
|
+
y = np.array(data, dtype=np.float32)
|
|
65
|
+
else:
|
|
66
|
+
raise TypeError
|
|
67
|
+
|
|
68
|
+
return y
|
|
69
|
+
|
|
70
|
+
@field_serializer("data", return_type=str)
|
|
71
|
+
def _serialize_x(data: Any) -> str:
|
|
72
|
+
return json.dumps(data.tolist())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
Array = Annotated[Union[ArrayF32, ArrayF64], Field(discriminator="dtype")]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: interpn
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
7
|
+
Requires-Dist: numpy>=2
|
|
8
|
+
Requires-Dist: pydantic>=2.5.2
|
|
9
|
+
Requires-Dist: pytest>=8.0 ; extra == 'test'
|
|
10
|
+
Requires-Dist: coverage>=7.3.2 ; extra == 'test'
|
|
11
|
+
Requires-Dist: ruff>=0.4.10 ; extra == 'test'
|
|
12
|
+
Requires-Dist: pyright==1.1.337 ; extra == 'test'
|
|
13
|
+
Requires-Dist: mktestdocs>=0.2.1 ; extra == 'test'
|
|
14
|
+
Requires-Dist: scipy>=1.11.4 ; extra == 'test'
|
|
15
|
+
Requires-Dist: matplotlib>=3.8 ; extra == 'test'
|
|
16
|
+
Requires-Dist: scipy>=1.11.4 ; extra == 'bench'
|
|
17
|
+
Requires-Dist: matplotlib>=3.8 ; extra == 'bench'
|
|
18
|
+
Requires-Dist: memory-profiler>=0.61.0 ; extra == 'bench'
|
|
19
|
+
Requires-Dist: mkdocs>=1.5.3 ; extra == 'doc'
|
|
20
|
+
Requires-Dist: mkdocstrings-python>=1.7.5 ; extra == 'doc'
|
|
21
|
+
Requires-Dist: mkdocs-material>=9.4.10 ; extra == 'doc'
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Provides-Extra: bench
|
|
24
|
+
Provides-Extra: doc
|
|
25
|
+
License-File: LICENSE-APACHE
|
|
26
|
+
License-File: LICENSE-MIT
|
|
27
|
+
Summary: N-dimensional interpolation/extrapolation methods
|
|
28
|
+
Author-email: James Logan <jlogan03@gmail.com>
|
|
29
|
+
Requires-Python: >=3.9, <3.14
|
|
30
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
31
|
+
|
|
32
|
+
# interpn
|
|
33
|
+
|
|
34
|
+
Python bindings to the `interpn` Rust library for N-dimensional interpolation and extrapolation.
|
|
35
|
+
|
|
36
|
+
[Docs](https://interpnpy.readthedocs.io/en/latest/) |
|
|
37
|
+
[Repo](https://github.com/jlogan03/interpnpy) |
|
|
38
|
+
[Rust Library (github)](https://github.com/jlogan03/interpn) |
|
|
39
|
+
[Rust Docs (docs.rs)](https://docs.rs/interpn/latest/interpn/)
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
| Feature →<br>↓ Interpolant Method | Regular<br>Grid | Rectilinear<br>Grid | Json<br>Serialization |
|
|
44
|
+
|-----------------------------------|-----------------|---------------------|-----------------------|
|
|
45
|
+
| Linear | ✅ | ✅ | ✅ |
|
|
46
|
+
| Cubic | ✅ | ✅ | ✅ |
|
|
47
|
+
|
|
48
|
+
The methods provided here, while more limited in scope than scipy's, are
|
|
49
|
+
|
|
50
|
+
* significantly faster for higher dimensions (1-3 orders of magnitude under most conditions)
|
|
51
|
+
* use almost no RAM (and perform no heap allocations at all)
|
|
52
|
+
* produce significantly improved floating-point error (by 1-2 orders of magnitude)
|
|
53
|
+
* are json-serializable using Pydantic
|
|
54
|
+
* can also be used easily in web and embedded applications via the Rust library
|
|
55
|
+
* are permissively licensed
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
See [here](https://interpnpy.readthedocs.io/en/latest/perf/) for more info about quality-of-fit, throughput, and memory usage.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install interpn
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Example: Available Methods
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import interpn
|
|
71
|
+
import numpy as np
|
|
72
|
+
|
|
73
|
+
# Build grid
|
|
74
|
+
x = np.linspace(0.0, 10.0, 5)
|
|
75
|
+
y = np.linspace(20.0, 30.0, 4)
|
|
76
|
+
grids = [x, y]
|
|
77
|
+
|
|
78
|
+
xgrid, ygrid = np.meshgrid(x, y, indexing="ij")
|
|
79
|
+
zgrid = (xgrid + 2.0 * ygrid) # Values at grid points
|
|
80
|
+
|
|
81
|
+
# Grid inputs for true regular grid
|
|
82
|
+
dims = [x.size, y.size]
|
|
83
|
+
starts = np.array([x[0], y[0]])
|
|
84
|
+
steps = np.array([x[1] - x[0], y[1] - y[0]])
|
|
85
|
+
|
|
86
|
+
# Initialize different interpolators
|
|
87
|
+
# Call like `linear_regular.eval([xs, ys])`
|
|
88
|
+
linear_regular = interpn.MultilinearRegular.new(dims, starts, steps, zgrid)
|
|
89
|
+
cubic_regular = interpn.MulticubicRegular.new(dims, starts, steps, zgrid)
|
|
90
|
+
linear_rectilinear = interpn.MultilinearRectilinear.new(grids, zgrid)
|
|
91
|
+
cubic_rectilinear = interpn.MulticubicRectilinear.new(grids, zgrid)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Example: Multilinear Interpolation on a Regular Grid
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import interpn
|
|
98
|
+
import numpy as np
|
|
99
|
+
|
|
100
|
+
# Build grid
|
|
101
|
+
x = np.linspace(0.0, 10.0, 5)
|
|
102
|
+
y = np.linspace(20.0, 30.0, 4)
|
|
103
|
+
|
|
104
|
+
xgrid, ygrid = np.meshgrid(x, y, indexing="ij")
|
|
105
|
+
zgrid = (xgrid + 2.0 * ygrid) # Values at grid points
|
|
106
|
+
|
|
107
|
+
# Grid inputs for true regular grid
|
|
108
|
+
dims = [x.size, y.size]
|
|
109
|
+
starts = np.array([x[0], y[0]])
|
|
110
|
+
steps = np.array([x[1] - x[0], y[1] - y[0]])
|
|
111
|
+
|
|
112
|
+
# Observation points pointed back at the grid
|
|
113
|
+
obs = [xgrid.flatten(), ygrid.flatten()]
|
|
114
|
+
|
|
115
|
+
# Initialize
|
|
116
|
+
interpolator = interpn.MultilinearRegular.new(dims, starts, steps, zgrid.flatten())
|
|
117
|
+
|
|
118
|
+
# Interpolate
|
|
119
|
+
out = interpolator.eval(obs)
|
|
120
|
+
|
|
121
|
+
# Check result
|
|
122
|
+
assert np.allclose(out, zgrid.flatten(), rtol=1e-13)
|
|
123
|
+
|
|
124
|
+
# Serialize and deserialize
|
|
125
|
+
roundtrip_interpolator = interpn.MultilinearRegular.model_validate_json(
|
|
126
|
+
interpolator.model_dump_json()
|
|
127
|
+
)
|
|
128
|
+
out2 = roundtrip_interpolator.eval(obs)
|
|
129
|
+
|
|
130
|
+
# Check result from roundtrip serialized/deserialized interpolator
|
|
131
|
+
assert np.all(out == out2)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# License
|
|
136
|
+
|
|
137
|
+
Licensed under either of
|
|
138
|
+
|
|
139
|
+
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
|
140
|
+
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
|
141
|
+
|
|
142
|
+
at your option.
|
|
143
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interpn-0.2.4.dist-info/METADATA,sha256=YNhUnyy5sFfpDfKCCiqx1coCRnt3i84b7Cfso6UeicE,4873
|
|
2
|
+
interpn-0.2.4.dist-info/WHEEL,sha256=eh90R9THiv1HYPhYUCnpm_RAErMfEQKvZWVMxF3uaCM,92
|
|
3
|
+
interpn-0.2.4.dist-info/licenses/LICENSE-APACHE,sha256=vnG2Zpa4ndnPKQhtz4GzjlNyndoyC6HGzYQxqZiloiA,566
|
|
4
|
+
interpn-0.2.4.dist-info/licenses/LICENSE-MIT,sha256=00q7nfKf9L7lT7vXsRm0qTY6L4vT1Fu_q1nGsNlBNLQ,1080
|
|
5
|
+
interpn/multicubic_rectilinear.py,sha256=AYeaidxiY2J-9VofxuRH1FQIpA9XuC703mBNPywFDdE,7997
|
|
6
|
+
interpn/multicubic_regular.py,sha256=FDKWrVTv9pAQCw_DuyQAqWiK1_SXdduiHwdpNRS3ZWI,8326
|
|
7
|
+
interpn/multilinear_rectilinear.py,sha256=BWDY7xH3wxPCrJS2pY7tGvNdTNr63pzLNlyWleUVJS8,7084
|
|
8
|
+
interpn/multilinear_regular.py,sha256=jOsmLAingzmzFh6m5PduST9me6__gjIwCo2ODtCaiOM,7414
|
|
9
|
+
interpn/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
interpn/raw.py,sha256=MhBso4GbnqCmBAyy3E3lGfhwiLhiHSeKGaStWXuFbIo,1034
|
|
11
|
+
interpn/serialization.py,sha256=COnpGVuxstxmKkSMQTvFQ58q1TKYYdMttJKzg9iErOk,2236
|
|
12
|
+
interpn/__init__.py,sha256=Re2WbLtsFqhhmpWszu4AzA_8M5WgpHZFzYDfNXQ-VGI,638
|
|
13
|
+
interpn/_interpn.cp313-win32.pyd,sha256=h2949rnu9NWKFtOm9Yco2gsKjOiV0uINS_fkH4oFZGU,475136
|
|
14
|
+
interpn-0.2.4.dist-info/RECORD,,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright (c) 2023 James Logan
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Copyright (c) 2024 James Logan
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any
|
|
4
|
+
person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the
|
|
6
|
+
Software without restriction, including without
|
|
7
|
+
limitation the rights to use, copy, modify, merge,
|
|
8
|
+
publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software
|
|
10
|
+
is furnished to do so, subject to the following
|
|
11
|
+
conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice
|
|
14
|
+
shall be included in all copies or substantial portions
|
|
15
|
+
of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
18
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
19
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
20
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
21
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
22
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
23
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
|
24
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
25
|
+
DEALINGS IN THE SOFTWARE.
|