1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840
|
import datetime as dt
import warnings
from distutils.version import LooseVersion
from functools import partial
from numbers import Number
from typing import Any, Callable, Dict, Hashable, Sequence, Union
import numpy as np
import pandas as pd
from . import utils
from .common import _contains_datetime_like_objects, ones_like
from .computation import apply_ufunc
from .duck_array_ops import datetime_to_numeric, timedelta_to_numeric
from .options import _get_keep_attrs
from .pycompat import is_duck_dask_array
from .utils import OrderedSet, is_scalar
from .variable import Variable, broadcast_variables
def _get_nan_block_lengths(obj, dim: Hashable, index: Variable):
"""
Return an object where each NaN element in 'obj' is replaced by the
length of the gap the element is in.
"""
# make variable so that we get broadcasting for free
index = Variable([dim], index)
# algorithm from https://github.com/pydata/xarray/pull/3302#discussion_r324707072
arange = ones_like(obj) * index
valid = obj.notnull()
valid_arange = arange.where(valid)
cumulative_nans = valid_arange.ffill(dim=dim).fillna(index[0])
nan_block_lengths = (
cumulative_nans.diff(dim=dim, label="upper")
.reindex({dim: obj[dim]})
.where(valid)
.bfill(dim=dim)
.where(~valid, 0)
.fillna(index[-1] - valid_arange.max())
)
return nan_block_lengths
class BaseInterpolator:
"""Generic interpolator class for normalizing interpolation methods"""
cons_kwargs: Dict[str, Any]
call_kwargs: Dict[str, Any]
f: Callable
method: str
def __call__(self, x):
return self.f(x, **self.call_kwargs)
def __repr__(self):
return "{type}: method={method}".format(
type=self.__class__.__name__, method=self.method
)
class NumpyInterpolator(BaseInterpolator):
"""One-dimensional linear interpolation.
See Also
--------
numpy.interp
"""
def __init__(self, xi, yi, method="linear", fill_value=None, period=None):
if method != "linear":
raise ValueError("only method `linear` is valid for the NumpyInterpolator")
self.method = method
self.f = np.interp
self.cons_kwargs = {}
self.call_kwargs = {"period": period}
self._xi = xi
self._yi = yi
if fill_value is None:
self._left = np.nan
self._right = np.nan
elif isinstance(fill_value, Sequence) and len(fill_value) == 2:
self._left = fill_value[0]
self._right = fill_value[1]
elif is_scalar(fill_value):
self._left = fill_value
self._right = fill_value
else:
raise ValueError("%s is not a valid fill_value" % fill_value)
def __call__(self, x):
return self.f(
x,
self._xi,
self._yi,
left=self._left,
right=self._right,
**self.call_kwargs,
)
class ScipyInterpolator(BaseInterpolator):
"""Interpolate a 1-D function using Scipy interp1d
See Also
--------
scipy.interpolate.interp1d
"""
def __init__(
self,
xi,
yi,
method=None,
fill_value=None,
assume_sorted=True,
copy=False,
bounds_error=False,
order=None,
**kwargs,
):
from scipy.interpolate import interp1d
if method is None:
raise ValueError(
"method is a required argument, please supply a "
"valid scipy.inter1d method (kind)"
)
if method == "polynomial":
if order is None:
raise ValueError("order is required when method=polynomial")
method = order
self.method = method
self.cons_kwargs = kwargs
self.call_kwargs = {}
if fill_value is None and method == "linear":
fill_value = np.nan, np.nan
elif fill_value is None:
fill_value = np.nan
self.f = interp1d(
xi,
yi,
kind=self.method,
fill_value=fill_value,
bounds_error=False,
assume_sorted=assume_sorted,
copy=copy,
**self.cons_kwargs,
)
class SplineInterpolator(BaseInterpolator):
"""One-dimensional smoothing spline fit to a given set of data points.
See Also
--------
scipy.interpolate.UnivariateSpline
"""
def __init__(
self,
xi,
yi,
method="spline",
fill_value=None,
order=3,
nu=0,
ext=None,
**kwargs,
):
from scipy.interpolate import UnivariateSpline
if method != "spline":
raise ValueError("only method `spline` is valid for the SplineInterpolator")
self.method = method
self.cons_kwargs = kwargs
self.call_kwargs = {"nu": nu, "ext": ext}
if fill_value is not None:
raise ValueError("SplineInterpolator does not support fill_value")
self.f = UnivariateSpline(xi, yi, k=order, **self.cons_kwargs)
def _apply_over_vars_with_dim(func, self, dim=None, **kwargs):
"""Wrapper for datasets"""
ds = type(self)(coords=self.coords, attrs=self.attrs)
for name, var in self.data_vars.items():
if dim in var.dims:
ds[name] = func(var, dim=dim, **kwargs)
else:
ds[name] = var
return ds
def get_clean_interp_index(
arr, dim: Hashable, use_coordinate: Union[str, bool] = True, strict: bool = True
):
"""Return index to use for x values in interpolation or curve fitting.
Parameters
----------
arr : DataArray
Array to interpolate or fit to a curve.
dim : str
Name of dimension along which to fit.
use_coordinate : str or bool
If use_coordinate is True, the coordinate that shares the name of the
dimension along which interpolation is being performed will be used as the
x values. If False, the x values are set as an equally spaced sequence.
strict : bool
Whether to raise errors if the index is either non-unique or non-monotonic (default).
Returns
-------
Variable
Numerical values for the x-coordinates.
Notes
-----
If indexing is along the time dimension, datetime coordinates are converted
to time deltas with respect to 1970-01-01.
"""
# Question: If use_coordinate is a string, what role does `dim` play?
from xarray.coding.cftimeindex import CFTimeIndex
if use_coordinate is False:
axis = arr.get_axis_num(dim)
return np.arange(arr.shape[axis], dtype=np.float64)
if use_coordinate is True:
index = arr.get_index(dim)
else: # string
index = arr.coords[use_coordinate]
if index.ndim != 1:
raise ValueError(
f"Coordinates used for interpolation must be 1D, "
f"{use_coordinate} is {index.ndim}D."
)
index = index.to_index()
# TODO: index.name is None for multiindexes
# set name for nice error messages below
if isinstance(index, pd.MultiIndex):
index.name = dim
if strict:
if not index.is_monotonic:
raise ValueError(f"Index {index.name!r} must be monotonically increasing")
if not index.is_unique:
raise ValueError(f"Index {index.name!r} has duplicate values")
# Special case for non-standard calendar indexes
# Numerical datetime values are defined with respect to 1970-01-01T00:00:00 in units of nanoseconds
if isinstance(index, (CFTimeIndex, pd.DatetimeIndex)):
offset = type(index[0])(1970, 1, 1)
if isinstance(index, CFTimeIndex):
index = index.values
index = Variable(
data=datetime_to_numeric(index, offset=offset, datetime_unit="ns"),
dims=(dim,),
)
# raise if index cannot be cast to a float (e.g. MultiIndex)
try:
index = index.values.astype(np.float64)
except (TypeError, ValueError):
# pandas raises a TypeError
# xarray/numpy raise a ValueError
raise TypeError(
f"Index {index.name!r} must be castable to float64 to support "
f"interpolation or curve fitting, got {type(index).__name__}."
)
return index
def interp_na(
self,
dim: Hashable = None,
use_coordinate: Union[bool, str] = True,
method: str = "linear",
limit: int = None,
max_gap: Union[int, float, str, pd.Timedelta, np.timedelta64, dt.timedelta] = None,
keep_attrs: bool = None,
**kwargs,
):
"""Interpolate values according to different methods."""
from xarray.coding.cftimeindex import CFTimeIndex
if dim is None:
raise NotImplementedError("dim is a required argument")
if limit is not None:
valids = _get_valid_fill_mask(self, dim, limit)
if max_gap is not None:
max_type = type(max_gap).__name__
if not is_scalar(max_gap):
raise ValueError("max_gap must be a scalar.")
if (
dim in self.indexes
and isinstance(self.indexes[dim], (pd.DatetimeIndex, CFTimeIndex))
and use_coordinate
):
# Convert to float
max_gap = timedelta_to_numeric(max_gap)
if not use_coordinate:
if not isinstance(max_gap, (Number, np.number)):
raise TypeError(
f"Expected integer or floating point max_gap since use_coordinate=False. Received {max_type}."
)
# method
index = get_clean_interp_index(self, dim, use_coordinate=use_coordinate)
interp_class, kwargs = _get_interpolator(method, **kwargs)
interpolator = partial(func_interpolate_na, interp_class, **kwargs)
if keep_attrs is None:
keep_attrs = _get_keep_attrs(default=True)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", "overflow", RuntimeWarning)
warnings.filterwarnings("ignore", "invalid value", RuntimeWarning)
arr = apply_ufunc(
interpolator,
self,
index,
input_core_dims=[[dim], [dim]],
output_core_dims=[[dim]],
output_dtypes=[self.dtype],
dask="parallelized",
vectorize=True,
keep_attrs=keep_attrs,
).transpose(*self.dims)
if limit is not None:
arr = arr.where(valids)
if max_gap is not None:
if dim not in self.coords:
raise NotImplementedError(
"max_gap not implemented for unlabeled coordinates yet."
)
nan_block_lengths = _get_nan_block_lengths(self, dim, index)
arr = arr.where(nan_block_lengths <= max_gap)
return arr
def func_interpolate_na(interpolator, y, x, **kwargs):
"""helper function to apply interpolation along 1 dimension"""
# reversed arguments are so that attrs are preserved from da, not index
# it would be nice if this wasn't necessary, works around:
# "ValueError: assignment destination is read-only" in assignment below
out = y.copy()
nans = pd.isnull(y)
nonans = ~nans
# fast track for no-nans and all-nans cases
n_nans = nans.sum()
if n_nans == 0 or n_nans == len(y):
return y
f = interpolator(x[nonans], y[nonans], **kwargs)
out[nans] = f(x[nans])
return out
def _bfill(arr, n=None, axis=-1):
"""inverse of ffill"""
import bottleneck as bn
arr = np.flip(arr, axis=axis)
# fill
arr = bn.push(arr, axis=axis, n=n)
# reverse back to original
return np.flip(arr, axis=axis)
def ffill(arr, dim=None, limit=None):
"""forward fill missing values"""
import bottleneck as bn
axis = arr.get_axis_num(dim)
# work around for bottleneck 178
_limit = limit if limit is not None else arr.shape[axis]
return apply_ufunc(
bn.push,
arr,
dask="parallelized",
keep_attrs=True,
output_dtypes=[arr.dtype],
kwargs=dict(n=_limit, axis=axis),
).transpose(*arr.dims)
def bfill(arr, dim=None, limit=None):
"""backfill missing values"""
axis = arr.get_axis_num(dim)
# work around for bottleneck 178
_limit = limit if limit is not None else arr.shape[axis]
return apply_ufunc(
_bfill,
arr,
dask="parallelized",
keep_attrs=True,
output_dtypes=[arr.dtype],
kwargs=dict(n=_limit, axis=axis),
).transpose(*arr.dims)
def _get_interpolator(method, vectorizeable_only=False, **kwargs):
"""helper function to select the appropriate interpolator class
returns interpolator class and keyword arguments for the class
"""
interp1d_methods = [
"linear",
"nearest",
"zero",
"slinear",
"quadratic",
"cubic",
"polynomial",
]
valid_methods = interp1d_methods + [
"barycentric",
"krog",
"pchip",
"spline",
"akima",
]
has_scipy = True
try:
from scipy import interpolate
except ImportError:
has_scipy = False
# prioritize scipy.interpolate
if (
method == "linear"
and not kwargs.get("fill_value", None) == "extrapolate"
and not vectorizeable_only
):
kwargs.update(method=method)
interp_class = NumpyInterpolator
elif method in valid_methods:
if not has_scipy:
raise ImportError("Interpolation with method `%s` requires scipy" % method)
if method in interp1d_methods:
kwargs.update(method=method)
interp_class = ScipyInterpolator
elif vectorizeable_only:
raise ValueError(
"{} is not a vectorizeable interpolator. "
"Available methods are {}".format(method, interp1d_methods)
)
elif method == "barycentric":
interp_class = interpolate.BarycentricInterpolator
elif method == "krog":
interp_class = interpolate.KroghInterpolator
elif method == "pchip":
interp_class = interpolate.PchipInterpolator
elif method == "spline":
kwargs.update(method=method)
interp_class = SplineInterpolator
elif method == "akima":
interp_class = interpolate.Akima1DInterpolator
else:
raise ValueError("%s is not a valid scipy interpolator" % method)
else:
raise ValueError("%s is not a valid interpolator" % method)
return interp_class, kwargs
def _get_interpolator_nd(method, **kwargs):
"""helper function to select the appropriate interpolator class
returns interpolator class and keyword arguments for the class
"""
valid_methods = ["linear", "nearest"]
try:
from scipy import interpolate
except ImportError:
raise ImportError("Interpolation with method `%s` requires scipy" % method)
if method in valid_methods:
kwargs.update(method=method)
interp_class = interpolate.interpn
else:
raise ValueError(
"%s is not a valid interpolator for interpolating "
"over multiple dimensions." % method
)
return interp_class, kwargs
def _get_valid_fill_mask(arr, dim, limit):
"""helper function to determine values that can be filled when limit is not
None"""
kw = {dim: limit + 1}
# we explicitly use construct method to avoid copy.
new_dim = utils.get_temp_dimname(arr.dims, "_window")
return (
arr.isnull()
.rolling(min_periods=1, **kw)
.construct(new_dim, fill_value=False)
.sum(new_dim, skipna=False)
) <= limit
def _localize(var, indexes_coords):
"""Speed up for linear and nearest neighbor method.
Only consider a subspace that is needed for the interpolation
"""
indexes = {}
for dim, [x, new_x] in indexes_coords.items():
if np.issubdtype(new_x.dtype, np.datetime64) and LooseVersion(
np.__version__
) < LooseVersion("1.18"):
# np.nanmin/max changed behaviour for datetime types in numpy 1.18,
# see https://github.com/pydata/xarray/pull/3924/files
minval = np.min(new_x.values)
maxval = np.max(new_x.values)
else:
minval = np.nanmin(new_x.values)
maxval = np.nanmax(new_x.values)
index = x.to_index()
imin = index.get_loc(minval, method="nearest")
imax = index.get_loc(maxval, method="nearest")
indexes[dim] = slice(max(imin - 2, 0), imax + 2)
indexes_coords[dim] = (x[indexes[dim]], new_x)
return var.isel(**indexes), indexes_coords
def _floatize_x(x, new_x):
"""Make x and new_x float.
This is particulary useful for datetime dtype.
x, new_x: tuple of np.ndarray
"""
x = list(x)
new_x = list(new_x)
for i in range(len(x)):
if _contains_datetime_like_objects(x[i]):
# Scipy casts coordinates to np.float64, which is not accurate
# enough for datetime64 (uses 64bit integer).
# We assume that the most of the bits are used to represent the
# offset (min(x)) and the variation (x - min(x)) can be
# represented by float.
xmin = x[i].values.min()
x[i] = x[i]._to_numeric(offset=xmin, dtype=np.float64)
new_x[i] = new_x[i]._to_numeric(offset=xmin, dtype=np.float64)
return x, new_x
def interp(var, indexes_coords, method, **kwargs):
"""Make an interpolation of Variable
Parameters
----------
var: Variable
index_coords:
Mapping from dimension name to a pair of original and new coordinates.
Original coordinates should be sorted in strictly ascending order.
Note that all the coordinates should be Variable objects.
method: string
One of {'linear', 'nearest', 'zero', 'slinear', 'quadratic',
'cubic'}. For multidimensional interpolation, only
{'linear', 'nearest'} can be used.
**kwargs:
keyword arguments to be passed to scipy.interpolate
Returns
-------
Interpolated Variable
See Also
--------
DataArray.interp
Dataset.interp
"""
if not indexes_coords:
return var.copy()
# default behavior
kwargs["bounds_error"] = kwargs.get("bounds_error", False)
result = var
# decompose the interpolation into a succession of independant interpolation
for indexes_coords in decompose_interp(indexes_coords):
var = result
# simple speed up for the local interpolation
if method in ["linear", "nearest"]:
var, indexes_coords = _localize(var, indexes_coords)
# target dimensions
dims = list(indexes_coords)
x, new_x = zip(*[indexes_coords[d] for d in dims])
destination = broadcast_variables(*new_x)
# transpose to make the interpolated axis to the last position
broadcast_dims = [d for d in var.dims if d not in dims]
original_dims = broadcast_dims + dims
new_dims = broadcast_dims + list(destination[0].dims)
interped = interp_func(
var.transpose(*original_dims).data, x, destination, method, kwargs
)
result = Variable(new_dims, interped, attrs=var.attrs)
# dimension of the output array
out_dims = OrderedSet()
for d in var.dims:
if d in dims:
out_dims.update(indexes_coords[d][1].dims)
else:
out_dims.add(d)
result = result.transpose(*tuple(out_dims))
return result
def interp_func(var, x, new_x, method, kwargs):
"""
multi-dimensional interpolation for array-like. Interpolated axes should be
located in the last position.
Parameters
----------
var: np.ndarray or dask.array.Array
Array to be interpolated. The final dimension is interpolated.
x: a list of 1d array.
Original coordinates. Should not contain NaN.
new_x: a list of 1d array
New coordinates. Should not contain NaN.
method: string
{'linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic'} for
1-dimensional interpolation.
{'linear', 'nearest'} for multidimensional interpolation
**kwargs:
Optional keyword arguments to be passed to scipy.interpolator
Returns
-------
interpolated: array
Interpolated array
Note
----
This requiers scipy installed.
See Also
--------
scipy.interpolate.interp1d
"""
if not x:
return var.copy()
if len(x) == 1:
func, kwargs = _get_interpolator(method, vectorizeable_only=True, **kwargs)
else:
func, kwargs = _get_interpolator_nd(method, **kwargs)
if is_duck_dask_array(var):
import dask.array as da
nconst = var.ndim - len(x)
out_ind = list(range(nconst)) + list(range(var.ndim, var.ndim + new_x[0].ndim))
# blockwise args format
x_arginds = [[_x, (nconst + index,)] for index, _x in enumerate(x)]
x_arginds = [item for pair in x_arginds for item in pair]
new_x_arginds = [
[_x, [var.ndim + index for index in range(_x.ndim)]] for _x in new_x
]
new_x_arginds = [item for pair in new_x_arginds for item in pair]
args = (
var,
range(var.ndim),
*x_arginds,
*new_x_arginds,
)
_, rechunked = da.unify_chunks(*args)
args = tuple([elem for pair in zip(rechunked, args[1::2]) for elem in pair])
new_x = rechunked[1 + (len(rechunked) - 1) // 2 :]
new_axes = {
var.ndim + i: new_x[0].chunks[i]
if new_x[0].chunks is not None
else new_x[0].shape[i]
for i in range(new_x[0].ndim)
}
# if usefull, re-use localize for each chunk of new_x
localize = (method in ["linear", "nearest"]) and (new_x[0].chunks is not None)
return da.blockwise(
_dask_aware_interpnd,
out_ind,
*args,
interp_func=func,
interp_kwargs=kwargs,
localize=localize,
concatenate=True,
dtype=var.dtype,
new_axes=new_axes,
)
return _interpnd(var, x, new_x, func, kwargs)
def _interp1d(var, x, new_x, func, kwargs):
# x, new_x are tuples of size 1.
x, new_x = x[0], new_x[0]
rslt = func(x, var, assume_sorted=True, **kwargs)(np.ravel(new_x))
if new_x.ndim > 1:
return rslt.reshape(var.shape[:-1] + new_x.shape)
if new_x.ndim == 0:
return rslt[..., -1]
return rslt
def _interpnd(var, x, new_x, func, kwargs):
x, new_x = _floatize_x(x, new_x)
if len(x) == 1:
return _interp1d(var, x, new_x, func, kwargs)
# move the interpolation axes to the start position
var = var.transpose(range(-len(x), var.ndim - len(x)))
# stack new_x to 1 vector, with reshape
xi = np.stack([x1.values.ravel() for x1 in new_x], axis=-1)
rslt = func(x, var, xi, **kwargs)
# move back the interpolation axes to the last position
rslt = rslt.transpose(range(-rslt.ndim + 1, 1))
return rslt.reshape(rslt.shape[:-1] + new_x[0].shape)
def _dask_aware_interpnd(var, *coords, interp_func, interp_kwargs, localize=True):
"""Wrapper for `_interpnd` through `blockwise`
The first half arrays in `coords` are original coordinates,
the other half are destination coordinates
"""
n_x = len(coords) // 2
nconst = len(var.shape) - n_x
# _interpnd expect coords to be Variables
x = [Variable([f"dim_{nconst + dim}"], _x) for dim, _x in enumerate(coords[:n_x])]
new_x = [
Variable([f"dim_{len(var.shape) + dim}" for dim in range(len(_x.shape))], _x)
for _x in coords[n_x:]
]
if localize:
# _localize expect var to be a Variable
var = Variable([f"dim_{dim}" for dim in range(len(var.shape))], var)
indexes_coords = {_x.dims[0]: (_x, _new_x) for _x, _new_x in zip(x, new_x)}
# simple speed up for the local interpolation
var, indexes_coords = _localize(var, indexes_coords)
x, new_x = zip(*[indexes_coords[d] for d in indexes_coords])
# put var back as a ndarray
var = var.data
return _interpnd(var, x, new_x, interp_func, interp_kwargs)
def decompose_interp(indexes_coords):
"""Decompose the interpolation into a succession of independant interpolation keeping the order"""
dest_dims = [
dest[1].dims if dest[1].ndim > 0 else [dim]
for dim, dest in indexes_coords.items()
]
partial_dest_dims = []
partial_indexes_coords = {}
for i, index_coords in enumerate(indexes_coords.items()):
partial_indexes_coords.update([index_coords])
if i == len(dest_dims) - 1:
break
partial_dest_dims += [dest_dims[i]]
other_dims = dest_dims[i + 1 :]
s_partial_dest_dims = {dim for dims in partial_dest_dims for dim in dims}
s_other_dims = {dim for dims in other_dims for dim in dims}
if not s_partial_dest_dims.intersection(s_other_dims):
# this interpolation is orthogonal to the rest
yield partial_indexes_coords
partial_dest_dims = []
partial_indexes_coords = {}
yield partial_indexes_coords
|