Graphing functions with Julia

The julia language is a new language and as such, certain design decisions are still being made. One key decision is the interface for creating graphics. At this point there are at least three different ones (Winston, Gadfly, and Gaston), and perhaps more will be generated before a dominant one is arrived at. As such, we don't try to teach the details of any one of them. That being said, we can do all we need with the easy-to-use plot function from the Gadfly package.

This function makes graphing a function as easy as specifying the domain to graph over, e.g.:

f(x) = exp(-x^2/2)
plot(f, -3, 3) 

The plot function of Gadfly

Using a package

Within each session you must first bring the package features into your workspace with the using command. Here is how we load the Gadfly package which will draw our graphics:

using Gadfly

If you wanted to use Winston's graphics, then the command would be using Winston.

Actually plotting

The most basic usage for plotting a function follows this pattern:

plot(function_object, from, to)

as in

plot(sin, 0, 2pi)

That creates the graphic. If you are working within IJulia it will be automatically displayed as an SVG file that allows you to zoom and pan with the mouse. The size of the graphics produced can be adjusted through a command like: set_default_plot_size(9inch, 6inch).

If working from a command line, Gadfly plots will be rendered in a browser. If desired, plots can also be written to a file using the draw function.

The above example used sin a function object. We can also use anonymous functions which are useful for one-off usage:

plot(x -> exp(x) + exp(-x), -2, 2) 

The last two examples show the two styles you can use to specify the function object. Either you have a name for the function object, such as sin, of 'f' (with no (x)!) or an anonymous function.

Again, we plot a function, this time a basic polynomial:

f(x) = x^2 - 2x + 2
plot(f, -3, 3) 

Using an anonymous function, we now graph that same function squared:

plot(x -> f(x)^2, -3, 3)

Inf, NaN values

Gadfly plots functions by creating about 250 paired points \((x, f(x))\), plots these, then connects the points with line segments. In the 250 function evaluations, it is of course quite possible that some of the points will return Inf or NaN.

The values which are Inf can not reasonably be plotted and when encountered Gadfly will issue a BoundsError, as in the value is out of bounds.

Values which are NaN are treated differently. Such points are simply not plotted, and no line segments are drawn causing the plot to be discontinuous. This convention can be utilized to good effect.

For example, this use truncates large values of a function:

trim_large(f; val=10) = x -> abs(f(x)) > val ? NaN : f(x)
f(x) = 1/x
plot(trim_large(f), -3, 3)

This function trim_large returns a function that just replaces any large values (in absolute value) of f with NaN. This also works with Inf values, as Inf can be ordered by >.

Practice

Question

Plot the function \(f(x) = x^3 - x\) over \([-2,2]\). How many zeros are there?

Question

Plot the function \(f(x) = x^3 - x\). When is the function positive?

Question

Plot the function \(f(x) = 3x^4 + 8x^3 - 18x^2\). Where (what \(x\) value) is the smallest value?

Question

Plot the function \(f(x) = 3x^4 + 8x^3 - 18x^2\). What is the smallest value?

Question

Plot the function \(f(x) = 3x^4 + 8x^3 - 18x^2\). When is the function increasing?

Question

Plot the polynomial function \(p(x) = x^3 - 7.11x^2 + 16.8503x - 13.311105\). How many real roots are there \(1\) or \(3\)?

Asymptotes

A rational function is nothing more than a ratio of polynomial functions, say \(f(x) = p(x)/q(x)\). One interesting thing about them is there can be asymptotes. These can be vertical (which can happen when \(q(x)=0\)), horizontal (as \(x\) gets large or small), or even slant.

The vertical asymptotes require care when plotting, as the naive style of plotting where a collection of points is connected by straight lines can render poor graphs when the scale of \(y\) values get too large. As well, the plot function does not handle cases where you get Inf for an answer (values that can come up when division by 0 is possible).

Keep in mind: while a graphic can highlight different features of a graph, it is often not possible to look at all of them on one graph.

Some features of a graph that are identifiable by calculus concepts are:

  • zeroes
  • vertical asymptotes
  • horizontal asymptotes (or even slant ones)
  • relative maximum and minimum
  • increasing and decreasing parts
  • changes in inflection

