Source code for undefined.UDFunction

import sys
# # temp solution for directory.
sys.path.append("./src/")
from undefined.Utils import check_division_by_zero, check_pow
import math
import numpy as np



[docs]class UDFunction: def __init__(self, val, der=1): """ This class is where we overload all the operators, which will be used to calculate the derivatives. Args: val (numeric or numpy ndarray): value of function der (int, optional): derivative of function. Defaults to 1. """ self._val = val self._der = der if hasattr(self._val, 'shape'): self._shape = self._val.shape else: self._shape = 1 self._left_child = None self._right_child = None @property def val(self): """ This is a decorator return rouded input self.val Returns: array: 2 decimal rounded input of self.value """ if isinstance(self._val, float): return round(self._val, 2) elif isinstance(self._val, np.ndarray): return np.round(self._val, 2) else: return self._val @property def der(self): """ This is a decorator return rouded input self.der Returns: array: 3 decimal rounded input of self.der """ if isinstance(self._der, float): return round(self._der, 3) elif isinstance(self._der, np.ndarray): return np.round(self._der, 3) else: return self._der def __str__(self): return f"value: {self.val} \n" + f"derivative: {self.der}" def __add__(self, other): """ This allows to do addition with UDFunction instances or scalar numbers, and calculate the value after taking the derivative. TypeError will raise if none of the self or other are UDFunction instances. Args: other (UDFunction or numeric): object to add with Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): new_val = self._val + other._val new_der = self._der + other._der elif isinstance(other, (int, float, np.ndarray)): new_val = self._val + other new_der = self._der else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __mul__(self, other): """ This allows to do multification with UDFunction instances or scalar numbers, , and calculate the value after taking the derivative. TypeError will raise if none of the self or other are UDFunction instances. Args: other (UDFunction or numeric): object to multiply with Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): new_val = self._val * other._val new_der = self._der * other._val + self._val * other._der elif isinstance(other, (int, float, np.ndarray)): new_val = self._val * other new_der = self._der * other else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __radd__(self, other): """ This is called when int/float or UDFunction instances + an instance of Variable class. Args: other (UDFunction or numeric): object to add with Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): new_val = self._val + other._val new_der = self._der + other._der elif isinstance(other, (int, float, np.ndarray)): new_val = self._val + other new_der = self._der else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __rmul__(self, other): """ This is called when int/float or UDFunction instances * an instance of Variable class. Args: other (UDFunction or numeric): object to multiply with Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): new_val = self._val * other._val new_der = self._der * other._val + self._val * other._der elif isinstance(other, (int, float, np.ndarray)): new_val = self._val * other new_der = self._der * other else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __neg__(self): """ This allows to negate UDFunction instances itself. Returns: UDFunction: object with neg value """ return -1 * self def __sub__(self, other): """ This allows to do subtraction with UDFunction instances or scalar numbers, , and calculate the value after taking the derivative. TypeError will raise if none of the self or other are UDFunction instances. Args: other (UDFunction or numeric): object to subtract with Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): new_val = self._val - other._val new_der = self._der - other._der elif isinstance(other, (int, float, np.ndarray)): new_val = self._val - other new_der = self._der else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __rsub__(self, other): """ This is called when int/float or UDFunction instances - an instance of Variable class. Args: other (UDFunction or numeric): object to subtract with Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): new_val = other._val - self._val new_der = other._der - self._der elif isinstance(other, (int, float, np.ndarray)): new_val = other - self._val new_der = - self._der else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __truediv__(self, other): """ This allows to do true division with UDFunction instances or scalar numbers, , and calculate the value after taking the derivative. TypeError will raise if none of the self or other are UDFunction instances. Args: other (UDFunction or numeric): object to (true) divide with Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): check_division_by_zero(other._val) new_val = self._val / other._val new_der = (self._der * other._val - self._val * other._der) / (other._val * other._val) elif isinstance(other, (int, float, np.ndarray)): check_division_by_zero(other) new_val = self._val / other new_der = self._der / other else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __rtruediv__(self, other): """ This is called when int/float or UDFunction instances / (divide) an instance of Variable class. Args: other (UDFunction or numeric): object to (true) divide with Returns: UDFunction: a new object with new_val and new_der """ check_division_by_zero(self._val) if isinstance(other, UDFunction): new_val = other._val / self._val new_der = (self._val * other._der - self._der * other._val) / (self._val * self._val) elif isinstance(other, (int, float, np.ndarray)): new_val = other / self._val new_der = - 1 * other * self._der / (self._val * self._val) else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __floordiv__(self, other): # self // other """ This allows to do floor division with UDFunction instances or scalar numbers, , and calculate the value after taking the derivative. TypeError will raise if none of the self or other are UDFunction instances. Args: other (UDFunction or numeric): object to (floor) divide with Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): check_division_by_zero(other._val) new_val = self._val // other._val new_der = (self._der * other._val - self._val * other._der) // (other._val * other._val) elif isinstance(other, (int, float, np.ndarray)): check_division_by_zero(other) new_val = self._val // other new_der = self._der // other else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __rfloordiv__(self, other): """ This is called when int/float or UDFunction instances // (floor divide) an instance of Variable class. Args: other (UDFunction or numeric): object to (floor) divide with Returns: UDFunction: a new object with new_val and new_der """ check_division_by_zero(self._val) if isinstance(other, UDFunction): new_val = other._val // self._val new_der = (self._val * other._der - self._der * other._val) // (self._val * self._val) elif isinstance(other, (int, float, np.ndarray)): new_val = other // self._val new_der = - 1 * other * self._der // (self._val * self._val) else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __pow__(self, other): """ This allows to do "to the power" with UDFunction instances or scalar numbers, and calculate the value after taking the derivative. ** operator. Args: other (any): object to take power of. Returns: UDFunction: a new object with new_val and new_der """ if isinstance(other, UDFunction): check_pow(self._val, other._val) if isinstance(self._val, (int, float)): new_val = self._val ** other._val if isinstance(other._val, np.ndarray): new_der_1 = other._val * \ np.power(self._val, other._val - 1) * self._der new_der_2 = math.log(self._val) * new_val * other._der new_der = new_der_1 + new_der_2 else: new_der_1 = other._val * \ self._val ** (other._val - 1) * self._der new_der_2 = math.log(self._val) * new_val * other._der new_der = new_der_1 + new_der_2 else: # self._val is of type ndarray if isinstance(other._val, np.ndarray): if other._val.shape[0] != self._val.shape[0]: raise ValueError( f"error raised by undefined: operands could not be broadcast together with shapes {other._val.shape} {self._val.shape}") else: new_val = self._val ** other._val new_der_1 = other._val * \ np.power(self._val, other._val - 1) * self._der new_der_2 = np.log(self._val) * \ new_val * other._der new_der = new_der_1 + new_der_2 else: new_val = self._val ** other._val new_der_1 = other._val * \ self._val ** (other._val - 1) * self._der new_der_2 = np.log(self._val) * new_val * other._der new_der = new_der_1 + new_der_2 elif isinstance(other, (int, float, np.ndarray)): check_pow(self._val, other) if isinstance(self._val, np.ndarray): new_val = np.power(self._val, other) new_der = other * \ np.power(self._val, other - 1) * self._der elif isinstance(self._val, (int, float)): new_val = self._val ** other new_der = other * self._val**(other - 1) * self._der return UDFunction(new_val, new_der) def __rpow__(self, other): """ This allows to do "to the power" with UDFunction instances or scalar numbers, and calculate the value after taking the derivative. ** operator. TypeError will raise if none of the self or other are UDFunction instances. Args: degree (numeric): object to take power of. Returns: UDFunction: a new object with new_val and new_der """ # other ^ self if isinstance(other, UDFunction): check_pow(other._val, self._val) if isinstance(other._val, (int, float)): new_val = other._val ** self._val new_der_1 = np.log(other._val) * new_val * self._der new_der_2 = self._val * \ other._val ** (self._val - 1) * other._der new_der = new_der_1 + new_der_2 else: if isinstance(self._val, np.ndarray): if other._val.shape[0] != self._val.shape[0]: raise ValueError( f"error raised by undefined: operands could not be broadcast together with shapes {other._val.shape} {self._val.shape}") else: new_val = other._val ** self._val new_der_1 = np.log(other._val) * new_val * self._der new_der_2 = self._val * \ np.power(other._val, (self._val - 1)) * other._der else: new_val = other._val ** self._val new_der_1 = np.log(other._val) * new_val * self._der new_der_2 = self._val * \ other._val ** (self._val - 1) * other._der elif isinstance(other, (int, float, np.ndarray)): check_pow(other, self._val) new_val = other ** self._val new_der = math.log(other) * new_val * self._der else: raise TypeError("error raised by undefined: unsupported attribute type.") return UDFunction(new_val, new_der) def __eq__(self, other): """compare whether the two UDFunction objects have the same values. Return true if equal, and false otherwise. raise TypeError is other is not a UDFunction object. Args: other ([UDFunction]) Returns: True if equal. Otherwise False """ if isinstance(other, UDFunction): return self.val == other.val elif isinstance(other, (int, float)): return self.val == other else: raise TypeError("error raised by undefined: Need a UDFunction object to compare") def __ne__(self, other): """compare whether the two UDFunction objects have different values. raise TypeError is other is not a UDFunction object. Args: other ([UDFunction]) Returns: True if not equal. Otherwise False """ if isinstance(other, UDFunction): return self.val != other.val elif isinstance(other, (int, float)): return self.val != other else: raise TypeError("error raised by undefined: Need a UDFunction object to compare") def __lt__(self, other): """overload the < operator raise TypeError is other is not a UDFunction object. Args: other ([UDFunction]) Returns: True if less than. Otherwise False """ if isinstance(other, UDFunction): return self.val < other.val elif isinstance(other, (int, float)): return self.val < other else: raise TypeError("error raised by undefined: Need a UDFunction object to compare") def __gt__(self, other): """overload the > operator raise TypeError is other is not a UDFunction object. Args: other ([UDFunction]) Returns: True if greater than. Otherwise False """ if isinstance(other, UDFunction): return self.val > other.val elif isinstance(other, (int, float)): return self.val > other else: raise TypeError("error raised by undefined: Need a UDFunction object to compare") def __le__(self, other): """overload the > operator raise TypeError is other is not a UDFunction object. Args: other ([UDFunction]) Returns: True if less than or equal to. Otherwise False """ if isinstance(other, UDFunction): return self.val <= other.val elif isinstance(other, (int, float)): return self.val <= other else: raise TypeError("error raised by undefined: Need a UDFunction object to compare") def __ge__(self, other): """overload the > operator raise TypeError is other is not a UDFunction object. Args: other ([UDFunction]) Returns: True if greater than or equal to. Otherwise False """ if isinstance(other, UDFunction): return self.val >= other.val elif isinstance(other, (int, float)): return self.val >= other else: raise TypeError("error raised by undefined: Need a UDFunction object to compare") def __round__(self, digit): '''overwrite the round method. Internal method for testing. ''' return round(self.val, digit)