55  Vector-valued functions, \(f:R \rightarrow R^n\)

This section uses these add-on packages:

using CalculusWithJulia
using Plots
plotly()
using SymPy
using Roots
using LinearAlgebra
using QuadGK

and

import DifferentialEquations
import DifferentialEquations: ODEProblem, Tsit5

We discuss functions of a single variable that return a vector in \(R^n\). There are many parallels to univariate functions (when \(n=1\)) and differences.

55.1 Definition

A function \(\vec{f}: R \rightarrow R^n\), \(n > 1\) is called a vector-valued function. Some examples:

\[ \vec{f}(t) = \langle \sin(t), 2\cos(t) \rangle, \quad \vec{g}(t) = \langle \sin(t), \cos(t), t \rangle, \quad \vec{h}(t) = \langle 2, 3 \rangle + t \cdot \langle 1, 2 \rangle. \]

The components themselves are also functions of \(t\), in this case univariate functions. Depending on the context, it can be useful to view vector-valued functions as a function that returns a vector, or a vector of the component functions.

The above example functions have \(n\) equal \(2\), \(3\), and \(2\) respectively. We will see that many concepts of calculus for univariate functions (\(n=1\)) have direct counterparts.

(We use \(\vec{f}\) above to emphasize the return value is a vector, but will quickly drop that notation and let context determine if \(f\) refers to a scalar- or vector-valued function.)

55.2 Representation in Julia

In Julia, the representation of a vector-valued function is straightforward: we define a function of a single variable that returns a vector. For example, the three functions above would be represented by:

f(t) = [sin(t), 2*cos(t)]
g(t) = [sin(t), cos(t), t]
h(t) = [2, 3] + t * [1, 2]
h (generic function with 1 method)

For a given t, these evaluate to a vector. For example:

h(2)
2-element Vector{Int64}:
 4
 7

We can create a vector of functions, e.g., F = [cos, sin, identity], but calling this object, as in F(t), would require some work, such as t = 1; [f(t) for f in F] or 1 .|> F.

F = [cos, sin, identity]
[f(1) for f in F]
3-element Vector{Real}:
 0.5403023058681398
 0.8414709848078965
 1

or

1 .|> F
3-element Vector{Real}:
 0.5403023058681398
 0.8414709848078965
 1

55.3 Space curves

A vector-valued function is typically visualized as a curve. That is, for some range, \(a \leq t \leq b\) the set of points \(\{\vec{f}(t): a \leq t \leq b\}\) are plotted. If, say in \(n=2\), we have \(x(t)\) and \(y(t)\) as the component functions, then the graph would also be the parametric plot of \(x\) and \(y\). The term planar curve is common for the \(n=2\) case and space curve for the \(n \geq 3\) case.

This plot represents the vectors with their tails at the origin.

There is a convention for plotting the component functions to yield a parametric plot within the Plots package (e.g., plot(x, y, a, b)). This can be used to make polar plots, where x is t -> r(t)*cos(t) and y is t -> r(t)*sin(t).

However, we will use a different approach, as the component functions are not naturally produced from the vector-valued function.

In Plots, the command plot(xs, ys), where, say, xs=[x1, x2, ..., xn] and ys=[y1, y2, ..., yn], will make a connect-the-dot plot between corresponding pairs of points. As previously discussed, this can be used as an alternative to plotting a function through plot(f, a, b): first make a set of \(x\) values, say xs=range(a, b, length=100); then the corresponding \(y\) values, say ys = f.(xs); and then plotting through plot(xs, ys).

Similarly, were a third vector, zs, for \(z\) components used, plot(xs, ys, zs) will make a \(3\)-dimensional connect the dot plot

However, our representation of vector-valued functions naturally generates a vector of points: [[x1,y1], [x2, y2], ..., [xn, yn]], as this comes from broadcasting f over some time values. That is, for a collection of time values, ts the command f.(ts) will produce a vector of points. (Technically a vector of vectors, but points if you identify the \(2\)-\(d\) vectors as points.)

To get the xs and ys from this is conceptually easy: just iterate over all the points and extract the corresponding component. For example, to get xs we would have a command like [p[1] for p in f.(ts)]. Similarly, the ys would use p[2] in place of p[1]. The unzip function from the CalculusWithJulia package does this for us. The name comes from how the zip function in base Julia takes two vectors and returns a vector of the values paired off. This is the reverse. As previously mentioned, unzip uses the invert function of the SplitApplyCombine package to invert the indexing (the \(j\)th component of the \(i\)th point can be referenced by vs[i][j] or invert(vs)[j][i]).

Visually, we have unzip performing this reassociation:

[[x1, y1, z1],         (⌈x1⌉,  ⌈y1⌉, ⌈z1⌉,
 [x2, y2, z2],          |x2|, |y2|, |z2|,
 [x3, y3, z3],   -->    |x3|, |y3|, |z3|,
     ⋮                         ⋮
 [xn, yn, zn]]          ⌊xn⌋,  ⌊yn⌋, ⌊zn⌋ )

To turn a collection of vectors into separate arguments for a function, splatting (the ...) is used.


Finally, with these definitions, we can visualize the three functions we have defined.

Here we show the plot of f over the values between \(0\) and \(2\pi\) and also add a vector anchored at the origin defined by f(1).

ts = range(0, 2pi, length=200)
xs, ys = unzip(f.(ts))
plot(xs, ys)
arrow!([0, 0], f(1))

The trace of the plot is an ellipse. If we describe the components as \(\vec{f}(t) = \langle x(t), y(t) \rangle\), then we have \(x(t)^2 + y(t)^2/4 = 1\). That is, for any value of \(t\), the resulting point satisfies the equation \(x^2 + y^2/4 =1\) for an ellipse.

The plot of \(g\) needs \(3\)-dimensions to render. For most plotting backends, the following should work with no differences, save the additional vector is anchored in \(3\) dimensions now:

ts = range(0, 6pi, length=200)
plot(unzip(g.(ts))...) # use splatting to avoid xs,ys,zs = unzip(g.(ts))
arrow!([0, 0, 0], g(2pi))

Here the graph is a helix; three turns are plotted. If we write \(g(t) = \langle x(t), y(t), z(t) \rangle\), as the \(x\) and \(y\) values trace out a circle, the \(z\) value increases. When the graph is viewed from above, as below, we see only \(x\) and \(y\) components, and the view is circular.

ts = range(0, 6pi, length=200)
plot(unzip(g.(ts))..., camera=(0, 90))

The graph of \(h\) shows that this function parameterizes a line in space. The line segment for \(-2 \leq t \leq 2\) is shown below:

ts = range(-2, 2, length=200)
plot(unzip(h.(ts))...)

55.3.1 The plot_parametric function

While the unzip function is easy to understand as a function that reshapes data from one format into one that plot can use, its usage is a bit cumbersome. The CalculusWithJulia package provides a function plot_parametric which hides the use of unzip and the splatting within a function definition.

The function borrows a calling style for Makie. The interval to plot over is specified first using a..b notation (which specifies a closed interval in the IntervalSets package), then the function is specified. Additional keyword arguments are passed along to plot.

plot_parametric(-2..2, h)
Note

Defining plotting functions in Julia for Plots is facilitated by the RecipesBase package. There are two common choices: creating a new function for plotting, as is done with plot_parametric and plot_polar; or creating a new type so that plot can dispatch to an appropriate plotting method. The latter would also be a reasonable choice, but wasn’t taken here. In any case, each can be avoided by creating the appropriate values for xs and ys (and possibly zs).

Example

Familiarity with equations for lines, circles, and ellipses is important, as these fundamental geometric shapes are often building blocks in the description of other more complicated things.

The point-slope equation of a line, \(y = y_0 + m \cdot (x - x_0)\) finds an analog. The slope, \(m\), is replaced with a vector \(\vec{v}\) and the point, \((x_0, y_0)\) is replaced with a vector \(\vec{p}\) identified with a point in the plane. A parameterization would then be \(\vec{f}(t) = \vec{p} + (t - t_0) \vec{v}\). From this, we have \(\vec{f}(t_0) = \vec{p}\).

The unit circle is instrumental in introducing the trigonometric functions though the identification of an angle \(t\) with a point on the unit circle \((x,y)\) through \(y = \sin(t)\) and \(x=\cos(t)\). With this identification certain properties of the trigonometric functions are immediately seen, such as the period of \(\sin\) and \(\cos\) being \(2\pi\), or the angles for which \(\sin\) and \(\cos\) are positive or even increasing. Further, this gives a natural parameterization for a vector-valued function whose plot yields the unit circle, namely \(\vec{f}(t) = \langle \cos(t), \sin(t) \rangle\). This parameterization starts (at \(t=0\)) at the point \((1, 0)\). More generally, we might have additional parameters \(\vec{f}(t) = \vec{p} + R \cdot \langle \cos(\omega(t-t_0)), \sin(\omega(t-t_0)) \rangle\) to change the origin, \(\vec{p}\); the radius, \(R\); the starting angle, \(t_0\); and the rotational frequency, \(\omega\).