For example, if you want to find zeroes of a function, you really want to look at areas of the graph where the \(y\) values are close to \(0\). However, if you have a vertical asymptote on the same graph, the \(y\) values might also be asked to show very large or small values. With only a finite number of pixels available, it is impossible to easily do both.

What to do? If you were on a smartphone, you might be tempted to pan around to avoid the asymptotes, then pinch and zoom narrow the graph to the feature of interest. With julia you basically do the same thing, though panning and zooming is done by changing the domain of the \(x\) values used in plotting.

Let's look at the function \(f(x) = 1/x\), which has an obvious vertical asymptote at \(0\).

One can try a simple plot:

plot(x -> 1/x, -3, 3) 

The issue at \(0\) is avoided, as the points chosen by Gadfly do not include \(0\). The asymptote appears as a strongly slanted line, as individual points are simply connected in a dot-to-dot manner.

Doing better without much work is done by simply restricting the part that is plotted. (Alternatively, you can use NaN values or multiple functions on one.) For this example, we can easily draw the positive part:

It is best to avoid the asymptote at \(0\) completely by backing away by enough to avoid the large range on the \(y\) axis. In this case, starting around \(1/10\) is reasonable:

plot(x -> 1/x, 1/10,  3)

Let's look at the rational function

\[ f(x) = \frac{(x-2)(x-3)}{x-1} \]

This will have a vertical asymptote at \(1\), zeros at \(2\) and \(3\) and a slant asymptote. A quick graph from \(-10\) to \(10\) shows just some of this:

f(x) = (x-2)*(x-3)/(x-1)
plot(f, -10, 10)

We can see the slant asymptote and the vertical asymptote, but have no chance of seeing the zeros of the local extrema. We can if we restrict our plot range.

For example, we graph to the right of the asymptote with:

plot(f, 1 + 0.5, 4)

This shows the two zeros and gives an idea of the relative minimum. Similarly, a plot of the left of the asymptote can be illustrative. Here we step back by a bit:

plot(f, -3, 1 - 0.1)    

It appears that the relative maximum occurs between \(-1\) and \(0\).

Here we see what happens to the asymptote. The scale of the \(y\) values is huge. We added a small amount to the left endpoint in case the function is not defined there, but this function takes the reciprocal of a small amount and makes it huge. Clearly we need to really avoid the issue. It isn't hard -- just add a little bit more to \(0\):

Example:

Trigonometric functions

Let

\[ f(x) = \frac{5}{\cos x} + \frac{10}{\sin x}. \]

Estimate graphically the minimum value over \((0, \pi/2)\).

The domain comes from the fact that \(\sin(0) = 0\) and \(\cos(\pi/2) = 0\), so we will have asymptotes at each. A simple graph shows there are issues:

As typical with vertical asymptotes, we can't see finer features of the graph when the asymptotes are drawn. To remedy, we again back off from the boundaries. Since \(\sin(x)\) behaves like \(x\) near \(0\), we pick delta = 0.3 again and expect a max near \(10/(3/10) \approx 33\).

delta = 0.3;
plot(f, 0 + delta, pi/2 - delta) 

With this, we see the minimum value is near \(y=20\) and occurs around \(0.9\).

Practice

Question

The function \(f(x) = (x^3 - 2x) / (2x^2 -10)\) is a rational function with issues when \(2x^2 = 10\), or \(x = -\sqrt{5}\) or \(\sqrt{5}\).

Plot this function from \(-5\) to \(5\). How many times does it cross the \(x\) axis?

Question

The function \(f(x) = (x^3 - 2x) / (2x^2 -10)\) has a slant asymptote. One can find the slope of this line using algebra, but if you prefer the computer, you can graph. Define both

f(x) = (x^3 - 2x) / (2x^2 -10)
g(x) = abs(x) > 5 ? f(x) : NaN

then plot g over \([-20, 20]\). Using algebra or this graph, estimate the slope?

Question

The rational function \(f(x) = (x^2 - 3x - 2) / (x^2 - 4)\) has issues at \(-2\) and \(2\). How many times does its graph cross the \(x\) axis?

Arrays

When we learn how to make a graph using paper and pencil, the "T" method is employed, so called as we draw a "T" and fill in values to plot for \(x\) and \(y\).

For example, a chalkboard after the instructor shows how to plot \(f(x) = x^2\) might have this drawn on it:

x |  y
------
1 |  1
2 |  4
3 |  9
4 | 16

We would like to be able to mimic the following procedures used above in julia:

  • choose a collection of \(x\) values
  • create the corresponding \(y\) values
  • plot the pairs \((x,y)\) and connect with lines

As these are the steps done to create the ordered pairs for a plot.

We have seen how variables can be used to refer to a single value, but we want to refer to more than one value now. A container for holding such is an Array. Arrays are implemented in most all computer languages, though the term can mean different things. We are looking for vectors, or a one-dimensional array. In general, an array is a multidimensional grid of values -- all of the same type (integer, floating point, functions, ..., or ANY).

For our purposes, we want vectors (one dimensional, \(n\) by 1 arrays in julia). These can be easily constructed in different ways.

Creating 1-dimensional arrays with []

One can directly create a 1-dimensional array with the [] syntax. For example, to put a sequence of numbers together we can do:

[1, 2 , 3, 4, 5]
## [1,2,3,4,5]

Or

[1, 1, 2, 3, 5, 8]
## [1,1,2,3,5,8]

These create "vectors." (Our printing in the notes displays these horizontally to save space, but the output would appear vertically if done at the console.) Row vectors (which are arrays, but not of type Vector) can be created by dropping the commas:

[13 21 34 55]
## 1x4 Array{Int64,2}:
##  13  21  34  55

The notation is that [a,b,c] combines a, b, and c vertically (vcat) and [a b c] combines them horizontally (hcat). The former is good to make column vectors for single values (scalars).

Row and column vectors are different! We will use just column vectors going forward.

Creating arithmetic sequences

A basic set of numbers used in programming are the values 1,2, ..., n. These are the simplest example of an arithmetic progression or sequence, which in general can start in a different place and have steps of a size different from \(1\):

\[ a, a + h, a+2h, a+3h, ..., a + nh \]

It should be possible to specify arithmetic sequences either by

  • the start and end points and the number of points employed

  • the start and end points and step size.

In julia the linspace function will do the former and the range operator the latter.

Here are 5 evenly spaced numbers from \(0\) to \(\pi/2\) given by linspace (linearly spaced numbers):

linspace(0, pi/2, 5)
## [0.0,0.39269908169872414,0.7853981633974483,1.1780972450961724,1.5707963267948966]

A column vector is returned.

The 5 above is optional -- leaving it out will specify the default of \(100\) points:

linspace(0, pi/2)
## [0.0,0.015866629563584814,0.03173325912716963,0.04759988869075444,0.06346651825433926,0.07933314781792407,0.09519977738150888,0.11106640694509369,0.12693303650867852,0.14279966607226333  …  1.427996660722633,1.443863290286218,1.4597299198498028,1.4755965494133878,1.4914631789769726,1.5073298085405573,1.523196438104142,1.5390630676677268,1.5549296972313118,1.5707963267948966]

The range operator

The "range" operator, :, is used to specify a step size, like \(h\) in the definition above. To get values which step by 1 we simply specify the start and end values:

1:4
## 1:4

That isn't so impressive. The description julia uses to show this value is exactly how we defined it, but this range is specifying the values 1, 2, 3, 4. To see that, we can put inside, [] to make an array:

[1:4]
## [1,2,3,4]

The range operator returns a Range object which internally is much more compact to store than the array, though at times less useful. (At those times, they can be converted to arrays as above.)

The range operator can also work with a step size:

a=0; b=10; h=3
a:h:b
## 0:3:9
[a:h:b]
## [0,3,6,9]

Notice, the value for b is treated as as suggestion, the range will stop without exceeding b.

The \(x\) values for a plot are typically a sequence of increasing values from \(a\) to \(b\). We would generally like to be able to specify the number of values to plot. This makes linspace the go-to choice to use. (Though Gadfly uses: a:(b-a)/n:b with n=250.)

Practice

Question

Which command will produce the sequence \(1,3,5,7,9,11, ..., 99\)?

Question

Which command produces 10 numbers between 0 and 10 that are evenly spaced?

Question

Which command does not produce the numbers \(1, 2, 3, 4, 5\)?

Question

Which command does produces the numbers \(1, 1, 2, 3, 5, 8, 13\)?

Indexing

