Source code for aviary.utils.math_utils

"""Smooth functions and their derivatives."""

from math import floor, log10
import numpy as np


[docs] def round_it(x, sig=None): """ Round a float to a specified significance. If the number is equal to zero, "0" will be returned, regardless of the number of significant digits specified If the number is NaN, directly returns it (stays NaN). Parameters ---------- x : str or float the float that needs to be rounded. sig : int the number of significant digits to include (If this is unspecified, the number will be rounded to two decimal places). Returns ------- The rounded number, or provided string if not convertible to float, or original number if it is NaN """ # default sig figs to 2 decimal places out if isinstance(x, str): try: x = float(x) except ValueError: return x if np.isnan(x): # return NaNs directly back to markdown report return x if not sig: sig = len(str(round(x))) + 2 if x != 0: return round(x, sig - int(floor(log10(abs(x)))) - 1) else: return 0
[docs] def deg2rad(d): """Complex step safe deg2rad.""" return d * np.pi / 180.0
[docs] def rad2deg(r): """Complex step safe rad2deg.""" return r * 180.0 / np.pi
[docs] def sigmoidX(x, x0, mu=1.0): """ Sigmoid used to smoothly transition between piecewise functions. Parameters ---------- x: float or array independent variable x0: float the center of symmetry. When x = x0, sigmoidX = 1/2. mu: float steepness parameter. Returns ------- float or array smoothed value from input parameter x. """ if mu == 0: raise ValueError('mu must be non-zero') if isinstance(x, np.ndarray): if np.isrealobj(x): dtype = float else: dtype = complex n_size = x.size y = np.zeros(n_size, dtype=dtype) # avoid overflow in squared term, underflow seems to be ok calc_idx = np.where((x.real - x0) / mu > -320) if isinstance(x0, np.ndarray) and len(x0) == n_size: y[calc_idx] = 1 / (1 + np.exp(-(x[calc_idx] - x0[calc_idx]) / mu)) else: y[calc_idx] = 1 / (1 + np.exp(-(x[calc_idx] - x0) / mu)) else: if isinstance(x, float): dtype = float else: dtype = complex y = 0 if (x - x0) * mu > -320: y = 1 / (1 + np.exp(-(x - x0) / mu)) if isinstance(dtype, float): y = y.real return y
[docs] def dSigmoidXdx(x, x0, mu=1.0): """ Derivative of sigmoid function. Parameters ---------- x: float or array independent variable x0: float the center of symmetry. When x = x0, sigmoidX = 1/2. mu: float steepness parameter. Returns ------- float or array smoothed derivative value from input parameter x. """ if mu == 0: raise ValueError('mu must be non-zero') if isinstance(x, np.ndarray): if np.isrealobj(x): dtype = float else: dtype = complex n_size = x.size y = np.zeros(n_size, dtype=dtype) term = np.zeros(n_size, dtype=dtype) term2 = np.zeros(n_size, dtype=dtype) # avoid overflow in squared term, underflow seems to be ok calc_idx = np.where((x.real - x0) / mu > -320) term[calc_idx] = np.exp(-(x[calc_idx] - x0) / mu) term2[calc_idx] = (1 + term[calc_idx]) * (1 + term[calc_idx]) y[calc_idx] = term[calc_idx] / mu / term2[calc_idx] else: y = 0 if (x - x0) * mu > -320: term = np.exp(-(x - x0) / mu) term2 = (1 + term) * (1 + term) y = term / mu / term2 if isinstance(dtype, float): y = y.real return y
[docs] def smooth_min(x, b, mu=100.0): """ Smooth approximation of the min function using the log-sum-exp trick. Parameters ---------- x (float or array-like): First value. b (float or array-like): Second value. mu (float): The smoothing factor. Higher values make it closer to the true minimum. Try between 75 and 275. Returns ------- float or array-like: The smooth approximation of min(x, b). """ sum_log_exp = np.log(np.exp(np.multiply(-mu, x)) + np.exp(np.multiply(-mu, b))) rv = -(1 / mu) * sum_log_exp return rv
[docs] def d_smooth_min(x, b, mu=100.0): """ Derivative of function smooth_min(x). Parameters ---------- x (float or array-like): First value. b (float or array-like): Second value. mu (float): The smoothing factor. Higher values make it closer to the true minimum. Try between 75 and 275. Returns ------- float or array-like: The smooth approximation of derivative of min(x, b). """ d_sum_log_exp = np.exp(np.multiply(-mu, x)) / ( np.exp(np.multiply(-mu, x)) + np.exp(np.multiply(-mu, b)) ) return d_sum_log_exp
[docs] def smooth_max(x, b, mu=10.0): """ Smooth approximation of the min function using the log-sum-exp trick. Parameters ---------- x (float or array-like): First value. b (float or array-like): Second value. mu (float): The smoothing factor. Higher values make it closer to the true maximum. Try between 75 and 275. Returns ------- float or array-like: The smooth approximation of max(x, b). """ mu_x = mu * x mu_b = mu * b m = np.maximum(mu_x, mu_b) sum_log_exp = (m + np.log(np.exp(mu_x - m) + np.exp(mu_b - m))) / mu return sum_log_exp
[docs] def d_smooth_max(x, b, mu=10.0): """ Derivative of function smooth_min(x). Parameters ---------- x (float or array-like): First value. b (float or array-like): Second value. mu (float): The smoothing factor. Higher values make it closer to the true minimum. Try between 75 and 275. Returns ------- float or array-like: The smooth approximation of derivative of min(x, b). """ mu_x = mu * x mu_b = mu * b m = np.maximum(mu_x, mu_b) numerator = np.exp(mu_x - m) denominator = np.exp(mu_x - m) + np.exp(mu_b - m) d_sum_log_exp = numerator / denominator return d_sum_log_exp
[docs] def sin_int4(val): """Define a smooth, differentialbe approximation to the 'int' function.""" return sin_int(sin_int(sin_int(sin_int(val)))) - 0.5
[docs] def dydx_sin_int4(val): """Define the derivative (dy/dx) of sin_int4, at x = val.""" y0 = sin_int(val) y1 = sin_int(y0) y2 = sin_int(y1) dydx3 = dydx_sin_int(y2) dydx2 = dydx_sin_int(y1) dydx1 = dydx_sin_int(y0) dydx0 = dydx_sin_int(val) dydx = dydx3 * dydx2 * dydx1 * dydx0 return dydx
# 'int' function can be approximated by recursively applying this sin function # which makes a smooth, differentialbe function (is there a good one?)
[docs] def sin_int(val): """ Define one step in approximating the 'int' function with a smooth, differentialbe function. """ int_val = val - np.sin(2 * np.pi * (val + 0.5)) / (2 * np.pi) return int_val
[docs] def dydx_sin_int(val): """Define the derivative (dy/dx) of sin_int, at x = val.""" dydx = 1.0 - np.cos(2 * np.pi * (val + 0.5)) return dydx
[docs] def smooth_int_tanh(x, mu=10.0): """Smooth approximation of int(x) using tanh.""" f = np.floor(x.real) + x.imag * 1j frac = x - f t = np.tanh(mu * (frac - 0.5)) s = 0.5 * (t + 1) y = f + s return y
[docs] def d_smooth_int_tanh(x, mu=10.0): """ Smooth approximation of int(x) using tanh. Returns (y, dy_dx). """ f = np.floor(x.real) + x.imag * 1j frac = x - f t = np.tanh(mu * (frac - 0.5)) dy_dx = 0.5 * mu * (1 - t**2) return dy_dx