Source code for xvamp.utils.interpolate

"""
Interpolation helper functions.
"""

# standard imports
import numpy as np
from dataclasses import dataclass, field


[docs] @dataclass class BoundedInterpolatingBasis: """ Smooth interpolator for models defined on bounded input. Uses sine and cosine squared as basis functions. Assumes constant values between the boundaries and the closest adjacent knots, and transitions between knots. Parameters ---------- lower Lower boundary upper Upper boundary knots Array of knot values """ lower: float upper: float knots: np.ndarray[np.floating] def __post_init__(self): # input check assert self.lower < self.upper assert self.knots.ndim == 1 assert np.logical_and( np.all(self.lower < self.knots), np.all(self.upper > self.knots) ) def __len__(self): """Number of basis functions""" return self.knots.size def __call__(self, nodes: np.ndarray[np.floating]) -> np.ndarray[np.floating]: """ Compute the basis functions that interpolates between knot-anchored models at given input nodes. Parameters ---------- nodes Locations at which to compute the values of the basis functions Returns ------- Coefficients that linearly combine the different models """ # input check assert nodes.ndim == 1 assert np.logical_and(np.all(self.lower <= nodes), np.all(self.upper >= nodes)) # get distance to nodes dist = nodes[:, None] - self.knots[None, :] # initialize output coef = np.zeros(dist.shape) # coefficients after nodes temp = 1 - (dist / np.diff(self.knots, append=np.nan)[None, :]) temp_mask = np.logical_and(temp > 0, temp <= 1) coef[temp_mask] = np.sin(temp[temp_mask] * np.pi / 2) ** 2 # coefficients before nodes temp = -dist / np.diff(self.knots, prepend=np.nan)[None, :] temp_mask = np.logical_and(temp > 0, temp <= 1) coef[temp_mask] = np.cos(temp[temp_mask] * np.pi / 2) ** 2 # coefficients between boundaries and adjacent knots coef[dist[:, 0] <= 0, 0] = 1 coef[dist[:, -1] >= 0, -1] = 1 # done return coef
[docs] @dataclass class PeriodicInterpolatingBasis: """ Smooth interpolator for models defined on periodic input. Uses sine and cosine squared as basis functions. Assumes transitions between knots (including across the boundaries), except where ``const_between_indices`` is set. Parameters ---------- lower Lower boundary upper Upper boundary, wraps to lower one knots Array of knot values const_between_indices Force a constant value between these knot indices (reduces the number of basis functions created) """ # input parameters lower: float upper: float knots: np.ndarray[np.floating] const_between_indices: list[tuple] = field(default_factory=list) # internal values period: float = field(init=False) output_columns: list[tuple] = field(init=False) def __post_init__(self): # input check assert self.lower < self.upper assert self.knots.ndim == 1 assert np.logical_and( np.all(self.lower < self.knots), np.all(self.upper > self.knots) ) assert isinstance(self.const_between_indices, list) assert all( isinstance(t, tuple) and (len(t) == 2) and all(tt < self.knots.size for tt in t) and (t[1] == (t[0] + 1) % self.knots.size) for t in self.const_between_indices ) # compute period self.period = self.upper - self.lower # get true output columns self.output_columns = list(range(self.knots.size)) for t in self.const_between_indices: self.output_columns.remove(t[1] % self.knots.size) def __len__(self): """Number of basis functions""" return len(self.output_columns) def __call__(self, nodes: np.ndarray[np.floating]) -> np.ndarray[np.floating]: """ Compute the basis functions that linearly interpolate between knot-anchored models at given input nodes. Parameters ---------- nodes Locations at which to compute the values of the basis functions Returns ------- Coefficients that linearly combine the different models """ # input check assert nodes.ndim == 1 # fold input into wrapped range nodes = (nodes - self.lower) % self.period + self.lower assert np.logical_and(np.all(self.lower <= nodes), np.all(self.upper >= nodes)) # get distance to nodes dist = (nodes[:, None] - self.knots[None, :]) % self.period dist[dist > self.period / 2] -= self.period # initialize output coef = np.zeros(dist.shape) # coefficients after nodes temp = 1 - ( dist / np.diff(np.r_[self.knots, self.knots[0] + self.period])[None, :] ) temp_mask = np.logical_and(temp > 0, temp <= 1) coef[temp_mask] = np.sin(temp[temp_mask] * np.pi / 2) ** 2 # coefficients before nodes temp = -dist / np.diff(np.r_[self.knots[-1] - self.period, self.knots])[None, :] temp_mask = np.logical_and(temp > 0, temp <= 1) coef[temp_mask] = np.cos(temp[temp_mask] * np.pi / 2) ** 2 # sum the columns for the range that should remain constant for t in self.const_between_indices: coef[:, t[0]] += coef[:, t[1]] coef = coef[:, self.output_columns] # done return coef