A column vector has a natural sense of first, second, ..., the \(n\)-th element. This allows julia to refer to the values by index (\(1\)-based, unlike some other computer languages). So, if x is an array, x[1] is the first value in that array. One can extract and assign values using indices. A simple example is:

x = [2, 4, 6, 8]
## [2,4,6,8]
x[1]
## 2
x[3]
## 6

There are some special values. The end value refers to the last (\(n\) th):

x[end]
## 8

The \(n\) -- or number of elements -- can be returned by length:

length(x)
## 4

A range object can be used for indices:

x[1:3]
## [2,4,6]

As can any other column vector (but not a row vector):

x[ [ 1, 2, 3] ]
## [2,4,6]

The value end can be used in a range when indexing:

x[2:end]
## [4,6,8]

(But not without indexing, as you can see by typing 2:end by itself.)

Practice

Question

Let

x = [1, 1, 2, 3, 5, 8, 13]
## [1,1,2,3,5,8,13]

What is the value of x[end - 1] + x[end]?

Question

Let

x = [1, 1, 2, 3, 5, 8, 13]
## [1,1,2,3,5,8,13]

What is the value of x[3]?

Question

When a vector is created, if possible the resulting values are converted to be the same type. Let

x = [1, 2.0]
## [1.0,2.0]
y = [1, 2.0, "three"]
## {1,2.0,"three"}

For x[1] and y[1] what does typeof return?

(The y container is of type Any which allows it to hold any type of object, the x container only holds values of a certain type.)

Mapping a function to multiple values

To specify the \(y\) values we wish to "map" the function f to each \(x\) value. In julia there are many different ways to do this, we list four.

  1. Map: The map function. In many areas of mathematics, one refers to a function as a "map" from the domain to the range. The implication is that the function takes all the \(x\) values to the corresponding \(y\) values at once (conceptually) and not one at a time. The map function will apply the function f to each value in the array x, basically taking [x1, x2, ..., xn] and returning [f(x1), f(x2), ..., f(xn)].

For example, let's look at the simple polynomial \(f(x) = -16x^2 + 32x\). We define our julia function with:

f(x) = -16x^2 + 32x

If we want to look at this function for \(x\) values between \(0\) and \(2\) we might define the \(x\) values with:

x = linspace(0, 2, 5)
## [0.0,0.5,1.0,1.5,2.0]

Then the map function will create the corresponding \(y\) values:

map(f, x)
## [0.0,12.0,16.0,12.0,0.0]

The syntax of map requires a slight pause. Here we do not actually call the function f, as in f(2). Rather, we pass the name of the function object to the map argument -- and map calls the function for each value in the column vector x and returns a corresponding column vector.

It is also quite common to use anonymous functions with map. For example:

map(u -> 10 * u^2, x)
## [0.0,2.5,10.0,22.5,40.0]

We use u for the dummy variable in the anonymous function, so as not to get it confused with the variable x holding our values, but this is not necessary.

  1. Comprehensions: Mathematically we grow familiar with set notation. One way to describe the values \(y\) we are talking about might be:

\[ y = \{ f(x): x \text{ in } [0, 2] \}, \]

where this is read the values \(f(x)\) for each \(x\) in the interval \([0,2]\).

Here we define values vals to represent the continuum of values in the interval \([0,2]\), then use a "comprehension" to create the set notation above. The syntax is similar:

vals = linspace(0, 2.0, 5)
## [0.0,0.5,1.0,1.5,2.0]
[f(x) for x in vals]  ## or {f(x) for x = vals}
## {0.0,12.0,16.0,12.0,0.0}

Square brackets are typical when using a comprehension, though curly braces are possible -- try it. While curly braces are more mathematically familiar -- they always return a container of type Any. Square brackets try to guess a better type, if possible.

In this example we still get a container of type Any with square brackets. While not critical, we would really like to have this be of type "float", as some math functions care about this distinction. Converting can be done a few ways:

  • by wrapping the entire thing in float (which coerces the values):
float( [ f(x) for x in vals ] )
## [0.0,12.0,16.0,12.0,0.0]
  • By using square brackets and declaring the type:
Float64[f(x) for x in vals]
## [0.0,12.0,16.0,12.0,0.0]

