How to use¶
The following series of demos will step through how to differentiate a wide
variety of functions with spacejam
. Check out Installation to get
started.
Demo I: Scalar function, scalar input¶
This is the simplest case, where the function you provide takes in a single scalar argument \((x=a)\) and outputs a single scalar value \(f(a)\).
For example, let’s take a look at the function \(f(x) = x^3\), which you can define below as:
import numpy as np
def f(x):
return np.array([x**3])
All spacejam
needs now is for you to specify a point \(\mathbf p\)
where you would like to evaluate your function at:
p = np.array([5]) # evaluation point
Now, evaluating your function and simultaneously computing the
derivative with spacejam
at this point is as easy as:
import spacejam as sj
ad = sj.AutoDiff(f, p)
The real part of ad
is now \(f(x=5) = 125\) and the dual part is
\(\left.\frac{\mathrm d f}{\mathrm d x}\right|_{x=5} = 75\) .
These real and dual parts are conveniently stored, respectively, as the r
and d
attributes in ad
and can easily be printed to examine:
print(f'f(x) evaluated at p:\n{ad.r}\n\n'
f'derivative of f(x) evaluated at p:\n{ad.d}')
f(x) evaluated at p:
[125.00]
derivative of f(x) evaluated at p:
[75.00]
Note
numpy
arrays are used when defining your function and returning
results because spacejam
can also operate on multivariable functions and
parameters, which we outline in Demo II: Scalar function with vector input.
and Demo III: Vector function with vector input.
Demo II: Scalar function with vector input¶
This next demo explores the case where a new example function \(f\) can accept vector input, for example \(\mathbf p = (x_1, x_2) = (5, 2)\) and return a single scalar value \(f(\mathbf p) = f(x_1, x_2) = 3x_1x_2 - 2x_2^3/x_1\)
The dual number objects are created in much the same way as in Demo I, where:
as described in Automatic Differentiation: A brief overview. Internally, this is accomplished with the idx
and x
argument in spacejam.dual
so that it knows which dual parts need
to be set to zero in the modified dual numbers above. spacejam.autodiff
then performs the following internally:
tl;dr: all that needs to be done is:
import numpy as np
import spacejam as sj
def f(x_1, x_2):
return np.array([3*x_1*x_2 - 2*x_2**3/x_1])
p = np.array([5, 2]) # evaluation point (x_1, x_2) = (5, 2)
ad = sj.AutoDiff(f, p) # create spacejam object
# check out the results
print(f'f(x) evaluated p:\n{ad.r}\n\n'
f'grad of f(x) evaluated at p:\n{ad.d}')
f(x) evaluated p:
[26.80]
grad of f(x) evaluated at p:
[6.64 10.20]
Demo III: Vector function with vector input¶
This final demo shows how to use spacejam
to simultaneously evaluate the
example vector function:
and its Jacobian:
at the point \(\mathbf{p} = (x_1, x_2) = (1, 2)\) .
The interface with spacejam
happens to be exactly the same as in the
previous two demos, only now your \(F(x)\) will return a 1D numpy
array
of functions \((f_1, f_2, f_3)\):
# your (m) system of equations:
# F(x_1, x_2, ..., x_m) = (f1, f2, ..., f_n)
def F(x_1, x_2):
f_1 = x_1**2 + x_1*x_2 + 2
f_2 = x_1*x_2**3 + x_1**2
f_3 = x_1 + x_1**2*x_2**2 + x_2**3/x_1 + x_2**4
return np.array([f_1, f_2, f_3])
# where you want them evaluated at:
# p = (x_1, x_2, ..., x_m)
p = np.array([1, 2])
# auto differentiate!
ad = sj.AutoDiff(F, p)
# check out the results
print(f'F(x) evaluated at p:\n{ad.r}\n\n'
f'Jacobian of F(x) evaluated at p:\n{ad.d}')
F(x) evaluated at p:
[[5.00]
[9.00]
[29.00]]
Jacobian of F(x) evaluated at p:
[[4.00 1.00]
[10.00 12.00]
[1.00 48.00]]
Internally, for each \(i\) th entry, in the 1D numpy
array
ad._full
, the real part is the \(i\) th component of
\(\mathbf{F}(\mathbf{p})\) and the dual part is the corresponding row in
the Jacobian \(\mathbf J\) evaluated at
\(\mathbf p = (x_1, x_2) = (1,2)\) .
This is done in spacejam.autodiff.AutoDiff._matrix
for you with:
Fs = np.empty((F(*p).size, 1)) # initialze empty F(p)
jac = np.empty((F(*p).size, p.size)) # initialize empty J F(p)
for i, f in enumerate(ad._full): # fill in each row of each
Fs[i] = f.r
jac[i] = f.d
print(f'formated F(p):\n{Fs}\n\nformated J F(p):\n{jac}')
formated F(p):
[[5.00]
[9.00]
[29.00]]
formated J F(p):
[[4.00 1.00]
[10.00 12.00]
[1.00 48.00]]
where ad._full
looks like:
print(ad._full)
[5.00 + eps [4.00 1.00] 9.00 + eps [10.00 12.00] 29.00 + eps [1.00 48.00]]
Note
You are also free to make your own dual numbers (for example \(z = 3 + \epsilon\ 4\)) by doing:
z = sj.Dual(3, 4)
print(z)
3.00 + eps 4.00
We also use numpy
to overload basic trig functions, exponential,
and natural log, which are not builtins in python. This is accessed by
doing:
result = np.cos(z)
print(result)
-0.99 - eps 0.56
spacejam
formats all numbers to two decimal places but internally
the whole number is stored.