Derivatives Lab Solution#

This lab allows students to experiment with different differentiation methods, including symbolic, finite difference, complex step differentiation, and automatic differentiation.

Symbolic Differentiation with SymPy#

We first try out symbolic differentiation using scipy. This is very similar to the class notes, except we will take derivatives of the \(\text{sinc}(x) = \sin(x) / x\).

# HANDSON: make sure that sympy is installed

!pip install sympy

import sympy as sp
Requirement already satisfied: sympy in /opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/site-packages (1.13.3)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/site-packages (from sympy) (1.3.0)
# HANDSON: use sympy to define the equation f(x) = sin(x) / x ...

x = sp.symbols('x')
f = sp.sin(x) / x
# HANDSON: ... and get its derivative

f_prime = sp.diff(f, x)
f_prime_simplified = sp.simplify(f_prime)
# Display f'(x)

f_prime
\[\displaystyle \frac{\cos{\left(x \right)}}{x} - \frac{\sin{\left(x \right)}}{x^{2}}\]
# Display simplified f'(x)

f_prime_simplified
\[\displaystyle \frac{x \cos{\left(x \right)} - \sin{\left(x \right)}}{x^{2}}\]
# We didn't do this in the notes,
# but it is possible to obtain numerical values of functions from sympy.

display(f.evalf(subs={x:1}))
display(f_prime_simplified.evalf(subs={x:1}))
\[\displaystyle 0.841470984807897\]
\[\displaystyle -0.301168678939757\]
# HANDSON: modify function `f(x)` and confirm that `sympy` is able to
# compute its derivatives

pass

To help visualize the results of derivative, let’s copy the plotting function from the class notes here:

import numpy as np
from matplotlib import pyplot as plt
from typing import Callable

def mkplot(g, X, Fp):
    if isinstance(g, Callable):
        f = g
    else:
        f = lambda x: g.evalf(subs={'x': x})
        
    Xd = np.linspace(min(X), max(X), num=1001)
    Fd = [f(x) for x in Xd]
    
    plt.plot(Xd, Fd, lw=5, alpha=0.25)
    for (x, fp) in zip(X, Fp):
        y = f(x)
        plt.plot(
            [x-0.05,    x+0.05],
            [y-0.05*fp, y+0.05*fp],
        )
X       = range(10)
F_prime = [f_prime.evalf(subs={'x':x}) for x in X]

mkplot(f, X, F_prime)
../_images/5a65d88fc3e168079f2c374f37cf97b5496d7910c91c94cc0b99c4898b84294b.png

Enhancing Dual Number Autodiff#

In the class, we implemented a Dual-number based autodiff scheme. It supports many basic operators except division and power.

Try implementing these extra operators and test them out with our visualization tool.

def V(x):
    """Select the value from a dual number.

    Work for both python built-in numbers (often used in function) and dual numbers.
    """
    if isinstance(x, Dual):
        return x[0]
    else:
        return x

def D(x):
    """Select the derivative from a dual number.

    Work for both python built-in numbers (often used in function) and dual numbers.
    """
    if isinstance(x, Dual):
        return x[1]
    else:
        return 0
class Dual(tuple):
    """Dual number for implementing autodiff in pure python"""

    def __new__(self, v, d=1): # tuple is immutable so we cannot use __init__()
        return tuple.__new__(Dual, (v, d))

    def __add__(self, r):
        return Dual(
            V(self) + V(r),
            D(self) + D(r),
        )
    def __radd__(self, l):
        return self + l # addition commutes

    def __sub__(self, r):
        return Dual(
            V(self) - V(r),
            D(self) - D(r),
        )
    def __rsub__(self, l):
        return Dual(
            V(l) - V(self),
            D(l) - D(self),
        )

    def __mul__(self, r):
        return Dual(
            V(self) * V(r),
            D(self) * V(r) + V(self) * D(r),
        )
    def __rmul__(self, l):
        return self * l # multiplication commutes

    def __truediv__(self, r):
        return Dual(
            V(self) / V(r),
            (D(self) * V(r) - V(self) * D(r)) / (V(r) * V(r)), # HANDSON: implement chain-rule for division
        )
    def __rtruediv__(self, l):
        return Dual(
            V(l) / V(self),
            (D(l) * V(self) - V(l) * D(self)) / (V(self) * V(self)), # HANDSON: implement chain-rule for division
        )

    def __pow__(self, r): # assume r is constant
        if r == 0:
            n = len(V(self))
            return Dual(np.ones(n), np.zeros(n)) # HANDSON: implement chain-rule for power
        elif r == 1:
            n = len(V(self))
            return Dual(V(self), np.ones(n)) # HANDSON: implement chain-rule for power
        else:
            return Dual(
                V(self)**r,
                r*(V(self)**(r-1)) * D(self), # HANDSON: implement chain-rule for power
            )
def sin(x):
    return Dual(
        np.sin(V(x)),
        np.cos(V(x)) * D(x)  # chain rule: d/dx sin(x) = cos(x) * x'
    )
def f(x):
    return x**3
    # return sin(x) / x

X     = np.linspace(1,2,num=11)
F, Fp = f(Dual(X))

mkplot(f, X, Fp)
../_images/90732b4f5eca0724745025f6e2a23317b94b86197645749e96a50534c40f01c6.png