An ellipse has a slightly more general equation than a circle and in simplest forms may satisfy the equation \(x^2/a^2 + y^2/b^2 = 1\), where when \(a=b\) a circle is being described. A vector-valued function of the form \(\vec{f}(t) = \langle a\cdot\cos(t), b\cdot\sin(t) \rangle\) will trace out an ellipse.

The above description of an ellipse is useful, but it can also be useful to re-express the ellipse so that one of the foci is at the origin. With this, the ellipse can be given in polar coordinates through a description of the radius:

\[ r(\theta) = \frac{a (1 - e^2)}{1 + e \cos(\theta)}. \]

Here, \(a\) is the semi-major axis (\(a > b\)); \(e\) is the eccentricity given by \(b = a \sqrt{1 - e^2}\); and \(\theta\) a polar angle.

Using the conversion to Cartesian equations, we have \(\vec{f}(\theta) = \langle r(\theta) \cos(\theta), r(\theta) \sin(\theta)\rangle\).

For example:

a, ecc = 20, 3/4
f(t) = a*(1-ecc^2)/(1 + ecc*cos(t)) * [cos(t), sin(t)]
plot_parametric(0..2pi, f, legend=false)
scatter!([0],[0], markersize=4)
Example

The Spirograph is “… a geometric drawing toy that produces mathematical roulette curves of the variety technically known as hypotrochoids and epitrochoids. It was developed by British engineer Denys Fisher and first sold in \(1965\).” These can be used to make interesting geometrical curves.

Following Wikipedia: Consider a fixed outer circle \(C_o\) of radius \(R\) centered at the origin. A smaller inner circle \(C_i\) of radius \(r < R\) rolling inside \(C_o\) and is continuously tangent to it. \(C_i\) will be assumed never to slip on \(C_o\) (in a real Spirograph, teeth on both circles prevent such slippage). Now assume that a point \(A\) lying somewhere inside \(C_{i}\) is located a distance \(\rho < r\) from \(C_i\)’s center.

The center of the inner circle will move in a circular manner with radius \(R-r\). The fixed point on the inner circle will rotate about this center. The accumulated angle may be described by the angle the point of contact of the inner circle with the outer circle. Call this angle \(t\).

Suppose the outer circle is centered at the origin and the inner circle starts (\(t=0\)) with center \((R-r, 0)\) and rotates around counterclockwise. Then if the point of contact makes angle \(t\), the arc length along the outer circle is \(Rt\). The inner circle will have moved a distance \(r t'\) in the opposite direction, so \(Rt =-r t'\) and solving the angle will be \(t' = -(R/r)t\).

If the initial position of the fixed point is at \((\rho, 0)\) relative to the origin, then the following function will describe the motion:

\[ \vec{s}(t) = (R-r) \cdot \langle \cos(t), \sin(t) \rangle + \rho \cdot \langle \cos(-\frac{R}{r}t), \sin(-\frac{R}{r}t) \rangle. \]

To visualize this we first define a helper function to draw a circle at point \(P\) with radius \(R\):

circle!(P, R; kwargs...) = plot_parametric!(0..2pi, t -> P + R * [cos(t), sin(t)]; kwargs...)
circle! (generic function with 1 method)

Then we have this function to visualize the spirograph for different \(t\) values:

function spiro(t; r=2, R=5, rho=0.8*r)

    cent(t) = (R-r) * [cos(t), sin(t)]

    p = plot(legend=false, aspect_ratio=:equal)
    circle!([0,0], R, color=:blue)
    circle!(cent(t), r, color=:black)

    tp(t) = -R/r * t

    s(t) = cent(t) + rho * [cos(tp(t)), sin(tp(t))]
    plot_parametric!(0..t, s, color=:red)

    p
end
spiro (generic function with 1 method)

And we can see the trace for \(t=\pi\):

spiro(pi)

The point of contact is at \((-R, 0)\), as expected. Carrying this forward to a full circle’s worth is done through:

spiro(2pi)

The curve does not match up at the start. For that, a second time around the outer circle is needed:

spiro(4pi)

Whether the curve will have a period or not is decided by the ratio of \(R/r\) being rational or irrational.

Example

In 1935 Marcel Duchamp showed a collection of “Rotorelief” discs at a French fair for inventors. Disk number 10 is comprised of several nested, off-center circles on disk that would be rotated to give a sense of movement. To mimic the effect:

  • for each circle, \(3\) points where selected using a mouse from an image and their pixels recorded;
  • as \(3\) points determine a circle, the center and radius of each circle can be solved for
  • the exterior of the disc is drawn (the last specified circle below);
  • each nested circle is drawn after its center is rotated by \(\theta\) radian;
  • an animation captures the movement for display.