From e10f137bfb1a9aefbb6c89bf30af27e1f61d4a7c Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sat, 29 Jan 2022 20:52:02 +1300 Subject: [PATCH 01/29] Begin initialising autodiff submodule --- docs/source/jmath.approximation.rst | 8 ++++++ docs/source/jmath.universal.rst | 8 ++++++ jmath/approximation/autodiff.py | 42 +++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 jmath/approximation/autodiff.py diff --git a/docs/source/jmath.approximation.rst b/docs/source/jmath.approximation.rst index 50fbd6a..0f5ac0f 100644 --- a/docs/source/jmath.approximation.rst +++ b/docs/source/jmath.approximation.rst @@ -4,6 +4,14 @@ jmath.approximation package Submodules ---------- +jmath.approximation.autodiff module +----------------------------------- + +.. automodule:: jmath.approximation.autodiff + :members: + :undoc-members: + :show-inheritance: + jmath.approximation.differentiation module ------------------------------------------ diff --git a/docs/source/jmath.universal.rst b/docs/source/jmath.universal.rst index 486ca4b..bbba1e2 100644 --- a/docs/source/jmath.universal.rst +++ b/docs/source/jmath.universal.rst @@ -4,6 +4,14 @@ jmath.universal package Submodules ---------- +jmath.universal.hyperbolic module +--------------------------------- + +.. automodule:: jmath.universal.hyperbolic + :members: + :undoc-members: + :show-inheritance: + jmath.universal.logarithms module --------------------------------- diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py new file mode 100644 index 0000000..bb8de26 --- /dev/null +++ b/jmath/approximation/autodiff.py @@ -0,0 +1,42 @@ +''' + Automatic Differentiation. +''' + +# - Imports + +from typing import Callable, Tuple, TypeVar + +# - Typing + +Numeric = TypeVar("Numeric", float, int, "Variable") + +# - Classes + +class Variable: + ''' + Represents a variable in a function for automatic differentiation. + ''' + pass + +class OperatorNode: + ''' + Node representing an operation in the graph. + + Parameters + ---------- + + operation + The operation the node represents + ''' + def __init__(self, operation: Callable[[float], float], values = Tuple[Numeric, Numeric]): + + self.operation = operation + self.values = values + + def __repr__(self): + '''Programming Representation''' + return f"OperatorNode({self.operation.__name__}, ({self.values[0]}, {self.values[1]}))" + + def __str__(self): + '''String Representation''' + return f"{self.operation.__name__}({self.values[0]}, {self.values[1]})" \ No newline at end of file From 9d1a758db6004955d42805787ef54679df54fe99 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sat, 5 Feb 2022 16:42:15 +1300 Subject: [PATCH 02/29] Start implementing normal(ish) function behaviour --- jmath/approximation/autodiff.py | 144 ++++++++++++++++++++++++++++++++ jmath/universal/tools.py | 15 +++- 2 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 jmath/approximation/autodiff.py diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py new file mode 100644 index 0000000..665216d --- /dev/null +++ b/jmath/approximation/autodiff.py @@ -0,0 +1,144 @@ +''' + Automatic Differentiation +''' + +# - Imports + +import operator as op +from ..uncertainties import Uncertainty +from typing import Union, Callable + +# - Globals + +''' + A map of common functions and their partial derivates. + Partial derivatives are with respect to their positional relative parameter. +''' +diff_map = { + op.add: (1, 1), + op.sub: (1, 1), + op.mul: ( + lambda x, y: y, + lambda x, y: x + ), + op.truediv: ( + lambda x, y: 1/y, + lambda x, y: -x/(y**2) + ) +} + +# - Classes + +class Variable: + ''' + Allows a function to be applied to a variable input. + + Parameters + ---------- + + func + The function that owns (outputs to) the variable. + ''' + def __init__(self, func: 'Function' = None): + + self.output_of = func + self.input_of = set() + self.value = None + + def __radd__(self, other): + + if isinstance(other, Variable): + other.log(op.add, other, self) + self.log(op.add, other, self) + + def differentiate(self): + '''Differentiate the Variable''' + return 1 + +class Function: + ''' + A Differentiable Function. + + Parameters + ---------- + + func + The callable function to be represented. + ''' + def __init__(self, func: Callable): + + self.inputs = [] + self.func = func + self.output = Variable(self) + + def input(self, *inputs: Variable): + ''' + Registers a new input borrowed by the Function. + Production order determines input order on Function use. + + Parameters + ---------- + + inputs + The inputs to register + ''' + if isinstance(inputs[0], (list, tuple)): + inputs = inputs[0] + for input in inputs: + input.input_of.add(self) + self.inputs.append(input) + + def __call__(self, *inputs: Union[int, float, Uncertainty, Variable, 'Function']) -> Union[int, float, Uncertainty, 'Function']: + '''Evaluate the function.''' + + if len(inputs) == 0: + # Called with no inputs + # Thus we need to pass the variable inputs + inputs = tuple([input.value for input in self.inputs]) + assert None not in inputs + return self.func(*inputs) + elif all(isinstance(input, (Variable, Function)) for input in inputs): + # If all inputs are functions/variables + # Clear current inputs + self.inputs = [] + # Then let's register the inputs + for input in inputs: + if isinstance(input, Variable): + self.input(input) + elif isinstance(input, Function): + self.input(input.output) + # Let's return the function for use + return self + + # Else we have a non-empty, non-variable input + # Incase a tuple is mistakenly passed. + if isinstance(inputs[0], tuple): + inputs = inputs[0] + + # Function must just have been called normally + # Now iterate through the inputs + # Setting variables where appropriate + for i, input in enumerate(inputs): + if isinstance(self.inputs[i], Variable): + self.inputs[i].value = input + + # Now call self with updated inputs + return self() + + def differentiate(self, with_respect_to: int) -> Union['Function', Callable]: + + diff = self.diffs[with_respect_to] + + if isinstance(diff, Function): + def diff_out(*args): + return diff(*args) * diff.differentiate(with_respect_to)(*args) + else: + def diff_out(*args): + return diff + + return diff_out + +# - Definitions + +add = Function(op.add)(Variable(), Variable()) +mul = Function(op.mul)(Variable(), Variable()) \ No newline at end of file diff --git a/jmath/universal/tools.py b/jmath/universal/tools.py index e82c7f9..c75560f 100644 --- a/jmath/universal/tools.py +++ b/jmath/universal/tools.py @@ -7,14 +7,16 @@ from typing import Union, Callable from ..uncertainties import Uncertainty from ..units import Unit +from ..approximation.autodiff import Variable, Function # - Typing -Supported = Union[float, int, Uncertainty, Unit] +Supported = Union[float, int, Uncertainty, Unit, Variable] +Numeric = Union[float, int] # - Functions -def generic_function(func: Callable[[float], float], input: Supported, *args) -> Supported: +def generic_function(func: Callable[[Numeric], Numeric], input: Supported, *args) -> Union[Supported, Function]: """ Applies a function with generic cases for special objects. @@ -26,6 +28,7 @@ def generic_function(func: Callable[[float], float], input: Supported, *args) -> args Arguments to send to the function """ + if isinstance(input, Unit): # Units # Return function applied to unit value @@ -33,6 +36,14 @@ def generic_function(func: Callable[[float], float], input: Supported, *args) -> elif isinstance(input, Uncertainty): # Uncertainties return input.apply(func, *args) + elif isinstance(input, Variable): + # Auto-diff Variables + # Build auto-diff function + auto_func = Function(func) + # Register variable as input + auto_func.input(input) + # Return the function + return auto_func else: # Anything else return func(input, *args) \ No newline at end of file From 1803d8defba41c55fe7936070d9982792a412bcf Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sun, 6 Feb 2022 11:19:54 +1300 Subject: [PATCH 03/29] Functions mostly working classicly, order broken --- jmath/approximation/autodiff.py | 83 +++++++++------------------------ 1 file changed, 22 insertions(+), 61 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index bf79b21..5e605ba 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -45,12 +45,6 @@ def __init__(self, func: 'Function' = None): self.input_of = set() self.value = None - def __radd__(self, other): - - if isinstance(other, Variable): - other.log(op.add, other, self) - self.log(op.add, other, self) - def differentiate(self): '''Differentiate the Variable''' return 1 @@ -67,78 +61,45 @@ class Function: ''' def __init__(self, func: Callable): - self.inputs = [] + self.vars = [] self.func = func self.output = Variable(self) - def input(self, *inputs: Variable): - ''' - Registers a new input borrowed by the Function. - Production order determines input order on Function use. - - Parameters - ---------- - - inputs - The inputs to register - ''' - if isinstance(inputs[0], (list, tuple)): - inputs = inputs[0] - for input in inputs: - input.input_of.add(self) - self.inputs.append(input) - def __call__(self, *inputs: Union[int, float, Uncertainty, Variable, 'Function']) -> Union[int, float, Uncertainty, 'Function']: '''Evaluate the function.''' if len(inputs) == 0: - # Called with no inputs - # Thus we need to pass the variable inputs - inputs = tuple([input.value for input in self.inputs]) - assert None not in inputs - return self.func(*inputs) + # Use existing values + inputs = tuple(var.value for var in self.vars) + # Now execute the function + self.output.value = self.func(*inputs) + return self.output.value elif all(isinstance(input, (Variable, Function)) for input in inputs): # If all inputs are functions/variables # Clear current inputs - self.inputs = [] + self.vars = [] # Then let's register the inputs for input in inputs: if isinstance(input, Variable): - self.input(input) + self.vars.append(input) + input.input_of.add(self) elif isinstance(input, Function): - self.input(input.output) + self.vars.append(input.output) + input.output.input_of.add(self) # Let's return the function for use return self - - # Else we have a non-empty, non-variable input - # Incase a tuple is mistakenly passed. - if isinstance(inputs[0], tuple): - inputs = inputs[0] - - # Function must just have been called normally - # Now iterate through the inputs - # Setting variables where appropriate - for i, input in enumerate(inputs): - if isinstance(self.inputs[i], Variable): - self.inputs[i].value = input - - # Now call self with updated inputs - return self() - - def differentiate(self, with_respect_to: int) -> Union['Function', Callable]: - - diff = self.diffs[with_respect_to] - - if isinstance(diff, Function): - def diff_out(*args): - return diff(*args) * diff.differentiate(with_respect_to)(*args) else: - def diff_out(*args): - return diff - - return diff_out + # Normal call + # We need to find the root functions and set their variables before setting ours + for i, var in enumerate(self.vars): + if var.output_of is not None: + var.output_of(*inputs) + else: + var.value = inputs[i] + + return self() # - Definitions -add = Function(op.add)(Variable(), Variable()) -mul = Function(op.mul)(Variable(), Variable()) +add = Function(op.add) +mul = Function(op.mul) From 84b271f735ed2a3b59dec70210ac876b5e2c69c9 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sun, 6 Feb 2022 11:32:57 +1300 Subject: [PATCH 04/29] Start work on differentiation code --- jmath/approximation/autodiff.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index 5e605ba..3e1adbb 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -16,7 +16,7 @@ ''' diff_map = { op.add: (1, 1), - op.sub: (1, 1), + op.sub: (1, -1), op.mul: ( lambda x, y: y, lambda x, y: x @@ -69,7 +69,11 @@ def __call__(self, *inputs: Union[int, float, Uncertainty, Variable, 'Function'] '''Evaluate the function.''' if len(inputs) == 0: - # Use existing values + # Call inner functions + for var in self.vars: + if var.output_of is not None: + var.output_of() + # Use var values inputs = tuple(var.value for var in self.vars) # Now execute the function self.output.value = self.func(*inputs) @@ -88,16 +92,21 @@ def __call__(self, *inputs: Union[int, float, Uncertainty, Variable, 'Function'] input.output.input_of.add(self) # Let's return the function for use return self - else: - # Normal call - # We need to find the root functions and set their variables before setting ours - for i, var in enumerate(self.vars): - if var.output_of is not None: - var.output_of(*inputs) - else: - var.value = inputs[i] - return self() + def diff_wrt(self, var: Variable) -> 'Function': + ''' + Produces the partial differential of the function with respect to the specified variable. + + Parameters + ---------- + + var + The variable to differentiate with repsect to. + ''' + # Get index of variable for THIS function + index = self.vars.index(var) + # Now use diff map to get the diff of this function + diff_func = diff_map(self.func) # - Definitions From a58f194f5e8a2d94d83bf22f718db1a3c56783cb Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Tue, 22 Feb 2022 17:50:28 +1300 Subject: [PATCH 05/29] Refined approach, still needs tweaked --- jmath/approximation/autodiff.py | 153 +++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 42 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index 3e1adbb..c0ef649 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -6,50 +6,102 @@ import operator as op from ..uncertainties import Uncertainty -from typing import Union, Callable +from typing import Union, Callable, Tuple -# - Globals +# - Typing -''' - A map of common functions and their partial derivates. - Partial derivatives are with respect to their positional relative parameter. -''' -diff_map = { - op.add: (1, 1), - op.sub: (1, -1), - op.mul: ( - lambda x, y: y, - lambda x, y: x - ), - op.truediv: ( - lambda x, y: 1/y, - lambda x, y: -x/(y**2) - ) -} +Supported = Union[int, float, Uncertainty, 'Function', 'Variable'] # - Classes class Variable: ''' Allows a function to be applied to a variable input. - - Parameters - ---------- - - func - The function that owns (outputs to) the variable. ''' - def __init__(self, func: 'Function' = None): + def __init__(self): - self.output_of = func + self.output_of = None self.input_of = set() self.value = None - def differentiate(self): - '''Differentiate the Variable''' - return 1 + def __add__(self, other: Supported) -> 'Function': + + if other == 0: + # Special case + return self + elif isinstance(other, (int, float, Uncertainty)): + # Numeric case + return Function(lambda x: x + other, lambda x: 1) + else: + # Variable case + return Function(op.add, (lambda x, y: 1, lambda x, y: 1)) -class Function: + def __radd__(self, other: Supported) -> 'Function': + + return self + other + + def __sub__(self, other: Supported) -> 'Function': + + if other == 0: + # Special case + return self + elif isinstance(other, (int, float, Uncertainty)): + # Numeric case + return Function(lambda x: x - other, lambda x: 1) + else: + # Variable case + return Function(op.sub, (lambda x, y: 1, lambda x, y: -1)) + + def __rsub__(self, other: Supported) -> 'Function': + + if isinstance(other, (int, float, Uncertainty)): + # Numeric case + return Function(lambda x: other - x, lambda x: -1) + else: + # Variable case + return Function(op.sub, (lambda x, y: 1, lambda x, y: -1)) + + def __mul__(self, other: Supported) -> 'Function': + + if other == 1: + # Special case + return self + elif isinstance(other, (int, float, Uncertainty)): + # Numeric case + return Function(lambda x: other * x, lambda x, y: other) + else: + # Variable case + return Function(op.mul, (lambda x, y: y, lambda x, y: x)) + + def __rmul__(self, other: Supported) -> 'Function': + + return self * other + + def __truediv__(self, other: Supported) -> 'Function': + + if other == 1: + # Special case + return other + elif other == 0: + # Error case + raise ZeroDivisionError + elif isinstance(other, (int, float, Uncertainty)): + # Numeric case + return Function(lambda x: x/other, lambda x: 1/other) + else: + # Variable case + return Function(op.truediv, (lambda x, y: 1/y, lambda x, y: -x/(y**2))) + + def __rtruediv__(self, other: Supported) -> 'Function': + + if isinstance(other, (int, float, Uncertainty)): + # Numeric case + return Function(lambda x: other/x, lambda x: -other/(x**2)) + else: + # Variable case + return Function(op.rtruediv, (lambda x, y: 1/y, lambda x, y: -x/(y**2))) + +class Function(Variable): ''' A Differentiable Function. @@ -58,12 +110,21 @@ class Function: func The callable function to be represented. + diff + A tuple of functions produced upon differentiation with respect to the function variables. ''' - def __init__(self, func: Callable): + def __init__(self, func: Callable, diff: Tuple[Callable]): self.vars = [] self.func = func - self.output = Variable(self) + self.diff = diff + self.output = Variable() + self.output.output_of = self + + # Check if diff is not a tuple + if not isinstance(diff, tuple): + # If not then we shall make it one + self.diff = (diff,) def __call__(self, *inputs: Union[int, float, Uncertainty, Variable, 'Function']) -> Union[int, float, Uncertainty, 'Function']: '''Evaluate the function.''' @@ -92,8 +153,12 @@ def __call__(self, *inputs: Union[int, float, Uncertainty, Variable, 'Function'] input.output.input_of.add(self) # Let's return the function for use return self + else: + # Real inputs + # Evaluate the function + return self.func(*inputs) - def diff_wrt(self, var: Variable) -> 'Function': + def differentiate(self, var: Variable) -> 'Function': ''' Produces the partial differential of the function with respect to the specified variable. @@ -103,12 +168,16 @@ def diff_wrt(self, var: Variable) -> 'Function': var The variable to differentiate with repsect to. ''' - # Get index of variable for THIS function - index = self.vars.index(var) - # Now use diff map to get the diff of this function - diff_func = diff_map(self.func) - -# - Definitions - -add = Function(op.add) -mul = Function(op.mul) + # Derivative produced + func = 0 + # Go down each 'branch' + for i, input_var in enumerate(self.vars): + branch = self.diff[i] + # Check if it is 'owned' by a function + if input_var.output_of is not None: + # Then derive that function by the same variable and add it + func += branch * input_var.output_of.differentiate(var) + else: + func = branch + + return func \ No newline at end of file From 0a94dc4aa9c1b34e0d7e680f4fd12c7172fe6d6d Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 25 Feb 2022 22:21:38 +1300 Subject: [PATCH 06/29] Another broken implementation --- jmath/approximation/autodiff.py | 138 ++++++++++++++------------------ 1 file changed, 60 insertions(+), 78 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index c0ef649..554b057 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -11,18 +11,32 @@ # - Typing Supported = Union[int, float, Uncertainty, 'Function', 'Variable'] +Numeric = Union[int, float, Uncertainty] # - Classes -class Variable: +class Function: ''' - Allows a function to be applied to a variable input. + Automatic Differentiation Function Object + + Parameters + ----------- + + func + Represented function. + diff + Tuple of partial derivatives of the function. ''' - def __init__(self): + def __init__(self, func: Callable, diff: Tuple[Callable]): - self.output_of = None - self.input_of = set() - self.value = None + self.inputs = [] + self.func = func + self.diff = diff + + # Check if diff is not a tuple + if not isinstance(diff, tuple): + # If not then we shall make it one + self.diff = (diff,) def __add__(self, other: Supported) -> 'Function': @@ -31,7 +45,9 @@ def __add__(self, other: Supported) -> 'Function': return self elif isinstance(other, (int, float, Uncertainty)): # Numeric case - return Function(lambda x: x + other, lambda x: 1) + f = Function(lambda x: x + other, lambda x: 1) + f.register(self) + return f else: # Variable case return Function(op.add, (lambda x, y: 1, lambda x, y: 1)) @@ -68,7 +84,9 @@ def __mul__(self, other: Supported) -> 'Function': return self elif isinstance(other, (int, float, Uncertainty)): # Numeric case - return Function(lambda x: other * x, lambda x, y: other) + f = Function(lambda x: other * x, lambda x, y: other) + f.register(self) + return f else: # Variable case return Function(op.mul, (lambda x, y: y, lambda x, y: x)) @@ -101,83 +119,47 @@ def __rtruediv__(self, other: Supported) -> 'Function': # Variable case return Function(op.rtruediv, (lambda x, y: 1/y, lambda x, y: -x/(y**2))) -class Function(Variable): - ''' - A Differentiable Function. - - Parameters - ---------- - - func - The callable function to be represented. - diff - A tuple of functions produced upon differentiation with respect to the function variables. - ''' - def __init__(self, func: Callable, diff: Tuple[Callable]): - - self.vars = [] - self.func = func - self.diff = diff - self.output = Variable() - self.output.output_of = self + def register(self, *inputs: 'Function', clear: bool = True): + ''' + Registers inputs to the function. - # Check if diff is not a tuple - if not isinstance(diff, tuple): - # If not then we shall make it one - self.diff = (diff,) + Parameters + ---------- - def __call__(self, *inputs: Union[int, float, Uncertainty, Variable, 'Function']) -> Union[int, float, Uncertainty, 'Function']: - '''Evaluate the function.''' - - if len(inputs) == 0: - # Call inner functions - for var in self.vars: - if var.output_of is not None: - var.output_of() - # Use var values - inputs = tuple(var.value for var in self.vars) - # Now execute the function - self.output.value = self.func(*inputs) - return self.output.value - elif all(isinstance(input, (Variable, Function)) for input in inputs): - # If all inputs are functions/variables - # Clear current inputs - self.vars = [] - # Then let's register the inputs - for input in inputs: - if isinstance(input, Variable): - self.vars.append(input) - input.input_of.add(self) - elif isinstance(input, Function): - self.vars.append(input.output) - input.output.input_of.add(self) - # Let's return the function for use - return self - else: - # Real inputs - # Evaluate the function - return self.func(*inputs) + inputs + Args, the functions to register as inputs. + clear + Clear function inputs before registering. + ''' + if clear: + self.inputs = [] + for input in inputs: + self.inputs.append(input) - def differentiate(self, var: Variable) -> 'Function': + def differentiate(self, wrt: 'Variable') -> Callable: ''' - Produces the partial differential of the function with respect to the specified variable. + Differentiates the function with respect to a variable. Parameters ---------- - var - The variable to differentiate with repsect to. + wrt + The variable to differentiate with respect to. ''' - # Derivative produced + # The differentiated function func = 0 - # Go down each 'branch' - for i, input_var in enumerate(self.vars): - branch = self.diff[i] - # Check if it is 'owned' by a function - if input_var.output_of is not None: - # Then derive that function by the same variable and add it - func += branch * input_var.output_of.differentiate(var) - else: - func = branch - - return func \ No newline at end of file + # Move across inputs + for i, input in enumerate(self.inputs): + # Get respective derivative + partial = Function(self.diff[i], lambda x: 1) + func += partial * input.differentiate(wrt) + + return func + +class Variable(Function): + ''' + Variables for function differentiation. + ''' + def __init__(self): + + super().__init__(lambda x: x, lambda x: 1) \ No newline at end of file From 222727041b0cb8a655e2af9683f7e03fe1f57581 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sun, 27 Feb 2022 15:56:27 +1300 Subject: [PATCH 07/29] IT WORKS! (Mostly) --- jmath/approximation/autodiff.py | 82 ++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index 554b057..5dd2500 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -34,9 +34,29 @@ def __init__(self, func: Callable, diff: Tuple[Callable]): self.diff = diff # Check if diff is not a tuple - if not isinstance(diff, tuple): + if not isinstance(self.diff, tuple): # If not then we shall make it one - self.diff = (diff,) + self.diff = (self.diff,) + + def __call__(self): + + if not(isinstance(self.func, Callable)): + return self.func + + # Input collection + inputs = [] + for input in self.inputs: + if isinstance(input, Variable): + # Variable case + inputs.append(input.value) + elif isinstance(input, (int, float, Uncertainty)): + # Const case + input.append(input) + else: + # Function case + inputs.append(input()) + + return self.func(*tuple(inputs)) def __add__(self, other: Supported) -> 'Function': @@ -45,12 +65,14 @@ def __add__(self, other: Supported) -> 'Function': return self elif isinstance(other, (int, float, Uncertainty)): # Numeric case - f = Function(lambda x: x + other, lambda x: 1) + f = Function(lambda x: x + other, 1) f.register(self) return f else: # Variable case - return Function(op.add, (lambda x, y: 1, lambda x, y: 1)) + f = Function(op.add, (1, 1)) + f.register(self, other) + return f def __radd__(self, other: Supported) -> 'Function': @@ -63,19 +85,27 @@ def __sub__(self, other: Supported) -> 'Function': return self elif isinstance(other, (int, float, Uncertainty)): # Numeric case - return Function(lambda x: x - other, lambda x: 1) + f = Function(lambda x: x - other, 1) + f.register(self) + return f else: # Variable case - return Function(op.sub, (lambda x, y: 1, lambda x, y: -1)) + f = Function(op.sub, (1, -1)) + f.register(self, other) + return f def __rsub__(self, other: Supported) -> 'Function': if isinstance(other, (int, float, Uncertainty)): # Numeric case - return Function(lambda x: other - x, lambda x: -1) + f = Function(lambda x: other - x, -1) + f.register(self) + return f else: # Variable case - return Function(op.sub, (lambda x, y: 1, lambda x, y: -1)) + f = Function(op.sub, (1, -1)) + f.register(self, other) + return f def __mul__(self, other: Supported) -> 'Function': @@ -84,12 +114,14 @@ def __mul__(self, other: Supported) -> 'Function': return self elif isinstance(other, (int, float, Uncertainty)): # Numeric case - f = Function(lambda x: other * x, lambda x, y: other) + f = Function(lambda x: other * x, other) f.register(self) return f else: # Variable case - return Function(op.mul, (lambda x, y: y, lambda x, y: x)) + f = Function(op.mul, (lambda x, y: y, lambda x, y: x)) + f.register(self, other) + return f def __rmul__(self, other: Supported) -> 'Function': @@ -105,19 +137,27 @@ def __truediv__(self, other: Supported) -> 'Function': raise ZeroDivisionError elif isinstance(other, (int, float, Uncertainty)): # Numeric case - return Function(lambda x: x/other, lambda x: 1/other) + f = Function(lambda x: x/other, 1/other) + f.register(self) + return f else: # Variable case - return Function(op.truediv, (lambda x, y: 1/y, lambda x, y: -x/(y**2))) + f = Function(op.truediv, (lambda x, y: 1/y, lambda x, y: -x/(y**2))) + f.register(self, other) + return f def __rtruediv__(self, other: Supported) -> 'Function': if isinstance(other, (int, float, Uncertainty)): # Numeric case - return Function(lambda x: other/x, lambda x: -other/(x**2)) + f = Function(lambda x: other/x, lambda x: -other/(x**2)) + f.register(self) + return f else: # Variable case - return Function(op.rtruediv, (lambda x, y: 1/y, lambda x, y: -x/(y**2))) + f = Function(op.rtruediv, (lambda x, y: 1/y, lambda x, y: -x/(y**2))) + f.register(self, other) + return f def register(self, *inputs: 'Function', clear: bool = True): ''' @@ -136,7 +176,7 @@ def register(self, *inputs: 'Function', clear: bool = True): for input in inputs: self.inputs.append(input) - def differentiate(self, wrt: 'Variable') -> Callable: + def differentiate(self, wrt: 'Variable') -> 'Function': ''' Differentiates the function with respect to a variable. @@ -151,7 +191,7 @@ def differentiate(self, wrt: 'Variable') -> Callable: # Move across inputs for i, input in enumerate(self.inputs): # Get respective derivative - partial = Function(self.diff[i], lambda x: 1) + partial = Function(self.diff[i], 1) func += partial * input.differentiate(wrt) return func @@ -162,4 +202,12 @@ class Variable(Function): ''' def __init__(self): - super().__init__(lambda x: x, lambda x: 1) \ No newline at end of file + super().__init__(lambda x: x, 1) + self.value = 0 + + def differentiate(self, wrt: 'Variable') -> int: + + if wrt == self: + return 1 + else: + return 0 \ No newline at end of file From fe31db8a3255fcbb46531f748cd2cc5449116f1f Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sun, 27 Feb 2022 21:09:40 +1300 Subject: [PATCH 08/29] Power handling --- jmath/approximation/autodiff.py | 42 +++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index 5dd2500..ca9ae6d 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -24,19 +24,19 @@ class Function: func Represented function. - diff + diffs Tuple of partial derivatives of the function. ''' - def __init__(self, func: Callable, diff: Tuple[Callable]): + def __init__(self, func: Callable, diffs: Tuple[Callable]): - self.inputs = [] + self.inputs = () self.func = func - self.diff = diff + self.diffs = diffs # Check if diff is not a tuple - if not isinstance(self.diff, tuple): + if not isinstance(self.diffs, tuple): # If not then we shall make it one - self.diff = (self.diff,) + self.diffs = (self.diffs,) def __call__(self): @@ -52,7 +52,7 @@ def __call__(self): elif isinstance(input, (int, float, Uncertainty)): # Const case input.append(input) - else: + elif isinstance(input, Function): # Function case inputs.append(input()) @@ -123,6 +123,20 @@ def __mul__(self, other: Supported) -> 'Function': f.register(self, other) return f + def __pow__(self, power: Union[int, float, Uncertainty]) -> 'Function': + + if power == 1: + f = Function(lambda x: x, 1) + f.register(self) + return f + elif power == 0: + return 1 + else: + # Non-trivial case + f = Function(lambda x: x**power, lambda x: power*x**(power - 1)) + f.register(self) + return f + def __rmul__(self, other: Supported) -> 'Function': return self * other @@ -159,7 +173,7 @@ def __rtruediv__(self, other: Supported) -> 'Function': f.register(self, other) return f - def register(self, *inputs: 'Function', clear: bool = True): + def register(self, *inputs: 'Function'): ''' Registers inputs to the function. @@ -168,13 +182,8 @@ def register(self, *inputs: 'Function', clear: bool = True): inputs Args, the functions to register as inputs. - clear - Clear function inputs before registering. ''' - if clear: - self.inputs = [] - for input in inputs: - self.inputs.append(input) + self.inputs = inputs def differentiate(self, wrt: 'Variable') -> 'Function': ''' @@ -191,7 +200,8 @@ def differentiate(self, wrt: 'Variable') -> 'Function': # Move across inputs for i, input in enumerate(self.inputs): # Get respective derivative - partial = Function(self.diff[i], 1) + partial = Function(self.diffs[i], 1) + partial.register(*self.inputs) func += partial * input.differentiate(wrt) return func @@ -204,6 +214,8 @@ def __init__(self): super().__init__(lambda x: x, 1) self.value = 0 + self.inputs = None + self.diffs = None def differentiate(self, wrt: 'Variable') -> int: From cad346eeb51c3fa86ad35ee694dab94471b5b642 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Thu, 3 Mar 2022 22:30:45 +1300 Subject: [PATCH 09/29] Implement some common derivatives --- jmath/approximation/autodiff.py | 16 ++++++++-------- jmath/universal/__init__.py | 2 +- jmath/universal/logarithms.py | 12 ++++++++++++ jmath/universal/tools.py | 24 ++++++++++++++---------- jmath/universal/trigonometry.py | 4 ++-- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index ca9ae6d..ede0957 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -24,19 +24,19 @@ class Function: func Represented function. - diffs - Tuple of partial derivatives of the function. + derivatives + Tuple of partial derivatives of the function with respect to function variables. ''' - def __init__(self, func: Callable, diffs: Tuple[Callable]): + def __init__(self, func: Callable, derivatives: Tuple[Callable]): self.inputs = () self.func = func - self.diffs = diffs + self.derivatives = derivatives # Check if diff is not a tuple - if not isinstance(self.diffs, tuple): + if not isinstance(self.derivatives, tuple): # If not then we shall make it one - self.diffs = (self.diffs,) + self.derivatives = (self.derivatives,) def __call__(self): @@ -200,7 +200,7 @@ def differentiate(self, wrt: 'Variable') -> 'Function': # Move across inputs for i, input in enumerate(self.inputs): # Get respective derivative - partial = Function(self.diffs[i], 1) + partial = Function(self.derivatives[i], None) partial.register(*self.inputs) func += partial * input.differentiate(wrt) @@ -215,7 +215,7 @@ def __init__(self): super().__init__(lambda x: x, 1) self.value = 0 self.inputs = None - self.diffs = None + self.derivatives = None def differentiate(self, wrt: 'Variable') -> int: diff --git a/jmath/universal/__init__.py b/jmath/universal/__init__.py index 79e781b..e6b084a 100644 --- a/jmath/universal/__init__.py +++ b/jmath/universal/__init__.py @@ -9,6 +9,6 @@ # - Defaults from .trigonometry import sin, asin, cos, acos, tan, atan -from .logarithms import log, log10, log2 +from .logarithms import log, log10, log2, ln from .natural import exp from .hyperbolic import sinh, asinh, cosh, acosh, tanh, atanh \ No newline at end of file diff --git a/jmath/universal/logarithms.py b/jmath/universal/logarithms.py index 4b4c6a7..7db342b 100644 --- a/jmath/universal/logarithms.py +++ b/jmath/universal/logarithms.py @@ -21,6 +21,18 @@ def log(value: Supported, base: float = math.e) -> Supported: """ return generic_function(math.log, value, base) +def ln(value: Supported) -> Supported: + """ + Calculates the natural logarithm of a number. + + Parameters + ---------- + + value + The value to compute the natural logarithm of. + """ + return generic_function(math.log, value, derivative = lambda x: 1/x) + def log10(value: Supported) -> Supported: """ Calculates the log base 10 of a number. diff --git a/jmath/universal/tools.py b/jmath/universal/tools.py index c75560f..8586460 100644 --- a/jmath/universal/tools.py +++ b/jmath/universal/tools.py @@ -4,19 +4,20 @@ # - Imports -from typing import Union, Callable +from email.mime import nonmultipart +from typing import Union, Callable, Tuple from ..uncertainties import Uncertainty from ..units import Unit from ..approximation.autodiff import Variable, Function # - Typing -Supported = Union[float, int, Uncertainty, Unit, Variable] +Supported = Union[float, int, Uncertainty, Unit, Variable, Function] Numeric = Union[float, int] # - Functions -def generic_function(func: Callable[[Numeric], Numeric], input: Supported, *args) -> Union[Supported, Function]: +def generic_function(func: Callable[[Numeric], Numeric], input: Supported, *args, derivative: Callable = None) -> Union[Supported, Function]: """ Applies a function with generic cases for special objects. @@ -27,6 +28,8 @@ def generic_function(func: Callable[[Numeric], Numeric], input: Supported, *args The function to apply. args Arguments to send to the function + derivatives + The partial derivatives of the function with respect to its variables """ if isinstance(input, Unit): @@ -36,14 +39,15 @@ def generic_function(func: Callable[[Numeric], Numeric], input: Supported, *args elif isinstance(input, Uncertainty): # Uncertainties return input.apply(func, *args) - elif isinstance(input, Variable): - # Auto-diff Variables + elif isinstance(input, (Function, Variable)): + # Auto-diff Variables/Functions + # Check that there is a derivative + if derivative is None: + return NotImplemented # Build auto-diff function - auto_func = Function(func) - # Register variable as input - auto_func.input(input) - # Return the function - return auto_func + f = Function(func, derivative) + f.register(input) + return f else: # Anything else return func(input, *args) \ No newline at end of file diff --git a/jmath/universal/trigonometry.py b/jmath/universal/trigonometry.py index 8dec133..aa5c94f 100644 --- a/jmath/universal/trigonometry.py +++ b/jmath/universal/trigonometry.py @@ -21,7 +21,7 @@ def sin(value: other.radian) -> Supported: value Value (in radians) to compute the sine of. ''' - return generic_function(math.sin, value) + return generic_function(math.sin, value, derivative = cos) @annotate def asin(value: Supported) -> other.radian: @@ -53,7 +53,7 @@ def cos(value: other.radian) -> Supported: value Value (in radians) to compute the cosine of ''' - return generic_function(math.cos, value) + return generic_function(math.cos, value, derivative = lambda x: -1 * sin(x)) @annotate def acos(value: Supported) -> other.radian: From 890ea13a06cd3b0bf54ca21acac037ed8b5316b2 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Thu, 3 Mar 2022 22:41:02 +1300 Subject: [PATCH 10/29] Slightly more flexible function def --- jmath/approximation/autodiff.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index ede0957..6abea54 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -38,23 +38,36 @@ def __init__(self, func: Callable, derivatives: Tuple[Callable]): # If not then we shall make it one self.derivatives = (self.derivatives,) - def __call__(self): + def __call__(self, **kwargs): if not(isinstance(self.func, Callable)): return self.func # Input collection inputs = [] + # Assigned variables collection + assigned = {} + # Start computing inputs for input in self.inputs: if isinstance(input, Variable): # Variable case - inputs.append(input.value) + # Check if the variable has been assigned a value + if input.id in assigned.keys(): + inputs.append(assigned[input.id]) + elif input.id in kwargs.keys(): + # Else does kwargs have the value? + assigned[input.id] = kwargs[input.id] + inputs.append(assigned[input.id]) + else: + # There is no value for the variable??? + # Throw a value error + raise KeyError(f"Variable '{input.id}' was not assigned a value on function call!") elif isinstance(input, (int, float, Uncertainty)): # Const case input.append(input) elif isinstance(input, Function): # Function case - inputs.append(input()) + inputs.append(input(**kwargs)) return self.func(*tuple(inputs)) @@ -185,7 +198,7 @@ def register(self, *inputs: 'Function'): ''' self.inputs = inputs - def differentiate(self, wrt: 'Variable') -> 'Function': + def differentiate(self, wrt: Union['Variable', str]) -> 'Function': ''' Differentiates the function with respect to a variable. @@ -209,17 +222,23 @@ def differentiate(self, wrt: 'Variable') -> 'Function': class Variable(Function): ''' Variables for function differentiation. + + Parameters + ---------- + + id + Unique identifier string. ''' - def __init__(self): + def __init__(self, id = None): super().__init__(lambda x: x, 1) - self.value = 0 + self.id = id self.inputs = None self.derivatives = None def differentiate(self, wrt: 'Variable') -> int: - if wrt == self: + if wrt == self or wrt == self.id: return 1 else: return 0 \ No newline at end of file From 08b5f0c9f7718ecaad4ade1fe6891f02056f6544 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Thu, 3 Mar 2022 22:57:53 +1300 Subject: [PATCH 11/29] Analyse function --- jmath/approximation/autodiff.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index 6abea54..bbc527c 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -5,6 +5,7 @@ # - Imports import operator as op +import inspect from ..uncertainties import Uncertainty from typing import Union, Callable, Tuple @@ -241,4 +242,33 @@ def differentiate(self, wrt: 'Variable') -> int: if wrt == self or wrt == self.id: return 1 else: - return 0 \ No newline at end of file + return 0 + +# - Functions + +def analyse(f: Callable) -> Function: + ''' + Automatically analyses the given function and produces a Function object. + + Parameters + ---------- + + f + The function to analyse + + Returns + ------- + + Function + A differentiable function object representing the given function. + ''' + # Get the list of parameters from the function + names = inspect.getargspec(f)[0] + # Convert these into variables for the function + vars = tuple(Variable(name) for name in names) + # Pass these to the function + f = f(*vars) + # Register the input variables + f.register(*vars) + # And return + return f \ No newline at end of file From c74e87d18ad60b5b4c4e30093d5292f1cbe23368 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Thu, 3 Mar 2022 23:03:54 +1300 Subject: [PATCH 12/29] Support multiple diff levels --- jmath/approximation/autodiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index bbc527c..b2e8a16 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -214,7 +214,7 @@ def differentiate(self, wrt: Union['Variable', str]) -> 'Function': # Move across inputs for i, input in enumerate(self.inputs): # Get respective derivative - partial = Function(self.derivatives[i], None) + partial = self.derivatives[i](*self.inputs) partial.register(*self.inputs) func += partial * input.differentiate(wrt) From f18f25a92d3a08d0fef04004a98c99db8bd52083 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Thu, 3 Mar 2022 23:18:30 +1300 Subject: [PATCH 13/29] Clean up input setting --- jmath/approximation/autodiff.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index b2e8a16..39decff 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -46,19 +46,13 @@ def __call__(self, **kwargs): # Input collection inputs = [] - # Assigned variables collection - assigned = {} # Start computing inputs for input in self.inputs: if isinstance(input, Variable): # Variable case # Check if the variable has been assigned a value - if input.id in assigned.keys(): - inputs.append(assigned[input.id]) - elif input.id in kwargs.keys(): - # Else does kwargs have the value? - assigned[input.id] = kwargs[input.id] - inputs.append(assigned[input.id]) + if input.id in kwargs.keys(): + inputs.append(kwargs[input.id]) else: # There is no value for the variable??? # Throw a value error From 8b50bc71dcf7606d8dfc27d9be0cd826ca2363f8 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Mon, 7 Mar 2022 18:49:05 +1300 Subject: [PATCH 14/29] Predefine common variables --- jmath/approximation/autodiff.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index 39decff..905408e 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -4,8 +4,10 @@ # - Imports +from decimal import Clamped import operator as op import inspect +import string from ..uncertainties import Uncertainty from typing import Union, Callable, Tuple @@ -30,7 +32,7 @@ class Function: ''' def __init__(self, func: Callable, derivatives: Tuple[Callable]): - self.inputs = () + self.inputs = None self.func = func self.derivatives = derivatives @@ -41,7 +43,7 @@ def __init__(self, func: Callable, derivatives: Tuple[Callable]): def __call__(self, **kwargs): - if not(isinstance(self.func, Callable)): + if not isinstance(self.func, Callable): return self.func # Input collection @@ -208,8 +210,11 @@ def differentiate(self, wrt: Union['Variable', str]) -> 'Function': # Move across inputs for i, input in enumerate(self.inputs): # Get respective derivative - partial = self.derivatives[i](*self.inputs) - partial.register(*self.inputs) + partial = self.derivatives[i] + if isinstance(partial, Callable): + partial = analyse(partial) + if isinstance(partial, Function): + partial.register(*self.inputs) func += partial * input.differentiate(wrt) return func @@ -265,4 +270,12 @@ def analyse(f: Callable) -> Function: # Register the input variables f.register(*vars) # And return - return f \ No newline at end of file + return f + +# - Main + +# Define all english letters as 'Variable's +# This code is very silly +# I'm not sure it should stay here +for letter in string.ascii_lowercase: + globals()[letter] = Variable(letter) \ No newline at end of file From 8fd5456d155f07131657e2dbb77a2d4111b28449 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Mon, 7 Mar 2022 18:51:34 +1300 Subject: [PATCH 15/29] Extend to include uppercase --- jmath/approximation/autodiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jmath/approximation/autodiff.py b/jmath/approximation/autodiff.py index 905408e..27d0a70 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/approximation/autodiff.py @@ -277,5 +277,5 @@ def analyse(f: Callable) -> Function: # Define all english letters as 'Variable's # This code is very silly # I'm not sure it should stay here -for letter in string.ascii_lowercase: +for letter in string.ascii_letters: globals()[letter] = Variable(letter) \ No newline at end of file From c7ec0d39da50bcb27f46765295e27135f6882deb Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Thu, 10 Mar 2022 19:14:40 +1300 Subject: [PATCH 16/29] Seperate auto-differentiation package --- jmath/{approximation => }/autodiff.py | 3 +-- jmath/universal/tools.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) rename jmath/{approximation => }/autodiff.py (99%) diff --git a/jmath/approximation/autodiff.py b/jmath/autodiff.py similarity index 99% rename from jmath/approximation/autodiff.py rename to jmath/autodiff.py index 27d0a70..337ebdd 100644 --- a/jmath/approximation/autodiff.py +++ b/jmath/autodiff.py @@ -4,11 +4,10 @@ # - Imports -from decimal import Clamped import operator as op import inspect import string -from ..uncertainties import Uncertainty +from .uncertainties import Uncertainty from typing import Union, Callable, Tuple # - Typing diff --git a/jmath/universal/tools.py b/jmath/universal/tools.py index 8586460..38826fe 100644 --- a/jmath/universal/tools.py +++ b/jmath/universal/tools.py @@ -4,11 +4,10 @@ # - Imports -from email.mime import nonmultipart -from typing import Union, Callable, Tuple +from typing import Union, Callable from ..uncertainties import Uncertainty from ..units import Unit -from ..approximation.autodiff import Variable, Function +from ..autodiff import Variable, Function # - Typing From 3ae09bb7b678f41add1cbdfd5f1722c34df6806b Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Thu, 10 Mar 2022 19:14:51 +1300 Subject: [PATCH 17/29] Rebuild docs to match --- docs/source/jmath.approximation.rst | 8 -------- docs/source/jmath.rst | 8 ++++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/jmath.approximation.rst b/docs/source/jmath.approximation.rst index 0f5ac0f..50fbd6a 100644 --- a/docs/source/jmath.approximation.rst +++ b/docs/source/jmath.approximation.rst @@ -4,14 +4,6 @@ jmath.approximation package Submodules ---------- -jmath.approximation.autodiff module ------------------------------------ - -.. automodule:: jmath.approximation.autodiff - :members: - :undoc-members: - :show-inheritance: - jmath.approximation.differentiation module ------------------------------------------ diff --git a/docs/source/jmath.rst b/docs/source/jmath.rst index 4868b87..47f70fa 100644 --- a/docs/source/jmath.rst +++ b/docs/source/jmath.rst @@ -20,6 +20,14 @@ Subpackages Submodules ---------- +jmath.autodiff module +--------------------- + +.. automodule:: jmath.autodiff + :members: + :undoc-members: + :show-inheritance: + jmath.complex module -------------------- From bba1958c7a7644a3f72f0c3beb3a855c777aea11 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 20:36:28 +1300 Subject: [PATCH 18/29] Fix broken function derivatives --- jmath/autodiff.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/jmath/autodiff.py b/jmath/autodiff.py index 337ebdd..317bf41 100644 --- a/jmath/autodiff.py +++ b/jmath/autodiff.py @@ -8,7 +8,7 @@ import inspect import string from .uncertainties import Uncertainty -from typing import Union, Callable, Tuple +from typing import Any, Union, Callable, Tuple # - Typing @@ -211,8 +211,7 @@ def differentiate(self, wrt: Union['Variable', str]) -> 'Function': # Get respective derivative partial = self.derivatives[i] if isinstance(partial, Callable): - partial = analyse(partial) - if isinstance(partial, Function): + partial = Function(partial, 0) partial.register(*self.inputs) func += partial * input.differentiate(wrt) @@ -230,14 +229,18 @@ class Variable(Function): ''' def __init__(self, id = None): - super().__init__(lambda x: x, 1) + super().__init__(lambda x: x, None) self.id = id self.inputs = None self.derivatives = None + def __call__(self, input: Any) -> Any: + + return input + def differentiate(self, wrt: 'Variable') -> int: - if wrt == self or wrt == self.id: + if ((wrt == self) or (wrt == self.id and self.id is not None)): return 1 else: return 0 From 65c3bf17e665534f1d3959e914e589782d4a1798 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 20:36:40 +1300 Subject: [PATCH 19/29] Add some more commonly needed derivatives --- jmath/universal/hyperbolic.py | 4 ++-- jmath/universal/natural.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jmath/universal/hyperbolic.py b/jmath/universal/hyperbolic.py index df9ab5f..afcfe2f 100644 --- a/jmath/universal/hyperbolic.py +++ b/jmath/universal/hyperbolic.py @@ -19,7 +19,7 @@ def cosh(value: Supported) -> Supported: value The number to compute the hyberbolic cosine of. """ - return generic_function(math.cosh, value) + return generic_function(math.cosh, value, derivative = sinh) def acosh(value: Supported) -> Supported: """ @@ -43,7 +43,7 @@ def sinh(value: Supported) -> Supported: value The number to compute the hyberbolic sine of. """ - return generic_function(math.sinh, value) + return generic_function(math.sinh, value, derivative = cosh) def asinh(value: Supported) -> Supported: """ diff --git a/jmath/universal/natural.py b/jmath/universal/natural.py index 30d61ea..098a711 100644 --- a/jmath/universal/natural.py +++ b/jmath/universal/natural.py @@ -25,4 +25,4 @@ def exp(value: Supported) -> Supported: value The value to calculate the exponential of. """ - return generic_function(math.exp, value) \ No newline at end of file + return generic_function(math.exp, value, derivative = exp) \ No newline at end of file From f2d86145c9d18e1ce7ef05a0f912b0debac425b5 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 20:41:00 +1300 Subject: [PATCH 20/29] Minor tweaks --- jmath/autodiff.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jmath/autodiff.py b/jmath/autodiff.py index 317bf41..487ef82 100644 --- a/jmath/autodiff.py +++ b/jmath/autodiff.py @@ -7,6 +7,7 @@ import operator as op import inspect import string +from types import FunctionType from .uncertainties import Uncertainty from typing import Any, Union, Callable, Tuple @@ -210,7 +211,7 @@ def differentiate(self, wrt: Union['Variable', str]) -> 'Function': for i, input in enumerate(self.inputs): # Get respective derivative partial = self.derivatives[i] - if isinstance(partial, Callable): + if isinstance(partial, FunctionType): partial = Function(partial, 0) partial.register(*self.inputs) func += partial * input.differentiate(wrt) @@ -227,7 +228,7 @@ class Variable(Function): id Unique identifier string. ''' - def __init__(self, id = None): + def __init__(self, id: str = None): super().__init__(lambda x: x, None) self.id = id From 94bea30866e3ccaf052163c7701c689a4ef45d09 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 20:44:04 +1300 Subject: [PATCH 21/29] Multi-layer differentiability --- jmath/autodiff.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jmath/autodiff.py b/jmath/autodiff.py index 487ef82..51c02de 100644 --- a/jmath/autodiff.py +++ b/jmath/autodiff.py @@ -212,8 +212,7 @@ def differentiate(self, wrt: Union['Variable', str]) -> 'Function': # Get respective derivative partial = self.derivatives[i] if isinstance(partial, FunctionType): - partial = Function(partial, 0) - partial.register(*self.inputs) + partial = partial(*self.inputs) func += partial * input.differentiate(wrt) return func From 570d32faf8cbe9e0f7adc72a8c2217e31192a6b5 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 20:48:18 +1300 Subject: [PATCH 22/29] Clarify derivative definition --- jmath/autodiff.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jmath/autodiff.py b/jmath/autodiff.py index 51c02de..a89bd59 100644 --- a/jmath/autodiff.py +++ b/jmath/autodiff.py @@ -29,6 +29,7 @@ class Function: Represented function. derivatives Tuple of partial derivatives of the function with respect to function variables. + Note that derivatives may be numeric values or functions with built-in operations and Functions. ''' def __init__(self, func: Callable, derivatives: Tuple[Callable]): From fb475c32074e4cbb244686696e42f31098b4cded Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 20:56:26 +1300 Subject: [PATCH 23/29] Crappy function printing --- jmath/autodiff.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/jmath/autodiff.py b/jmath/autodiff.py index a89bd59..f442543 100644 --- a/jmath/autodiff.py +++ b/jmath/autodiff.py @@ -42,6 +42,25 @@ def __init__(self, func: Callable, derivatives: Tuple[Callable]): # If not then we shall make it one self.derivatives = (self.derivatives,) + def __str__(self): + + # Get parameters + params = tuple() + if self.inputs is not None: + params = tuple(str(input) for input in self.inputs) + + # Cases for operations + if self.func == op.mul: + return f"{params[0]}*{params[1]}" + elif self.func == op.truediv: + return f"{params[0]}/{params[1]}" + elif self.func == op.sub: + return f"{params[0]}-{params[1]}" + elif self.func == op.add: + return f"{params[0]}+{params[1]}" + + return f"{self.func.__name__}{str(params)[:-2]})" + def __call__(self, **kwargs): if not isinstance(self.func, Callable): @@ -235,6 +254,10 @@ def __init__(self, id: str = None): self.inputs = None self.derivatives = None + def __str__(self): + + return self.id + def __call__(self, input: Any) -> Any: return input From 48f480f69af3d6af010fc2f301d3ce14e7a73175 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 20:56:36 +1300 Subject: [PATCH 24/29] Tack this on too --- jmath/autodiff.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jmath/autodiff.py b/jmath/autodiff.py index f442543..800e1d6 100644 --- a/jmath/autodiff.py +++ b/jmath/autodiff.py @@ -59,6 +59,7 @@ def __str__(self): elif self.func == op.add: return f"{params[0]}+{params[1]}" + # Standard function return f"{self.func.__name__}{str(params)[:-2]})" def __call__(self, **kwargs): From b27b2e00d65f27352123b0c17247fdbf6f7f4a44 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 21:04:46 +1300 Subject: [PATCH 25/29] Short hand differentiation --- jmath/autodiff.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jmath/autodiff.py b/jmath/autodiff.py index 800e1d6..126e55b 100644 --- a/jmath/autodiff.py +++ b/jmath/autodiff.py @@ -7,6 +7,7 @@ import operator as op import inspect import string +from functools import wraps from types import FunctionType from .uncertainties import Uncertainty from typing import Any, Union, Callable, Tuple @@ -238,6 +239,10 @@ def differentiate(self, wrt: Union['Variable', str]) -> 'Function': return func + @wraps(differentiate) + def d(self, wrt: Union['Variable', str]) -> 'Function': + return self.differentiate(wrt) + class Variable(Function): ''' Variables for function differentiation. From 527c5aace3c79f4465f4e7e11ccbdc4281796305 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Fri, 11 Mar 2022 21:04:52 +1300 Subject: [PATCH 26/29] Start writing tests --- tests/test_autodiff.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_autodiff.py diff --git a/tests/test_autodiff.py b/tests/test_autodiff.py new file mode 100644 index 0000000..8ec20d4 --- /dev/null +++ b/tests/test_autodiff.py @@ -0,0 +1,18 @@ +''' + Tests the auto-differentiation module +''' + +# - Imports + +from ..jmath.autodiff import analyse, Variable, Function, x, y, z, a, b, c +from .tools import random_integer, repeat + +# - Tests + +@repeat +def test_linear_derivatives(): + '''Tests the derivatives of linear functions.''' + m = random_integer() + c = random_integer() + y = m*x + c + assert y.d(x) == m \ No newline at end of file From 19fb46fc3d6d0a7070f0c9ca9dc6866153243820 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sat, 12 Mar 2022 15:06:35 +1300 Subject: [PATCH 27/29] Minor differentiator tweaks --- jmath/autodiff.py | 10 +++++++++- jmath/universal/trigonometry.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/jmath/autodiff.py b/jmath/autodiff.py index 126e55b..fef80a6 100644 --- a/jmath/autodiff.py +++ b/jmath/autodiff.py @@ -135,7 +135,7 @@ def __rsub__(self, other: Supported) -> 'Function': return f else: # Variable case - f = Function(op.sub, (1, -1)) + f = Function(op.sub, (-1, 1)) f.register(self, other) return f @@ -144,6 +144,8 @@ def __mul__(self, other: Supported) -> 'Function': if other == 1: # Special case return self + elif other == 0: + return 0 elif isinstance(other, (int, float, Uncertainty)): # Numeric case f = Function(lambda x: other * x, other) @@ -155,6 +157,12 @@ def __mul__(self, other: Supported) -> 'Function': f.register(self, other) return f + def __neg__(self) -> 'Function': + + f = Function(op.neg, -1) + f.register(self) + return f + def __pow__(self, power: Union[int, float, Uncertainty]) -> 'Function': if power == 1: diff --git a/jmath/universal/trigonometry.py b/jmath/universal/trigonometry.py index aa5c94f..9f086b8 100644 --- a/jmath/universal/trigonometry.py +++ b/jmath/universal/trigonometry.py @@ -53,7 +53,7 @@ def cos(value: other.radian) -> Supported: value Value (in radians) to compute the cosine of ''' - return generic_function(math.cos, value, derivative = lambda x: -1 * sin(x)) + return generic_function(math.cos, value, derivative = lambda x: -sin(x)) @annotate def acos(value: Supported) -> other.radian: From 762d257c0bee7447b9830df70c869b0325c9cac9 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sat, 12 Mar 2022 15:15:23 +1300 Subject: [PATCH 28/29] Trigonometric tests --- tests/test_autodiff.py | 16 ++++++++++++++-- tests/tools.py | 10 +++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/test_autodiff.py b/tests/test_autodiff.py index 8ec20d4..492e68c 100644 --- a/tests/test_autodiff.py +++ b/tests/test_autodiff.py @@ -5,6 +5,8 @@ # - Imports from ..jmath.autodiff import analyse, Variable, Function, x, y, z, a, b, c +from ..jmath import sin, cos, ln, exp, Uncertainty +from math import pi from .tools import random_integer, repeat # - Tests @@ -12,7 +14,17 @@ @repeat def test_linear_derivatives(): '''Tests the derivatives of linear functions.''' - m = random_integer() + m = random_integer(non_zero = True) c = random_integer() y = m*x + c - assert y.d(x) == m \ No newline at end of file + assert y.d(x) == m + +@repeat +def test_sin_derivative(): + '''Tests derivatives of the sine function.''' + a = random_integer(non_zero = True) + f = random_integer(non_zero = True) + c = random_integer() + y = a*sin(f*x) + c + y_x = y.d(x) + assert y_x(x = 0) == a*f \ No newline at end of file diff --git a/tests/tools.py b/tests/tools.py index a540d5b..598f17c 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -21,7 +21,7 @@ def inner(): return inner -def random_integer(min: int = -100, max: int = 100) -> int: +def random_integer(min: int = -100, max: int = 100, non_zero = False) -> int: """ Generates a random integer. Wrapper of random.randint. @@ -33,8 +33,12 @@ def random_integer(min: int = -100, max: int = 100) -> int: max The maximum value int to produce. """ - - return randint(min, max) + r = 0 + while r == 0: + r = randint(min, max) + if not non_zero: + break + return r def random_integers(length: int): """ From 382e19f49d720b3572533eb607889b9341931a28 Mon Sep 17 00:00:00 2001 From: Jordan Hay Date: Sat, 12 Mar 2022 16:48:31 +1300 Subject: [PATCH 29/29] More autodiff tests! --- tests/test_autodiff.py | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/test_autodiff.py b/tests/test_autodiff.py index 492e68c..f41803a 100644 --- a/tests/test_autodiff.py +++ b/tests/test_autodiff.py @@ -4,21 +4,27 @@ # - Imports -from ..jmath.autodiff import analyse, Variable, Function, x, y, z, a, b, c -from ..jmath import sin, cos, ln, exp, Uncertainty -from math import pi +from ..jmath.autodiff import x, y, z, a, b, c +from ..jmath import sin, ln, exp from .tools import random_integer, repeat # - Tests @repeat -def test_linear_derivatives(): +def test_linear_derivative(): '''Tests the derivatives of linear functions.''' m = random_integer(non_zero = True) c = random_integer() y = m*x + c assert y.d(x) == m +@repeat +def test_power_rule(): + '''Tests the power rule.''' + n = random_integer(2, 100) + y = x**n + assert y.d(x)(x = 1) == n + @repeat def test_sin_derivative(): '''Tests derivatives of the sine function.''' @@ -27,4 +33,25 @@ def test_sin_derivative(): c = random_integer() y = a*sin(f*x) + c y_x = y.d(x) - assert y_x(x = 0) == a*f \ No newline at end of file + assert y_x(x = 0) == a*f + +@repeat +def test_natural_log_derivative(): + '''Tests the derivative of the natural log.''' + a = random_integer(non_zero = True) + y = a*ln(x) + assert y.d(x)(x = 1) == a + +@repeat +def test_exponential_chain_rule(): + '''Tests the chain rule with the natural log.''' + a = random_integer(non_zero = True) + b = random_integer(non_zero = True) + y = a*exp(b * x) + assert y.d(x)(x = 0) == a*b + +def test_trivial_partial(): + '''Tests a trivial partial derivative example.''' + f = x * y + assert f.d(x) == y + assert f.d(y) == x \ No newline at end of file