Calculus tutorial

Contents

Calculus tutorial#

Click the binder button Binder or this link bit.ly/calctut3 to run this notebooks interactively.

Abstract Calculus is the study of the properties of functions. The operations of calculus are used to describe the limit behaviour of functions, calculate their rates of change, and calculate the areas under their graphs. In this section we’ll learn about the SymPy functions for calculating limits, derivatives, integrals, and summations.

%pip install --quiet  sympy numpy scipy seaborn ministats
Note: you may need to restart the kernel to use updated packages.
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

sns.set_theme(
    context="paper",
    style="whitegrid",
    palette="colorblind",
    rc={"font.family": "serif",
        "font.serif": ["Palatino", "DejaVu Serif", "serif"],
        "figure.figsize": (5, 1.7)},
)
%config InlineBackend.figure_format = 'retina'

# simple float __repr__
np.set_printoptions(legacy='1.25')
# Temporary for figure generation
import os
from ministats.utils import savefigure
# Temporary to extract ASCII text for codeblocks
from sympy import init_printing
init_printing()
# init_printing(pretty_print=False)

Introduction#

Example 1: file download#

Download rate#

Inverse problem#

def fprime(t):
    return 2   # [MB/sec]

dt = 1         # [sec]
tau = 10  # after ten seconds
sum([fprime(t)*dt for t in range(0,tau)])
../_images/2844642a1dd79e33539127d4749a3d5285c4e07c7a81d7da988f995106bf0a8a.png
tau = 360  # after 360 seconds
sum([fprime(t)*dt for t in range(0,tau)])
../_images/fa49a3a12df7fcb4b93878f7749d425675c147a36b26cda264e9f67c1278bfb4.png

Infinity#

Example 2: Euler’s number#

(1+1/2)**2
../_images/e18ac381ef5f5d014cf3e07249a16db52fd05c3eb885bbccabc9cd0246e0587a.png
(1+1/3)**3
../_images/a57e22bb5f3d00aadec04fbd64e94f507e05b772feaad7c6a10a8a591f10aaef.png
(1+1/4)**4
../_images/27e24c3191961bc83a38bd359e9776d25bff2a5daba34f0ac70cc41fe2343f8c.png
(1+1/12)**12
../_images/bfddccafc909b8695097239c268ed8c592446edd492273606a539421e83d7217.png
(1+1/365)**365
../_images/bbd757f87c535e6c4ee53e01691deb11bd7db0a12e20f65e02934928f15e17ec.png
(1+1/1000)**1000
../_images/28e8caae135d8ad837ef29367aec46baef2316f9a597e899397db697a6a7ef7e.png
import sympy as sp
n = sp.symbols("n")
sp.limit( (1+1/n)**n, n, sp.oo).evalf()
../_images/b3fff4236437650c68764260e7facbe3ba21a671c9d807a594e24b8c660a59e6.png

Applications#

Doing calculus#

Symbolic calculations using pen and paper#

Symbolic calculations using SymPy#

%pip install -q sympy
Note: you may need to restart the kernel to use updated packages.
import sympy as sp

# define symbolic variable x
x = sp.symbols("x")

The symbols function creates SymPy symbolic variables. Unlike ordinary Python variables that hold a particular value, SymPy variables act as placeholders that can take on any value. We can use the symbolic variables to create expressions, just like we do with math variables in pen-and-paper calculations.

expr = 4 - x**2
expr
../_images/a9bddc012c3aafd28f6542bca72c856025572c3b7984dec69a778a3e4d8bf8e2.png
sp.factor(expr)
../_images/9a09c1bff5f2b1e6485a22f645845a8067b3e2e636d630f21dc35526ea7c41aa.png

To substitute a given value into an expression, call the .subs() method, passing in a python dictionary object { key:val, ... } with the symbol–value substitutions you want to make:

expr.subs({x:1})
../_images/e1cffecb0770426ef56a147187234b3a65058b23f4f24134ae7f8a495737c5d5.png
sp.solve(expr, x)
../_images/4ca89f87843cd35c3adb922a1cd6a1d587c72d7718673d36f2c230a431431919.png

The function solve is the main workhorse in SymPy. This incredibly powerful function knows how to solve all kinds of equations. In fact solve can solve pretty much any equation! When high school students learn about this function, they get really angry—why did they spend five years of their life learning to solve various equations by hand, when all along there was this solve thing that could do all the math for them? Don’t worry, learning math is never a waste of time.

The function solve takes two arguments. Use solve(expr,var) to solve the equation expr==0 for the variable var. You can rewrite any equation in the form expr==0 by moving all the terms to one side of the equation; the solutions to \(A(x) = B(x)\) are the same as the solutions to \(A(x) - B(x) = 0\).

For example, to solve the quadratic equation \(x^2 + 2x - 8 = 0\), use

sp.solve( x**2 + 2*x - 8, x)
../_images/b95aff3fd9bdad6adb2f4a47feb30ea9d45ebfc0cd23a86e7d912f4803810d06.png

In this case the equation has two solutions so solve returns a list. Check that \(x = 2\) and \(x = -4\) satisfy the equation \(x^2 + 2x - 8 = 0\).

Simplify any math expression

sp.simplify(2*x + 3*x - sp.sin(x) + 42)
../_images/2152b5720bbc448ce14ab582609cad160367cdf7fbcaade2b8e4b5b0f0aae717.png
sp.expand( (x-4)*(x+2) )
../_images/3660ed9dbabad02031becbc5b992d002d3c6f9b3e03e78beb1626f6e2c4c60cf.png

Collect the terms in expression that contain different powers of x.

a, b, c = sp.symbols("a b c")
sp.collect(x**2 + x*b + a*x + a*b, x)
../_images/8cc2a3c22f5d41fadf2123d474726d6110c3ba97afcb66499b26301dd312efaf.png

Numerical computing using NumPy#

%pip install -q numpy
Note: you may need to restart the kernel to use updated packages.
import numpy as np
np.array([1.0, 1.1, 1.2, 1.3, 1.4, 1.5])
array([1. , 1.1, 1.2, 1.3, 1.4, 1.5])
np.linspace(1.0, 1.5, 6)
array([1. , 1.1, 1.2, 1.3, 1.4, 1.5])
xs = np.linspace(-2, 2, 21)
xs
array([-2. , -1.8, -1.6, -1.4, -1.2, -1. , -0.8, -0.6, -0.4, -0.2,  0. ,
        0.2,  0.4,  0.6,  0.8,  1. ,  1.2,  1.4,  1.6,  1.8,  2. ])
def h(x):
    return 4 - x**2

fhs = h(xs)
fhs
array([0.  , 0.76, 1.44, 2.04, 2.56, 3.  , 3.36, 3.64, 3.84, 3.96, 4.  ,
       3.96, 3.84, 3.64, 3.36, 3.  , 2.56, 2.04, 1.44, 0.76, 0.  ])

Scientific computing using SciPy#

%pip install -q scipy
Note: you may need to restart the kernel to use updated packages.
from scipy.integrate import quad
quad(h, 0, 2)[0]
../_images/75659015538d12b05b9a821c66c75dc382ef4653c768a3686bc395ce60818f5b.png

Math prerequisites#

Notation for sets and intervals#

S = {1, 2, 3}
T = {3, 4, 5, 6}

print("S ∪ T =", S.union(T))
print("S ∩ T =", S.intersection(T))
print("S \\ T =", S.difference(T))
S ∪ T = {1, 2, 3, 4, 5, 6}
S ∩ T = {3}
S \ T = {1, 2}

The number line#

Number intervals#

Functions#

In Python, we define functions using the def keyword.

For example, the code cell below defines the function \(f(x)=\frac{1}{2}x^2\), then evaluate it for the input \(x=3\).

# define the function f that takes input x
def f(x):
    return 0.5 * x**2

# calling the function g on input x=4
f(3)
../_images/3b7b0268d85cf01af740f721af40e4058724d86dd2c18b0b88cd9954b8dbf7de.png

Function graphs#

The graph of the function \(f(x)\) is obtained by plotting a curve that passes through the set of input-output coordinate pairs \((x, f(x))\).

import numpy as np
xs = np.linspace(-3, 3, 1000)
fxs = f(xs)
import seaborn as sns
sns.lineplot(x=xs, y=fxs);

