Source code for vivyd.excitation.excitation
from ..typing import arrf64
from ..core import is_taichi_used, ti_or_fallback as ti, TaichiCompatible
from numpy import ascontiguousarray, asarray
[docs]
@ti.data_oriented
class Excitation(TaichiCompatible):
"""
A class for representing any signal that can be interpolated and used
to affect models.
Parameters
----------
x : arrf64
A 1D array containing the signal values at discrete time points.
dt : float
The time step between the discrete time points corresponding to the
signal values in `x`.
.. Important::
To benefit from Taichi acceleration, ensure that the `Excitation` class is
instantiated within a Taichi context.
"""
def __init__(self, x: arrf64, dt: float):
if x.ndim != 1:
raise ValueError("x must be a 1D array")
if is_taichi_used():
self._x = ti.field(dtype=float, shape=len(x))
self._x.from_numpy(ascontiguousarray(x))
else:
self._x = x
self._dt = dt
self.n = len(x)
@property
def x(self):
return asarray(self._x) # Numpy tries to generate a view, avoiding unnecessary copying when possible.
[docs]
def interpolate(self, t: float) -> float:
"""
Interpolate the signal at a given time point.
Parameters
----------
t : float
A time point at which to interpolate the signal.
Returns
-------
float
The interpolated signal value at the specified time point.
specified time points.
Raises
------
ValueError
If any time point in `t` is outside the range [0, (n-1)*dt], where
`n` is the number of discrete time points in `x`.
"""
if t < 0 or t > (self.n - 1) * self._dt:
raise ValueError("t must be within the range [0, (n-1)*dt]")
if is_taichi_used():
_call = self._call_taichi
else:
_call = self._call_python
return _call(t)
[docs]
def __call__(self, t: float) -> float:
"""Shorthand for :meth:`interpolate`."""
return self.interpolate(t)
def _call_python(self, t: float) -> float:
i = t / self._dt
if i == int(i):
return self.x[int(i)]
else:
i_inf = int(i)
i_sup = i_inf + 1
x_inf = self.x[i_inf]
x_sup = self.x[i_sup]
t_mid = t - i_inf * self._dt
return x_inf + (x_sup - x_inf) * t_mid / self._dt
@ti.func
def _call_taichi_func(
self,
t: float
) -> float:
i_true = t / self._dt
i_inf = int(i_true)
i_sup = i_inf + 1
return ti.select(
ti.abs(i_true - ti.cast(i_inf, ti.f64)) < 1e-12,
self._x[i_inf],
self._x[i_inf] + (self._x[i_sup] - self._x[i_inf]) * (t - i_inf * self._dt) / self._dt
)
@ti.kernel
def _call_taichi_kernel(
self,
t: float
) -> float:
return self._call_taichi_func(t)
def _call_taichi(self, t: float) -> float:
return self._call_taichi_kernel(t)