from abc import abstractmethod
from collections.abc import Sequence
import numpy as np
from .base import Base, Property
from .types.array import StateVectors
from .types.state import State
[docs]
class Kernel(Base):
"""Kernel base type
A Kernel provides a means to translate state space or measurement space into kernel space.
"""
@abstractmethod
def __call__(self, state1, state2=None, **kwargs):
r"""
Compute the kernel state of a pair of :class:`~.State` objects
Parameters
----------
state1 : :class:`~.State`
state2 : :class:`~.State`
Returns
-------
StateVectors
kernel state of a pair of input :class:`~.State` objects
"""
raise NotImplementedError
def update_parameters(self, kwargs):
for parameter in type(self).properties:
if parameter in kwargs.keys():
setattr(self, parameter, kwargs[parameter])
@property
def parameters(self):
return {parameter: getattr(self, parameter) for parameter in type(self).properties}
@staticmethod
def _get_state_vectors(state1, state2):
if isinstance(state1, State):
state_vector1 = state1.state_vector
else:
state_vector1 = state1
if state2 is None:
state_vector2 = state_vector1
else:
if isinstance(state2, State):
state_vector2 = state2.state_vector
else:
state_vector2 = state2
return state_vector1, state_vector2
[docs]
class AdditiveKernel(Kernel):
"""Additive kernel
Elementwise addition of corresponding kernel state values. Similar to an OR operation.
"""
kernel_list: Sequence[Kernel] = Property(doc="List of kernels")
def __call__(self, state1, state2=None, **kwargs):
return np.sum([kernel(state1, state2, **kwargs) for kernel in self.kernel_list], axis=0)
[docs]
class MultiplicativeKernel(Kernel):
"""Multiplicative kernel
Elementwise multiplication of corresponding kernel state values. Similar to an AND operation.
"""
kernel_list: Sequence[Kernel] = Property(doc="List of kernels")
def __call__(self, state1, state2=None, **kwargs):
return np.prod([kernel(state1, state2, **kwargs) for kernel in self.kernel_list], axis=0)
[docs]
class PolynomialKernel(Kernel):
r"""Polynomial Kernel
This kernel returns the polynomial kernel of order :math:`p` state from a pair of
:class:`.State` objects.
The polynomial kernel of state vectors :math:`\mathbf{x}` and :math:`\mathbf{x}^\prime` is
defined as:
.. math::
\mathtt{k}(\mathbf{x}, \mathbf{x}^\prime) =
\left(\alpha \left\langle \mathbf{x}, \mathbf{x}^\prime \right\rangle + c \right) ^ p
"""
power: int = Property(doc="The polynomial power :math:`p`.")
c: float = Property(default=1,
doc="Free parameter trading off the influence of higher-order versus "
"lower-order terms in the polynomial. Default is 1.")
ialpha: float = Property(default=1e1, doc="Slope. Range is [1e0, 1e4]. Default is 1e1.")
def __call__(self, state1, state2=None, **kwargs):
self.update_parameters(kwargs)
state_vector1, state_vector2 = self._get_state_vectors(state1, state2)
return (state_vector1.T @ state_vector2 / self.ialpha + self.c) ** self.power
[docs]
class LinearKernel(PolynomialKernel):
r"""Linear Kernel
This kernel returns the linear kernel state vector from a pair of :class:`~.State`
objects.
The linear kernel of state vectors :math:`\mathbf{x}` and :math:`\mathbf{x}^\prime` is
defined as:
.. math::
\mathtt{k}\left(\mathbf{x}, \mathbf{x}^\prime\right) =
\mathbf{x}^T\mathbf{x}^\prime
The linear kernel can capture the first-order moments of a distribution, such as the mean
and covariance.
"""
@property
def power(self):
r"""The linear polynomial power, :math:`p=1`"""
return 1
@property
def c(self):
return 0
@property
def ialpha(self):
return 1
[docs]
class QuadraticKernel(PolynomialKernel):
r"""Quadratic Kernel type
This kernel returns the quadratic kernel state vector from a pair of :class:`~.State`
objects.
The quadratic kernel of state vectors :math:`\mathbf{x}` and :math:`\mathbf{x}^\prime` is
defined as:
.. math::
\mathtt{k}\left(\mathbf{x}, \mathbf{x}^\prime\right) =
\left(\alpha \langle \mathbf{x}, \mathbf{x}^\prime \rangle + c\right)^2
The quadratic kernel can capture the second-order moments of a distribution, such as the
covariance and correlations between pairs of variables.
The quadratic kernel is appropriate when the data is nonlinear but still relatively simple.
"""
@property
def power(self):
r"""The quadratic polynomial power, :math:`p=2`"""
return 2
[docs]
class QuarticKernel(PolynomialKernel):
r"""Quartic Kernel
This kernel returns the quartic kernel state from a pair of :class:`~.State` objects.
The quartic kernel of state vectors :math:`\mathbf{x}` and :math:`\mathbf{x}^\prime` is defined
as:
.. math::
\mathtt{k}(\mathbf{x}, \mathbf{x}^\prime) =
\left(\alpha \langle \mathbf{x}, \mathbf{x}^\prime \rangle + c\right)^4
The quartic kernel can capture higher-order moments beyond the mean and covariance, such as
skewness and kurtosis.
THe quartic kernel can be used when the data is highly nonlinear and complex.
"""
@property
def power(self):
r"""The quartic polynomial power, :math:`p=4`"""
return 4
[docs]
class GaussianKernel(Kernel):
r"""Gaussian Kernel
This kernel returns the Gaussian kernel state vector from a pair of
:class:`~.State` objects.
The Gaussian kernel of state vectors :math:`\mathbf{x}` and
:math:`\mathbf{x}^\prime` is defined as:
.. math::
\mathtt{k}(\mathbf{x}, \mathbf{x}^\prime) =
\mathrm{exp}\left(-\frac{||\mathbf{x} - \mathbf{x}^\prime||^{2}}{2\pi\sigma^2}\right)
"""
variance: float = Property(
default=1e1,
doc=r"Denoted as :math:`\sigma^2` in the equation above. Determines the width of the "
r"Gaussian kernel. Range is [1e0, 1e2].")
def __call__(self, state1, state2=None, **kwargs):
r"""Calculate the Gaussian Kernel transformation for a pair of :class:`~.State` objects.
Parameters
----------
state1 : :class:`~.State`
state2 : :class:`~.State`
Returns
-------
StateVectors
Transformed state vector in kernel space.
"""
self.update_parameters(kwargs)
state_vector1, state_vector2 = self._get_state_vectors(state1, state2)
diff_tilde_x = (state_vector1[:, :, None] - state_vector2[:, None, :]) ** 2
diff_tilde_x_sum = np.sum(diff_tilde_x, axis=0)
k_tilde_x = np.exp(-diff_tilde_x_sum/(2*self.variance)) / np.sqrt(2*np.pi*self.variance)
return StateVectors(k_tilde_x)
class _StateKernel(Kernel):
kernel: Kernel = Property(doc="Base Kernel class")
mapping: list = Property(default=None, doc="List of mappings of the components to be used in "
"the kernel from the state vector.")
@property
def parameters(self):
return self.kernel.parameters
def update_parameters(self, kwargs):
return self.kernel.update_parameters(kwargs)
def _get_states(self, state1, state2):
if state2 is None:
state2 = state1
if self.mapping is None:
self.mapping = list(range(state1[0].state_vector.shape[0]))
state1 = np.hstack([state.state_vector[self.mapping] for state in state1])
state2 = np.hstack([state.state_vector[self.mapping] for state in state2])
return state1, state2
def __call__(self, state1, state2=None, **kwargs):
r"""
Compute the kernel state of a pair of objects containing States
Parameters
----------
state1 : :class:`~.Track`
state2 : :class:`~.Track`
Returns
-------
StateVectors
kernel state of a pair of input :class:`~.State` objects
"""
state1, state2 = self._get_states(state1, state2)
return self.kernel.__call__(state1, state2, **kwargs)
[docs]
class TrackKernel(_StateKernel):
def __call__(self, state1, state2=None, **kwargs):
r"""
Compute the kernel state of a pair of :class:`~.Track` objects
Parameters
----------
state1 : :class:`~.Track`
state2 : :class:`~.Track`
Returns
-------
StateVectors
kernel state of a pair of input :class:`~.State` objects
"""
state1, state2 = self._get_states(state1, state2)
return self.kernel.__call__(state1, state2, **kwargs)
[docs]
class MeasurementKernel(_StateKernel):
def __call__(self, state1, state2=None, **kwargs):
r"""
Compute the kernel state of a pair of lists of measurements as :class:`~.List` objects
Parameters
----------
state1 : :class:`~.List`
state2 : :class:`~.List`
Returns
-------
StateVectors
kernel state of a pair of input :class:`~.State` objects
"""
state1, state2 = self._get_states(state1, state2)
return self.kernel.__call__(state1, state2, **kwargs)