# FIGURES ONLY
ax = plt.gca()
ax.set_yticks([0,1,2,3,4])
ax.set_xlabel("$x$")
ax.set_ylabel("$f(x)$")
filename = os.path.join("figures/tutorials/calculus", "graph_of_function_f_eq_halfx2.pdf")
savefigure(plt.gca(), filename)
Saved figure to figures/tutorials/calculus/graph_of_function_f_eq_halfx2.pdf
Saved figure to figures/tutorials/calculus/graph_of_function_f_eq_halfx2.png
../_images/ce21f66a9f6dce67eb52b1fa9872a1cd03f6e60f0002ca21a9a065e0c60bf72c.png

The function linspace(-3,3,1000) creates an array of 1000 points in the interval \([-3,3]\). We store this sequence of inputs into the variable xs. Next, we computer the output value \(f(x)\) for each of the inputs in the array xs, and store the result in the array fxs. Finally, we use the function sns.lineplot() to generate the plot.

from ministats.calculus import plot_func

plot_func(f, xlim=[-3,3]);

Inverse functions#

The inverse function \(f^{-1}\) performs the \emph{inverse operation} of the function \(f\). If you start from some \(x\), apply \(f\), then apply \(f^{-1}\), you’ll arrive—full circle—back to the original input \(x\):

\[ f^{-1}\!\big( \; f(x) \; \big) = x. \]

The inverse of the function \(f(x) = \frac{1}{2}x^2\) is the function \(f^{-1}(x) = \sqrt{2x}\). Earlier we computed \(f(3) = 4.5\). If we apply the inverse function to \(4.5\), we get \(f^{-1}(4.5) = \sqrt{2 \cdot 4.5} = \sqrt{9} = 3\).

from math import sqrt
sqrt(2*4.5)
../_images/01ff05c9f653e52988c5d817ecc0384ce1b08b6523dc1cc7a301bd4cc575bc7d.png

The logarithmic function \(\log(x)\) is the inverse of the exponential function \(e^x\). If if we compute the exponential function of a number, then apply the logarithmic function, we get back the original input.

from math import exp, log
log(exp(5))
../_images/5841d7530d0200cec2091e23271a5512ff349a6f58cf0ff6545c002fd4a52fb9.png

Function properties#

Function inventory#

def line(x):
    return x

ax = plot_func(line, xlim=[-2,2])
ax.set_title("(a) Graph of the function $f(x) = x$");
# from ministats.calculus import plot_func

def plot_func(f, xlim=[-1,1], ylim=None, ax=None, title=None):
    xs = np.linspace(xlim[0], xlim[1], 1000)
    fxs = np.array([f(x) for x in xs])
    ax = sns.lineplot(x=xs, y=fxs, ax=ax)
    ax.set_title(title)
    ax.set_ylim(ylim)
    return ax

fig, axs = plt.subplots(2, 3, figsize=(7,4))


# (a) line
def line(x):
    return x
ax_a = axs[0][0]
plot_func(line, xlim=[-2,2], ylim=[-2,2], ax=ax_a, title="(a) $f(x) = x$")

# (b) quadratic
ax_b = axs[0][1]
def quadratic(x):
    return x**2
plot_func(quadratic, xlim=[-2,2], ylim=[-0.1,5], ax=ax_b, title="(b) $f(x) = x^2$")

# (c) exp
ax_c = axs[0][2]
plot_func(np.exp, xlim=[-2,2], ylim=[-0.2,8], ax=ax_c, title="(c) $f(x) = e^x$")

# (d) |x|
ax_d = axs[1][0]
plot_func(np.abs, xlim=[-2,2], ylim=[-2,2], ax=ax_d, title="(d) $f(x) = |x|$")

# (e) square root
ax_e = axs[1][1]
plot_func(np.sqrt, xlim=[0,4], ylim=[-0.1,2.8], ax=ax_e, title="(e) $f(x) = \\sqrt{x}$")
ax_e.set_xticks([0,1,2,3,4])

# (f) log
ax_f = axs[1][2]
plot_func(np.log, xlim=[0.00001,8], ylim=[-2,2.5], ax=ax_f, title="(f) $f(x) = \\ln(x)$")
ax_f.set_xlim([-0.3,8])

fig.tight_layout()

# FIGURES ONLY
filename = os.path.join("figures/tutorials/calculus", "panel_function_graphs1.pdf")
savefigure(plt.gca(), filename)
Saved figure to figures/tutorials/calculus/panel_function_graphs1.pdf
Saved figure to figures/tutorials/calculus/panel_function_graphs1.png
../_images/be421dde7352fcf848a1f1f235a3762eeb8e71db38c1b88218b0fdcf645e52ac.png

Geometry#

Circle#

import math
math.pi
../_images/97e6207cf713e2483356df0c6e00a2b2bb26657ea9a67580086563733b735276.png

Rectangle#

Triangle#

Trigonometry#

The trigonometric functions sin and cos take inputs in radians:

sp.sin(sp.pi/6)
../_images/ebcaf7d2bf4ae277423bd8f2c2b0ae26fa629bede7e45df7467354bf5000bd06.png
sp.cos(sp.pi/6)
../_images/5a9ef3835304aa457f2c0bf92f4b0a41dbf058b50cd78f5d4391e4592ec9b062.png

For angles in degrees, you need a conversion factor of \(\frac{\pi}{180}\)[rad/\(^\circ\)]:

sp.sin(30*sp.pi/180)  # 30 deg = pi/6 rads
../_images/ebcaf7d2bf4ae277423bd8f2c2b0ae26fa629bede7e45df7467354bf5000bd06.png
# TODO: move to ministats.figures

fig, axs = plt.subplots(1, 3, figsize=(7,2))

xlim = [-0.1, 4*np.pi+0.1]

# (a) sin
ax_a = axs[0]
plot_func(np.sin, xlim=xlim, ylim=[-2,2], ax=ax_a, title=r"(a) $f(\theta) = \sin(\theta)$")


# (b) cos
ax_b = axs[1]
plot_func(np.cos, xlim=xlim, ylim=[-2,2], ax=ax_b, title=r"(b) $f(\theta) = \cos(\theta)$")


# (c) tan
ax_c = axs[2]
def plot_tan(xlim=[-1,1], ylim=None, ax=None, title=None):
    num_periods = int(xlim[1] / np.pi)
    xleft = xlim[0]
    for k in range(0, num_periods+1):
        xright = np.pi/2 + k*np.pi + -0.0001
        if xright > xlim[1]:
            xright = xlim[1]
        xs = np.linspace(xleft, xright, 1000)
        fxs = np.array([np.tan(x) for x in xs])
        ax = sns.lineplot(x=xs, y=fxs, ax=ax, color="C0")
        xleft = np.pi/2 + k*np.pi + 0.0001
    ax.set_title(title)
    ax.set_ylim(ylim)
plot_tan(xlim=xlim, ylim=[-10,10], ax=ax_c, title=r"(c) $f(\theta) = \tan(\theta)$")

fig.tight_layout()

# FIGURES ONLY
filename = os.path.join("figures/tutorials/calculus", "panel_function_graphs2.pdf")
savefigure(plt.gca(), filename)
Saved figure to figures/tutorials/calculus/panel_function_graphs2.pdf
Saved figure to figures/tutorials/calculus/panel_function_graphs2.png
../_images/a9a79311ccf16e4393aa1b8a00eac26d3374b006241606a11a61ebdd1ff8b701.png

Limits#

We use limits to describe, with mathematical precision, infinitely large quantities, infinitely small quantities, and procedures with infinitely many steps.

Example: area of a circle#

Consider a polygon with \(n\) sides inscribed in inside of a circle of radius \(r\).

import math

def calc_area(n, r=1):
    theta = 2 * math.pi / (2 * n)
    a = r * math.cos(theta)
    b = r * math.sin(theta)
    area = 2 * n * a * b / 2
    return area

for n in [6, 8, 10, 50, 100, 1000, 10000]:
    area_n = calc_area(n)
    error = area_n - math.pi
    print(f"{n=}, {area_n=}, {error=}")
