Source code for CADETProcess.transform
"""
=========================================
Transform (:mod:`CADETProcess.transform`)
=========================================
.. currentmodule:: CADETProcess.transform
This module provides functionality for transforming data.
.. autosummary::
:toctree: generated/
TransformBase
NoTransform
NormLinearTransform
NormLogTransform
AutoTransform
"""
from abc import ABC, abstractmethod, abstractproperty
import numpy as np
import matplotlib.pyplot as plt
from CADETProcess import plotting
[docs]
class TransformBase(ABC):
"""
Base class for parameter transformation.
This class provides an interface for transforming an input parameter space to some
output parameter space.
Attributes
----------
lb_input : {float, array-like}
Lower bounds of the input parameter space.
ub_input : {float, array-like}
Upper bounds of the input parameter space.
lb : {float, array-like}
Lower bounds of the output parameter space.
ub : {float, array-like}
Upper bounds of the output parameter space.
allow_extended_input : bool
If True, the input value may exceed the lower/upper bounds.
Else, an exception is thrown.
allow_extended_output : bool
If True, the output value may exceed the lower/upper bounds.
Else, an exception is thrown.
Raises
------
ValueError
If lb_input and ub_input have different shapes.
Notes
-----
- This is an abstract base class and cannot be instantiated directly.
- The `transform` method must be implemented by a subclass.
Examples
--------
>>> class MyTransform(TransformBase):
... def transform(self, x):
... return x ** 2
...
>>> t = MyTransform(lb_input=0, ub_input=10, lb=-100, ub=100)
>>> t.transform(3)
9
"""
def __init__(
self,
lb_input=-np.inf, ub_input=np.inf,
allow_extended_input=False, allow_extended_output=False):
"""Initialize TransformBase
Parameters
----------
lb_input : {float, array-like}, optional
Lower bounds of the input parameter space. The default is -inf.
ub_input : {float, array-like}, optional
Upper bounds of the input parameter space. The default is inf.
allow_extended_input : bool, optional
If True, the input value may exceed the lower/upper bounds.
Else, an exception is thrown.
The default is False.
allow_extended_output : bool, optional
If True, the output value may exceed the lower/upper bounds.
Else, an exception is thrown.
The default is False.
"""
self.lb_input = lb_input
self.ub_input = ub_input
self.allow_extended_input = allow_extended_input
self.allow_extended_output = allow_extended_output
@abstractproperty
def is_linear(self):
"""bool: True if transformation is linear."""
pass
@property
def lb_input(self):
"""{float, array-like}: The lower bounds of the input parameter space."""
return self._lb_input
@lb_input.setter
def lb_input(self, lb_input):
self._lb_input = lb_input
@property
def ub_input(self):
"""{float, array-like}: The upper bounds of the input parameter space."""
return self._ub_input
@ub_input.setter
def ub_input(self, ub_input):
self._ub_input = ub_input
@abstractproperty
def lb(self):
"""{float, array-like}: The lower bounds of the output parameter space.
Must be implemented in the child class.
"""
pass
@abstractproperty
def ub(self):
"""{float, array-like}: The upper bounds of the output parameter space.
Must be implemented in the child class.
"""
pass
[docs]
def transform(self, x):
"""Transform the input parameter space to the output parameter space.
Applies the transformation function _transform to x after performing input
bounds checking. If the transformed value exceeds the output bounds, an error
is raised.
Parameters
----------
x : {float, array}
Input parameter values.
Returns
-------
{float, array}
Transformed parameter values.
"""
if (
not self.allow_extended_input and
not np.all((self.lb_input <= x) * (x <= self.ub_input))):
raise ValueError("Value exceeds input bounds.")
x = self._transform(x)
if (
not self.allow_extended_output and
not np.all((self.lb <= x) * (x <= self.ub))):
raise ValueError("Value exceeds output bounds.")
return x
@abstractmethod
def _transform(self, x):
"""Transform the input parameter space to the output parameter space.
Must be implemented in the child class.
Parameters
----------
x : {float, array}
Input parameter values.
Returns
-------
{float, array}
Transformed parameter values.
"""
pass
[docs]
def untransform(self, x):
"""Transform the output parameter space to the input parameter space.
Applies the transformation function _untransform to x after performing output
bounds checking. If the transformed value exceeds the input bounds, an error
is raised.
Parameters
----------
x : {float, array}
Output parameter values.
Returns
-------
{float, array}
Transformed parameter values.
"""
if (
not self.allow_extended_output and
not np.all((self.lb <= x) * (x <= self.ub))):
raise ValueError("Value exceeds output bounds.")
x = self._untransform(x)
if (
not self.allow_extended_input and
not np.all((self.lb_input <= x) * (x <= self.ub_input))):
raise ValueError("Value exceeds input bounds.")
return x
@abstractmethod
def _untransform(self, x):
"""Transform the output parameter space to the input parameter space.
Must be implemented in the child class.
Parameters
----------
x : {float, array}
Output parameter values.
Returns
-------
{float, array}
Transformed parameter values.
"""
pass
[docs]
@plotting.create_and_save_figure
def plot(self, ax, use_log_scale=False):
"""
Plot the transformed space against the input space.
Parameters
----------
ax : matplotlib.axes.Axes
The axes object to plot on.
use_log_scale : bool, optional
If True, use a logarithmic scale for the x-axis.
"""
allow_extended_input = self.allow_extended_input
self.allow_extended_input = True
y = np.linspace(self.lb, self.ub)
x = self.untransform(y)
ax.plot(x, y)
ax.set_xlabel('Input Space')
ax.set_ylabel('Transformed Space')
if use_log_scale:
ax.set_xscale('log')
self.allow_extended_input = allow_extended_input
def __str__(self):
"""Return the class name as a string."""
return self.__class__.__name__
[docs]
class NoTransform(TransformBase):
"""A class that implements no transformation.
Returns the input values without any transformation.
See Also
--------
TransformBase : The base class for parameter transformation.
"""
@property
def is_linear(self):
return True
@property
def lb(self):
"""{float, array-like}: The lower bounds of the output parameter space."""
return self.lb_input
@property
def ub(self):
"""{float, array-like}: The upper bounds of the output parameter space."""
return self.ub_input
def _transform(self, x):
"""Transform the input value to output value.
Parameters
----------
x : {float, array-like}
The input value(s) to be transformed.
Returns
-------
{float, array-like}
The transformed output value(s).
"""
return x
def _untransform(self, x):
"""Untransform the output value to input value.
Parameters
----------
x : {float, array-like}
The output value(s) to be untransformed.
Returns
-------
{float, array-like}
The untransformed input value(s).
"""
return x
[docs]
class NormLinearTransform(TransformBase):
"""A class that implements a normalized linear transformation.
Transforms the input value to the range [0, 1] by normalizing it using
the lower and upper bounds of the input parameter space.
See Also
--------
TransformBase : The base class for parameter transformation.
"""
@property
def is_linear(self):
return True
@property
def lb(self):
"""{float, array-like}: The lower bounds of the output parameter space."""
return 0
@property
def ub(self):
"""{float, array-like}: The upper bounds of the output parameter space."""
return 1
def _transform(self, x):
"""Transform the input value to output value.
Parameters
----------
x : {float, array-like}
The input value(s) to be transformed.
Returns
-------
{float, array-like}
The transformed output value(s).
"""
return (x - self.lb_input) / (self.ub_input - self.lb_input)
def _untransform(self, x):
"""Untransform the output value to input value.
Parameters
----------
x : {float, array-like}
The output value(s) to be untransformed.
Returns
-------
{float, array-like}
The untransformed input value(s).
"""
return (self.ub_input - self.lb_input) * x + self.lb_input
[docs]
class NormLogTransform(TransformBase):
"""A class that implements a normalized logarithmic transformation.
Transforms the input value to the range [0, 1] using a logarithmic
transformation with the lower and upper bounds of the input parameter space.
See Also
--------
TransformBase : The base class for parameter transformation.
"""
@property
def is_linear(self):
return False
@property
def lb(self):
"""{float, array-like}: The lower bounds of the output parameter space."""
return 0
@property
def ub(self):
"""{float, array-like}: The upper bounds of the output parameter space."""
return 1
def _transform(self, x):
"""Transform the input value to output value.
Parameters
----------
x : {float, array-like}
The input value(s) to be transformed.
Returns
-------
{float, array-like}
The transformed output value(s).
"""
if self.lb_input <= 0:
x_ = x + (abs(self.lb_input) + 1)
ub = 1 + (self.ub_input - self.lb_input)
return np.log(x_) / (np.log(ub))
else:
return \
np.log(x/self.lb_input) / np.log(self.ub_input/self.lb_input)
def _untransform(self, x):
"""Transform the input value to output value.
Parameters
----------
x : {float, array-like}
The input value(s) to be transformed.
Returns
-------
{float, array-like}
The transformed output value(s).
"""
if self.lb_input <= 0:
return \
np.exp(x * np.log(self.ub_input - self.lb_input + 1)) \
+ self.lb_input - 1
else:
return \
self.lb_input * np.exp(x * np.log(self.ub_input/self.lb_input))
[docs]
class AutoTransform(TransformBase):
"""A class that implements an automatic parameter transformation.
Transforms the input value to the range [0, 1] using either
the :class:`NormLinearTransform` or the :class:`NormLogTransform`
based on the input parameter space.
Attributes
----------
linear : :class:`NormLinearTransform`
Instance of the linear normalization transform.
log : :class:`NormLogTransform`
Instance of the logarithmic normalization transform.
See Also
--------
TransformBase
NormLinearTransform
NormLogTransform
"""
def __init__(self, *args, threshold=1000, **kwargs):
"""Initialize an AutoTransform object.
Parameters
----------
*args : tuple
Arguments for the :class:`TransformBase` class.
threshold : int, optional
The maximum threshold to switch from linear to logarithmic
transformation. The default is 1000.
**kwargs : dict
Keyword arguments for the :class:`TransformBase` class.
"""
self.linear = NormLinearTransform()
self.log = NormLogTransform()
super().__init__(*args, **kwargs)
self.threshold = threshold
self.linear.allow_extended_input = self.allow_extended_input
self.linear.allow_extended_output = self.allow_extended_input
self.log.allow_extended_input = self.allow_extended_input
self.log.allow_extended_output = self.allow_extended_input
@property
def is_linear(self):
return self.use_linear
@property
def use_linear(self):
"""bool: Indicates whether linear transformation is used."""
if self.lb_input <= 0:
return \
np.log10(self.ub_input - self.lb_input) \
<= np.log10(self.threshold)
else:
return self.ub_input/self.lb_input <= self.threshold
@property
def use_log(self):
"""bool: Indicates whether logarithmic transformation is used."""
return not self.use_linear
@property
def lb(self):
"""{float, array-like}: The lower bounds of the output parameter space."""
return 0
@property
def ub(self):
"""{float, array-like}: The upper bounds of the output parameter space."""
return 1
@property
def lb_input(self):
"""{float, array-like}: The lower bounds of the input parameter space."""
return self._lb_input
@lb_input.setter
def lb_input(self, lb_input):
self.linear.lb_input = lb_input
self.log.lb_input = lb_input
self._lb_input = lb_input
@property
def ub_input(self):
"""{float, array-like}: The upper bounds of the input parameter space."""
return self._ub_input
@ub_input.setter
def ub_input(self, ub_input):
self.linear.ub_input = ub_input
self.log.ub_input = ub_input
self._ub_input = ub_input
def _transform(self, x):
"""Transform the input value to output value.
Parameters
----------
x : {float, array-like}
The input value(s) to be transformed.
Returns
-------
{float, array-like}
The transformed output value(s).
"""
if self.use_log:
return self.log.transform(x)
else:
return self.linear.transform(x)
def _untransform(self, x):
"""Untransforms the output value to input value.
Parameters
----------
x : {float, array-like}
The output value(s) to be transformed.
Returns
-------
{float, array-like}
The untransformed output value(s).
"""
if self.use_log:
return self.log.untransform(x)
else:
return self.linear.untransform(x)