Source code for undefined.GraphGenerator

import sys
# # temp solution for directory.
sys.path.append("./src/")
import matplotlib.pyplot as plt
import networkx as nx
import math
import numpy as np
from undefined.Utils import UDPrimitive, time, check_division_by_zero, check_pow


[docs]class UDGraph: def __init__(self, val, func=UDPrimitive.VAR, varname="intermediate"): # TODO """ 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._func = func self._varname = varname if hasattr(self._val, 'shape'): self._valshape = self._val.shape else: self._valshape = 1 self._parents = [] self._params = {} @property def val(self): # TODO """ This is a decorator return rouded input self.val Returns: array: 4 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 def __str__(self): """return the results in format. Used in building the computational graph in the reverse mode. Returns: formatted string with the value. """ return f"{self._func}\n".replace("UDPrimitive.", "") + f"Value:{self.val}\n" def __repr__(self): """return the computational graph root. Returns: formatted string with the value for the computational graph root. """ res = f'Computational Graph ({self.val}, {self._func})' for parent in self._parents: res += '\n|\n|<-(parent)-' + \ '\n| '.join(repr(parent).split('\n')) return res 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, UDGraph): new_val = self._val + other._val new_func = UDPrimitive.ADD udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._parents.append(other) elif isinstance(other, (int, float, np.ndarray)): new_val = self._val + other new_func = UDPrimitive.ADD udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): new_val = self._val * other._val new_func = UDPrimitive.MUL udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._parents.append(other) elif isinstance(other, (int, float, np.ndarray)): new_val = self._val * other new_func = UDPrimitive.MUL udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): new_val = self._val + other._val new_func = UDPrimitive.RADD udgraph = UDGraph(new_val, new_func) udgraph._parents.append(other) udgraph._parents.append(self) elif isinstance(other, (int, float, np.ndarray)): new_val = self._val + other new_func = UDPrimitive.RADD udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): new_val = self._val * other._val new_func = UDPrimitive.RMUL udgraph = UDGraph(new_val, new_func) udgraph._parents.append(other) udgraph._parents.append(self) elif isinstance(other, (int, float, np.ndarray)): new_val = self._val * other new_func = UDPrimitive.RMUL udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): new_val = self._val - other._val new_func = UDPrimitive.SUB udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._parents.append(other) elif isinstance(other, (int, float, np.ndarray)): new_val = self._val - other new_func = UDPrimitive.SUB udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): new_val = other._val - self._val new_func = UDPrimitive.RSUB udgraph = UDGraph(new_val, new_func) udgraph._parents.append(other) udgraph._parents.append(self) elif isinstance(other, (int, float, np.ndarray)): new_val = other - self._val new_func = UDPrimitive.RSUB udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): check_division_by_zero(other._val) new_val = self._val / other._val new_func = UDPrimitive.TRUEDIV udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._parents.append(other) elif isinstance(other, (int, float, np.ndarray)): check_division_by_zero(other) new_val = self._val / other new_func = UDPrimitive.TRUEDIV udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): new_val = other._val / self._val new_func = UDPrimitive.RTRUEDIV udgraph = UDGraph(new_val, new_func) udgraph._parents.append(other) udgraph._parents.append(self) elif isinstance(other, (int, float, np.ndarray)): new_val = other / self._val new_func = UDPrimitive.RTRUEDIV udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): check_division_by_zero(other._val) new_val = self._val // other._val new_func = UDPrimitive.FLOORDIV udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._parents.append(other) elif isinstance(other, (int, float, np.ndarray)): check_division_by_zero(other) new_val = self._val // other new_func = UDPrimitive.FLOORDIV udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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, UDGraph): new_val = other._val // self._val new_func = UDPrimitive.RFLOORDIV udgraph = UDGraph(new_val, new_func) udgraph._parents.append(other) udgraph._parents.append(self) elif isinstance(other, (int, float, np.ndarray)): new_val = other // self._val new_func = UDPrimitive.RFLOORDIV udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["constant"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph 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: degree (numeric): object to take power of. Returns: UDFunction: a new object with new_val and new_der """ new_func = UDPrimitive.POW if isinstance(other, UDGraph): check_pow(self._val, other._val) if isinstance(self._val, (int, float)): new_val = self._val ** other._val else: 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 else: new_val = self._val ** other._val udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._parents.append(other) 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) elif isinstance(self._val, (int, float)): new_val = self._val ** other udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["degree"] = other return udgraph 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 """ new_func = UDPrimitive.RPOW if isinstance(other, UDGraph): check_pow(other._val, self._val) if isinstance(other._val, (int, float)): new_val = other._val ** self._val 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 else: new_val = other._val ** self._val udgraph = UDGraph(new_val, new_func) udgraph._parents.append(other) udgraph._parents.append(self) elif isinstance(other, (int, float, np.ndarray)): check_pow(other, self._val) new_val = other ** self._val udgraph = UDGraph(new_val, new_func) udgraph._parents.append(self) udgraph._params["base"] = other else: raise TypeError( "error raised by undefined: unsupported attribute type.") return udgraph def __eq__(self, other): """compare whether the two UDGraph objects have the same values. Return true if equal, and false otherwise. raise TypeError is other is not a UDGraph object. Args: other ([UDGraph]) Returns: True if equal. Otherwise False """ if isinstance(other, UDGraph): return self.val == other.val elif isinstance(other, (int, float)): return self.val == other else: raise TypeError( "error raised by undefined: Need a UDGraph object to compare") def __ne__(self, other): """compare whether the two UDGraph objects have different values. raise TypeError is other is not a UDGraph object. Args: other ([UDGraph]) Returns: True if not equal. Otherwise False """ if isinstance(other, UDGraph): return self.val != other.val elif isinstance(other, (int, float)): return self.val != other else: raise TypeError( "error raised by undefined: Need a UDGraph object to compare") def __lt__(self, other): """overload the < operator raise TypeError is other is not a UDGraph object. Args: other ([UDGraph]) Returns: True if less than. Otherwise False """ if isinstance(other, UDGraph): return self.val < other.val elif isinstance(other, (int, float)): return self.val < other else: raise TypeError( "error raised by undefined: Need a UDGraph object to compare") def __gt__(self, other): """overload the > operator raise TypeError is other is not a UDGraph object. Args: other ([UDGraph]) Returns: True if greater than. Otherwise False """ if isinstance(other, UDGraph): return self.val > other.val elif isinstance(other, (int, float)): return self.val > other else: raise TypeError( "error raised by undefined: Need a UDGraph object to compare") def __le__(self, other): """overload the > operator raise TypeError is other is not a UDGraph object. Args: other ([UDGraph]) Returns: True if less than or equal to. Otherwise False """ if isinstance(other, UDGraph): return self.val <= other.val elif isinstance(other, (int, float)): return self.val <= other else: raise TypeError( "error raised by undefined: Need a UDGraph object to compare") def __ge__(self, other): """overload the > operator raise TypeError is other is not a UDGraph object. Args: other ([UDGraph]) Returns: True if greater than or equal to. Otherwise False """ if isinstance(other, UDGraph): return self.val >= other.val elif isinstance(other, (int, float)): return self.val >= other else: raise TypeError( "error raised by undefined: Need a UDGraph object to compare") def __hash__(self): """hash function for udgraph Returns: int: hash value for udgraph """ return hash((str(self._val), self._func))
[docs]class GeneratorHelper: @classmethod def _var(self, udgraph: UDGraph, variable: UDGraph, seed_dic): """This private helper function for UDGraph is used to get the values. This is used in the primitive. Generally speaking, this is the function to get node value in the computational graph. Args: udgraph (UDGraph) variable (UDGraph) Returns: node value """ if seed_dic and len(seed_dic) > 0: seed = seed_dic[udgraph._varname][variable._varname] if isinstance(udgraph._val, np.ndarray): return seed * np.ones(udgraph._valshape) else: return seed else: if variable._varname is not udgraph._varname: if isinstance(udgraph._val, np.ndarray): return np.zeros(udgraph._valshape) else: return 0 else: if isinstance(udgraph._val, np.ndarray): return np.ones(udgraph._valshape) else: return 1 @classmethod def _add(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for addition operation's derivative result. This is used in the primitive and __add__() in UDGraph. If there is one parent node, return the parental node value Otherwise, return the two parental nodes' addition result Args: udgraph (UDGraph) variable (UDGraph) Returns: the addition derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) + GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) @classmethod def _radd(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for addition operation's derivative result. This is used in the primitive and __radd__() in UDGraph. If there is one parent node, return the parental node value Otherwise, return the two parental nodes' addition result Args: udgraph (UDGraph) variable (UDGraph) Returns: the right addition derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) + GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) @classmethod def _mul(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for multiplication operation's derivative result. This is used in the primitive and __mul__() in UDGraph. If there is one parent node, return node multiplication result to the constant. Otherwise, return the two parental node multiplication result. Args: udgraph (UDGraph) variable (UDGraph) Returns: the multiplication derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return udgraph._params["constant"] * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) * g2._val + GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) * g1._val @classmethod def _rmul(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for multiplication operation's derivative result. This is used in the primitive and __rmul__() in UDGraph. If there is one parent node, return node multiplication result to the constant. Otherwise, return the two parental node multiplication result. Args: udgraph (UDGraph) variable (UDGraph) Returns: the right multiplication derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return udgraph._params["constant"] * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) * g2._val + GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) * g1._val @classmethod def _neg(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """ This is a helper function to convert the value to negative. Used in __ne__() function in UDGraph Args: udgraph (UDGraph): [description] variable (UDGraph): [description] Returns: the negative value of the node value. """ g1 = udgraph._parents[0] return -1 * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) @classmethod def _sub(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for subtraction operation's derivative result. This is used in the primitive and __sub__() in UDGraph. If there is one parent node, return the parental node value Otherwise, return the two parental nodes' subtraction result Args: udgraph (UDGraph) variable (UDGraph) Returns: the subtraction derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) - GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) @classmethod def _rsub(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for subtraction operation's derivative result. This is used in the primitive and __rsub__() in UDGraph. If there is one parent node, return the parental node value Otherwise, return the two parental nodes' subtraction result Args: udgraph (UDGraph) variable (UDGraph) Returns: the right subtraction derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return -1 * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) - GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) @classmethod def _truediv(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for true division operation's derivative result. This is used in the primitive and __truediv__() in UDGraph. If there is one parent node, return the parental node value Otherwise, return the two parental nodes' true division result Args: udgraph (UDGraph) variable (UDGraph) Returns: the true division derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) / udgraph._params["constant"] else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return (GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) * g2._val - GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) * g1._val) / (g2._val * g2._val) @classmethod def _rtruediv(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for true division operation's derivative result. This is used in the primitive and __rtruediv__() in UDGraph. If there is one parent node, return the parental node value Otherwise, return the two parental nodes' true division result Args: udgraph (UDGraph) variable (UDGraph) Returns: the right true division derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return -1 * udgraph._params["constant"] * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) / (g1._val * g1._val) else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return (GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) * g2._val - GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) * g1._val) / (g2._val * g2._val) @classmethod def _floordiv(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for floor division operation's derivative result. This is used in the primitive and __floordiv__() in UDGraph. If there is one parent node, return the parental node value Otherwise, return the two parental nodes' floor division result Args: udgraph (UDGraph) variable (UDGraph) Returns: the floor division derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) // udgraph._params["constant"] else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return (GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) * g2._val - GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) * g1._val) // (g2._val * g2._val) @classmethod def _rfloordiv(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for floor division operation's derivative result. This is used in the primitive and __rfloordiv__() in UDGraph. If there is one parent node, return the parental node value Otherwise, return the two parental nodes' floor division result Args: udgraph (UDGraph) variable (UDGraph) Returns: the right floor division derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) // udgraph._params["constant"] else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] return (GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) * g2._val - GraphGenerator.function_dic[g2._func](g2, variable, seed_dic) * g1._val) // (g2._val * g2._val) @classmethod def _pow(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for power operation's derivative result. This is used in the primitive and __pow__() in UDGraph. If there is one parent node, return the parental node power derivative result Otherwise, return the two parental nodes' power derivative result Args: udgraph (UDGraph) variable (UDGraph) Returns: the power derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] if isinstance(g1._val, np.ndarray): return udgraph._params["degree"] * np.power(g1._val, udgraph._params["degree"] - 1) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: return udgraph._params["degree"] * g1._val ** (udgraph._params["degree"] - 1) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: # len == 2 g1, g2 = udgraph._parents[0], udgraph._parents[1] if isinstance(g2._val, np.ndarray): if isinstance(g1._val, np.ndarray): der1 = g2._val * np.power( g1._val, g2._val - 1) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) der2 = np.log(g1._val) * udgraph._val * \ GraphGenerator.function_dic[g2._func]( g2, variable, seed_dic) return der1 + der2 else: der1 = g2._val * g1._val ** ( g2._val - 1) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) der2 = math.log(g1._val) * udgraph._val * \ GraphGenerator.function_dic[g2._func]( g2, variable, seed_dic) return der1 + der2 else: # g2._val is int if isinstance(g1._val, np.ndarray): der1 = g2._val * \ np.power(g1._val, g2._val - 1) * \ GraphGenerator.function_dic[g1._func]( g1, variable, seed_dic) der2 = np.log(g1._val) * udgraph._val * \ GraphGenerator.function_dic[g2._func]( g2, variable, seed_dic) return der1 + der2 else: der1 = g2._val * g1._val ** ( g2._val - 1) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) der2 = math.log(g1._val) * udgraph._val * \ GraphGenerator.function_dic[g2._func]( g2, variable, seed_dic) return der1 + der2 @classmethod def _rpow(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """This private helper function is used for power operation's derivative. This is used in the primitive and __rpow__() in UDGraph. If there is one parent node, return the parental node power derivative result Otherwise, return the two parental nodes' power derivative result Args: udgraph (UDGraph) variable (UDGraph) Returns: the right power derivative result. """ if len(udgraph._parents) == 1: g1 = udgraph._parents[0] return math.log(udgraph._params["base"]) * udgraph._val * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: g1, g2 = udgraph._parents[0], udgraph._parents[1] if isinstance(g2._val, np.ndarray): if isinstance(g1._val, np.ndarray): der1 = g2._val * np.power( g1._val, g2._val - 1) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) der2 = np.log(g1._val) * udgraph._val * \ GraphGenerator.function_dic[g2._func]( g2, variable, seed_dic) return der1 + der2 else: der1 = g2._val * g1._val ** ( g2._val - 1) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) der2 = math.log(g1._val) * udgraph._val * \ GraphGenerator.function_dic[g2._func]( g2, variable, seed_dic) return der1 + der2 else: # g2._val is int if isinstance(g1._val, np.ndarray): der1 = g2._val * \ np.power(g1._val, g2._val - 1) * \ GraphGenerator.function_dic[g1._func]( g1, variable, seed_dic) der2 = np.log(g1._val) * udgraph._val * \ GraphGenerator.function_dic[g2._func]( g2, variable, seed_dic) return der1 + der2 else: der1 = g2._val * g1._val ** ( g2._val - 1) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) der2 = math.log(g1._val) * udgraph._val * \ GraphGenerator.function_dic[g2._func]( g2, variable, seed_dic) return der1 + der2 @classmethod def _cos(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """Calculate the derivative for cosine in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Raises: TypeError: if unlawful input to the cosine function Returns: cosine's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] if isinstance(g1._val, (int, float)): return - 1 * math.sin(g1._val) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) elif isinstance(g1._val, np.ndarray): return -1 * np.sin(g1._val) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: raise TypeError( "error raised by undefined: unsupported attribute type.") @classmethod def _sin(self, udgraph: UDGraph, variable: UDGraph, seed_dic): """Calculate the derivative for sine in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Raises: TypeError: if unlawful input to the sine function Returns: sine's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] if isinstance(g1._val, (int, float)): return math.cos(g1._val) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) elif isinstance(g1._val, np.ndarray): return np.cos(g1._val) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: raise TypeError( "error raised by undefined: unsupported attribute type.") @classmethod def _tan(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """Calculate the derivative for tangent in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Raises: TypeError: if unlawful input to the tangent function Returns: tangent's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] if isinstance(g1._val, (int, float)): return (1 / (math.cos(g1._val)) ** 2) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) elif isinstance(g1._val, np.ndarray): return (1 / (np.cos(g1._val)) ** 2) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: raise TypeError( "error raised by undefined: unsupported attribute type.") @classmethod def _arccos(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """Calculate the derivative for arccosine in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Raises: TypeError: if unlawful input to the arccosine function Returns: arccosine's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] if isinstance(g1._val, (int, float)): return (-1 / math.sqrt(1 - g1._val**2)) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) elif isinstance(g1._val, np.ndarray): return (-1 / np.sqrt(1 - g1._val**2)) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: raise TypeError( "error raised by undefined: unsupported attribute type.") @classmethod def _arcsin(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """Calculate the derivative for arcsine in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Raises: TypeError: if unlawful input to the arcsine function Returns: arcsine's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] if isinstance(g1._val, (int, float)): return (1 / math.sqrt(1 - g1._val**2)) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) elif isinstance(g1._val, np.ndarray): return (1 / np.sqrt(1 - g1._val**2)) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: raise TypeError( "error raised by undefined: unsupported attribute type.") @classmethod def _arctan(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """Calculate the derivative for arctangent in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Raises: TypeError: if unlawful input to the arctangent function Returns: arctangent's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] if isinstance(g1._val, (int, float, np.ndarray)): return (1 / (1 + g1._val**2)) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: raise TypeError( "error raised by undefined: unsupported attribute type.") @classmethod def _sqrt(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """Calculate the derivative for squared root in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Raises: TypeError: if unlawful input to the squared root function Returns: squared root's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] if isinstance(g1._val, (int, float)): return 0.5 * math.pow(g1._val, -0.5) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) elif isinstance(g1._val, np.ndarray): return 0.5 * np.power(g1._val, -0.5) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: raise TypeError( "error raised by undefined: unsupported attribute type.") @classmethod def _exp(self, udgraph: UDGraph, variable: UDGraph, seed_dic=None): """Calculate the derivative for exponential in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Raises: TypeError: if unlawful input to the exponential function Returns: exponential's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] if isinstance(g1._val, (int, float)): return math.exp(g1._val) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) elif isinstance(g1._val, np.ndarray): return np.exp(g1._val) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic) else: raise TypeError( "error raised by undefined: unsupported attribute type.") @classmethod def _log(self, udgraph: UDGraph, variable: UDGraph, seed_dic): """Calculate the derivative for log in reverse mode. Args: udgraph (UDGraph) variable (UDGraph) Returns: log's derivatives results calculated in reverse mode. """ g1 = udgraph._parents[0] return 1 / (math.log(udgraph._params["base"]) * g1._val) * GraphGenerator.function_dic[g1._func](g1, variable, seed_dic)
[docs]class GraphGenerator: function_dic = { UDPrimitive.VAR: GeneratorHelper._var, UDPrimitive.ADD: GeneratorHelper._add, UDPrimitive.RADD: GeneratorHelper._radd, UDPrimitive.MUL: GeneratorHelper._mul, UDPrimitive.RMUL: GeneratorHelper._rmul, UDPrimitive.NEG: GeneratorHelper._neg, UDPrimitive.SUB: GeneratorHelper._sub, UDPrimitive.RSUB: GeneratorHelper._rsub, UDPrimitive.TRUEDIV: GeneratorHelper._truediv, UDPrimitive.RTRUEDIV: GeneratorHelper._rtruediv, UDPrimitive.FLOORDIV: GeneratorHelper._floordiv, UDPrimitive.RFLOORDIV: GeneratorHelper._rfloordiv, UDPrimitive.POW: GeneratorHelper._pow, UDPrimitive.RPOW: GeneratorHelper._rpow, UDPrimitive.COS: GeneratorHelper._cos, UDPrimitive.SIN: GeneratorHelper._sin, UDPrimitive.TAN: GeneratorHelper._tan, UDPrimitive.SQRT: GeneratorHelper._sqrt, UDPrimitive.EXP: GeneratorHelper._exp, UDPrimitive.LOG: GeneratorHelper._log, UDPrimitive.ACOS: GeneratorHelper._arccos, UDPrimitive.ASIN: GeneratorHelper._arcsin, UDPrimitive.ATAN: GeneratorHelper._arctan, } def __init__(self, g, variables, seeds_dic=None): """initialize the input instances. """ self._udgraph = g self._variables = variables self._nxgraph = None self._seeds_dic = seeds_dic def _generate_inner(self, g: UDGraph, nxgraph: nx.DiGraph): """build the computational graph """ for parent in g._parents: nxgraph.add_edge(parent, g) for k, v in g._params.items(): nxgraph.add_edge(f"{k}: {v}", g) for parent in g._parents: self._generate_inner(parent, nxgraph)
[docs] def generate_graph(self): """ generate the visualization graph """ if not self._nxgraph: self._nxgraph = nx.DiGraph() self._generate_inner(self._udgraph, self._nxgraph) nx.draw_networkx(self._nxgraph, with_labels=True, font_size=6) plt.savefig(f"{time()}.png")
[docs] def generate_str(self): """Output the computational graph in str and maintain the structure. """ return repr(self._udgraph)
[docs] def generate_derivative(self, var_name): """A wrapper function to generate the derivative given the input key value. Used in the trace function. """ if var_name not in self._variables.keys(): raise TypeError( "error raised by undefined: variable not defined in function") variable = self._variables[var_name] return np.round(GraphGenerator.function_dic[self._udgraph._func](self._udgraph, variable, self._seeds_dic), 3)