n=6, area_n=2.5980762113533156, error=-0.5435164422364775
n=8, area_n=2.8284271247461903, error=-0.3131655288436028
n=10, area_n=2.938926261462365, error=-0.2026663921274281
n=50, area_n=3.133330839107606, error=-0.008261814482187102
n=100, area_n=3.1395259764656687, error=-0.0020666771241244497
n=1000, area_n=3.141571982779476, error=-2.0670810317202637e-05
n=10000, area_n=3.1415924468812855, error=-2.067085076440378e-07
n, r = sp.symbols("n r")
A_n = n * r**2 * sp.cos(sp.pi/n) * sp.sin(sp.pi/n)
sp.limit(A_n, n, sp.oo)
../_images/7cea10f597767639a303872c8987a480e981404e4e479204469d448631d5b68f.png
1-sp.N(1/sp.E)
../_images/c8614175d9c9e7ee1b02e612359a9181f22fddcf0e0867b093c0122f40de49b2.png

Infinity#

The infinity symbol is denoted oo (two lowercase os) in SymPy. Infinity is not a number but a process: the process of counting forever. Thus, \(\infty + 1 = \infty\), \(\infty\) is greater than any finite number, and \(1/\infty\) is an infinitely small number. Sympy knows how to correctly treat infinity in expressions:

from sympy import oo
oo+1
../_images/60024b7788ea4288ee118f497ebd83dfe4c7085a606d753885c3b55aac9eed7c.png
5000 < oo
../_images/c79e45ecc63b372ba9ecbb3adb11e9fe0b2a8ee476eafe0651dd983c417b07be.png
1/oo
../_images/80018bc8da3bf5fb26bd0cec86efd8ffeabd8b17199eba04127b222bd5069fb4.png

Limits at infinity#

Limits are also useful to describe the behaviour of functions. Consider the function \(f(x) = \frac{1}{x}\). The limit command shows us what happens to \(f(x)\) as \(x\) goes to infinity:

x = sp.symbols("x")
sp.limit(1/x, x, oo)
../_images/80018bc8da3bf5fb26bd0cec86efd8ffeabd8b17199eba04127b222bd5069fb4.png

As \(x\) becomes larger and larger, the fraction \(\frac{1}{x}\) becomes smaller and smaller. In the limit where \(x\) goes to infinity, \(\frac{1}{x}\) approaches zero: \(\lim_{x\to\infty}\frac{1}{x} = 0\).

Example 2#

x = sp.symbols("x")

sp.limit( (2*x+1)/x , x, sp.oo)
../_images/891aa50d32d259b3d598baca0cefa29181b3ab047e34aac2b593fc1e0fbb5047.png

Euler’s number as a limit#

The number \(e\) is defined as the limit \(e \equiv \lim_{n\to\infty}\left(1+\frac{1}{n}\right)^n\):

n = sp.symbols("n")
sp.limit( (1+1/n)**n, n, sp.oo)
../_images/9de3e0a3947bee1706446492e65051fda6ab35c217e3c518bb97e97f08d0e4f1.png
sp.limit( (1+1/n)**n, n, sp.oo).evalf()
../_images/b3fff4236437650c68764260e7facbe3ba21a671c9d807a594e24b8c660a59e6.png

This limit expression describes the annual growth rate of a loan with a nominal interest rate of 100% and infinitely frequent compounding. Borrow \(\$1000\) in such a scheme, and you’ll owe \(\$2718.28\) after one year.

Euler’s number as a series#

Euler’s constant \(e = 2.71828\dots\) is defined one of several ways,

\[ e \equiv \lim_{n\to\infty}\left(1+\frac{1}{n}\right)^n \equiv \lim_{\epsilon\to 0}(1+\epsilon)^{1/\epsilon} \equiv \sum_{n=0}^{\infty}\frac{1}{n!}, \]

and is denoted E in SymPy. Using exp(x) is equivalent to E**x.

The functions log and ln both compute the logarithm base \(e\):

Limits to a number#

Continuity#

Limit formulas#

Computing limits using SymPy#

Applications of limits#

Limits for derivatives#

Limit for integrals#

Limits for series#

Derivatives#

