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 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
+ ]
@@ -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
+ ![ND throughput 1 obs](./docs/throughput_vs_dims_1_obs.svg)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.8.3)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win32
@@ -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.