#!/usr/bin/env python
"""Module for various mapping functions."""
import numpy as np
[docs]
def scale01ToDom(xx, dom):
"""Scaling an array to a given domain, assuming \
the inputs are in [0,1]^d.
Args:
xx (np.ndarray): Nxd input array.
dom (np.ndarray): dx2 domain.
Returns:
np.ndarray: Nxd scaled array.
Note:
If input is outside [0,1]^d, a warning is given, but the scaling will happen nevertheless.
"""
if np.any(xx<0.0) or np.any(xx>1.0):
print("Warning: some elements are outside the [0,1] range.")
return xx*np.abs(dom[:,1]-dom[:,0])+np.min(dom, axis=1)
[docs]
def scaleDomTo01(xx, dom):
"""Scaling an array from a given domain to [0,1]^d.
Args:
xx (np.ndarray): Nxd input array.
dom (np.ndarray): dx2 domain.
Returns:
np.ndarray: Nxd scaled array.
Note:
If input is outside domain, a warning is given, but the scaling will happen nevertheless.
"""
xxsc = (xx-np.min(dom, axis=1)) / np.abs(dom[:,1]-dom[:,0])
if np.any(xxsc<0.0) or np.any(xxsc>1.0):
print("Warning: some elements are outside the [0,1] range.")
return xxsc
[docs]
def scaleTo01(xx):
"""Scale an array to [0,1], using dimension-wise min and max.
Args:
xx (np.ndarray): Initial 2d array
Returns:
np.ndarray: Scaled array.
"""
return (xx - np.min(xx, axis=0)) / (np.max(xx, axis=0) - np.min(xx, axis=0))
[docs]
def standardize(xx):
"""Normalize an array, i.e. map it to zero mean and unit variance.
Args:
xx (np.ndarray): Initial 2d array
Returns:
np.ndarray: Normalized array.
"""
return (xx - np.mean(xx)) / np.std(xx)
[docs]
class XMap():
"""Base class for a map."""
[docs]
def __init__(self):
"""Initialization."""
...
def __call__(self, x):
raise NotImplementedError("Base XMap call is not implemented")
[docs]
def forw(self, x):
"""Forward map.
Args:
x (np.ndarray): 2d numpy input array.
Returns:
np.ndarray: 2d numpy output array.
"""
return self.__call__(x)
[docs]
def inv(self, xs):
"""Inverse of the map.
Args:
xs (np.ndarray): 2d numpy array.
Returns:
np.ndarray: if implemented, 2d numpy array.
"""
raise NotImplementedError("Base XMap inverse is not implemented")
[docs]
class Expon(XMap):
"""Exponential map."""
def __init__(self):
super().__init__()
def __call__(self, x):
return np.exp(x)
[docs]
def inv(self, xs):
return np.log(xs)
[docs]
class Logar(XMap):
"""Logarithmic map."""
def __init__(self):
super().__init__()
def __call__(self, x):
return np.log(x)
[docs]
def inv(self, xs):
return np.exp(xs)
[docs]
class ComposeMap(XMap):
"""Composition of two maps."""
[docs]
def __init__(self, map1, map2):
"""Initialize with the two maps to be composed.
Args:
map1 (XMap): Inner map
map2 (XMap): Outer map
"""
super().__init__()
self.map1 = map1
self.map2 = map2
def __repr__(self):
return f"ComposeMap({self.map1=}, {self.map2=}"
def __call__(self, x):
return self.map2(self.map1(x))
[docs]
def inv(self, xs):
return self.map1.inv(self.map2.inv(xs))
[docs]
class LinearScaler(XMap):
"""Linear scaler map."""
[docs]
def __init__(self, shift=None, scale=None):
"""Initialize with shift and scale.
Args:
shift (np.ndarray, optional): Shift array, broadcast-friendly
scale (np.ndarray, optional): Scale array, broadcast-friendly
"""
super().__init__()
self.shift = shift
self.scale = scale
return
def __repr__(self):
return f"Scaler({self.shift=}, {self.scale=}"
def __call__(self, x):
if self.shift is None:
xs = x - 0.0
else:
xs = x - self.shift
if self.scale is None:
xs /= 1.0
else:
xs /= self.scale
return xs
[docs]
def inv(self, xs):
if self.scale is None:
x = xs * 1.0
else:
x = xs * self.scale#.reshape(1,-1)
if self.shift is None:
x += 0.0
else:
x += self.shift
return x
[docs]
class Standardizer(LinearScaler):
"""Standardizer map, linearly scaling data to zero mean and unit variance."""
[docs]
def __init__(self, x):
"""Initialize with a given 2d array.
Args:
x (np.ndarray): Data according to which the standardization happens.
Note:
This also can be accomplished by function `normalize`
"""
super().__init__(shift=np.mean(x, axis=0), scale=np.std(x, axis=0))
return
[docs]
class Normalizer(LinearScaler):
"""Normalizer map, linearly scaling data to [0,1]."""
[docs]
def __init__(self, x, nugget=0.0):
"""Initialize with a given 2d array and a nugget to keep slightly above zero.
Args:
x (np.ndarray): Data according to which the normalization happens.
nugget (float, optional): Small value to keep data above zero if needed.
Note:
When nugget is 0, this also can be accomplished by function `scaleTo01`
"""
super().__init__(shift=np.min(x, axis=0)-nugget,
scale=np.max(x, axis=0)-np.min(x, axis=0))
return
[docs]
class Domainizer(LinearScaler):
"""Domainizer map, linearly scaling data (assumed to be in [0,1]) to a given domain.
Note:
This also can be accomplished by functions `scaleDomTo01` and its inverse `scale01ToDom`.
"""
[docs]
def __init__(self, dom):
"""Initialize with a given domain.
Args:
dom (np.ndarray): Domain of size `(d,2)` according to which the normalization happens.
"""
super().__init__(shift=dom[:,0], scale=dom[:,1]-dom[:,0])
return
[docs]
class Affine(XMap):
"""Affine map."""
[docs]
def __init__(self, weight=None, bias=None):
"""Initializes with weight and bias arrays.
Args:
weight (np.ndarray, optional): 2d array
bias (np.ndarray, optional): 1d array
"""
super().__init__()
self.weight = weight
self.bias = bias
return
def __repr__(self):
return f"Scaler({self.weight=}, {self.bias=}"
def __call__(self, x):
if self.weight is None:
xs = x * 1.0
else:
xs = x @ self.W.T
if self.bias is None:
xs += 0.0
else:
xs += self.bias
return xs
[docs]
def inv(self, xs):
if self.bias is None:
x = xs - 0.0
else:
x = xs - self.bias
if self.weight is None:
x *= 1.0
else:
x = x @ np.linalg.inv(self.W.T)
return x