Source code for dual_autodiff.autodiff_tools

from .dual import Dual
import numpy as np
from math import (
    sin as math_sin,
    cos as math_cos,
    tan as math_tan,
    asin as math_asin,
    acos as math_acos,
    atan as math_atan,
    sinh as math_sinh,
    cosh as math_cosh,
    tanh as math_tanh,
    exp as math_exp,
    log as math_log,
    sqrt as math_sqrt,
    pow as math_pow,
)


# This file integrated the class's mathema functions with the math module's functions
# Allows more seemless integration of the Dual class with the math module


# Call Dual's sin() method
[docs]def sin(x): """ Compute the sin of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The sine of the input. Examples: >>> from dual_autodiff import sin, Dual 1. With a scalar input: >>> sin(0) 0.0 2. With a Dual number input: >>> sin(Dual(0, 1)) Dual(real=0.0, dual=1.0) 3. With a Dual numpy array input: >>> sin(np.array([Dual(2,3), Dual(3,4)])) array([Dual(real=0.9093..., dual=-1.2484...), Dual(real=0.1411..., dual=-3.9600...)], dtype=object) """ # If input is a Dual number, call the sin() method from Dual class if isinstance(x, Dual): return x.sin() # If input is a numpy array, call the np.sin method which will call the Dual class's sin method if isinstance(x, np.ndarray): return np.sin(x) # If input is a scalar, call the math.sin method return math_sin(x)
# Call Dual's cos() method
[docs]def cos(x): """ Compute the cos of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The cosine of the input. Examples: >>> from dual_autodiff import cos, Dual 1. With a scalar input: >>> cos(0) 1.0 2. With a Dual number input: >>> cos(Dual(0, 1)) Dual(real=1.0, dual=0.0) 3. With a Dual numpy array input: >>> cos(np.array([Dual(2,3), Dual(3,4)])) array([Dual(real=-0.4161..., dual=-2.7278...), Dual(real=-0.9899..., dual=-0.1411...)], dtype=object) """ # If input is a Dual number, call the cos() method from Dual class if isinstance(x, Dual): return x.cos() # If input is a numpy array, call the np.cos method which will call the Dual class's cos method if isinstance(x, np.ndarray): return np.cos(x) # If input is a scalar, call the math.cos method return math_cos(x)
# Call Dual's tan() method # Errors thrown by Dual class (dual.py) automatically
[docs]def tan(x): """ Compute the tangent of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The tangent of the input. Raises: ValueError: The tangent function is undefined as cosine of real part equals 0. Examples: >>> from dual_autodiff import tan, Dual 1. With a scalar input: >>> tan(0) 0.0 2. With a Dual number input: >>> tan(Dual(0, 1)) Dual(real=0.0, dual=1.0) 3. With a Dual numpy array input: >>> tan(np.array([Dual(2,3), Dual(3,4)])) array([Dual(real=-2.1850..., dual=5.7744...), Dual(real=-0.1425..., dual=16.2574...)], dtype=object) """ # If input is a Dual number, call the tan() method from Dual class if isinstance(x, Dual): return x.tan() # If input is a numpy array, call the np.tan method which will call the Dual class's tan method if isinstance(x, np.ndarray): return np.tan(x) # If input is a scalar, call the math.tan method return math_tan(x)
# Call Dual's arcsin() method # Errors thrown by Dual class (dual.py) automatically
[docs]def arcsin(x): """ Compute the arcsine of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The arcsine of the input. Raises: ValueError: If a real part is outside the range [-1, 1]. Examples: >>> from dual_autodiff import arcsin, Dual 1. With a scalar input: >>> arcsin(0) 0.0 2. With a Dual number input: >>> arcsin(Dual(0, 1)) Dual(real=0.0, dual=1.0) 3. With a Dual numpy array input: >>> arcsin(np.array([Dual(0.5,1), Dual(0.3,2)])) array([Dual(real=0.5235..., dual=1.1547...), Dual(real=0.3046..., dual=2.0910...)], dtype=object) """ # If input is a Dual number, call the arcsin() method from Dual class if isinstance(x, Dual): return x.arcsin() # If input is a numpy array, call the np.arcsin method which will call the Dual class's arcsin method if isinstance(x, np.ndarray): return np.arcsin(x) # If input is a scalar, call the math.asin method return math_asin(x)
# Call Dual's arccos() method # Errors thrown by Dual class (dual.py) automatically
[docs]def arccos(x): """ Compute the arccosine of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Raises: ValueError: If the real part is outside the range [-1, 1]. Examples: >>> from dual_autodiff import arccos, Dual 1. With a scalar input: >>> arccos(1) 0.0 2. With a Dual number input: >>> arccos(Dual(1, 1)) Dual(real=0.0, dual=-1.0) 3. With a Dual numpy array input: >>> arccos(np.array([Dual(0.5,1), Dual(0.3,2)])) array([Dual(real=1.0471..., dual=-1.1547...), Dual(real=1.2661..., dual=-2.0910...)], dtype=object) """ # If input is a Dual number, call the acos() method from Dual class if isinstance(x, Dual): return x.arccos() # If input is a numpy array, call the np.arccos method which will call the Dual class's acos method if isinstance(x, np.ndarray): return np.arccos(x) # If input is a scalar, call the math.acos method return math_acos(x)
# Call Dual's arctan() method
[docs]def arctan(x): """ Compute the arctangent of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The arctangent of the input. Examples: >>> from dual_autodiff import arctan, Dual 1. With a scalar input: >>> arctan(1) 0.7853981633974483 2. With a Dual number input: >>> arctan(Dual(1, 1)) Dual(real=0.7853981633974483, dual=0.5) 3. With a Dual numpy array input: >>> arctan(np.array([Dual(1,1), Dual(0.5,2)])) array([Dual(real=0.7853..., dual=0.5), Dual(real=0.4636..., dual=1.6)], dtype=object) """ # If input is a Dual number, call the atan() method from Dual class if isinstance(x, Dual): return x.arctan() # If input is a numpy array, call the np.arctan method which will call the Dual class's atan method if isinstance(x, np.ndarray): return np.arctan(x) # If input is a scalar, call the math.atan method return math_atan(x)
# Call Dual's sinh() method
[docs]def sinh(x): """ Compute the hyperbolic sine of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The hyperbolic sine of the input. Examples: >>> from dual_autodiff import sinh, Dual 1. With a scalar input: >>> sinh(1) 1.1752011936438014 2. With a Dual number input: >>> sinh(Dual(1, 1)) Dual(real=1.1752011936438014, dual=1.5430806348152437) 3. With a Dual numpy array input: >>> sinh(np.array([Dual(1,1), Dual(0.5,2)])) array([Dual(real=1.1752..., dual=1.5430...), Dual(real=0.5210..., dual=2.1276...)], dtype=object) """ # If input is a Dual number, call the sinh() method from Dual class if isinstance(x, Dual): return x.sinh() # If input is a numpy array, call the np.sinh method which will call the Dual class's sinh method if isinstance(x, np.ndarray): return np.sinh(x) # If input is a scalar, call the math.sinh method return math_sinh(x)
# Call Dual's cosh() method
[docs]def cosh(x): """ Compute the hyperbolic cosine of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The hyperbolic cosine of the input. Examples: >>> from dual_autodiff import cosh, Dual 1. With a scalar input: >>> cosh(1) 1.5430806348152437 2. With a Dual number input: >>> cosh(Dual(1, 1)) Dual(real=1.5430806348152437, dual=1.1752011936438014) 3. With a Dual numpy array input: >>> cosh(np.array([Dual(1,1), Dual(0.5,2)])) array([Dual(real=1.5430..., dual=1.1752...), Dual(real=1.1276..., dual=2.5210...)], dtype=object) """ # If input is a Dual number, call the cosh() method from Dual class if isinstance(x, Dual): return x.cosh() # If input is a numpy array, call the np.cosh method which will call the Dual class's cosh method if isinstance(x, np.ndarray): return np.cosh(x) # If input is a scalar, call the math.cosh method return math_cosh(x)
# Call Dual's tanh() method
[docs]def tanh(x): """ Compute the hyperbolic tangent of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The hyperbolic tangent of the input. Examples: >>> from dual_autodiff import tanh, Dual 1. With a scalar input: >>> tanh(1) 0.7615941559557649 2. With a Dual number input: >>> tanh(Dual(1, 1)) Dual(real=0.7615941559557649, dual=0.41997434161402614) 3. With a Dual numpy array input: >>> tanh(np.array([Dual(1,1), Dual(0.5,2)])) array([Dual(real=0.7615..., dual=0.4199...), Dual(real=0.4621..., dual=1.7864...)], dtype=object) """ # If input is a Dual number, call the tanh() method from Dual class if isinstance(x, Dual): return x.tanh() # If input is a numpy array, call the np.tanh method which will call the Dual class's tanh method if isinstance(x, np.ndarray): return np.tanh(x) # If input is a scalar, call the math.tanh method return math_tanh(x)
# Call Dual's exp() method
[docs]def exp(x): """ Compute the exponential of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The exponential of the input. Examples: >>> from dual_autodiff import exp, Dual 1. With a scalar input: >>> exp(1) 2.718281828459045 2. With a Dual number input: >>> exp(Dual(1, 1)) Dual(real=2.718281828459045, dual=2.718281828459045) 3. With a Dual numpy array input: >>> exp(np.array([Dual(1,1), Dual(0.5,2)])) array([Dual(real=2.7182..., dual=2.7182...), Dual(real=1.6487..., dual=3.2974...)], dtype=object) """ # If input is a Dual number, call the exp() method from Dual class if isinstance(x, Dual): return x.exp() # If input is a numpy array, call the np.exp method which will call the Dual class's exp method if isinstance(x, np.ndarray): return np.exp(x) # If input is a scalar, call the math.exp method return math_exp(x)
# Call Dual's log() method # Errors thrown by Dual class (dual.py) automatically
[docs]def log(x): """ Compute the natural logarithm of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The natural logarithm of the input. Raises: ValueError: If a real part of dual number is non-positive Examples: >>> from dual_autodiff import log, Dual 1. With a scalar input: >>> log(1) 0.0 2. With a Dual number input: >>> log(Dual(1, 1)) Dual(real=0.0, dual=1.0) 3. With a Dual numpy array input: >>> log(np.array([Dual(1,1), Dual(2,2)])) array([Dual(real=0.0, dual=1.0), Dual(real=0.6931..., dual=1.0)], dtype=object) """ # If input is a Dual number, call the log() method from Dual class if isinstance(x, Dual): return x.log() # If input is a numpy array, call the np.log method which will call the Dual class's log method if isinstance(x, np.ndarray): return np.log(x) # If input is a scalar, call the math.log method return math_log(x)
# Call Dual's sqrt() method # Errors thrown by Dual class (dual.py) automatically
[docs]def sqrt(x): """ Compute the square root of a number, a Dual number, or a numpy array of Dual numbers. Parameters: x (float, Dual, or numpy.ndarray): The input value/values. Returns: float, Dual, or numpy.ndarray: The square root of the input. Raises: ValueError: If the real part of dual number is negative. Examples: >>> from dual_autodiff import sqrt, Dual 1. With a scalar input: >>> sqrt(4) 2.0 2. With a Dual number input: >>> sqrt(Dual(4, 1)) Dual(real=2.0, dual=0.25) 3. With a Dual numpy array input: >>> sqrt(np.array([Dual(4,1), Dual(9,2)])) array([Dual(real=2.0, dual=0.25), Dual(real=3.0, dual=0.3333...)], dtype=object) """ # If input is a Dual number, call the sqrt() method from Dual class if isinstance(x, Dual): return x.sqrt() # If input is a numpy array, call the np.sqrt method which will call the Dual class's sqrt method if isinstance(x, np.ndarray): return np.sqrt(x) # If input is a scalar, call the math.sqrt method return math_sqrt(x)
# Call Dual's pow() method # Errors thrown by Dual class (dual.py) automatically
[docs]def pow(x, n): """ Compute a number, a Dual number, or a numpy array of Dual numbers raised to a power. Parameters: x (float, Dual, or numpy.ndarray): The base. n (float): The exponent. Returns: float, Dual, or numpy.ndarray: The result of raising `x` to the power `n`. Raises: TypeError: If n is not an int or float. Examples: >>> from dual_autodiff import pow, Dual 1. With a scalar input: >>> pow(2, 3) 8 2. With a Dual number input: >>> pow(Dual(2, 1), 3) Dual(real=8, dual=12.0) 3. With a Dual numpy array input: >>> pow(np.array([Dual(2,1), Dual(3,2)]), 3) array([Dual(real=8, dual=12.0), Dual(real=27, dual=54.0)], dtype=object) """ # If input is a Dual number, call the pow() method from Dual class if isinstance(x, Dual): return x.pow(n) # If input is a numpy array, call the np.power method which will call the Dual class's pow method if isinstance(x, np.ndarray): return np.power(x, n) # If input is a scalar, call the math.pow method return math_pow(x, n)
# Evaluates a function on a dual number and returns the dual part of result # Corresponds to derivative - ie performs automatic differentiation
[docs]def auto_diff(func, x): """ Evaluates the derivative of a function f at x using Dual number: x + ε. Parameters: func (callable): The function to differentiate. x (float, int, or numpy.ndarray): The point(s) where the derivative is evaluated. Returns: tuple: A tuple containing the real and dual parts of the result. If x is a scalar, returns (real, dual). If x is a numpy array, returns two numpy arrays (real_array, dual_array). Raises: TypeError: If func is not callable. TypeError: If input x is not a float, int, or numpy.ndarray containing scalar values. Examples: >>> from dual_autodiff import auto_diff 1. With a scalar input: >>> auto_diff(lambda x: x**3 + 2*x**2 + x, 2) (14.0, 17.0) 2. With a numpy array input: >>> auto_diff(lambda x: x**3 + 2*x**2 + x, np.array([1, 2, 3])) (array([ 4.0, 14.0, 34.0]), array([ 4.0, 17.0, 40.0])) """ # Check that func is callable if not callable(func): raise TypeError(f"func must be a callable function, got {type(func).__name__}.") # Check that input x is a scalar (float/int) or numpy array if not isinstance(x, (float, int, np.ndarray)): raise TypeError(f"x must be a scalar number (float/int) or numpy.ndarray, got {type(x).__name__}.") if isinstance(x, (float, int)): # If x is a scalar, evaluate the function directly value = func(Dual(x, 1)) # This accounts for the case in which the function is constant and therefore returns a constant non-dual number # Assumes this is the case and creates a dual number with derivative 0 if not isinstance(value, Dual): value = Dual(value, 0) else: # Checks that is x is a numpy array, it contains scalar values if not np.issubdtype(x.dtype, np.number): raise TypeError("numpy.ndarray must contain scalar values (float/int).") # Compute the real and dual parts # As above accounts for the case in which the function is constant and therefore returns a constant non-dual number real_parts = [func(Dual(float(xi), 1)).real if isinstance(func(Dual(float(xi), 1)), Dual) else func(Dual(float(xi), 1)) for xi in x] dual_parts = [func(Dual(float(xi), 1)).dual if isinstance(func(Dual(float(xi), 1)), Dual) else 0 for xi in x] return np.array(real_parts), np.array(dual_parts) return value.real, value.dual
# Evaluates multiple functions value and derivative at x using Dual number/ automatic differentiation # Input is a list of functions and a point x/ array of points # Returns a list of tuples, each containing the real and dual parts of the result for each function # uses auto_diff to evaluate each function
[docs]def multi_auto_diff(funcs, x): """ Evaluates the derivatives of multiple functions at x using Dual number: x + ε. Parameters: funcs (list of callables): The functions to differentiate. x (float, int, or numpy.ndarray): The point(s) where the derivatives are evaluated. Returns: list of tuples: A list of tuples, each containing the real and dual parts of the result for each function. If x is a scalar, each tuple is (real, dual). If x is a numpy array, each tuple contains two numpy arrays (real_array, dual_array). Raises: TypeError: If any func in funcs is not callable. TypeError: If input x is not a float, int, or numpy.ndarray containing scalar values. Examples: >>> from dual_autodiff import multi_auto_diff 1. With a scalar input: >>> multi_auto_diff([lambda x: x**3, lambda x: x**2], 2) [(8.0, 12.0), (4.0, 4.0)] 2. With a numpy array input: >>> multi_auto_diff([lambda x: x**3, lambda x: x**2], np.array([1, 2, 3])) [(array([ 1.0, 8.0, 27.0]), array([ 3.0, 12.0, 27.0])), (array([1.0, 4.0, 9.0]), array([2.0, 4.0, 6.0]))] """ results = [] for func in funcs: if not callable(func): raise TypeError(f"Each func must be a callable function, got {type(func).__name__}.") results.append(auto_diff(func, x)) # output structure: [[func1_f(x)], [func1_f'(x)], [func2_f(x)], [func2_f'(x)], ...] return results