Either way, perhaps map is a bit less trouble. (For ranges it is just syntactic sugar, as a map is defined by a comprehension.) Regardless, we mostly use comprehensions going forward as they mirror a more familiar mathematical syntax and generalize to functions of more than one variable nicely.

  1. Using a for loop: The for loop is a very common pattern in computer programming. For speed reasons, some languages (e.g., MATLAB and R) try to avoid for loops for a "vectorized" approach (see below), but julia works well with for loops, and they are generally faster than a vectorized approach.

A for loop simply loops over each value in the iterable data vector x giving it a temporary name as it goes. To use a for loop to create the \(y\) values requires several steps. The first creates a container to hold the new values, using the similar function to make a vector of the same size as x below (but fills it with nonsense):

y = similar(x);
for i in 1:length(x)
   y[i] = f( x[i] )
end

The for loop ends with the keyword end. Here we loop over each index of x and assign to the corresponding y value f(x[i]).

Conceptually this is the opposite of map where we think of the function acting on the entire column vector x. Instead, we iterate one-by-one over the values of x saving the function values as we go. The use of for loops is called imperative programming, as you describe each step. The use of functions like map is called declarative programming as you simply declare what you want and the computer language does the rest.

In some languages for loops are avoided if possible, as they can be slower. As well, they can require extra bookkeeping, such as needing to allocate a container for the answers. That being said, in julia they are widely used for speed and storage reasons. As well, they are used when we need to refer to more than one index. An example of that is the following way to create a Fibonacci pattern from the formula \(x_i = x_{i-1} + x_{i-2}\):

x = zeros(Int, 10);         ## pre-allocate an integer array
x[1:2] = [1,1]
## [1,1]
for i in 3:length(x)
   x[i] =  x[i-1] + x[i-2]
end

Related to a for loop is the while loop. This will repeat as long as some condition is true. The following pattern reproduces a for loop:

i, n = 1, length(x)
## (1,10)
while (i <= n)
  print( x[i], " " )            ## do something ...
  i = i + 1
end

We will use while loops when we iterate some process and are waiting until some computed value gets close to \(0\).

  1. Vectorization: Finally, we note that julia allows for "vectorization" of values. That means, many functions work on an array all at once. Many
sin(x)
## [0.8414709848078965,0.8414709848078965,0.9092974268256817,0.1411200080598672,-0.9589242746631385,0.9893582466233818,0.4201670368266409,0.8366556385360561,0.5290826861200238,-0.9997551733586199]

but not all:

x^2
## "MethodError(*,([1,1,2,3,5,8,13,21,34,55],[1,1,2,3,5,8,13,21,34,55]))"

The latter fails, whereas the sin function naturally returns the 5 values for x. The reason is the definitions of multiplication, division and powers are mathematically different for single numbers (scalars) than for column vectors, or more generally matrices. Like MATLAB, julia chose the simplest notation to do matrix multiplication, powers, and division. To get element-by-element operations, we prefix the operator with a "dot", as in .^:

x .^ 2
## [1,1,4,9,25,64,169,441,1156,3025]

So, if we were to redefine our function \(f(x) = -16x^2 + 32x\) with:

f(x) = -16x.^2 + 32x

Then we could have simply done:

f(x)
## [16,16,0,-48,-240,-768,-2288,-6384,-17408,-46640]

Though this is easy to do, it is also rather tedious, as putting in the "dots" is, for some reason, not that intuitive, so mistakes are often made. (It can surprisingly, also be slower to perform.)

We do use this style to generate simple values, for example here we step through some powers of \(1/10\) without much fuss:

(1/10).^(1:5)
## [0.1,0.010000000000000002,0.0010000000000000002,0.00010000000000000005,1.0000000000000003e-5]

Example

Putting this altogether, to create the "T"-table used to graph \(y=x^2\), we could do any of these:

begin
  x = [1:4]
  y = map(u -> u^2, x)
  [x y]                           ## integer type
end
## 4x2 Array{Int64,2}:
##  1   1
##  2   4
##  3   9
##  4  16

or

begin
  x = [1, 2, 3, 4]
  y = [x^2 for x in x]
  [x y]                           ## Any type, as y is -- not the same
end
## 4x2 Array{Any,2}:
##  1   1
##  2   4
##  3   9
##  4  16

or

begin
  x = 1:4
  y = similar(x)
  for i in 1:length(x) 
      y[i] = x[i]^2         ## integer, as x[i] is
  end
  [x y]
