Module MAPLEAF.Motion.Interpolation
Interpolation rigid body states and scalar values. State interpolation used in flight animations. Scalar interpolation used for interpolation of transonic aerodynamic forces.
Expand source code
'''
Interpolation rigid body states and scalar values.
State interpolation used in flight animations.
Scalar interpolation used for interpolation of transonic aerodynamic forces.
'''
from bisect import bisect
from functools import lru_cache
import numpy as np
import scipy.linalg as linalg
from scipy.interpolate import LinearNDInterpolator, NearestNDInterpolator
__all__ = [ "linInterp", "linInterpWeights", "calculateCubicInterpCoefficients", "cubicInterp", "NoNaNLinearNDInterpolator" ]
def linInterp(X, Y, desiredX):
'''
Arguments:
X: Sorted list or numpy array of numeric x-values
Y: Sorted list or numpy array of numeric y-values
desiredX: Numeric x-value, indicating point to interpolate to
Returns:
desiredY: The linearly-interpolated y value at x=desiredX
Notes:
Uses binary search (bisect) to locate interpolation interval
Faster than built-in methods for our application (see test/LinInterpSpeed.py)
'''
interpPt = bisect(X, desiredX)
if interpPt >= len(X):
return Y[len(X)-1]
elif interpPt < 1:
return Y[0]
else:
lgX = X[interpPt]
smX = X[interpPt-1]
lgY = Y[interpPt]
smY = Y[interpPt-1]
return (lgY - smY)*(desiredX - smX)/(lgX - smX) + smY
def linInterpWeights(X, desiredX):
'''
Expects the list X is sorted
Returns smallYIndex, smallYWeight, largeYIndex, largeYWeight:
Ex: X = [ 0, 1, 2, 3 ], desiredX = 0.75
smallYIndex = 0
smallYWeight = 0.25
largeYIndex = 1
largeYWeight = 0.75
Then, to calculate the interpolate value:
interpVal = Y[smallYIndex]*smallYWeight + Y[largeYIndex]*largeYWeight
'''
interpPt = bisect(X, desiredX)
#Edge cases
if interpPt >= len(X):
return 0, 0, -1, 1
elif interpPt < 1:
return 0, 1, 0, 0
# Normal cases
smallYIndex = interpPt -1
largeYIndex = interpPt
largeYWeight = (desiredX - X[smallYIndex]) / (X[largeYIndex] - X[smallYIndex])
smallYWeight = 1 - largeYWeight
return smallYIndex, smallYWeight, largeYIndex, largeYWeight
@lru_cache(20)
def getInvAMatrix(X1, X2):
'''
Computes the inverse of the A matrix used in `calculateCubicInterpCoefficients` below.
During a simulation, these will only change from component to component, not from time step to time step, hence the cache
'''
AMatrix = \
np.array([ [ 1, X1, X1**2, X1**3 ],
[ 1, X2, X2**2, X2**3 ],
[ 0, 1, 2*X1, 3*X1**2 ],
[ 0, 1, 2*X2, 3*X2**2 ] ])
return linalg.inv(AMatrix)
def calculateCubicInterpCoefficients(X1, X2, Y1, Y2, dydx1, dydx2):
''' Returns coefficients for a cubic polynomial that matches values and derivatives at x1 and x2 '''
# AMatrix and B, together define the following equations which constrain the cubic interpolation
# f(x=x1) == Y1
# f(x=x2) == Y2
# df/dx (x=x1) == dydx1
# df/dx (x=x2) == dydx2
Ainv = getInvAMatrix(X1, X2)
B = np.array([ [Y1],
[Y2],
[dydx1],
[dydx2]])
return Ainv.dot(B)
def cubicInterp(X, X1, X2, Y1, Y2, Y1_plusDx, Y2_plusDx, dx):
dy_dx_x1 = (Y1_plusDx - Y1) / dx
dy_dx_x2 = (Y2_plusDx - Y2) / dx
interpCoeffs = calculateCubicInterpCoefficients(X1, X2, Y1, Y2, dy_dx_x1, dy_dx_x2)
return float(interpCoeffs[0] + interpCoeffs[1]*X + interpCoeffs[2]*X**2 + interpCoeffs[3]*X**3)
class NoNaNLinearNDInterpolator():
def __init__(self, keys, values, tablePath=None) -> None:
self.linearInterpolator = LinearNDInterpolator(keys, values)
self.nearestInterpolator = NearestNDInterpolator(keys, values)
self.tablePath = tablePath
def __call__(self, *keyVector):
linearResult = self.linearInterpolator(*keyVector)
if np.isnan(linearResult).any():
# Occurs if the requested values are outside of the bounds of the table being interpolated over
# In that case just return the nearest result
print("WARNING: Interpolation requested outside of bounds in table: {}. Current key vector = {}. Extrapolation not supported, returning nearest result instead".format(self.tablePath, keyVector))
return self.nearestInterpolator(*keyVector)
else:
return linearResult
Functions
def calculateCubicInterpCoefficients(X1, X2, Y1, Y2, dydx1, dydx2)
-
Returns coefficients for a cubic polynomial that matches values and derivatives at x1 and x2
Expand source code
def calculateCubicInterpCoefficients(X1, X2, Y1, Y2, dydx1, dydx2): ''' Returns coefficients for a cubic polynomial that matches values and derivatives at x1 and x2 ''' # AMatrix and B, together define the following equations which constrain the cubic interpolation # f(x=x1) == Y1 # f(x=x2) == Y2 # df/dx (x=x1) == dydx1 # df/dx (x=x2) == dydx2 Ainv = getInvAMatrix(X1, X2) B = np.array([ [Y1], [Y2], [dydx1], [dydx2]]) return Ainv.dot(B)
def cubicInterp(X, X1, X2, Y1, Y2, Y1_plusDx, Y2_plusDx, dx)
-
Expand source code
def cubicInterp(X, X1, X2, Y1, Y2, Y1_plusDx, Y2_plusDx, dx): dy_dx_x1 = (Y1_plusDx - Y1) / dx dy_dx_x2 = (Y2_plusDx - Y2) / dx interpCoeffs = calculateCubicInterpCoefficients(X1, X2, Y1, Y2, dy_dx_x1, dy_dx_x2) return float(interpCoeffs[0] + interpCoeffs[1]*X + interpCoeffs[2]*X**2 + interpCoeffs[3]*X**3)
def linInterp(X, Y, desiredX)
-
Arguments
X: Sorted list or numpy array of numeric x-values Y: Sorted list or numpy array of numeric y-values desiredX: Numeric x-value, indicating point to interpolate to
Returns
desiredY
- The linearly-interpolated y value at x=desiredX
Notes
Uses binary search (bisect) to locate interpolation interval Faster than built-in methods for our application (see test/LinInterpSpeed.py)
Expand source code
def linInterp(X, Y, desiredX): ''' Arguments: X: Sorted list or numpy array of numeric x-values Y: Sorted list or numpy array of numeric y-values desiredX: Numeric x-value, indicating point to interpolate to Returns: desiredY: The linearly-interpolated y value at x=desiredX Notes: Uses binary search (bisect) to locate interpolation interval Faster than built-in methods for our application (see test/LinInterpSpeed.py) ''' interpPt = bisect(X, desiredX) if interpPt >= len(X): return Y[len(X)-1] elif interpPt < 1: return Y[0] else: lgX = X[interpPt] smX = X[interpPt-1] lgY = Y[interpPt] smY = Y[interpPt-1] return (lgY - smY)*(desiredX - smX)/(lgX - smX) + smY
def linInterpWeights(X, desiredX)
-
Expects the list X is sorted Returns smallYIndex, smallYWeight, largeYIndex, largeYWeight: Ex: X = [ 0, 1, 2, 3 ], desiredX = 0.75 smallYIndex = 0 smallYWeight = 0.25 largeYIndex = 1 largeYWeight = 0.75
Then, to calculate the interpolate value: interpVal = Y[smallYIndex]*smallYWeight + Y[largeYIndex]*largeYWeight
Expand source code
def linInterpWeights(X, desiredX): ''' Expects the list X is sorted Returns smallYIndex, smallYWeight, largeYIndex, largeYWeight: Ex: X = [ 0, 1, 2, 3 ], desiredX = 0.75 smallYIndex = 0 smallYWeight = 0.25 largeYIndex = 1 largeYWeight = 0.75 Then, to calculate the interpolate value: interpVal = Y[smallYIndex]*smallYWeight + Y[largeYIndex]*largeYWeight ''' interpPt = bisect(X, desiredX) #Edge cases if interpPt >= len(X): return 0, 0, -1, 1 elif interpPt < 1: return 0, 1, 0, 0 # Normal cases smallYIndex = interpPt -1 largeYIndex = interpPt largeYWeight = (desiredX - X[smallYIndex]) / (X[largeYIndex] - X[smallYIndex]) smallYWeight = 1 - largeYWeight return smallYIndex, smallYWeight, largeYIndex, largeYWeight
Classes
class NoNaNLinearNDInterpolator (keys, values, tablePath=None)
-
Expand source code
class NoNaNLinearNDInterpolator(): def __init__(self, keys, values, tablePath=None) -> None: self.linearInterpolator = LinearNDInterpolator(keys, values) self.nearestInterpolator = NearestNDInterpolator(keys, values) self.tablePath = tablePath def __call__(self, *keyVector): linearResult = self.linearInterpolator(*keyVector) if np.isnan(linearResult).any(): # Occurs if the requested values are outside of the bounds of the table being interpolated over # In that case just return the nearest result print("WARNING: Interpolation requested outside of bounds in table: {}. Current key vector = {}. Extrapolation not supported, returning nearest result instead".format(self.tablePath, keyVector)) return self.nearestInterpolator(*keyVector) else: return linearResult