Source code for CADETProcess.transform

"""
=========================================
Transform (:mod:`CADETProcess.transform`)
=========================================

.. currentmodule:: CADETProcess.transform

This module provides functionality for transforming data.


.. autosummary::
    :toctree: generated/

    TransformerBase
    NullTransformer
    NormLinearTransformer
    NormLogTransformer
    AutoTransformer

"""  # noqa

from abc import ABC, abstractmethod
from typing import Any, Optional

import matplotlib.pyplot as plt
import numpy as np

from CADETProcess import plotting
from CADETProcess.numerics import round_to_significant_digits


[docs] class TransformerBase(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 or np.ndarray Lower bounds of the input parameter space. ub_input : float or np.ndarray Upper bounds of the input parameter space. lb : float or np.ndarray Lower bounds of the output parameter space. ub : float or np.ndarray 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 MyTransformer(TransformerBase): ... def transform(self, x: float) -> float: ... return x ** 2 ... >>> t = MyTransformer(lb_input=0, ub_input=10, lb=-100, ub=100) >>> t.transform(3) 9 """ def __init__( self, lb_input: float | np.ndarray = -np.inf, ub_input: float | np.ndarray = np.inf, allow_extended_input: Optional[bool] = False, allow_extended_output: Optional[bool] = False, ) -> None: """ Initialize TransformerBase. Parameters ---------- lb_input : float or np.ndarray, optional Lower bounds of the input parameter space. Default is -inf. ub_input : float or np.ndarray, optional Upper bounds of the input parameter space. Default is inf. allow_extended_input : bool, optional If True, the input value may exceed the lower/upper bounds. Else, an exception is thrown. Default is False. allow_extended_output : bool, optional If True, the output value may exceed the lower/upper bounds. Else, an exception is thrown. 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 @property @abstractmethod def is_linear(self) -> bool: """Return whether the transformation is linear.""" pass @property def lb_input(self) -> float | np.ndarray: """Return the lower bounds of the input parameter space.""" return self._lb_input @lb_input.setter def lb_input(self, lb_input: float | np.ndarray) -> None: self._lb_input = lb_input @property def ub_input(self) -> float | np.ndarray: """Return the upper bounds of the input parameter space.""" return self._ub_input @ub_input.setter def ub_input(self, ub_input: float | np.ndarray) -> None: self._ub_input = ub_input @property @abstractmethod def lb(self) -> float | np.ndarray: """Return the lower bounds of the output parameter space.""" pass @property @abstractmethod def ub(self) -> float | np.ndarray: """Return the upper bounds of the output parameter space.""" pass
[docs] def transform(self, x: float | np.ndarray) -> float | np.ndarray: """ 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 or np.ndarray Input parameter values. Returns ------- float or np.ndarray Transformed parameter values. Raises ------ ValueError If `x` exceeds input or output bounds and `allow_extended_*` is False. """ 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: float | np.ndarray) -> float | np.ndarray: """ Apply the transformation from input to output parameter space. Must be implemented in the child class. Parameters ---------- x : float or np.ndarray Input parameter values. Returns ------- float or np.ndarray Transformed parameter values. """ pass
[docs] def untransform( self, x: float | np.ndarray, significant_digits: Optional[int] = None, ) -> float | np.ndarray: """ Transform the output parameter space back to the input parameter space. Parameters ---------- x : float or np.ndarray Output parameter values. significant_digits : int, optional float | np.ndarray of significant figures to which variable can be rounded. If None, variable is not rounded. Returns ------- float or np.ndarray Transformed parameter values. """ x_ = round_to_significant_digits(x, digits=significant_digits) 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_) x_ = round_to_significant_digits(x_, digits=significant_digits) 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: float | np.ndarray) -> float | np.ndarray: """ Apply the inverse transformation from output to input parameter space. Must be implemented in the child class. Parameters ---------- x : float or np.ndarray Output parameter values. Returns ------- float or np.ndarray Transformed parameter values. """ pass
[docs] @plotting.figure_utils def plot( self, use_log_scale: bool = False, ax: Optional[plt.Axes] = None, setup_figure_kwargs: Optional[dict] = None, ) -> tuple[plt.Figure, plt.Axes]: """ Plot the transformed space against the input space. Parameters ---------- use_log_scale : bool, optional If True, use a logarithmic scale for the x-axis. ax : Optional[plt.Axes], default=None Optional Matplotlib Axes. If not provided, a new figure is created. setup_figure_kwargs : Optional[dict], default=None Additional options to setup the figure. Returns ------- tuple[plt.Figure, plt.Axes] The Matplotlib Figure and Axes. """ allow_extended_input = self.allow_extended_input self.allow_extended_input = True y = np.linspace(self.lb, self.ub) x = self.untransform(y) if ax is None: fig, ax = plotting.setup_figure(**setup_figure_kwargs) ax.set_xlabel("Input Space") ax.set_ylabel("Transformed Space") if use_log_scale: ax.set_xscale("log") else: fig = ax.get_figure() ax.plot(x, y) self.allow_extended_input = allow_extended_input return fig, ax
def __str__(self) -> str: """Return the class name as a string.""" return self.__class__.__name__
[docs] class NullTransformer(TransformerBase): """ A transformer that performs no transformation. This class simply returns the input values as output without modification. See Also -------- TransformerBase : The base class for parameter transformation. """ @property def is_linear(self) -> bool: """Return True, as this is a linear transformation.""" return True @property def lb(self) -> float | np.ndarray: """Return the lower bound of the output space (same as input lower bound).""" return self.lb_input @property def ub(self) -> float | np.ndarray: """Return the upper bound of the output space (same as input upper bound).""" return self.ub_input def _transform(self, x: float | np.ndarray) -> float | np.ndarray: """ Return the input value(s) as output without modification. Parameters ---------- x : float or np.ndarray The input value(s) to be transformed. Returns ------- float or np.ndarray The transformed output value(s) (same as input). """ return x def _untransform(self, x: float | np.ndarray) -> float | np.ndarray: """ Return the output value(s) as input without modification. Parameters ---------- x : float or np.ndarray The output value(s) to be untransformed. Returns ------- float or np.ndarray The untransformed input value(s) (same as output). """ return x
[docs] class NormLinearTransformer(TransformerBase): """ A transformer that normalizes values linearly to the range [0, 1]. This transformation scales the input value between the given lower and upper bounds into a normalized range of [0,1]. See Also -------- TransformerBase : The base class for parameter transformation. """ @property def is_linear(self) -> bool: """Return True, as this is a linear transformation.""" return True @property def lb(self) -> float: """Return the lower bound of the output space (0).""" return 0.0 @property def ub(self) -> float: """Return the upper bound of the output space (1).""" return 1.0 def _transform(self, x: float | np.ndarray) -> float | np.ndarray: """ Normalize input values to the range [0,1]. Parameters ---------- x : float or np.ndarray The input value(s) to be transformed. Returns ------- float or np.ndarray The transformed output value(s) in the range [0,1]. """ return (x - self.lb_input) / (self.ub_input - self.lb_input) def _untransform(self, x: float | np.ndarray) -> float | np.ndarray: """ Denormalize output values back to the original range. Parameters ---------- x : float or np.ndarray The output value(s) in the normalized range [0,1]. Returns ------- float or np.ndarray The untransformed input value(s) in the original range. """ return (self.ub_input - self.lb_input) * x + self.lb_input
[docs] class NormLogTransformer(TransformerBase): """ A transformer that normalizes values logarithmically to the range [0, 1]. This transformation scales input values logarithmically between the given lower and upper bounds into a normalized range of [0,1]. See Also -------- TransformerBase : The base class for parameter transformation. """ @property def is_linear(self) -> bool: """Return False, as this is a non-linear transformation.""" return False @property def lb(self) -> float: """Return the lower bound of the output space (0).""" return 0.0 @property def ub(self) -> float: """Return the upper bound of the output space (1).""" return 1.0 def _transform(self, x: float | np.ndarray) -> float | np.ndarray: """ Normalize input values to the range [0,1] using a logarithmic transformation. Parameters ---------- x : float or np.ndarray The input value(s) to be transformed. Returns ------- float or np.ndarray The transformed output value(s) in the range [0,1]. Raises ------ ValueError If `lb_input` is non-positive, the transformation shifts all values accordingly. """ 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: float | np.ndarray) -> float | np.ndarray: """ Denormalize output values back to the original range using logarithmic inverse. Parameters ---------- x : float or np.ndarray The output value(s) in the normalized range [0,1]. Returns ------- float or np.ndarray The untransformed input value(s) in the original range. """ 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 AutoTransformer(TransformerBase): """ A transformer that automatically selects between linear and logarithmic transformations. Transforms the input value to the range [0, 1] using either the :class:`NormLinearTransformer` or the :class:`NormLogTransformer` based on the input parameter space. Attributes ---------- linear : NormLinearTransformer Instance of the linear normalization transform. log : NormLogTransformer Instance of the logarithmic normalization transform. threshold : int The maximum threshold to switch from linear to logarithmic transformation. See Also -------- TransformerBase NormLinearTransformer NormLogTransformer """ def __init__(self, *args: Any, threshold: int = 100, **kwargs: Any) -> None: """ Initialize an AutoTransformer object. Parameters ---------- *args : tuple Arguments for the :class:`TransformerBase` class. threshold : int, optional The maximum threshold to switch from linear to logarithmic transformation. The default is 100. **kwargs : dict Keyword arguments for the :class:`TransformerBase` class. """ self.linear = NormLinearTransformer() self.log = NormLogTransformer() super().__init__(*args, **kwargs) self.threshold = threshold self.linear.allow_extended_input = self.allow_extended_input self.linear.allow_extended_output = self.allow_extended_output self.log.allow_extended_input = self.allow_extended_input self.log.allow_extended_output = self.allow_extended_output @property def is_linear(self) -> bool: """Return True if linear transformation is used, otherwise False.""" return self.use_linear @property def use_linear(self) -> bool: """Determine whether linear transformation should be used.""" if self.lb_input <= 0: return np.log10(self.ub_input - self.lb_input) < np.log10(self.threshold) return (self.ub_input / self.lb_input) < self.threshold @property def use_log(self) -> bool: """Return True if logarithmic transformation is used, otherwise False.""" return not self.use_linear @property def lb(self) -> float: """Return the lower bound of the output parameter space (0).""" return 0.0 @property def ub(self) -> float: """Return the upper bound of the output parameter space (1).""" return 1.0 @property def lb_input(self) -> float | np.ndarray: """Return the lower bounds of the input parameter space.""" return self._lb_input @lb_input.setter def lb_input(self, lb_input: float | np.ndarray) -> None: """Set the lower bounds of the input parameter space.""" self.linear.lb_input = lb_input self.log.lb_input = lb_input self._lb_input = lb_input @property def ub_input(self) -> float | np.ndarray: """Return the upper bounds of the input parameter space.""" return self._ub_input @ub_input.setter def ub_input(self, ub_input: float | np.ndarray) -> None: """Set the upper bounds of the input parameter space.""" self.linear.ub_input = ub_input self.log.ub_input = ub_input self._ub_input = ub_input def _transform(self, x: float | np.ndarray) -> float | np.ndarray: """ Transform the input value to an output value in the range [0, 1]. Parameters ---------- x : float or np.ndarray The input value(s) to be transformed. Returns ------- float or np.ndarray The transformed output value(s). """ return self.log._transform(x) if self.use_log else self.linear._transform(x) def _untransform(self, x: float | np.ndarray) -> float | np.ndarray: """ Untransform the output value back to the input parameter space. Parameters ---------- x : float or np.ndarray The output value(s) in the transformed range. Returns ------- float or np.ndarray The untransformed input value(s). """ return self.log._untransform(x) if self.use_log else self.linear._untransform(x)