end
## 4x2 Array{Int64,2}:
##  1   1
##  2   4
##  3   9
##  4  16
begin
  x = linspace(1.0, 4.0, 4)
  y = x.^2 
  [x y]                        ## Float type, as x is here
end
## 4x2 Array{Float64,2}:
##  1.0   1.0
##  2.0   4.0
##  3.0   9.0
##  4.0  16.0

The comments show that each is slightly different for technical reasons, but all display the same values.

Practice

Questions

Does this command produce the values \(\{.1, .01, .001, .0001\}\)?

x = [(1/10)^i for i in 1:4];

Questions

Does this command produce the numbers \(10, 100, 1000, 10,000\)?

map(i -> 10^i, 1:4);

Questions

Let \(f(x) = x^2 - 2x\). Which command will produce a \(y\) values for plotting \(f\) over \([0, 2]\)?

Questions

Will this command produce \(y\) values for plotting \(f(x)\) over \([0,1]\)?

[f(x) for x in 0:1/100:1];

Questions

Sometimes the function is used to ask a question. For example, this command answers if the values are prime or not:

[isprime(i) for i in 1:10]
## [false,true,true,false,true,false,true,false,false,false]

This shows there are 4 primes in 1 to 10. How many are there between 100 and 110?

(It is easier to write map(isprime, 1:10) in this case.)

Graphing points connected with lines

If one has two vectors, x and y of equal size, then creating a graphic for them is straightforward. The basic syntax is

plot(x=x, y=y, Geom.line)

For example, to plot \(y=x^2\) over \([-1,1]\) we might do:

f(x) = x^2
xs = linspace(-1, 1)
ys = map(f, xs)
plot(x=xs, y=ys, Geom.line)

The specification Geom.line instructs Gadfly how to present the data, in this case connected with lines. One can add Geom.point to get the points filled in as well. This is the default setting, so if omitted a scatter plot is drawn.

Two functions at once

Now that we know about vectors, we can use them to illustrate how to graph two (or more) functions at once over the same interval -- just make a vector of functions and hand this off to plot.

For example, to graph both the sine and cosine function we have:

plot([sin, cos], 0, 2pi) 

Or to compare the effects of a simple transformation:

f(x) = x^2
g(x) = 4 + f(x-3)
plot([f, g], -4, 4)

To print a heavier \(x\)-axis, we can graph the function \(y=0\), specified through the anonymous function x -> 0:

f(x) = x^2 - 2
plot([f, x -> 0], -2, 2)

Similarly, this example adds a straight line at the origin, but only when the cosine is positive:

plot([sin, x -> cos(x) > 0 ? 0.0 : NaN], 0, 2pi) ## need 0.0 and not simply 0 here

Vertically stacking figures

Not all pairs of functions will naturally appear on the same \(x\) and \(y\) axes, as the scale of them may be way off. In fact that is typical. Yet, you may still wish to compare them. The vstack command in the Compose package (this package does the drawing for Gadfly) allows us to vertically stack graphics. (There is also hstack for horizontal stacks.)

The simplest usage just has one add graphics with commas:

using Compose
begin
  p1 = plot(x -> x^2 + 3x + 10, -4, 4);
  p2 = plot(x ->  2x + 3      , -4, 4);
  vstack(p1, p2)
end

The vstack function stacks plots vertically. There is an accompanying hstack command for putting graphics side by side.

Practice

Question

Define \(f(x)\) to be a triangular function as follows:

f(x) = abs(x) > 1 ? 0.0 : 1.0 - abs(x)

In many applications, the following transformation is employed:

\[ g(x, c, h) = \frac{1}{h} f(\frac{x - c}{h}) \]

For constants \(h\) and \(c\).

Make a graph of both \(f(x)\) and \(g(x, 1, 1/2)\) over the interval \([-2,3]\). Consult the graph to see which statement is true?

Question

We saw that this command will produce two graphs:

plot([sin, x -> cos(x) > 0 ? 0 : NaN], 0, 2pi)

What is the sine curve doing when the flat line is drawn?

Question

Make a graph of \(f(x) = x\), \(g(x) = \tan(x)\), and \(h(x) = \sin(x)\). Over the interval \([0,\pi/4]\). Based on this graph which of the following below seems correct?