The derivative function, denoted \(f'(x)\), \(\frac{d}{dx}f(x)\), \(\frac{df}{dx}\), or \(\frac{dy}{dx}\), describes the rate of change of the function \(f(x)\).

Derivatives as slope calculations#

Numerical derivative calculations#

def differentiate(f, x, delta=1e-9):
    """
    Compute the derivative of the function `f` at `x`
    using the rise-over-run calculation for run `delta`.
    """
    df = f(x+delta) - f(x)
    dx = delta
    return df / dx
def f(x):
    return 0.5 * x**2

differentiate(f, 1)
../_images/f78c1974610cfa20f3c5e28c633a7ce4258fe7c6fb8b95a119cb625cba5ccc26.png

Derivative formulas#

Derivative rules#

Higher derivatives#

Examples#

Computing derivatives analytically using SymPy#

The SymPy function diff computes the derivative of any expression:

m, x, b = sp.symbols("m x b")
sp.diff(m*x + b, x)
../_images/06327eac65ec635d35586273408eec0ef98fd37c1d474e779f134a055731f662.png
x, n = sp.symbols("x n")
sp.simplify(sp.diff(x**n, x))
../_images/49d39ab5b734738d242dfcd2c0cd14e930c31a75db479542632e7a790e343a6d.png

The exponential function \(f(x)=e^x\) is special because it is equal to its derivative:

from sympy import exp
sp.diff(exp(x), x)
../_images/0b7b8d60b644ce0fa5e09d7a8f3423ee730787a82798bc950a63776a068b96b6.png

Examples revisited#

sp.diff(sp.exp(x**2), x)
../_images/748732a7f0303fdde9a809d1e4b946cf2ade0b4755b2cfc6852bc4204f0c88ec.png
sp.diff(sp.sin(x)*sp.exp(x**2), x)
../_images/194f12b1d3e9e24abc8cd5110ac21d405d714c212633523c196db7359c7de567.png
sp.diff(sp.sin(x**2), x)
../_images/d18d8ed8caa02c4c6e2b433eaf8957e9c80e10d61eca2974ccd3592182116db0.png

Applications of derivatives#

Tangent lines#

The tangent line to the function \(f(x)\) at \(x=x_0\) is the line that passes through the point \((x_0, f(x_0))\) and has the same slope as the function at that point. The tangent line to the function \(f(x)\) at the point \(x=x_0\) is described by the equation

\[ T_1(x) = f(x_0) \ + \ f'(x_0)(x-x_0). \]

What is the equation of the tangent line to \(f(x)=\frac{1}{2}x^2\) at \(x_0=1\)?

fs = x**2 / 2
fs
../_images/1eb94966358fca8d86f216643ffa4123f126e799456e0a23882bce9222e084a4.png
dfsdx = sp.diff(fs, x)
dfsdx
../_images/e3ae3accdf80458a97d176cb062dcee80f9f9e72af1daaaf2009edcff6975dfb.png
a = 1
T_1 = fs.subs({x:a}) + dfsdx.subs({x:a})*(x - a)
T_1
../_images/cc75ed8441a019271e341e47f4b46e01043853424fb6c67cb9c503a6aaabca63.png

Optimization#

Optimization is about choosing an input for a function \(f(x)\) that results in the best value for \(f(x)\). The best value usually means the maximum value (if the function represents something desirable like profits) or the minimum value (if the function represents something undesirable like costs).

The derivative \(f'(x)\) encodes the information about the slope of \(f(x)\). Positive slope \(f'(x)>0\) means \(f(x)\) is increasing, negative slope \(f'(x)<0\) means \(f(x)\) is decreasing, and zero slope \(f'(x)=0\) means the graph of the function is horizontal. The critical points of a function \(f(x)\) are the solutions to the equation \(f'(x)=0\). Each critical point is a candidate to be either a maximum or a minimum of the function.

Analytical optimization using SymPy#

Let’s find the critical points of the function \(f(x)=x^3-2x^2+x\) and use the information from its second derivative to find the maximum of the function on the interval \(x \in [0,1]\).

x = sp.symbols('x')
fs = x**3 - 2*x**2 + x
sp.plot(fs, (x,0,1.5));
sp.diff(fs, x).factor()
../_images/a429df12bdc177a1fcb90d3820ee3d323a8e064c3214ad7254d0e8ba9ed14b10.png
sols = sp.solve( sp.diff(fs,x),  x)
sols  # critical values
../_images/89bfb2e3f7c64b24fe0eb1276bbce9cda09764d761c76941e723a787bf4838f7.png
sp.diff(fs, x, 2).subs({x:sols[0]})
../_images/deee8cb9d1fe1b33843fb10fe8240736770d63e346168885206c17c1d91d3086.png
sp.diff(fs, x, 2).subs({x:sols[1]})
../_images/891aa50d32d259b3d598baca0cefa29181b3ab047e34aac2b593fc1e0fbb5047.png

It we look at the graph of this function, we see the point \(x=\frac{1}{3}\) is a local maximum because it is a critical point of \(f(x)\) where the curvature is negative, meaning \(f(x)\) looks like the peak of a mountain at \(x=\frac{1}{3}\). The maximum value of \(f(x)\) on the interval \(x\in [0,1]\) is \(f\!\left(\frac{1}{3}\right)=\frac{4}{27}\). The point \(x=1\) is a local minimum because it is a critical point with positive curvature, meaning \(f(x)\) looks like the bottom of a valley at \(x=1\).

Numerical optimization#

def gradient_descent(f, x0=0, alpha=0.05, tol=1e-10):
    """
    Find the minimum of the function `f` using gradient descent.
    """
    current_x = x0
    change = 1
    while change > tol:
        df_at_x = differentiate(f, current_x)
        next_x = current_x - alpha * df_at_x
        change = abs(next_x - current_x)
        current_x = next_x
    return current_x
def q(x):
    return (x - 5)**2

gradient_descent(q, x0=10)
../_images/ae2bbc2abb0b179bb7db8efa3e4cb7fdc5f3a5313705a680daef72756383ca16.png

Numerical optimization using SciPy#

Let’s solve the same optimization problem using the function minimize from scipy.optimize.

from scipy.optimize import minimize

res = minimize(q, x0=10)

res["x"][0]  # = argmin f(x)
../_images/20b186719100733122d195566c16798a74ed813e568b2c7448f3bc930d802992.png

Integrals#

Integrals as area calculations#

The integral of \(f(x)\) corresponds to the computation of the area under the graph of \(f(x)\). The area under \(f(x)\) between the points \(x=a\) and \(x=b\) is denoted as follows:

\[ A(a,b) = \int_a^b f(x) \: dx. \]

Example 1: integral of a constant function#

def f(x):
    return 3
from ministats.calculus import plot_integral
ax = plot_integral(f, a=0, b=5, xlim=[-0.4,7], flabel="f")
ax.text(2.5, 1.5, "$A_f(0,\\!5)$", ha="center", fontsize="x-large");

Example 2: integral of a linear function#

def g(x):
    return x
from ministats.calculus import plot_integral
ax = plot_integral(g, a=0, b=5, xlim=[0,7], flabel="g")
ax.text(3.5, 1.5, "$A_g(0,\\!5)$", ha="center", fontsize="x-large");

Example 3: integral of a polynomial#

def h(x):
    return 4 - x**2
from ministats.calculus import plot_integral
ax = plot_integral(h, a=0, b=2, xlim=[-0.03,2], flabel="h")
ax.set_xticks(np.arange(0,2.2,0.2))
ax.text(0.8, 1.2, "$A_h(0,\\!2)$", ha="center", fontsize="x-large");

# FIGURES ONLY
filename = os.path.join("figures/tutorials/calculus", "simple_integral_hx_eq_x.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/simple_integral_hx_eq_x.pdf
Saved figure to figures/tutorials/calculus/simple_integral_hx_eq_x.png
../_images/2f5d59ce3de8ba8a1229c10d0aa43e76c2ba150cc4052ebb37642817ba1e5a14.png

Computing integrals numerically#

Computing integral of \(f(x)\) “numerically” means we’re splitting the region of integration into many (think thousands or millions) of strips, computing the areas of these strips, then adding up the areas to obtain the total area under the graph of \(f(x)\). The approximation to the area under \(f(x)\) between \(x=a\) and \(x=b\) using \(n\) rectangular strips corresponds to the following formula:

\[ A_f(a,b) \approx \sum_{k=1}^{n} f(a + k\Delta x)\,\Delta x, \]

where \(\Delta x = \frac{b-a}{n}\) is the width of the rectangular strips. The right endpoint of the \(k\)\textsuperscript{th} is located at \(x_k = a + k\Delta x\), so the height of the rectangular strips \(f(x_k)\) varies as \(k\) goes from between \(k=1\) (first strip) and \(k=n\) (last strip).

def integrate(f, a, b, n=10000):
    """
    Computes the area under the graph of `f`
    between `x=a` and `x=b` using `n` rectangles.
    """
    dx = (b - a) / n                       # width of rectangular strips
    xs = [a + k*dx for k in range(1,n+1)]  # right-corners of the strips
    fxs = [f(x) for x in xs]               # heights of the strips
    area = sum([fx*dx for fx in fxs])      # total area
    return area

Example 3 continued: integral of a polynomial#

def h(x):
    return 4 - x**2
integrate(h, a=0, b=2, n=10)
../_images/00966a142dd784aff2a5cef3d662d4ea7d6a2b490ba911711e623ad6b2fe435f.png
from ministats.calculus import plot_riemann_sum

ax = plot_riemann_sum(h, a=0, b=2, xlim=[-0.01,2.01], n=10, flabel="h")
ax.set_xticks(np.arange(0,2.2,0.2));
Riemann sum with n=10 rectangles: approx. area ≈ 4.92000
../_images/3321fd003f999c848c03d351876a33f605ead6361290535a03c926f3c70c93e6.png
integrate(h, a=0, b=2, n=20)
../_images/43bcc54108d70d96ecf1b43466b90e0540c56c094f65a23696aa040573749711.png
plot_riemann_sum(h, a=0, b=2, xlim=[-0.01,2.01], n=20, flabel="h");
Riemann sum with n=20 rectangles: approx. area ≈ 5.13000
../_images/e1c3fefd1065b13d5fb0f84d15cb580bc9262ecf5362a108297daaa5c92f115b.png
# FIGURES ONLY
fig, axs = plt.subplots(1, 2, figsize=(7,2), sharey=True)

plot_riemann_sum(h, a=0, b=2, xlim=[-0.01,2.01], n=10, flabel="h", ax=axs[0])
axs[0].set_xticks(np.arange(0,2.2,0.2))
area_10 = integrate(h, a=0, b=2, n=10)
axs[0].set_title(f"(a) Approximation $A_h(0,\\!2) \\approx {area_10}$ when $n=10$.")
axs[0].set_xlabel(None)

plot_riemann_sum(h, a=0, b=2, xlim=[-0.01,2.01], n=20, flabel="h", ax=axs[1])
axs[1].set_xticks(np.arange(0,2.2,0.2))
axs[1].set_xlabel(None)
area_20 = integrate(h, a=0, b=2, n=20)
axs[1].set_title(f"(b) Approximation $A_h(0,\\!2) \\approx {area_20}$ when $n=20$.")
fig.tight_layout()

filename = os.path.join("figures/tutorials/calculus", "riemann_sum_n10_n20.pdf")
savefigure(plt.gca(), filename)
Riemann sum with n=10 rectangles: approx. area ≈ 4.92000
Riemann sum with n=20 rectangles: approx. area ≈ 5.13000
Saved figure to figures/tutorials/calculus/riemann_sum_n10_n20.pdf
Saved figure to figures/tutorials/calculus/riemann_sum_n10_n20.png
../_images/aaf41fc3989ce1ce9150952ea4339a7f8d659bd33734aa6c3e21f023287669a1.png
integrate(h, a=0, b=2, n=50)
../_images/51c1ae580f504797c055bb2feb1dcdd1c79644660e07cd9b674e14898eea2cc5.png
integrate(h, a=0, b=2, n=100)
../_images/d9a27d5bcddce40c0d9009373e77c383a3684c79d20121707ee299e260639661.png
# FIGURES ONLY
fig, axs = plt.subplots(1, 2, figsize=(7,2), sharey=True)

plot_riemann_sum(h, a=0, b=2, xlim=[-0.01,2.01], n=50, flabel="h", ax=axs[0])
area_50 = integrate(h, a=0, b=2, n=50)
axs[0].set_title(f"(a) $A_h(0,\\!2) \\approx {area_50}$ when $n=50$.")
axs[0].set_xlabel(None)

plot_riemann_sum(h, a=0, b=2, xlim=[-0.01,2.01], n=100, flabel="h", ax=axs[1])
area_100 = integrate(h, a=0, b=2, n=100)
axs[1].set_title(f"(b) $A_h(0,\\!2) \\approx {area_100}$ when $n=100$.")
axs[1].set_ylabel(None)
axs[1].set_xlabel(None)

fig.tight_layout()
filename = os.path.join("figures/tutorials/calculus", "riemann_sum_n50_n100.pdf")
savefigure(fig, filename)
Riemann sum with n=50 rectangles: approx. area ≈ 5.25280
Riemann sum with n=100 rectangles: approx. area ≈ 5.29320
Saved figure to figures/tutorials/calculus/riemann_sum_n50_n100.pdf
Saved figure to figures/tutorials/calculus/riemann_sum_n50_n100.png
../_images/83321fc5d00d264c162113caff604fa8058f26dda77b144edf2c45da14b96881.png
integrate(h, a=0, b=2, n=1000)
../_images/e3cd2b1c68a30b29610e245e8edf8bec570cbd7fe429b65dce7c19c351328270.png
integrate(h, a=0, b=2, n=10000)
../_images/a3c91d5847d5466a2f41eccdf1888dc32313db337588368f1b6b291db97ee888.png
integrate(h, a=0, b=2, n=1_000_000)
../_images/c1b9ce82f37b2be6e061a1a00de835171eeca42a05ce90d3feb9b851556e6567.png

Example 1 and 2 revisited#

def f(x):
    return 3

integrate(f, a=0, b=5, n=100000)
../_images/fdf23351a7b64d995b1070cdb1583c4ab7c034d9bc820813d73aba2e1474237d.png
def g(x):
    return x

integrate(g, a=0, b=5, n=100000)
../_images/24a1e0e068aa873de972f57f890e2e30800d10c40bc93af6a8283578c19b75d1.png

Formal definition of the integral#

Riemann sum

As \(n\) goes to infinity…

Integrals as functions#

The integral function \(F\) corresponds to the area calculation as a function of the upper limit of integration:

\[ F(c) \equiv \int_0^c \! f(x)\:dx\,. \]

The area under \(f(x)\) between \(x=a\) and \(x=b\) is obtained by calculating the change in the integral function:

\[ A(a,b) = \int_a^b \! f(x)\:dx = F(b)-F(a). \]

In SymPy we use integrate(f, x) to obtain the integral function \(F(x)\) of any function \(f(x)\): \(F(x) = \int_0^x f(u)\,du\).

Example 1 revisited#

ax = plot_integral(f, a=0, b=4.6, xlim=[-0.2,7], flabel="f")
ax.set_ylim(0,4.1)
ax.set_yticks([0,1,2,3,4])
ax.text(2.3, 1.3, "$F_0(b)=\\int_{\\!0}^{\\!b} f(x)dx$", ha="center", fontsize="x-large");
ax.text(4.6, -0.7, "$b$", ha="center")

# FIGURES ONLY
filename = os.path.join("figures/tutorials/calculus", "simple_integral_function_fx_eq_3.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/simple_integral_function_fx_eq_3.pdf
Saved figure to figures/tutorials/calculus/simple_integral_function_fx_eq_3.png
../_images/ef16ea90ad8cd4bd7694c49cf13619981c2f41170fe2f1c8309940dbc1f6fb24.png

Example 2 revisited#

ax = plot_integral(g, a=0, b=4.6, xlim=[-0.03,6.2], flabel="g")
ax.set_ylim(0, 5.5)
ax.set_yticks([0,1,2,3,4,5])
ax.text(3.4, 1.2, "$G_0(b)=\\int_{\\!0}^{\\!b} g(x)dx$", ha="center", fontsize="large");
ax.text(4.6, -1, "$b$", ha="center")

# FIGURES ONLY
filename = os.path.join("figures/tutorials/calculus", "simple_integral_function_gx_eq_x.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/simple_integral_function_gx_eq_x.pdf
Saved figure to figures/tutorials/calculus/simple_integral_function_gx_eq_x.png
../_images/b0f6ec58a9f0da0fabede34464507fa572adb9fac5fc4300d75fc13d819bc5ee.png

Example 3 revisited#

ax = plot_integral(h, a=0, b=1.3, xlim=[-0.03,2], flabel="h")
ax.set_xticks(np.arange(0,2.2,0.2))
ax.text(0.65, 1.2, "$H_0(b)=\\int_{\\!0}^{\\!b} h(x)dx$", ha="center", fontsize="x-large");
ax.text(1.3, -0.9, "$b$", ha="center")

# FIGURES ONLY
filename = os.path.join("figures/tutorials/calculus", "integral_function_hx_eq_x.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/integral_function_hx_eq_x.pdf
Saved figure to figures/tutorials/calculus/integral_function_hx_eq_x.png
../_images/6d82827906f908600c1819415f0dce17c2d18f00088fea251ffee435791d4b10.png

Properties of integrals#

Fundamental theorem of calculus#

The integral is the “inverse operation” of the derivative. If you perform the integral operation followed by the derivative operation on some function, you’ll obtain the same function:

\[ \left(\frac{d}{dx} \circ \int dx \right) f(x) \equiv \frac{d}{dx} \int_c^x f(u)\:du = f(x). \]
fx = x**2
Fx = sp.integrate(fx, x)
Fx
../_images/f2947cfa3b504fbf19eb45eefd7f2a954354fd3acc310606a5345931c6548ee4.png
sp.diff(Fx, x)
../_images/43c3fbcd18deee7dd207b7ef05b266b12f072f816c41771bcba2fdcac2e6f535.png

Alternately, if you compute the derivative of a function \(F(x)\) followed by the integral, you will obtain the original function (up to a constant):

\[ \left( \int dx \circ \frac{d}{dx}\right) F(x) \equiv \int_c^x F'(u)\;du = F(x) + C. \]
Fx = x**3
dFdx = sp.diff(Fx,x)
dFdx
../_images/c88fa704fd0b49315d1870bea127ebda9e998ef479c30e7b4c515273b5bcc1cf.png
sp.integrate(dFdx, x)
../_images/9630b2290c0d00b6d4312249f5a4ce5d8626f3adb9f070920951bb9c0c9ea198.png

Using derivative formulas in reverse#

The fundamental theorem of calculus gives us a symbolic trick for computing the integrals functions \(\int f(x)\,dx\) for many simple functions.

If you can find a function \(F(x)\) such that \(F'(x) = f(x)\), then the integral of \(f(x)\) is \(F(x)\) plus some constant:

\[ \int f(x)\,dx = F(x) + C. \]
fx = m + sp.exp(x) + 1/x + sp.cos(x) - sp.sin(x)
sp.integrate(fx, x)
../_images/f9c29e96865bc2783998adbdd161737efce626eb8a9b7afe714d1cc069a710e5.png

Techniques of integration#

Substitution trick#

Integration by parts#

Other techniques#

Computing integrals numerically using SciPy#

We’ll now show some examples using the function quad form the module scipy.integrate. The function quad(f,a,b) is short for quadrature, which is an old name for computing areas.

Example 1N#

Let’s integrate the function \(f(x) = 3\).

from scipy.integrate import quad

def f(x):
    return 3
    
quad(f, 0, 5)
../_images/87e754d5ca0f937c232f5f939d6e9976afd33f788c436f926992e2637f91d4e3.png

The function quad returns two numbers as output: the value of the integral and a precision parameter. In output of the code, tells us the value of the integral is \(\int_{0}^5 f(x)\,dx\) is 15.0 and guarantees the accuracy of this value up to an error of \(1.7\times 10^{-13}\).

Since we’re usually only interested in the value of the integral, we often select the first output of quad so you’ll see the code like quad(...)[0] in all the code examples below.

quad(f, 0, 5)[0]
../_images/9525572c4089a3f3d6685cb27ac7856e32c1eab11657e07f69d674086ac73871.png

Example 2N#

Define the function \(g(x) = x\).

def g(x):
    return x

quad(g, 0, 5)[0]
../_images/627a30b166cdd5f35c1b3377a9fdca6252ecb1c2e6cdcb9bfe4924806d3e17a4.png

Example 3N#

Lets now compute \(\int_{-1}^4 h(x)\,dx\)

def h(x):
    return 4 - x**2

quad(h, -2, 2)[0]
../_images/a5686cc7e48f92150217f52f8fb12aabc146bec6a110f8681b81395d8e29dcb8.png

Computing integrals symbolically using SymPy#

This is known as an indefinite integral since the limits of integration are not defined.

In contrast, a definite integral computes the area under \(f(x)\) between \(x=a\) and \(x=b\). Use integrate(f, (x,a,b)) to compute the definite integrals of the form \(A(a,b)=\int_a^b f(x) \, dx\):

import sympy as sp
x, a, b = sp.symbols("x a b")

The symbols function creates SymPy symbolic variables. Unlike ordinary Python variables that hold a particular value, SymPy variables act as placeholders that can take on any value.

We can use the symbolic variables to create expressions, just like we do with math variables in pen-and-paper calculations.

Example 1S: Constant function#

\(f(x) = 3\)

fx = 3
fx
../_images/e1cffecb0770426ef56a147187234b3a65058b23f4f24134ae7f8a495737c5d5.png

We’ll use the SymPy function integrate for computing integrals. We call this function by passing in the expression we want to integrate as the first argument. The second argument is a triple \((x,a,b)\), which specifies the variable of integration \(x\), the lower limit of integration \(a\), and the upper limit of integration \(b\).

sp.integrate(fx, (x,a,b))  # = A_f(a,b)
../_images/591e271786aad77f52846333f944443d6a3acb07615e1010794cfcaad1baf5bc.png

The answer \(3\cdot (b-a)\) is the general expression for calculating the area under \(f(x)=c\), for between any starting point \(x=a\) and end point \(x=b\). Geometrically, this is just a height-times-width formula for the area of a rectangle.

To compute the specific integral between \(a=0\) and \(b=5\) under \(f(x)=3\), we use the subs (substitute) method, passing in a Python dictionary of the values we want to “plug” into the general expression.

sp.integrate(fx, (x,a,b)).subs({a:0, b:5})
../_images/6a587c29c07f677cd983099369009ee36abbc9388045b36d8d0074b3f80d0b59.png

The integral function (indefinite integral) \(F_0(b) = \int_0^b f(x) dx\) is obtained as follows.

F0b = sp.integrate(fx, (x,0,b))  # = F_0(b)
F0b
../_images/80388b69dde2497ef202a98d8372514972e6d2c8ac316c40cf21060dfe32cbb8.png

We can obtain the area by using \(A_f(0,5) = F_0(5) - F_0(0)\):

F0b.subs({b:5}) - F0b.subs({b:0})
../_images/6a587c29c07f677cd983099369009ee36abbc9388045b36d8d0074b3f80d0b59.png

Example 2S: Linear function#

\(g(x) = mx\)

gx = x
gx
../_images/e3ae3accdf80458a97d176cb062dcee80f9f9e72af1daaaf2009edcff6975dfb.png

The integral function \(G_0(b) = \int_0^b g(x) dx\) is obtained by leaving the variable upper limit of integrartion.

G0b = sp.integrate(gx, (x,0,b))  # = G_0(b)
G0b
../_images/cca493c7bbf2d07b7331747b698ffc925ac57d1e8f9010c6ee6c90538778dd98.png

We can obtain the definite intagral using \(\int_0^5 g(x)\,dx = G_0(5) - G_0(0)\):

G0b.subs({b:5}) - G0b.subs({b:0})
../_images/2ddc7fc8a5dbd17930d72a9b3409a59625a504d8bd82dd024095d4c2200d46c6.png

SymPy computed the exact answer for us as a fraction \(\frac{25}{2}\). We can force SymPy to produce a numeric answer by calling \tt{sp.N} on this expression.

sp.N(G0b.subs({b:5}) - G0b.subs({b:0}))
../_images/627a30b166cdd5f35c1b3377a9fdca6252ecb1c2e6cdcb9bfe4924806d3e17a4.png

Example 3S: Polynomial function#

\(h(x) = 4 - x^2\)

hx = 4 - x**2
hx
../_images/a9bddc012c3aafd28f6542bca72c856025572c3b7984dec69a778a3e4d8bf8e2.png
H0b = sp.integrate(hx, (x,0,b))
H0b
../_images/32a45cbc568082bfd3c29c1d9d4e13002d7639a7f607cf4820c62f2cf6f8551d.png

We can obtain the area by using \(A_h(0,2) = H_0(2) - H_0(0)\).

H0b.subs({b:2}) - H0b.subs({b:0})
../_images/29728b7e0a20fa8aad1f5572b213c20775f78747f7c6969b7123d8be13a5f61a.png

Applications of integration#

Kinematics#

The uniform acceleration motion (UAM) equations…

Let’s analyze the case where the net force on the object is constant. A constant force causes a constant acceleration \(a = \frac{F}{m} = \textrm{constant}\). If the acceleration function is constant over time \(a(t)=a\). We find \(v(t)\) and \(x(t)\) as follows:

t, a, v_i, x_i = sp.symbols('t a v_i x_i')
vt = v_i + sp.integrate(a, (t, 0,t) )
vt
../_images/b56e0aeb4715e18465e5367c5157166d9f268eb4d492194d1e411513d0a0bb71.png
xt = x_i + sp.integrate(vt, (t,0,t))
xt
../_images/06802ebf42ee20aedd4199dbb9253ea2b25c0d14cb858567a9009e814c078dae.png

You may remember these equations from your high school physics class. They are the uniform accelerated motion (UAM) equations:

\[\begin{align*} a(t) &= a, \\ v(t) &= v_i + at, \\[-2mm] x(t) &= x_i + v_it + \frac{1}{2}at^2. \end{align*}\]

In high school, you probably had to memorize these equations. Now you know how to derive them yourself starting from first principles.

Solving differential equations#

The fundamental theorem of calculus is important because it tells us how to solve differential equations. If we have to solve for \(f(x)\) in the differential equation \(\frac{d}{dx}f(x) = g(x)\), we can take the integral on both sides of the equation to obtain the answer \(f(x) = \int g(x)\,dx + C\).

A differential equation is an equation that relates some unknown function \(f(x)\) to its derivative. An example of a differential equation is \(f'(x)=f(x)\). What is the function \(f(x)\) which is equal to its derivative? You can either try to guess what \(f(x)\) is or use the dsolve function:

Kinematics revisited#
t, a, k, lam = sp.symbols("t a k \\lambda")
x = sp.Function("x")
newton_eqn = sp.diff(x(t), t, t) - a
sp.dsolve(newton_eqn, x(t)).rhs
../_images/034e52a819d5e181caea7edf09d20e995b13582fd7b202336be728c5a4ed6b79.png
Bacterial growth#
b = sp.Function('b')

bio_eqn = k*b(t) - sp.diff(b(t), t)
sp.dsolve(bio_eqn, b(t)).rhs
../_images/301e1f3a7f3a98fc0a85e8f3b51112b3ac476431c5b94db80390f338a8923618.png
Radioactive decay#
r = sp.Function('r')
radioactive_eqn = lam*r(t) + sp.diff(r(t), t)
sp.dsolve(radioactive_eqn, r(t)).rhs
../_images/682c5c61210ca7266b6c109006b687cda66d5f1aaed6ba4c465ccfb372a2b24d.png
Simple harmonic motion (SHM)#
w = sp.symbols("w", nonnegative=True)
SHM_eqn = sp.diff(x(t), t, t) + w**2*x(t)
sp.dsolve(SHM_eqn, x(t)).rhs
../_images/a3154978e718883683054c3bf553dc9131afff9c3c1438cc6425d77c2e1c96b5.png

Probability calculations#

def fZ(z):
    return 1 / np.sqrt(2*np.pi) * exp(-z**2 / 2)
    
quad(fZ, a=-1, b=1)[0]
../_images/0b2edbbbdf109c844af6a543cb7c214796f3e4a1539913fed9895ebc0275da09.png
quad(fZ, a=-2, b=2)[0]
../_images/9d050def403fcf014af8bc3847395d08a090f34b0d93375419225e7676eddb9a.png
quad(fZ, a=-3, b=3)[0]
../_images/19ad4f549469e5449d1d5d8565873bd01b4e08aca9ab373ff3659be2aee241a9.png

Sequences and series#

Sequences#

Sequences are functions that take whole numbers as inputs. Instead of continuous inputs \(x\in \mathbb{R}\), sequences take natural numbers \(k\in\mathbb{N}\) as inputs. We denote sequences as \(a_k\) instead of the usual function notation \(a(k)\).

We define a sequence by specifying an expression for its \(k^\mathrm{th}\) term.

from ministats.calculus import plot_seq

def n_k(k):
    return  k

print([n_k(k) for k in range(0,11)])

plot_seq(n_k, start=0, stop=10, label="$n_k$");
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
../_images/b19c3892a8fd4e5289c722a872331160e4db9ebeedc504b006b815549459ebd3.png
def q_k(k):
    return  k**2

print([q_k(k) for k in range(1,11)])

plot_seq(q_k, start=0, stop=10, label="$q_k$");
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
../_images/1a4bd86a827bd1e7ef877880484afa2fe53d4d9b4c130694fe9551707f6f0dee.png
def h_k(k):
    return 1 / k

print([round(h_k(k),4) for k in range(1,10)])

plot_seq(h_k, start=1, stop=20, label="$h_k$");
[1.0, 0.5, 0.3333, 0.25, 0.2, 0.1667, 0.1429, 0.125, 0.1111]
../_images/03b9e5cfd1d84757fb4b3f079ef7dae79fa97d8026aec2e5c972df0a3254f6f5.png
def a_k(k):
    return (-1)**(k+1) / k

print([round(a_k(k),4) for k in range(1,10)])

plot_seq(a_k, start=1, stop=20, label="$a_k$");
[1.0, -0.5, 0.3333, -0.25, 0.2, -0.1667, 0.1429, -0.125, 0.1111]
../_images/d4c4d8155d114345a18374bf1d29ea3182cdc480a7cab3fbecbc01a71d6fcb5b.png
from math import factorial

def f_k(k):
    return 1 / factorial(k)

print([round(f_k(k),6) for k in range(0,10)])

plot_seq(f_k, start=0, stop=10, label="$f_k$");
[1.0, 1.0, 0.5, 0.166667, 0.041667, 0.008333, 0.001389, 0.000198, 2.5e-05, 3e-06]
../_images/6b902c7740071d887fb5983a0cdf03229a9084fe9014fbe9a44fa827abda0714.png
def g_k(k, r=0.5):
    return r**k

print([round(g_k(k),5) for k in range(0,11)])

plot_seq(g_k, start=0, stop=10, label="$g_k$");
[1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.01562, 0.00781, 0.00391, 0.00195, 0.00098]
../_images/7b507ac5a125027e398e1059d24e5e5ce0751ca74c3b31a318c67587fc3b1d26.png
def b_k(k):
    return 2**k

print([b_k(k) for k in range(0,11)])

plot_seq(b_k, start=0, stop=10, label="$b_k$");
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
../_images/ea413a95bed0d228e3f0d08e88521a7be379a81f6e61840ccfb32f4f4fd73b9f.png
k = sp.symbols("k")

a_k = 1 / k
b_k = 1 / sp.factorial(k)

Substitute the desired value of \(k\) to see the value of the \(k^\mathrm{th}\) term:

a_k.subs({k:5})
../_images/a13be3569599dd0e603b9ccb07b65134dd3f8c4401b37d0b751cc7f641a36481.png

The Python list comprehension syntax [item for item in list] can be used to print the sequence values for some range of indices:

[ a_k.subs({k:i}) for i in range(1,8) ]
../_images/f3a40d8ac3ada0bbc6ac190a3c1f3820f153d8a883ebd48a60632b7360e08efa.png
[ b_k.subs({k:i}) for i in range(0,8) ]
../_images/eff2c65aa4fc06820a60e4a6941b3bb62a27f25aacd7a0e86bba5a165eeaa1ed.png

Observe that \(a_k\) is not defined for \(k=0\) since \(\frac{1}{0}\) is a division-by-zero error. In other words, the domain of \(a_k\) is the nonnegative natural numbers \(a_k:\mathbb{N}_+ \to \mathbb{R}\). Observe how quickly the factorial function \(k!=1\cdot2\cdot3\cdots(k-1)\cdot k\) grows: \(7!= 5040\), \(10!=3628800\), \(20! > 10^{18}\).

We’re often interested in calculating the limits of sequences as \(k\to \infty\). What happens to the terms in the sequence when \(k\) becomes large?

sp.limit(a_k, k, sp.oo)
../_images/80018bc8da3bf5fb26bd0cec86efd8ffeabd8b17199eba04127b222bd5069fb4.png
sp.limit(b_k, k, sp.oo)
../_images/80018bc8da3bf5fb26bd0cec86efd8ffeabd8b17199eba04127b222bd5069fb4.png

Both \(h_k=\frac{1}{k}\) and \(f_k = \frac{1}{k!}\) converge to \(0\) as \(k\to\infty\).

Exact sums#

Series#

Suppose we’re given a sequence \(a_k\) and we want to compute the sum of all the values in this sequence \(\sum_{k}^\infty a_k\). Series are sums of sequences. Summing the values of a sequence \(a_k:\mathbb{N}\to \mathbb{R}\) is analogous to taking the integral of a function \(f:\mathbb{R} \to \mathbb{R}\).

from ministats.calculus import plot_series

def f_k(k):
    return 1 / factorial(k)

plot_series(f_k, start=0, stop=10, label="$b_k$");
The sum of the first 11 terms of the sequence is 2.718282
../_images/68d117f3777083e5630fc641163dc472cde003381d96a3fac4bc80c9ab5fad7c.png

Example 1: sum of a geometric series#

plot_series(g_k, start=0, stop=10, label="$g_k$");
The sum of the first 11 terms of the sequence is 1.999023
../_images/316d6f1e543808fc6be0149cfe386fdc622b12a1d55f5ac71d59553579610c6e.png
sum([g_k(k) for k in range(0, 10)])
../_images/acb92fe0bcb1c3943208b6de9249e2a5d1b0da9f5fee64b8c1e1616f41169893.png
sum([g_k(k) for k in range(0, 20)])
../_images/ace07182c73b102ce2259244f034bbc0d264a844040205a5a24208414a6a0cd7.png
sum([g_k(k) for k in range(0, 100)])
../_images/0dbbe87812b8f082eb75344bf0716c3319d31c9b0a1fa86ac120b1fdda7e9a9c.png

Convergent and divergent series#

sum([h_k(k) for k in range(1, 20)])
../_images/dfcd1889ba8898bc44c39affb0fe0764bbc55e6bb86a57fa38614d4b7005e157.png
sum([h_k(k) for k in range(1, 100)])
../_images/71bf43a32482184d29707558243b5d2eb1ceb2065c4a763e92c7349bfe1b4fbf.png
sum([h_k(k) for k in range(1, 1000)])
../_images/36502b29c9777e87c00157c1d681203c5dd30395eb9349014e601e69e3326cc0.png
sum([h_k(k) for k in range(1, 1_000_000)])
../_images/ca3e2adef83b3345a5111f4db3eaf3efead91485acf0164f9a413fcc6fe7c327.png
import sympy as sp
k = sp.symbols("k")
sp.summation(1/k, (k,1,sp.oo))
../_images/60024b7788ea4288ee118f497ebd83dfe4c7085a606d753885c3b55aac9eed7c.png

Example 2#

Using a Python for-loop, we can obtain an approximation to \(e\) that is accurate to six decimals by summing 10 terms in the series:

import math

def f_k(n): 
    return 1 / math.factorial(n)

sum([f_k(k) for k in range(0, 10)])
../_images/1026e950a6bf8c3c1a7f3f96a09e3507feb87e90bce55fbe0530f41a15659d09.png
sum([f_k(k) for k in range(0, 15)])
../_images/265de701c017f490a967f99cf2aa13becf20a0c4e5d725875b958f4762d87e61.png
sum([f_k(k) for k in range(0, 20)])
../_images/b3fff4236437650c68764260e7facbe3ba21a671c9d807a594e24b8c660a59e6.png

The approximation we obtain using 20 terms in a series is a very good approximation to the exact value.

math.e
../_images/b3fff4236437650c68764260e7facbe3ba21a671c9d807a594e24b8c660a59e6.png

The SymPy function sp.summation to compute the infinite sum.

import sympy as sp
k = sp.symbols("k")
sp.summation(1/sp.factorial(k), (k,0,sp.oo))
../_images/9de3e0a3947bee1706446492e65051fda6ab35c217e3c518bb97e97f08d0e4f1.png

Example 3#

def a_k(k):
    return (-1)**(k+1) / k

sum([a_k(k) for k in range(1,10+1)])
../_images/40c92c67de38292a7566921b838b28a8ae6f4dd8ff96c8814974ceb9758af678.png
sum([a_k(k) for k in range(1,100+1)])
../_images/9d35f629e820b2ea5882023bce204af3b77058a27cf5b68229c62ab9ff16fccf.png
sum([a_k(k) for k in range(1,1000+1)])
../_images/2dedfb93c11761bfcea3c18bbb247e2ac9114fc57d694d80072ef5a91d3688b7.png
sum([a_k(k) for k in range(1,1_000_000+1)])
../_images/9e3cf13c53dfc9a1e2751ee57fc0c6f196baae793145055eebe286371480d2f2.png
sp.summation((-1)**(k+1) / k, (k,1,sp.oo))
../_images/f18de6cf84985a622456fc1208c3f30964d784f91268369974fcbb12297e77c9.png
math.log(2)
../_images/6055a8a977ae7e66925609d9615257915727dfdb9617fa0920674b445194ce96.png

To work with series in SymPy, use the summation function whose syntax is analogous to the integrate function:

sp.summation(1/sp.factorial(k), (k,0,sp.oo))
../_images/9de3e0a3947bee1706446492e65051fda6ab35c217e3c518bb97e97f08d0e4f1.png

We say the series \(\sum h_k\) diverges to infinity (or is divergent) while the series \(\sum f_k\) converges (or is convergent). As we sum together more and more terms of the sequence \(f_k\), the total becomes closer and closer to some finite number. In this case, the infinite sum \(\sum_{k=0}^\infty \frac{1}{k!}\) converges to the number \(e=2.71828\ldots\).

sp.E.evalf()  # true value
../_images/b3fff4236437650c68764260e7facbe3ba21a671c9d807a594e24b8c660a59e6.png

Taylor series#

Wait, there’s more! Not only can we use series to approximate numbers, we can also use them to approximate functions.

A power series is a series whose terms contain different powers of the variable \(x\). The \(k^\mathrm{th}\) term in a power series is a function of both the sequence index \(k\) and the input variable \(x\).

For example, the power series of the function \(\exp(x)=e^x\) is

\[ \exp(x) \equiv 1 + x + \frac{x^2}{2} + \frac{x^3}{3!} + \frac{x^4}{4!} + \frac{x^5}{5!} + \cdots = \sum_{k=0}^\infty \frac{x^k}{k!}. \]

This is, IMHO, one of the most important ideas in calculus: you can compute the value of \(\exp(5)\) by taking the infinite sum of the terms in the power series with \(x=5\):

# exp_xk = x**k / sp.factorial(k)
# sp.summation( exp_xk.subs({x:5}), (k,0,sp.oo)).evalf()
sp.exp(5).evalf()  # the true value
../_images/8d13e57279f000ce5d6b3a97b7a5aa17a55603a08cb59bedb1c6a3edd7aad136.png

Note that SymPy is actually smart enough to recognize that the infinite series you’re computing corresponds to the closed-form expression \(e^5\):

# sp.summation( exp_xk.subs({x:5}), (k,0,sp.oo))

The coefficients in the power series of a function (also known as the Taylor series) The formula for the \(k^\mathrm{th}\) term in the Taylor series of \(f(x)\) expanded at \(x=c\) is \(a_k(x) = \frac{f^{(k)}(c)}{k!}(x-c)^k\), where \(f^{(k)}(c)\) is the value of the \(k^\mathrm{th}\) derivative of \(f(x)\) evaluated at \(x=c\).

Obtaining Taylor series using SymPy#

The SymPy function series is a convenient way to obtain the series of any function fun. Calling sp.series(fun,var,x0,n) will show you the series expansion of expr near var=x0 including all powers of var less than n.

import sympy as sp
x = sp.symbols("x")

sp.series(1/(1-x), x, x0=0, n=8)
../_images/ec995cf151d53d52506c00165ceb19059999b7b9e55f8c115f681c89607b1504.png
sp.series(1/(1+x), x, x0=0, n=8)
../_images/b7834ed4d9d6b63cd2f986eaa5592c0337d2d0b095366f6a48fde35f2f702582.png
sp.series(sp.E**x, x, x0=0, n=7)
../_images/b182e25f2b2ff557c46288e5673d7b2756bf7e8d37ae2c3d6c7ccbf612f8f7ba.png
sp.series(sp.sin(x), x, x0=0, n=8)
../_images/c0e212093d6a053545c910ac212175cc09a08882f93d62ff69492a2cb9474879.png
sp.series(sp.cos(x), x, x0=0, n=8)
../_images/2b959a8d1bee646cd0ae51f351c3c793fe0414bf51ac53a5aeff32c5af739dd6.png
sp.series(sp.ln(x+1), x, x0=0, n=6)
../_images/811560a0aa7ab547aeb8a8396e66a5eddf33fc1ccb3081b4ad69d91b7a8130c6.png

Multivariable calculus#

Multivariable functions#

import numpy as np
import matplotlib.pyplot as plt

xmax = 2.01
ymax = 4.02

x = np.linspace(-xmax, xmax, 300)
y = np.linspace(-ymax, ymax, 300)
X, Y = np.meshgrid(x, y)
Z = 4 - X**2 - Y**2/4
# Surface plot
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, projection="3d")
Zpos = np.ma.masked_where(Z < 0, Z)
surf = ax.plot_surface(X, Y, Zpos,
                       # rstride=3, cstride=3,
                       alpha=0.6,
                       linewidth=0.1,
                       antialiased=True,
                       cmap="viridis")
ax.set_box_aspect((2, 2, 1))
ax.set_xlabel("$x$")
ax.set_ylabel("$y$")
ax.set_zlabel("$f(x,y)$");

filename = os.path.join("figures/tutorials/calculus", "mulivar_surface_plot_paraboloid.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/mulivar_surface_plot_paraboloid.pdf
Saved figure to figures/tutorials/calculus/mulivar_surface_plot_paraboloid.png
../_images/219be0d376215fb99db7a2501b55eaeb3e16c5613ca6b4e8e068756292913cf8.png
fig, ax = plt.subplots(figsize=(6,3))
cplot = ax.contour(X, Y, Z,
                   levels=[0,1,2,3,3.9],
                   cmap="viridis")
plt.clabel(cplot, inline=1, fontsize=9)
ax.set_xlabel('$x$')
ax.set_ylabel('$y$')

filename = os.path.join("figures/tutorials/calculus", "mulivar_countour_plot_paraboloid.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/mulivar_countour_plot_paraboloid.pdf
Saved figure to figures/tutorials/calculus/mulivar_countour_plot_paraboloid.png
../_images/9c24e246cd22e903c7ad4c6814824102808eda7b9dceb3ec1d718ea357233e25.png

Partial derivatives#

Gradient#

Partial integration#

from ministats.book.figures import plot_slices_through_paraboloid


ax = plot_slices_through_paraboloid(direction="x")

filename = os.path.join("figures/tutorials/calculus", "mulivar_x_slices_through_paraboloid.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/mulivar_x_slices_through_paraboloid.pdf
Saved figure to figures/tutorials/calculus/mulivar_x_slices_through_paraboloid.png
../_images/f4fff02652145600a603b35ef3cb4628957047ad104eefd4318938bdfc252a6a.png
ax = plot_slices_through_paraboloid(direction="y")

filename = os.path.join("figures/tutorials/calculus", "mulivar_y_slices_through_paraboloid.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/mulivar_y_slices_through_paraboloid.pdf
Saved figure to figures/tutorials/calculus/mulivar_y_slices_through_paraboloid.png
../_images/ffa0a1ac3cc324346fdf19ea7b1ec7c700d127adc21a44137e56c549b2b7a63b.png

Double integrals#

Applications of multivariable calculus#

Vector calculus#

Definitions#

from ministats.book.figures import plot_point_charge_field

ax = plot_point_charge_field(elev=20, azim=40, grid_lim=1.5, n_points=6)

filename = os.path.join("figures/tutorials/calculus", "vector_calc_E_field_positive_charge.pdf")
savefigure(ax, filename)
Saved figure to figures/tutorials/calculus/vector_calc_E_field_positive_charge.pdf
Saved figure to figures/tutorials/calculus/vector_calc_E_field_positive_charge.png
../_images/c9ca8d09f52b7fa46c81aab4ce127f98d135e173e4294d9b2ae96b9b5d154a5b.png

Path integrals#

Surface integrals#

Applications of vector calculus#

CUT MATERIAL#

We’ll discuss sp.dsolve again in the section on mechanics.

Trapezoid approximation (optional)#

Let’s now use another approach based on the trapezoid approximation.

We must build array of inputs \(x\) and outputs \(g(x)\) of the function, then pass it to trapz so it carries out the calculation.

# from scipy.integrate import trapezoid

# m = 1000
# xs = np.linspace(0, 5, m)
# gxs = g(xs)

# trapezoid(gxs, xs)