align fix; theorem style; condition number

This commit is contained in:
jverzani
2024-10-31 14:22:21 -04:00
parent 3e7e3a9727
commit 18aae2aa93
61 changed files with 1705 additions and 819 deletions

View File

@@ -533,7 +533,7 @@ sqrt(11^2 + 12^2)
##### Example
A formula from statistics to compute the variance of a binomial random variable for parameters $p$ and $n$ is $\sqrt{n p (1-p)}$. Compute this value for $p=1/4$ and $n=10$.
A formula from statistics to compute the variance of a binomial random variable with parameters $p$ and $n$ is $\sqrt{n p (1-p)}$. Compute this value for $p=1/4$ and $n=10$.
```{julia}
@@ -568,20 +568,54 @@ Not all computations on a calculator are valid. For example, the Google calculat
In `Julia`, there is a richer set of error types. The value `0/0` will in fact not be an error, but rather a value `NaN`. This is a special floating point value indicating "not a number" and is the result for various operations. The output of $\sqrt{-1}$ (computed via `sqrt(-1)`) will indicate a domain error:
```{julia}
#| error: true
sqrt(-1)
```
Other calls may result in an overflow error:
```{julia}
#| error: true
factorial(1000)
```
How `Julia` handles overflow is a study in tradeoffs. For integer operations that demand high performance, `Julia` does not check for overflow. So, for example, if we are not careful strange answers can be had. Consider the difference here between powers of 2:
How `Julia` handles overflow is a study in tradeoffs. For integer operations that demand high performance, `Julia` does not check for overflow. So, for example, if we are not careful strange answers can be had. Consider the difference here between these powers of 2:
```{julia}
2^62, 2^63
2^62, 2^63, 2^64
```
On a machine with $64$-bit integers, the first of these two values is correct, the second, clearly wrong, as the answer given is negative. This is due to overflow. The cost of checking is considered too high, so no error is thrown. The user is expected to have a sense that they need to be careful when their values are quite large. (Or the user can use floating point numbers, which though not always exact, can represent much bigger values and are exact for a reasonably wide range of integer values.)
On a machine with $64$-bit integers, the first of these two values is correct, the third is clearly "wrong," and the second clearly "wrong," as the answer given is negative.
Wrong is in quotes, as though they are mathematically incorrect, computationally they are correct. The last two are due to overflow. The cost of checking is considered too high, so no error is thrown and the values represent what happens at the machine level.
The user is expected to have a sense that they need to be careful when their values are quite large. But the better recommendation is that the user use floating point numbers, which as easy as typing `2.0^63`. Though not always exact, floating point values can represent a much bigger range values and are exact for a reasonably wide range of integer values.
::: {.callout-note}
## Bit-level details
We can see in the following, using using the smaller 8-bit type, what goes on internally with successive powers of `2`: the bit pattern is found by shifting the previous one over to the left, consistent with what happens at the bit level when multiplying by `2`:
```
[bitstring(Int8(2)^i) for i in 1:8]
```
The last line is similar to what happens to `2^64` which is also `0`, as seen. The second to last line requires some understanding of how integers are represented internally. Of the 8 bits, the last 7 represent which powers of `2` (`2^6`, `2^5`, ..., `2^1`, `2^0`) are included. The first `1` represents `-2^7`. So all zeros, as we see `2^8` is, means `0`, but `"10000000"` means `-2^7`. The largest *positive* number would be represented as `"01111111"` or ``2^6 + 2^5 + \cdots + 2^1 + 2^0 = 2^7 - 1``. These values can be seen using `typemin` and `typemax`:
```{julia}
typemin(Int8), typemax(Int8)
```
For 64-bit, these are:
```{julia}
typemin(Int), typemax(Int)
```
And we see, `2^63` is also just `typemin(Int)`.
:::
:::{.callout-warning}

View File

@@ -8,7 +8,8 @@ This section uses the following add-on packages:
```{julia}
using CalculusWithJulia
using Plots; plotly()
using Plots
plotly()
```
@@ -27,7 +28,7 @@ The family of exponential functions is defined by $f(x) = a^x, -\infty< x < \inf
For a given $a$, defining $a^n$ for positive integers is straightforward, as it means multiplying $n$ copies of $a.$ From this, for *integer powers*, the key properties of exponents: $a^x \cdot a^y = a^{x+y}$, and $(a^x)^y = a^{x \cdot y}$ are immediate consequences. For example with $x=3$ and $y=2$:
$$
\begin{align*}
a^3 \cdot a^2 &= (a\cdot a \cdot a) \cdot (a \cdot a) \\
&= (a \cdot a \cdot a \cdot a \cdot a) \\
@@ -36,7 +37,7 @@ a^3 \cdot a^2 &= (a\cdot a \cdot a) \cdot (a \cdot a) \\
&= (a\cdot a \cdot a \cdot a\cdot a \cdot a) \\
&= a^6 = a^{3\cdot 2}.
\end{align*}
$$
For $a \neq 0$, $a^0$ is defined to be $1$.
@@ -167,7 +168,7 @@ Later we will see an easy way to certify this statement.
##### The mathematical constant $e$
Euler's number, $e$, may be defined several ways. One way is to define $e^x$ by the limit $(1+x/n)^n$. Then $e=e^1$. The value is an irrational number. This number turns up to be the natural base to use for many problems arising in Calculus. In `Julia` there are a few mathematical constants that get special treatment, so that when needed, extra precision is available. The value `e` is not immediately assigned to this value, rather `` is. This is typed `\euler[tab]`. The label `e` is thought too important for other uses to reserve the name for representing a single number. However, users can issue the command `using Base.MathConstants` and `e` will be available to represent this number. When the `CalculusWithJulia` package is loaded, the value `e` is defined to be the floating point number returned by `exp(1)`. This loses the feature of arbitrary precision, but has other advantages.
Euler's number, $e$, may be defined several ways. One way is to define $e^x$ by the limit as $n$ grows infinitely large of $(1+x/n)^n$. Then $e=e^1$. The value is an irrational number. This number turns up to be the natural base to use for many problems arising in calculus. In `Julia` there are a few mathematical constants that get special treatment, so that when needed, extra precision is available. The value `e` is not immediately assigned to this value, rather `` is. This is typed `\euler[tab]`. The label `e` is thought too important for other uses to reserve the name for representing a single number. However, users can issue the command `using Base.MathConstants` and `e` will be available to represent this number. When the `CalculusWithJulia` package is loaded, the value `e` is defined to be the floating point number returned by `exp(1)`. This loses the feature of arbitrary precision, but has other advantages.
A [cute](https://www.mathsisfun.com/numbers/e-eulers-number.html) appearance of $e$ is in this problem: Let $a>0$. Cut $a$ into $n$ equal pieces and then multiply them. What $n$ will produce the largest value? Note that the formula is $(a/n)^n$ for a given $a$ and $n$.
@@ -215,32 +216,32 @@ co2_1970 = [(1860, 293), (1870, 293), (1880, 294), (1890, 295), (1900, 297),
co2_2021 = [(1960, 318), (1970, 325), (1980, 338), (1990, 358), (2000, 370),
(2010, 390), (2020, 415)]
xs,ys = unzip(co2_1970)
plot(xs, ys, legend=false)
plot(co2_1970, legend=false) # vector of points interface
plot!(co2_2021)
𝒙s, 𝒚s = unzip(co2_2021)
plot!(𝒙s, 𝒚s)
exp_model(;r, x0, P0) = x -> P0 * exp(r * (x - x0))
r = 0.002
x, P = 1960, 313
plot!(x -> P₀ * exp(r * (x - x₀)), 1950, 1990, linewidth=5, alpha=0.25)
x0, P0 = 1960, 313
plot!(exp_model(; r, x0, P0), 1950, 1990, linewidth=5, alpha=0.25)
𝒓 = 0.005
𝒙₀, 𝑷₀ = 2000, 370
plot!(x -> 𝑷₀ * exp(𝒓 * (x - 𝒙₀)), 1960, 2020, linewidth=5, alpha=0.25)
r = 0.005
x0, P0 = 2000, 370
plot!(exp_model(; r, x0, P0), 1960, 2020, linewidth=5, alpha=0.25)
```
(The `unzip` function is from the `CalculusWithJulia` package and will be explained in a subsequent section.) We can see that the projections from the year $1970$ hold up fairly well.
We can see that the projections from the year $1970$ hold up fairly well.
On this plot we added two *exponential* models. at $1960$ we added a *roughly* $0.2$ percent per year growth (a rate mentioned in an accompanying caption) and at $2000$ a roughly $0.5$ percent per year growth. The former barely keeping up with the data.
On this plot we added two *exponential* models. at $1960$ we added a *roughly* $0.2$ percent per year growth (a rate mentioned in an accompanying caption) and at $2000$ a roughly $0.5$ percent per year growth. The former barely keeping up with the data. (To do so, we used a parameterized function making for easier code reuse.)
The word **roughly** above could be made exact. Suppose we knew that between $1960$ and $1970$ the rate went from $313$ to $320$. If this followed an exponential model, then $r$ above would satisfy:
$$
P_{1970} = P_{1960} e^{r * (1970 - 1960)}
P_{1970} = P_{1960} e^{r \cdot (1970 - 1960)}
$$
or on division $320/313 = e^{r\cdot 10}$. Solving for $r$ can be done as explained next and yields $0.002211\dots$.
@@ -271,6 +272,7 @@ xs = range(-2, stop=2, length=100)
ys = f.(xs)
plot(xs, ys, color=:blue, label="2ˣ") # plot f
plot!(ys, xs, color=:red, label="f⁻¹") # plot f^(-1)
xs = range(1/4, stop=4, length=100)
plot!(xs, log2.(xs), color=:green, label="log₂") # plot log2
```
@@ -306,10 +308,10 @@ The half-life of a radioactive material is the time it takes for half the materi
The carbon $14$ isotope is a naturally occurring isotope on Earth, appearing in trace amounts. Unlike Carbon $12$ and $13$ it decays, in this case with a half life of $5730$ years (plus or minus $40$ years). In a [technique](https://en.wikipedia.org/wiki/Radiocarbon_dating) due to Libby, measuring the amount of Carbon 14 present in an organic item can indicate the time since death. The amount of Carbon $14$ at death is essentially that of the atmosphere, and this amount decays over time. So, for example, if roughly half the carbon $14$ remains, then the death occurred about $5730$ years ago.
A formula for the amount of carbon $14$ remaining $t$ years after death would be $P(t) = P_0 \cdot 2^{-t/5730}$.
A formula for the expected amount of carbon $14$ remaining $t$ years after death would be $P(t) = P_0 \cdot 2^{-t/5730}$.
If $1/10$ of the original carbon $14$ remains, how old is the item? This amounts to solving $2^{-t/5730} = 1/10$. We have: $-t/5730 = \log_2(1/10)$ or:
If $1/10$ of the original carbon $14$ remains, how old is the item? This amounts to solving $2^{-t/5730} = 1/10$. We have after applying the inverse function: $-t/5730 = \log_2(1/10)$, or:
```{julia}
@@ -382,19 +384,16 @@ a^{(\log_b(x)/\log_b(b))} = (b^{\log_b(a)})^{(\log_b(x)/\log_b(a))} =
b^{\log_b(a) \cdot \log_b(x)/\log_b(a) } = b^{\log_b(x)} = x.
$$
In short, we have these three properties of logarithmic functions:
If $a, b$ are positive bases; $u,v$ are positive numbers; and $x$ is any real number then:
In short, we have these three properties of logarithmic functions when $a, b$ are positive bases; $u,v$ are positive numbers; and $x$ is any real number:
$$
\begin{align*}
\log_a(uv) &= \log_a(u) + \log_a(v), \\
\log_a(u^x) &= x \log_a(u), \text{ and} \\
\log_a(u) &= \log_b(u)/\log_b(a).
\end{align*}
$$
##### Example

View File

@@ -7,7 +7,8 @@ This section will use the following add-on packages:
```{julia}
using CalculusWithJulia, Plots
using CalculusWithJulia
using Plots
plotly()
```
@@ -153,10 +154,14 @@ The definition of $s(x)$ above has two cases:
$$
s(x) = \begin{cases} -1 & s < 0\\ 1 & s > 0. \end{cases}
s(x) =
\begin{cases}
-1 & x < 0 \\
1 & x > 0.
\end{cases}
$$
We learn to read this as: when $s$ is less than $0$, then the answer is $-1$. If $s$ is greater than $0$ the answer is $1.$ Often - but not in this example - there is an "otherwise" case to catch those values of $x$ that are not explicitly mentioned. As there is no such "otherwise" case here, we can see that this function has no definition when $x=0$. This function is often called the "sign" function and is also defined by $\lvert x\rvert/x$. (`Julia`'s `sign` function actually defines `sign(0)` to be `0`.)
We learn to read this as: when $x$ is less than $0$, then the answer is $-1$. If $x$ is greater than $0$ the answer is $1.$ Often - but not in this example - there is an "otherwise" case to catch those values of $x$ that are not explicitly mentioned. As there is no such "otherwise" case here, we can see that this function has no definition when $x=0$. This function is often called the "sign" function and is also defined by $\lvert x\rvert/x$. (`Julia`'s `sign` function actually defines `sign(0)` to be `0`.)
How do we create conditional statements in `Julia`? Programming languages generally have "if-then-else" constructs to handle conditional evaluation. In `Julia`, the following code will handle the above condition:
@@ -333,8 +338,8 @@ We utilize this as follows. Suppose we wish to solve $f(x) = 0$ and we have two
```{julia}
q(x) = x^2 - 2
𝒂, 𝒃 = 1, 2
𝒄 = secant_intersection(q, 𝒂, 𝒃)
a, b = 1, 2
c = secant_intersection(q, a, b)
```
In our example, we see that in trying to find an answer to $f(x) = 0$ ( $\sqrt{2}\approx 1.414\dots$) our value found from the intersection point is a better guess than either $a=1$ or $b=2$:
@@ -342,17 +347,17 @@ In our example, we see that in trying to find an answer to $f(x) = 0$ ( $\sqrt{2
```{julia}
#| echo: false
plot(q, 𝒂, 𝒃, linewidth=5, legend=false)
plot!(zero, 𝒂, 𝒃)
plot!([𝒂, 𝒃], q.([𝒂, 𝒃]))
scatter!([𝒄], [q(𝒄)])
plot(q, a, b, linewidth=5, legend=false)
plot!(zero, a, b)
plot!([a, b], q.([a, b]))
scatter!([c], [q(c)])
```
Still, `q(𝒄)` is not really close to $0$:
Still, `q(c)` is not really close to $0$:
```{julia}
q(𝒄)
q(c)
```
*But* it is much closer than either $q(a)$ or $q(b)$, so it is an improvement. This suggests renaming $a$ and $b$ with the old $b$ and $c$ values and trying again we might do better still:
@@ -360,9 +365,9 @@ q(𝒄)
```{julia}
#| hold: true
𝒂, 𝒃 = 𝒃, 𝒄
𝒄 = secant_intersection(q, 𝒂, 𝒃)
q(𝒄)
a, b = b, c
c = secant_intersection(q, a, b)
q(c)
```
Yes, now the function value at this new $c$ is even closer to $0$. Trying a few more times we see we just get closer and closer. Here we start again to see the progress
@@ -370,10 +375,10 @@ Yes, now the function value at this new $c$ is even closer to $0$. Trying a few
```{julia}
#| hold: true
𝒂,𝒃 = 1, 2
a,b = 1, 2
for step in 1:6
𝒂, 𝒃 = 𝒃, secant_intersection(q, 𝒂, 𝒃)
current = (c=𝒃, qc=q(𝒃))
a, b = b, secant_intersection(q, a, b)
current = (c=b, qc=q(b))
@show current
end
```
@@ -486,7 +491,14 @@ During this call, values for `m` and `b` are found from how the function is call
mxplusb(0; m=3, b=2)
```
Keywords are used to mark the parameters whose values are to be changed from the default. Though one can use *positional arguments* for parameters - and there are good reasons to do so - using keyword arguments is a good practice if performance isn't paramount, as their usage is more explicit yet the defaults mean that a minimum amount of typing needs to be done. Keyword arguments are widely used with plotting commands, as there are numerous options to adjust, but typically only a handful adjusted per call.
Keywords are used to mark the parameters whose values are to be changed from the default. Though one can use *positional arguments* for parameters - and there are good reasons to do so - using keyword arguments is a good practice if performance isn't paramount, as their usage is more explicit yet the defaults mean that a minimum amount of typing needs to be done.
Keyword arguments are widely used with plotting commands, as there are numerous options to adjust, but typically only a handful adjusted per call. The `Plots` package whose commands we illustrate throughout these notes starting with the next section has this in its docs: `Plots.jl` follows two simple rules with data and attributes:
* Positional arguments correspond to input data
* Keyword arguments correspond to attributes
##### Example
@@ -526,7 +538,14 @@ p = (g=9.8, v0=200, theta = 45*pi/180, k=1/2)
trajectory(100, p)
```
The style isn't so different from using keyword arguments, save the extra step of unpacking the parameters. The *big* advantage is consistency the function is always called in an identical manner regardless of the number of parameters (or variables).
The style isn't so different from using keyword arguments, save the extra step of unpacking the parameters. Unpacking as above has shortcut syntax to extract selected fields by name:
```{julia}
(; v0, theta) = p
v0, theta
```
The *big* advantage of bundling parameters into a container is consistency the function is always called in an identical manner regardless of the number of parameters (or variables).
## Multiple dispatch
@@ -568,7 +587,7 @@ methods(log, (Number,))
##### Example
A common usage of multiple dispatch is, as is done with `log` above, to restrict the type of an argument and define a method for just this type. Types in `Julia` can be abstract or concrete. This distinction is important when construction *composite types* (which we are not doing here), but otherwise not so important. In the following example, we use the abstract types `Integer`, `Real`, and `Complex` to define methods for a function we call `twotox`:
A common usage of multiple dispatch is, as is done with `log` above, to restrict the type of an argument and define a method for just this type. Types in `Julia` can be abstract or concrete. This distinction is important when constructing *composite types* (which we are not doing here), but otherwise not so important. In the following example, we use the abstract types `Integer`, `Real`, and `Complex` to define methods for a function we call `twotox`:
```{julia}
function twotox(x::Integer)
@@ -706,7 +725,7 @@ When the `->` is seen a function is being created.
:::{.callout-warning}
## Warning
Generic versus anonymous functions. Julia has two types of functions, generic ones, as defined by `f(x)=x^2` and anonymous ones, as defined by `x -> x^2`. One gotcha is that `Julia` does not like to use the same variable name for the two types. In general, Julia is a dynamic language, meaning variable names can be reused with different types of variables. But generic functions take more care, as when a new method is defined it gets added to a method table. So repurposing the name of a generic function for something else is not allowed. Similarly, repurposing an already defined variable name for a generic function is not allowed. This comes up when we use functions that return functions as we have different styles that can be used: When we defined `l = shift_right(f, c=3)` the value of `l` is assigned an anonymous function. This binding can be reused to define other variables. However, we could have defined the function `l` through `l(x) = shift_right(f, c=3)(x)`, being explicit about what happens to the variable `x`. This would add a method to the generic function `l`. Meaning, we get an error if we tried to assign a variable to `l`, such as an expression like `l=3`. We generally employ the latter style, even though it involves a bit more typing, as we tend to stick to methods of generic functions for consistency.
Generic versus anonymous functions. Julia has two types of functions, generic ones, as defined by `f(x)=x^2` and anonymous ones, as defined by `x -> x^2`. One gotcha is that `Julia` does not like to use the same variable name for the two types. In general, Julia is a dynamic language, meaning variable names can be reused with different types of variables. But generic functions take more care, as when a new method is defined it gets added to a method table. So repurposing the name of a generic function for something else is not allowed. Similarly, repurposing an already defined variable name for a generic function is not allowed. This comes up when we use functions that return functions as we have different styles that can be used: When we defined `l = shift_right(f, c=3)` the value of `l` is assigned an anonymous function. This binding can be reused to define other variables. However, we could have defined the function `l` through `l(x) = shift_right(f, c=3)(x)`, being explicit about what happens to the variable `x`. This would add a method to the generic function `l`. Meaning, we get an error if we tried to assign a variable to `l`, such as an expression like `l=3`. The latter style is inefficient, so is not preferred.
:::

View File

@@ -40,10 +40,12 @@ plot(f, 0, 4, legend=false)
plot!([2,2,0], [0,f(2),f(2)])
```
The graph of a function is a representation of points $(x,f(x))$, so to *find* $f(c)$ from the graph, we begin on the $x$ axis at $c$, move vertically to the graph (the point $(c, f(c))$), and then move horizontally to the $y$ axis, intersecting it at $f(c)$. The figure shows this for $c=2$, from which we can read that $f(c)$ is about $4$. This is how an $x$ is associated to a single $y$.
The graph of a function is a representation of points $(x,f(x))$, so to *find* $y = f(c)$ from the graph, we begin on the $x$ axis at $c$, move vertically to the graph (the point $(c, f(c))$), and then move horizontally to the $y$ axis, intersecting it at $y = f(c)$. The figure shows this for $c=2$, from which we can read that $f(c)$ is about $4$. This is how an $x$ is associated to a single $y$.
If we were to *reverse* the direction, starting at $f(c)$ on the $y$ axis and then moving horizontally to the graph, and then vertically to the $x$-axis we end up at a value $c$ with the correct $f(c)$. This operation will form a function **if** the initial movement horizontally is guaranteed to find *no more than one* value on the graph. That is, to have an inverse function, there can not be two $x$ values corresponding to a given $y$ value. This observation is often visualized through the "horizontal line test" - the graph of a function with an inverse function can only intersect a horizontal line at most in one place.
If we were to *reverse* the direction, starting at $y = f(c)$ on the $y$ axis and then moving horizontally to the graph, and then vertically to the $x$-axis we end up at a value $c$ with the correct $f(c)$. This allows solving for $x$ knowing $y$ in $y=f(x)$.
The operation described will form a function **if** the initial movement horizontally is guaranteed to find *no more than one* value on the graph. That is, to have an inverse function, there can not be two $x$ values corresponding to a given $y$ value. This observation is often visualized through the "horizontal line test" - the graph of a function with an inverse function can only intersect a horizontal line at most in one place.
More formally, a function is called *one-to-one* *if* for any two $a \neq b$, it must be that $f(a) \neq f(b)$. Many functions are one-to-one, many are not. Familiar one-to-one functions are linear functions ($f(x)=a \cdot x + b$ with $a\neq 0$), odd powers of $x$ ($f(x)=x^{2k+1}$), and functions of the form $f(x)=x^{1/n}$ for $x \geq 0$. In contrast, all *even* functions are *not* one-to-one, as $f(x) = f(-x)$ for any nonzero $x$ in the domain of $f$.
@@ -70,13 +72,13 @@ However, typically we have a rule describing our function. What is the process t
When we solve algebraically for $x$ in $y=9/5 \cdot x + 32$ we do the same thing as we do verbally: we subtract $32$ from each side, and then divide by $9/5$ to isolate $x$:
$$
\begin{align*}
y &= 9/5 \cdot x + 32\\
y - 32 &= 9/5 \cdot x\\
(y-32) / (9/5) &= x.
\end{align*}
$$
From this, we have the function $g(y) = (y-32) / (9/5)$ is the inverse function of $f(x) = 9/5\cdot x + 32$.
@@ -102,7 +104,7 @@ Suppose a transformation of $x$ is given by $y = f(x) = (ax + b)/(cx+d)$. This f
From the expression $y=f(x)$ we *algebraically* solve for $x$:
$$
\begin{align*}
y &= \frac{ax +b}{cx+d}\\
y \cdot (cx + d) &= ax + b\\
@@ -110,7 +112,7 @@ ycx - ax &= b - yd\\
(cy-a) \cdot x &= b - dy\\
x &= -\frac{dy - b}{cy-a}.
\end{align*}
$$
We see that to solve for $x$ we need to divide by $cy-a$, so this expression can not be zero. So, using $x$ as the dummy variable, we have
@@ -128,14 +130,14 @@ The function $f(x) = (x-1)^5 + 2$ is strictly increasing and so will have an inv
Again, we solve algebraically starting with $y=(x-1)^5 + 2$ and solving for $x$:
$$
\begin{align*}
y &= (x-1)^5 + 2\\
y - 2 &= (x-1)^5\\
(y-2)^{1/5} &= x - 1\\
(y-2)^{1/5} + 1 &= x.
\end{align*}
$$
We see that $f^{-1}(x) = 1 + (x - 2)^{1/5}$. The fact that the power $5$ is an odd power is important, as this ensures a unique (real) solution to the fifth root of a value, in the above $y-2$.
@@ -171,14 +173,14 @@ The [inverse function theorem](https://en.wikipedia.org/wiki/Inverse_function_th
Consider the function $f(x) = (1+x^2)^{-1}$. This bell-shaped function is even (symmetric about $0$), so can not possibly be one-to-one. However, if the domain is restricted to $[0,\infty)$ it is. The restricted function is strictly decreasing and its inverse is found, as follows:
$$
\begin{align*}
y &= \frac{1}{1 + x^2}\\
1+x^2 &= \frac{1}{y}\\
x^2 &= \frac{1}{y} - 1\\
x &= \sqrt{(1-y)/y}, \quad 0 < y \leq 1.
\end{align*}
$$
Then $f^{-1}(x) = \sqrt{(1-x)/x}$ where $0 < x \leq 1$. The somewhat complicated restriction for the the domain coincides with the range of $f(x)$. We shall see next that this is no coincidence.
@@ -268,9 +270,9 @@ We drew a line connecting $(1/2, f(1/2))$ to $(f(1/2),1/2)$. We can see that it
One consequence of this symmetry, is that if $f$ is strictly increasing, then so is its inverse.
!!!note In the above we used `cbrt(x)` and not `x^(1/3)`. The latter usage assumes that $x \geq 0$ as it isn't guaranteed that for all real exponents the answer will be a real number. The `cbrt` function knows there will always be a real answer and provides it.
::: {.callout-note}
In the above we used `cbrt(x)` and not `x^(1/3)`. The latter usage assumes that $x \geq 0$ as it isn't guaranteed that for all real exponents the answer will be a real number. The `cbrt` function knows there will always be a real answer and provides it.
:::
### Lines

View File

@@ -32,7 +32,7 @@ The [https://mybinder.org/](https://mybinder.org/) service in particular allows
* [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jverzani/CalculusWithJuliaBinder.jl/sympy?labpath=blank-notebook.ipynb) (Image with SymPy, longer to load)
[Google colab](https://colab.research.google.com/) offers a free service with more computing power than `binder`, though setup is a bit more fussy. To use `colab`, you need to execute a command that downloads `Julia` and installs the `CalculusWithJulia` package and a plotting package. (Modify the `pkg"add ..."` command to add other desired packages):
[Google colab](https://colab.research.google.com/) offers a free service with more computing power than `binder`, though setup is a bit more fussy. To use `colab` along with these notes, you need to execute a command that downloads `Julia` and installs the `CalculusWithJulia` package and a plotting package. (Modify the `pkg"add ..."` command to add other desired packages; update the julia version as necessary):
```
# Installation cell
@@ -50,6 +50,9 @@ julia -e 'using Pkg; Pkg.add(url="https://github.com/mth229/BinderPlots.jl")'
echo 'Now change the runtime type'
```
(The `BinderPlots` is a light-weight, barebones, plotting package that uses `PlotlyLight` to render graphics with commands mostly following those of the `Plots` package. Though suitable for most examples herein, the `Plots` package could instead be installed)
After this executes (which can take quite some time, as in a few minutes) under the `Runtime` menu select `Change runtime type` and then select `Julia`.
After that, in a cell execute these commands to load the two installed packages:
@@ -63,11 +66,10 @@ As mentioned, other packages can be chosen for installation.
## Interacting with `Julia`
At a basic level, `Julia` provides a means to read commands or instructions, evaluate those commands, and then print or return those commands. At a user level, there are many different ways to interact with the reading and printing. For example:
At a basic level, `Julia` provides an interactive means to read commands or instructions, evaluate those commands, and then print or return those commands. At a user level, there are many different ways to interact with the reading and printing. For example:
* The REPL. The `Julia` terminal is the built-in means to interact with `Julia`. A `Julia` Terminal has a command prompt, after which commands are typed and then sent to be evaluated by the `enter` key. The terminal may look something like the following where `2+2` is evaluated:
@@ -83,7 +85,7 @@ $ julia
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.7.0 (2021-11-30)
| | |_| | | | (_| | | Version 1.11.1 (2024-10-16)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
@@ -104,8 +106,8 @@ The `Pluto` interface has some idiosyncrasies that need explanation:
* Cells can only have one command within them. Multiple-command cells must be contained in a `begin` block or a `let` block.
* By default, the cells are *reactive*. This means when a variable in one cell is changed, then any references to that variable are also updated like a spreadsheet. This is fantastic for updating several computations at once. However it means variable names can not be repeated within a page. Pedagogically, it is convenient to use variable names and function names (e.g., `x` and `f`) repeatedly, but this is only possible *if* they are within a `let` block or a function body.
* To not repeat names, but to be able to reference a value from cell-to-cell, some Unicode variants are used within a page. Visually these look familiar, but typing the names requires some understanding of Unicode input. The primary usages is *bold italic* (e.g., `\bix[tab]` or `\bif[tab]`) or *bold face* (e.g. `\bfx[tab]` or `\bff[tab]`).
* The notebooks snapshot the packages they depend on, which is great for reproducibility, but may mean older versions are silently used.
* To not repeat names, but to be able to reference a value from cell-to-cell, some Unicode variants may be used within a page. Visually these look familiar, but typing the names requires some understanding of Unicode input. The primary usages is *bold italic* (e.g., `\bix[tab]` or `\bif[tab]`) or *bold face* (e.g. `\bfx[tab]` or `\bff[tab]`).
* The notebooks snapshot the packages they depend on, which is great for reproducibility, but may lead to older versions of the packages being silently used.
## Augmenting base `Julia`
@@ -152,7 +154,7 @@ This command instructs `Julia` to look at its *general registry* for the `Calcul
:::{.callout-note}
## Note
In a terminal setting, there is a package mode, entered by typing `]` as the leading character and exited by entering `<backspace>` at a blank line. This mode allows direct access to `Pkg` with a simpler syntax. The command above would be just `add CalculusWithJulia`.)
In a terminal setting, there is a package mode, entered by typing `]` as the leading character and exited by entering `<backspace>` at a blank line. This mode allows direct access to `Pkg` with a simpler syntax. The command above would be just `add CalculusWithJulia`. As well, when a package is not installed, calling `using SomePackage` will prompt the user if they wish to install the package in the current environment.)
:::
@@ -167,11 +169,11 @@ For these notes, the following packages, among others, are used:
```{julia}
#| eval: false
Pkg.add("CalculusWithJulia") # for some simplifying functions and a few packages (SpecialFunctions, ForwardDiff)
Pkg.add("Plots") # for basic plotting
Pkg.add("SymPy") # for symbolic math
Pkg.add("Roots") # for solving `f(x)=0`
Pkg.add("QuadGk") # for integration
Pkg.add("CalculusWithJulia") # for some convenience functions and a few packages (SpecialFunctions, ForwardDiff)
Pkg.add("Plots") # for basic plotting
Pkg.add("SymPy") # for symbolic math
Pkg.add("Roots") # for numerically solving `f(x)=0` and `f(x)=g(x)`
Pkg.add("QuadGk") # for 1-dimensional numeric integration
Pkg.add("HQuadrature") # for higher-dimensional integration
```
@@ -236,7 +238,7 @@ Integer operations may silently overflow, producing odd answers, at first glance
2^64
```
(Though the output is predictable, if overflow is taken into consideration appropriately.)
(Though the output is predictable, knowing why requires understanding of how the hardware implements these operations.)
When different types of numbers are mixed, `Julia` will usually promote the values to a common type before the operation:
@@ -249,7 +251,10 @@ When different types of numbers are mixed, `Julia` will usually promote the valu
`Julia` will first add `2` and `1//2` promoting `2` to rational before doing so. Then add the result, `5//2` to `0.5` by promoting `5//2` to the floating point number `2.5` before proceeding.
`Julia` uses a special type to store a handful of irrational constants such as `pi`. The special type allows these constants to be treated without round off, until they mix with other floating point numbers. There are some functions that require these be explicitly promoted to floating point. This can be done by calling `float`.
`Julia` uses a special type to store a handful of irrational constants such as `pi`. The special type allows these constants to be treated without round off, until they mix with other floating point numbers. An irrational value for `e` is not exported; the `CalculusWithJulia` exports a floating point value `e=exp(1)`.
There are some functions that require these be explicitly promoted to floating point. This can be done by calling `float`.
The standard mathematical operations are implemented by `+`, `-`, `*`, `/`, `^`. Parentheses are used for grouping.
@@ -275,6 +280,9 @@ Values will be promoted to a common type (or type `Any` if none exists). For exa
(Vectors are used as a return type from some functions, as such, some familiarity is needed.)
Other common container types are variables of vectors (higher-dimensional arrarys, offset arrays, etc.) tuples (for heterogeneous, immutable, indexed values); named tuples (which add a name to each value in a tuple); and dictionaries (for associative relationships between a key and a value).
Regular arithmetic sequences can be defined by either:
@@ -325,7 +333,7 @@ u + a_really_long_name + a0 - b0 + α
Within `Pluto`, names are idiosyncratic: within the global scope, only a single usage is possible per notebook; functions and variables can be freely renamed; structures can be redefined or renamed; ...
Outside of `Pluto`, names may be repurposed, even with values of different types (`Julia` is a dynamic language), save for (generic) function names, which have some special rules and can only be redefined as another function. Generic functions are central to `Julia`'s design. Generic functions use a method table to dispatch on, so once a name is assigned to a generic function, it can not be used as a variable name; the reverse is also true.
Outside of `Pluto`, names may be repurposed, even with values of different types (`Julia` is a dynamic language), save for (generic) function names, which have some special rules and can only be redefined as another method for the function. Generic functions are central to `Julia`'s design. Generic functions use a method table to dispatch on, so once a name is assigned to a generic function, it can not be used as a variable name; the reverse is also true.
## Functions
@@ -362,7 +370,7 @@ log
Besides `^`, there are `sqrt` and `cbrt` for powers. In addition basic functions for exponential and logarithmic functions:
```{verbatim}
```
sqrt, cbrt
exp
log # base e
@@ -375,7 +383,7 @@ log10, log2, # also log(b, x)
The `6` standard trig functions are implemented; their implementation for degree arguments; their inverse functions; and the hyperbolic analogs.
```{verbatim}
```
sin, cos, tan, csc, sec, cot
asin, acos, atan, acsc, asec, acot
sinh, cosh, tanh, csch, sech, coth
@@ -385,7 +393,7 @@ asinh, acosh, atanh, acsch, asech, acoth
If degrees are preferred, the following are defined to work with arguments in degrees:
```{verbatim}
```
sind, cosd, tand, cscd, secd, cotd
```
@@ -444,7 +452,7 @@ a = 4
f(3) # now 2 * 4 + 3
```
User-defined functions can have $0$, $1$ or more arguments:
User-defined functions can have $0$, $1$ or more positional arguments:
```{julia}
@@ -454,7 +462,7 @@ area(w, h) = w*h
Julia makes different *methods* for *generic* function names, so function definitions whose argument specification is different are for different uses, even if the name is the same. This is *polymorphism*. The practical use is that it means users need only remember a much smaller set of function names, as attempts are made to give common expectations to the same name. (That is, `+` should be used only for "add" ing objects, however defined.)
Functions can be defined with *keyword* arguments that may have defaults specified:
Functions can also be defined with *keyword* arguments that may have defaults specified:
```{julia}
@@ -465,11 +473,13 @@ f(1, m=10) # uses m=10, b=0 -> 10 * 1 + 0
f(1, m=10, b=5) # uses m=10, b=5 -> 10 * 1 + 5
```
Keyword arguments are not considered for dispatch.
Longer functions can be defined using the `function` keyword, the last command executed is returned:
```{julia}
function 𝒇(x)
function f(x)
y = x^2
z = y - 3
z
@@ -534,7 +544,7 @@ sin.(xs) # gives back [sin(1), sin(2), sin(3), sin(4), sin(5)]
For "infix" operators, the dot precedes the operator, as in this example instructing pointwise multiplication of each element in `xs`:
```{juila}
```{julia}
xs .* xs
```
@@ -566,8 +576,9 @@ With `Plots` loaded, we can plot a function by passing the function object by na
plot(sin, 0, 2pi) # plot a function - by name - over an interval [a,b]
```
!!! note This is in the form of **the** basic pattern employed: `verb(function_object, arguments...)`. The verb in this example is `plot`, the object `sin`, the arguments `0, 2pi` to specify `[a,b]` domain to plot over.
::: {.callout-note}
This is in the form of **the** basic pattern employed: `verb(function_object, arguments...)`. The verb in this example is `plot`, the object `sin`, the arguments `0, 2pi` to specify `[a,b]` domain to plot over.
:::
Plotting more than one function over `[a,b]` is achieved through the `plot!` function, which modifies the existing plot (`plot` creates a new one) by adding a new layer:
@@ -578,6 +589,8 @@ plot!(cos, 0, 2pi)
plot!(zero, 0, 2pi) # add the line y=0
```
(There are alternatives to plot functions or other traces all at once.)
Individual points are added with `scatter` or `scatter!`:
@@ -587,7 +600,7 @@ plot!(cos, 0, 2pi)
scatter!([pi/4, pi+pi/4], [sin(pi/4), sin(pi + pi/4)])
```
(The extra argument `legend=false` suppresses the automatic legend drawing. There are many other useful arguments to adjust a graphic. For example, passing `markersize=10` to the `scatter!` command would draw the points larger than the default.)
(The extra argument `legend=false` suppresses the automatic legend drawing. There are many other useful keyword arguments to adjust attributes of a trace of a graphic. For example, passing `markersize=10` to the `scatter!` command would draw the points larger than the default.)
Plotting an *anonymous* function is a bit more immediate than the two-step approach of defining a named function then calling `plot` with this as an argument:
@@ -607,6 +620,20 @@ ys = [sin(2x) + sin(3x) + sin(4x) for x in xs]
plot(xs, ys)
```
There are different plotting interfaces. Though not shown, all of these `plot` commands produce a plot of `f`, though with minor differences:
```{julia}
#| eval: false
xs = range(a, b, length=251)
ys = f.(xs)
plot(f, a, b) # recipe for a function
plot(xs, f) # alternate recipe
plot(xs, ys) # plot coordinates as two vectors
plot([(x,f(x)) for x in xs]) # plot a vector o points
```
The choice should depend on convenience.
## Equations
@@ -623,7 +650,7 @@ In `Julia` the equals sign is **only** for *assignment* and *mutation*. The *lef
## Symbolic math
Symbolic math is available through an add-on package `SymPy` (among others). Once loaded, symbolic variables are created with the macro `@syms`:
Symbolic math is available through an add-on package `SymPy` (among others). Once loaded, symbolic variables in `SymPy` are created with the macro `@syms`:
```{julia}

View File

@@ -62,8 +62,7 @@ Similarly, each type is printed slightly differently.
The key distinction is between integers and floating points. While floating point values include integers, and so can be used exclusively on the calculator, the difference is that an integer is guaranteed to be an exact value, whereas a floating point value, while often an exact representation of a number is also often just an *approximate* value. This can be an advantage floating point values can model a much wider range of numbers.
Now in nearly all cases the differences are not noticeable. Take for instance this simple calculation involving mixed types.
In nearly all cases the differences are not noticeable. Take for instance this simple calculation involving mixed types.
```{julia}
@@ -89,10 +88,10 @@ These values are *very* small numbers, but not exactly $0$, as they are mathemat
---
The only common issue is with powers. `Julia` tries to keep a predictable output from the input types (not their values). Here are the two main cases that arise where this can cause unexpected results:
The only common issue is with powers. We saw this previously when discussing a distinction between `2^64` and `2.0^64. `Julia` tries to keep a predictable output from the input types (not their values). Here are the two main cases that arise where this can cause unexpected results:
* integer bases and integer exponents can *easily* overflow. Not only `m^n` is always an integer, it is always an integer with a fixed storage size computed from the sizes of `m` and `n`. So the powers can quickly get too big. This can be especially noticeable on older $32$-bit machines, where too big is $2^{32} = 4,294,967,296$. On $64$-bit machines, this limit is present but much bigger.
* integer bases and integer exponents can *easily* overflow. Not only `m^n` is always an integer, it is always an integer with a fixed storage size computed from the sizes of `m` and `n`. So the powers can quickly get too big. This can be especially noticeable on older $32$-bit machines, where too big is $2^{32} = 4,294,967,296$. On $64$-bit machines, this limit is present but much bigger.
Rather than give an error though, `Julia` gives seemingly arbitrary answers, as can be seen in this example on a $64$-bit machine:
@@ -102,13 +101,13 @@ Rather than give an error though, `Julia` gives seemingly arbitrary answers, as
2^62, 2^63
```
(They aren't arbitrary, rather integer arithmetic is implemented as modular arithmetic.)
(They aren't arbitrary, as explained previosly.)
This could be worked around, as it is with some programming languages, but it isn't, as it would slow down this basic computation. So, it is up to the user to be aware of cases where their integer values can grow to big. The suggestion is to use floating point numbers in this domain, as they have more room, at the cost of sometimes being approximate values.
This could be worked around, as it is with some programming languages, but it isn't, as it would slow down this basic computation. So, it is up to the user to be aware of cases where their integer values can grow to big. The suggestion is to use floating point numbers in this domain, as they have more room, at the cost of sometimes being approximate values for fairly large values.
* the `sqrt` function will give a domain error for negative values:
* the `sqrt` function will give a domain error for negative values:
```{julia}
@@ -161,11 +160,12 @@ Integers are often used casually, as they come about from parsing. As with a cal
### Floating point numbers
[Floating point](http://en.wikipedia.org/wiki/Floating_point) numbers are a computational model for the real numbers. For floating point numbers, $64$ bits are used by default for both $32$- and $64$-bit systems, though other storage sizes can be requested. This gives a large ranging - but still finite - set of real numbers that can be represented. However, there are infinitely many real numbers just between $0$ and $1$, so there is no chance that all can be represented exactly on the computer with a floating point value. Floating point then is *necessarily* an approximation for all but a subset of the real numbers. Floating point values can be viewed in normalized [scientific notation](http://en.wikipedia.org/wiki/Scientific_notation) as $a\cdot 2^b$ where $a$ is the *significand* and $b$ is the *exponent*. Save for special values, the significand $a$ is normalized to satisfy $1 \leq \lvert a\rvert < 2$, the exponent can be taken to be an integer, possibly negative.
[Floating point](http://en.wikipedia.org/wiki/Floating_point) numbers are a computational model for the real numbers. For floating point numbers, $64$ bits are used by default for both $32$- and $64$-bit systems, though other storage sizes can be requested. This gives a large range - but still finite - set of real numbers that can be represented. However, there are infinitely many real numbers just between $0$ and $1$, so there is no chance that all can be represented exactly on the computer with a floating point value. Floating point then is *necessarily* an approximation for all but a subset of the real numbers. Floating point values can be viewed in normalized [scientific notation](http://en.wikipedia.org/wiki/Scientific_notation) as $a\cdot 2^b$ where $a$ is the *significand* and $b$ is the *exponent*. Save for special values, the significand $a$ is normalized to satisfy $1 \leq \lvert a\rvert < 2$, the exponent can be taken to be an integer, possibly negative.
As per IEEE Standard 754, the `Float64` type gives 52 bits to the precision (with an additional implied one), 11 bits to the exponent and the other bit is used to represent the sign. Positive, finite, floating point numbers have a range approximately between $10^{-308}$ and $10^{308}$, as 308 is about $\log_{10} 2^{1023}$. The numbers are not evenly spread out over this range, but, rather, are much more concentrated closer to $0$.
The use of 32-bit floating point values is common, as some widley used computer chips expect this. These values have a narrower range of possible values.
:::{.callout-warning}
## More on floating point numbers
@@ -205,8 +205,10 @@ The special coding `aeb` (or if the exponent is negative `ae-b`) is used to repr
avogadro = 6.022e23
```
Here `e` is decidedly *not* the Euler number, rather syntax to separate the exponent from the mantissa.
::: {.callout-note}
## Not `e`
Here `e` is decidedly *not* the Euler number, rather **syntax** to separate the exponent from the mantissa.
:::
The first way of representing this number required using `10.0` and not `10` as the integer power will return an integer and even for 64-bit systems is only valid up to `10^18`. Using scientific notation avoids having to concentrate on such limitations.
@@ -296,7 +298,7 @@ That is adding `1/10` and `2/10` is not exactly `3/10`, as expected mathematical
1/10 + (2/10 + 3/10) == (1/10 + 2/10) + 3/10
```
* For real numbers subtraction of similar-sized numbers is not exceptional, for example $1 - \cos(x)$ is positive if $0 < x < \pi/2$, say. This will not be the case for floating point values. If $x$ is close enough to $0$, then $\cos(x)$ and $1$ will be so close, that they will be represented by the same floating point value, `1.0`, so the difference will be zero:
* Mathematically, or real numbers, subtraction of similar-sized numbers is not exceptional, for example $1 - \cos(x)$ is positive if $0 < x < \pi/2$, say. This will not be the case for floating point values. If $x$ is close enough to $0$, then $\cos(x)$ and $1$ will be so close, that they will be represented by the same floating point value, `1.0`, so the difference will be zero:
```{julia}
@@ -306,7 +308,7 @@ That is adding `1/10` and `2/10` is not exactly `3/10`, as expected mathematical
### Rational numbers
Rational numbers can be used when the exactness of the number is more important than the speed or wider range of values offered by floating point numbers. In `Julia` a rational number is comprised of a numerator and a denominator, each an integer of the same type, and reduced to lowest terms. The operations of addition, subtraction, multiplication, and division will keep their answers as rational numbers. As well, raising a rational number to a positive, integer value will produce a rational number.
Rational numbers can be used when the exactness of the number is more important than the speed or wider range of values offered by floating point numbers. In `Julia` a rational number is comprised of a numerator and a denominator, each an integer of the same type, and reduced to lowest terms. The operations of addition, subtraction, multiplication, and division will keep their answers as rational numbers. As well, raising a rational number to an integer value will produce a rational number.
As mentioned, these are constructed using double slashes:
@@ -643,5 +645,5 @@ Finding the value through division introduces a floating point deviation. Which
#| echo: false
as = [`1/10^21`, `1e-21`]
explanation = "The scientific notation is correct. Due to integer overflow `10^21` is not the same number as `10.0^21`."
buttonq(as, 2; explanation=explanation)
buttonq(as, 2; explanation)
```

View File

@@ -30,7 +30,7 @@ A scalar, univariate function, such as $f(x) = 1 - x^2/2$, can be thought of in
* It can be represented through a rule of what it does to $x$, as above. This is useful for computing numeric values.
* it can be interpreted verbally, as in *square* $x$, take half then *subtract* from one. This can give clarity to what the function does.
* It can be thought of in terms of its properties: a polynomial, continuous, $U$-shaped, an approximation for $\cos(x)$ near $0$, $\dots$
* It can be thought of in terms of its properties: a polynomial, continuous, upside down $U$-shaped, an approximation for $\cos(x)$ near $0$, $\dots$
* it can be visualized graphically. This is useful for seeing the qualitative behavior of a function.
@@ -243,15 +243,15 @@ For instances where a *specific* set of $x$ values is desired to be used, the `r
```{julia}
𝒙s = range(0, 2pi, length=10)
𝒚s = sin.(𝒙s)
xs = range(0, 2pi, length=10)
ys = sin.(xs)
```
Finally, to plot the set of points and connect with lines, the $x$ and $y$ values are passed along as vectors:
```{julia}
plot(𝒙s, 𝒚s)
plot(xs, ys)
```
This plots the points as pairs and then connects them in order using straight lines. Basically, it creates a dot-to-dot graph. The above graph looks primitive, as it doesn't utilize enough points.
@@ -490,7 +490,7 @@ For plotting points with `scatter`, or `scatter!` the markers can be adjusted vi
* `scatter(..., marker=:square)`: change the marker (uses a symbol, not a string to specify)
Of course, zero, one, or more of these can be used on any given call to `plot`, `plot!`, `scatter` or `scatter!`.
Of course, zero, one, or more of these can be used on any given call to `plot`, `plot!`, `scatter`, or `scatter!`.
#### Example: Bresenham's algorithm
@@ -575,9 +575,9 @@ The most "famous" parametric graph is one that is likely already familiar, as it
```{julia}
𝒇(x) = cos(x); 𝒈(x) = sin(x)
𝒕s = range(0, 2pi, length=100)
plot(𝒇.(𝒕s), 𝒈.(𝒕s), aspect_ratio=:equal) # make equal axes
f(x) = cos(x); g(x) = sin(x)
ts = range(0, 2pi, length=100)
plot(f.(ts), g.(ts), aspect_ratio=:equal) # make equal axes
```
Any point $(a,b)$ on this graph is represented by $(\cos(t), \sin(t))$ for some value of $t$, and in fact multiple values of $t$, since $t + 2k\pi$ will produce the same $(a,b)$ value as $t$ will.
@@ -599,8 +599,8 @@ DataFrame(θ=θs, x=cos.(θs), y=sin.(θs))
```{julia}
#| hold: true
θs =[0, pi/6, pi/4, pi/3, pi/2, 2pi/3, 3pi/4, 5pi/6, pi]
plot(𝒇.(θs), 𝒈.(θs), legend=false, aspect_ratio=:equal)
scatter!(𝒇.(θs), 𝒈.(θs))
plot(f.(θs), g.(θs), legend=false, aspect_ratio=:equal)
scatter!(f.(θs), g.(θs))
```
---
@@ -610,7 +610,7 @@ As with the plot of a univariate function, there is a convenience interface for
```{julia}
plot(𝒇, 𝒈, 0, 2pi, aspect_ratio=:equal)
plot(f, g, 0, 2pi, aspect_ratio=:equal)
```
##### Example
@@ -876,6 +876,24 @@ answ = 2
radioq(choices, answ)
```
###### Question
The `plot` function can have its data specified through a vector of points. Such data can be generated through a comprehension. Does this command plot the given expression avoiding the need to define a function?
```{julia}
#| eval: false
plot([(x, sin(x^2)-sin(x)) for x in range(-pi, pi, 100)])
```
```{julia}
#| echo: false
explanation = """
Yes, it does. Whether this is more convenient than say `plot(x -> sin(x^2) - sin(x), -pi, pi)` is a different question.
"""
buttonq(["Yes", "No"], 1; explanation)
```
###### Question

View File

@@ -28,7 +28,7 @@ Polynomials are a particular class of expressions that are simple enough to have
Here we discuss some vocabulary and basic facts related to polynomials and show how the add-on `SymPy` package can be used to model polynomial expressions within `SymPy`. `SymPy` provides a Computer Algebra System (CAS) for `Julia`. In this case, by leveraging a mature `Python` package [SymPy](https://www.sympy.org/). Later we will discuss the `Polynomials` package for polynomials.
For our purposes, a *monomial* is simply a non-negative integer power of $x$ (or some other indeterminate symbol) possibly multiplied by a scalar constant. For example, $5x^4$ is a monomial, as are constants, such as $-2=-2x^0$ and the symbol itself, as $x = x^1$. In general, one may consider restrictions on where the constants can come from, and consider more than one symbol, but we won't pursue this here, restricting ourselves to the case of a single variable and real coefficients.
For our purposes, a *monomial* is simply a non-negative integer power of $x$ (or some other indeterminate symbol) possibly multiplied by a scalar constant. For example, $5x^4$ is a monomial, as are constants, such as $-2$ (it being $-2x^0$) and the symbol $x$ itself (it begin $x^1$. In general, one may consider restrictions on where the constants can come from, and consider more than one symbol, but we won't pursue this here, restricting ourselves to the case of a single variable and real coefficients.
A *polynomial* is a sum of monomials. After combining terms with same powers, a non-zero polynomial may be written uniquely as:
@@ -158,7 +158,7 @@ The above shows that multiple symbols can be defined at once. The annotation `x:
:::{.callout-note}
## Note
Macros in `Julia` are just transformations of the syntax into other syntax. The `@` indicates they behave differently than regular function calls.
Macros in `Julia` are just transformations of the syntax into other syntax. The leading `@` indicates they behave differently than regular function calls.
:::
@@ -235,7 +235,7 @@ To illustrate, to do the task above for the polynomial $-16x^2 + 100$ we could h
p(x => (x-1)^2)
```
This "call" notation takes pairs (designated by `a=>b`) where the left-hand side is the variable to substitute for, and the right-hand side the new value. The value to substitute can depend on the variable, as illustrated; be a different variable; or be a numeric value, such as $2$:
This "call" notation takes pairs (designated by `a=>b`) where the left-hand side is the variable to substitute for, and the right-hand side the new value. (This mirrors a similar use with `replace`.) The value to substitute can depend on the variable, as illustrated; be a different variable; or be a numeric value, such as $2$:
```{julia}
@@ -264,15 +264,15 @@ Suppose we have the polynomial $p = ax^2 + bx +c$. What would it look like if we
```{julia}
@syms E F
p = a*x^2 + b*x + c
p(x => x-E) + F
p = a*x^2 + b*x + c
p(x => x-E) + F
```
And expanded this becomes:
```{julia}
expand(p(x => x-E) + F)
expand(p(x => x-E) + F)
```
### Conversion of symbolic numbers to Julia numbers
@@ -287,7 +287,17 @@ p = -16x^2 + 100
y = p(2)
```
The value, $36$ is still symbolic, but clearly an integer. If we are just looking at the output, we can easily translate from the symbolic value to an integer, as they print similarly. However the conversion to an integer, or another type of number, does not happen automatically. If a number is needed to pass along to another `Julia` function, it may need to be converted. In general, conversions between different types are handled through various methods of `convert`. However, with `SymPy`, the `N` function will attempt to do the conversion for you:
The value, $36$ is still symbolic, but clearly an integer. If we are just looking at the output, we can easily translate from the symbolic value to an integer, as they print similarly. However the conversion to an integer, or another type of number, does not happen automatically. If a number is needed to pass along to another `Julia` function, it may need to be converted. In general, conversions between different types are handled through various methods of `convert`.
For real numbers, an easy to call conversion is available through the `float` method:
```{julia}
float(y)
```
The use of the generic `float` method returns a floating point number. `SymPy` objects have their own internal types. To preserve these on conversion to a related `Julia` value, the `N` function from `SymPy` is useful:
```{julia}
@@ -296,7 +306,7 @@ p = -16x^2 + 100
N(p(2))
```
Where `convert(T,x)` requires a specification of the type to convert `x` to, `N` attempts to match the data type used by SymPy to store the number. As such, the output type of `N` may vary (rational, a BigFloat, a float, etc.) For getting more digits of accuracy, a precision can be passed to `N`. The following command will take the symbolic value for $\pi$, `PI`, and produce about $60$ digits worth as a `BigFloat` value:
Where `convert(T, x)` requires a specification of the type to convert `x` to, `N` attempts to match the data type used by SymPy to store the number. As such, the output type of `N` may vary (rational, a BigFloat, a float, etc.) For getting more digits of accuracy, a precision can be passed to `N`. The following command will take the symbolic value for $\pi$, `PI`, and produce about $60$ digits worth as a `BigFloat` value:
```{julia}
@@ -329,7 +339,7 @@ pp = lambdify(p)
pp(2)
```
The `lambdify` function uses the name of the similar `SymPy` function which is named after Pythons convention of calling anoynmous function "lambdas." The use above is straightforward. Only slightly more complicated is the use when there are multiple symbolic values. For example:
The `lambdify` function uses the name of the similar `SymPy` function which is named after Python's convention of calling anoynmous function "lambdas." The use above is straightforward. Only slightly more complicated is the use when there are multiple symbolic values. For example:
```{julia}
@@ -366,7 +376,7 @@ This graph illustrates the key features of polynomial graphs:
* there may be values for `x` where the graph crosses the $x$ axis (real roots of the polynomial);
* there may be peaks and valleys (local maxima and local minima)
* except for constant polynomials, the ultimate behaviour for large values of $\lvert x\rvert$ is either both sides of the graph going to positive infinity, or negative infinity, or as in this graph one to the positive infinity and one to negative infinity. In particular, there is no *horizontal asymptote*.
* except for constant polynomials, the ultimate behaviour for large values of $|x|$ is either both sides of the graph going to positive infinity, or negative infinity, or as in this graph one to the positive infinity and one to negative infinity. In particular, there is no *horizontal asymptote*.
To investigate this last point, let's consider the case of the monomial $x^n$. When $n$ is even, the following animation shows that larger values of $n$ have greater growth once outside of $[-1,1]$:
@@ -390,7 +400,7 @@ plotly()
ImageFile(imgfile, caption)
```
Of course, this is expected, as, for example, $2^2 < 2^4 < 2^6 < \cdots$. The general shape of these terms is similar - $U$ shaped, and larger powers dominate the smaller powers as $\lvert x\rvert$ gets big.
Of course, this is expected, as, for example, $2^2 < 2^4 < 2^6 < \cdots$. The general shape of these terms is similar - $U$ shaped, and larger powers dominate the smaller powers as $|x|$ gets big.
For odd powers of $n$, the graph of the monomial $x^n$ is no longer $U$ shaped, but rather constantly increasing. This graph of $x^5$ is typical:
@@ -443,7 +453,7 @@ $$
x^5 \cdot (2 - \frac{1}{x^4} + \frac{1}{x^5}).
$$
For large $\lvert x\rvert$, the last two terms in the product on the right get close to $0$, so this expression is *basically* just $2x^5$ - the leading term.
For large $|x|$, the last two terms in the product on the right get close to $0$, so this expression is *basically* just $2x^5$ - the leading term.
---
@@ -523,7 +533,7 @@ The `SymPy` function `expand` will perform these algebraic manipulations without
expand((x-1)*(x-2)*(x-3))
```
Factoring a polynomial is several weeks worth of lessons, as there is no one-size-fits-all algorithm to follow. There are some tricks that are taught: for example factoring differences of perfect squares, completing the square, the rational root theorem, $\dots$. But in general the solution is not automated. The `SymPy` function `factor` will find all rational factors (terms like $(qx-p)$), but will leave terms that do not have rational factors alone. For example:
Factoring a polynomial is several weeks worth of lessons, as there is no one-size-fits-all algorithm available to teach to algebra students. There are some tricks that are taught: for example factoring differences of perfect squares, completing the square, the rational root theorem, $\dots$. But in general the solution is not automated without some more advanced techniques. The `SymPy` function `factor` will find all rational factors (terms like $(qx-p)$), but will leave terms that do not have rational factors alone. For example:
```{julia}
@@ -563,7 +573,7 @@ However, *polynomial functions* are easily represented by `Julia`, for example,
f(x) = -16x^2 + 100
```
The distinction is subtle, the expression is turned into a function just by adding the `f(x) =` preface. But to `Julia` there is a big distinction. The function form never does any computation until after a value of $x$ is passed to it. Whereas symbolic expressions can be manipulated quite freely before any numeric values are specified.
The distinction is subtle, the expression is turned into a function just by adding the "`f(x) =`" preface. But to `Julia` there is a big distinction. The function form never does any computation until after a value of $x$ is passed to it. Whereas symbolic expressions can be manipulated quite freely before any numeric values are specified.
It is easy to create a symbolic expression from a function - just evaluate the function on a symbolic value:
@@ -581,7 +591,7 @@ p = f(x)
p(2)
```
For many uses, the distinction is unnecessary to make, as the many functions will work with any callable expression. One such is `plot` either `plot(f, a, b)` or `plot(f(x),a, b)` will produce the same plot using the `Plots` package.
For many uses, the distinction is unnecessary to make, as the many functions will work with any callable expression. For `Plots` there is a recipe either `plot(f, a, b)` or `plot(f(x), a, b)` will produce the same plot using the `Plots` package.
## Questions

View File

@@ -288,33 +288,35 @@ Finding roots with `SymPy` can also be done through its `solve` function, a func
```{julia}
solve(x^2 + 2x - 3)
solve(x^2 + 2x - 3 ~ 0, x)
```
The answer is a vector of values that when substituted in for the free variable `x` produce $0.$ The call to `solve` does not have an equals sign. To solve a more complicated expression of the type $f(x) = g(x),$ one can solve $f(x) - g(x) = 0,$ use the `Eq` function, or use `f ~ g`.
The answer is a vector of values that when substituted in for the free variable `x` produce $0.$
We use the `~` notation to define an equation to pass to `solve`. This convention is not necessary here, as `SymPy` will assume an expression passed to solve is an equation set to `0`, but is pedagogically useful. Equations do not have an equals sign, which is reserved for assignment. To solve a more complicated expression of the type $f(x) = g(x),$ one can solve $f(x) - g(x) = 0,$ use the `Eq` function, or use `f ~ g`.
When the expression to solve has more than one free variable, the variable to solve for should be explicitly stated with a second argument. For example, here we show that `solve` is aware of the quadratic formula:
When the expression to solve has more than one free variable, the variable to solve for should be explicitly stated with a second argument. (The specification above is unnecessary.) For example, here we show that `solve` is aware of the quadratic formula:
```{julia}
@syms a b::real c::positive
solve(a*x^2 + b*x + c, x)
solve(a*x^2 + b*x + c ~ 0, x)
```
The `solve` function will respect assumptions made when a variable is defined through `symbols` or `@syms`:
```{julia}
solve(a^2 + 1) # works, as a can be complex
solve(a^2 + 1 ~ 0, a) # works, as a can be complex
```
```{julia}
solve(b^2 + 1) # fails, as b is assumed real
solve(b^2 + 1 ~ 0, b) # fails, as b is assumed real
```
```{julia}
solve(c + 1) # fails, as c is assumed positive
solve(c + 1 ~ 0, c) # fails, as c is assumed positive
```
Previously, it was mentioned that `factor` only factors polynomials with integer coefficients over rational roots. However, `solve` can be used to factor. Here is an example:
@@ -328,7 +330,7 @@ Nothing is found, as the roots are $\pm \sqrt{2}$, irrational numbers.
```{julia}
rts = solve(x^2 - 2)
rts = solve(x^2 - 2 ~ 0, x)
prod(x-r for r in rts)
```
@@ -337,7 +339,7 @@ Solving cubics and quartics can be done exactly using radicals. For example, her
```{julia}
@syms y # possibly complex
solve(y^4 - 2y - 1)
solve(y^4 - 2y - 1 ~ 0, y)
```
Third- and fourth-degree polynomials can be solved in general, with increasingly more complicated answers. The following finds one of the answers for a general third-degree polynomial:
@@ -347,7 +349,7 @@ Third- and fourth-degree polynomials can be solved in general, with increasingly
#| hold: true
@syms a[0:3]
p = sum(a*x^(i-1) for (i,a) in enumerate(a))
rts = solve(p, x)
rts = solve(p ~ 0, x)
rts[1] # there are three roots
```
@@ -355,7 +357,7 @@ Some fifth degree polynomials are solvable in terms of radicals, however, `solve
```{julia}
solve(x^5 - x + 1)
solve(x^5 - x + 1 ~ 0, x)
```
(Though there is no formula involving only radicals like the quadratic equation, there is a formula for the roots in terms of a function called the [Bring radical](http://en.wikipedia.org/wiki/Bring_radical).)
@@ -399,7 +401,7 @@ The `solve` function can be used to get numeric approximations to the roots. It
```{julia}
#| hold: true
rts = solve(x^5 - x + 1 ~ 0)
rts = solve(x^5 - x + 1 ~ 0, x)
N.(rts) # note the `.(` to broadcast over all values in rts
```
@@ -411,25 +413,25 @@ Here we see another example:
```{julia}
ex = x^7 -3x^6 + 2x^5 -1x^3 + 2x^2 + 1x^1 - 2
solve(ex)
solve(ex ~ 0, x)
```
This finds two of the seven possible roots, the remainder of the real roots can be found numerically:
```{julia}
N.(solve(ex))
N.(solve(ex ~ 0, x))
```
### The solveset function
SymPy is phasing in the `solveset` function to replace `solve`. The main reason being that `solve` has too many different output types (a vector, a dictionary, ...). The output of `solveset` is always a set. For tasks like this, which return a finite set, we use the `elements` function to access the individual answers. To illustrate:
SymPy is phasing in the `solveset` function to replace `solve`. The main reason being that `solve` has too many different output types (a vector, a dictionary, ...). The output of `solveset` is always a set. For tasks like this, which return a finite set, we use the `collect` function to access the individual answers. To illustrate:
```{julia}
p = 8x^4 - 8x^2 + 1
p_rts = solveset(p)
p_rts = solveset(p ~ 0, x)
```
The `p_rts` object, a `Set`, does not allow indexed access to its elements. For that `collect` will work to return a vector:
@@ -443,7 +445,7 @@ To get the numeric approximation, we can broadcast:
```{julia}
N.(solveset(p))
N.(solveset(p ~ 0, x))
```
(There is no need to call `collect` -- though you can -- as broadcasting over a set falls back to broadcasting over the iteration of the set and in this case returns a vector.)
@@ -497,7 +499,7 @@ in fact there are three, two are *very* close together:
```{julia}
N.(solve(h))
N.(solve(h ~ 0, x))
```
:::{.callout-note}
@@ -545,7 +547,7 @@ For another example, if we looked at $f(x) = x^5 - 100x^4 + 4000x^3 - 80000x^2 +
```{julia}
j = x^5 - 100x^4 + 4000x^3 - 80000x^2 + 799999x - 3199979
N.(solve(j))
N.(solve(j ~ 0, x))
```
### Cauchy's bound on the magnitude of the real roots.

View File

@@ -93,7 +93,7 @@ This variable is a `Polynomial` object, so can be manipulated as a polynomial; w
r = (x-2)^2 * (x-1) * (x+1)
```
The product is expanded for storage by `Polynomials`, which may not be desirable for some uses. A new variable can produced by calling `variable()`; so we could have constructed `p` by:
The product is expanded for storage by `Polynomials`, which may not be desirable for some uses. A new variable can be produced by calling `variable()`; so we could have constructed `p` by:
```{julia}
@@ -106,7 +106,7 @@ A polynomial in factored form, as `r` above is, can be constructed from its root
```{julia}
fromroots([2,2,1,-1])
fromroots([2, 2, 1, -1])
```
The `fromroots` function is basically the [factor theorem](https://en.wikipedia.org/wiki/Factor_theorem) which links the factored form of the polynomial with the roots of the polynomial: $(x-k)$ is a factor of $p$ if and only if $k$ is a root of $p$. By combining a factor of the type $(x-k)$ for each specified root, the polynomial can be constructed by multiplying its factors. For example, using `prod` and a generarator, we would have:
@@ -115,7 +115,7 @@ The `fromroots` function is basically the [factor theorem](https://en.wikipedia.
```{julia}
#| hold: true
x = variable()
prod(x - k for k in [2,2,1,-1])
prod(x - k for k in [2, 2, 1, -1])
```
The `Polynomials` package has different ways to represent polynomials, and a factored form can also be used. For example, the `fromroots` function constructs polynomials from the specified roots and `FactoredPolynomial` leaves these in a factored form:
@@ -156,11 +156,11 @@ Polynomial objects have a plot recipe defined plotting from the `Plots` pack
plot(r, legend=false) # suppress the legend
```
The choice of domain is heuristically identified; and it can be manually adjusted, as with:
The choice of domain is heuristically identified; it can be manually adjusted, as with:
```{julia}
plot(r, 1.5, 2.5, legend=false)
plot(r, 1.5, 2.5; legend=false)
```
## Roots
@@ -205,6 +205,13 @@ p = (x-1)^5
Polynomials.Multroot.multroot(p)
```
Converting to the `FactoredPolynomial` type also does this work:
```{julia}
convert(FactoredPolynomial, p)
```
Floating point error can also prevent the finding of real roots. For example, this polynomial has $3$ real roots, but `roots` finds but $1$, as the two nearby ones are identified as complex:
@@ -251,21 +258,22 @@ A line, $y=mx+b$ can be a linear polynomial or a constant depending on $m$, so w
Knowing we can succeed, we approach the problem of $3$ points, say $(x_0, y_0)$, $(x_1,y_1)$, and $(x_2, y_2)$. There is a polynomial $p = a\cdot x^2 + b\cdot x + c$ with $p(x_i) = y_i$. This gives $3$ equations for the $3$ unknown values $a$, $b$, and $c$:
$$
\begin{align*}
a\cdot x_0^2 + b\cdot x_0 + c &= y_0\\
a\cdot x_1^2 + b\cdot x_1 + c &= y_1\\
a\cdot x_2^2 + b\cdot x_2 + c &= y_2\\
\end{align*}
$$
Solving this with `SymPy` is tractable. A comprehension is used below to create the $3$ equations; the `zip` function is a simple means to iterate over $2$ or more iterables simultaneously:
Solving this with `SymPy` is tractable. A generator is used below to create the $3$ equations; the `zip` function is a simple means to iterate over $2$ or more iterables simultaneously:
```{julia}
SymPy.@syms a b c xs[0:2] ys[0:2]
eqs = [a*xi^2 + b*xi + c ~ yi for (xi,yi) in zip(xs, ys)]
abc = SymPy.solve(eqs, [a,b,c])
eqs = tuple((a*xi^2 + b*xi + c ~ yi for (xi,yi) in zip(xs, ys))...)
abc = SymPy.solve(eqs, (a,b,c))
```
As can be seen, the terms do get quite unwieldy when treated symbolically. Numerically, the `fit` function from the `Polynomials` package will return the interpolating polynomial. To compare,
@@ -294,8 +302,8 @@ A related problem, that will arise when finding iterative means to solve for zer
```{julia}
#| hold: true
SymPy.@syms a b c xs[0:2] ys[0:2]
eqs = [a*yi^2 + b*yi + c ~ xi for (xi, yi) in zip(xs,ys)]
abc = SymPy.solve(eqs, [a,b,c])
eqs = tuple((a*yi^2 + b*yi + c ~ xi for (xi, yi) in zip(xs,ys))...)
abc = SymPy.solve(eqs, (a,b,c))
abc[c]
```
@@ -306,8 +314,8 @@ We can graphically see the result for the specific values of `xs` and `ys` as fo
#| hold: true
#| echo: false
SymPy.@syms a b c xs[0:2] ys[0:2]
eqs = [a*yi^2 + b*yi + c ~ xi for (xi, yi) in zip(xs,ys)]
abc = SymPy.solve(eqs, [a,b,c])
eqs = tuple((a*yi^2 + b*yi + c ~ xi for (xi, yi) in zip(xs,ys))...)
abc = SymPy.solve(eqs, (a,b,c))
abc[c]
𝒙s, 𝒚s = [1,2,3], [3,1,2]
@@ -390,12 +398,13 @@ radioq(choices, answ, keep_order=true)
Consider the polynomial $p(x) = a_1 x - a_3 x^3 + a_5 x^5$ where
$$
\begin{align*}
a_1 &= 4(\frac{3}{\pi} - \frac{9}{16}) \\
a_3 &= 2a_1 -\frac{5}{2}\\
a_5 &= a_1 - \frac{3}{2}.
\end{align*}
$$
* Form the polynomial `p` by first computing the $a$s and forming `p=Polynomial([0,a1,0,-a3,0,a5])`
@@ -546,12 +555,13 @@ This last answer is why $p$ is called an *interpolating* polynomial and this que
The Chebyshev ($T$) polynomials are polynomials which use a different basis from the standard basis. Denote the basis elements $T_0$, $T_1$, ... where we have $T_0(x) = 1$, $T_1(x) = x$, and for bigger indices $T_{i+1}(x) = 2xT_i(x) - T_{i-1}(x)$. The first others are then:
$$
\begin{align*}
T_2(x) &= 2xT_1(x) - T_0(x) = 2x^2 - 1\\
T_3(x) &= 2xT_2(x) - T_1(x) = 2x(2x^2-1) - x = 4x^3 - 3x\\
T_4(x) &= 2xT_3(x) - T_2(x) = 2x(4x^3-3x) - (2x^2-1) = 8x^4 - 8x^2 + 1
\end{align*}
$$
With these definitions what is the polynomial associated to the coefficients $[0,1,2,3]$ with this basis?

View File

@@ -66,7 +66,7 @@ Rather than express sequences by the $a_0$, $h$, and $n$, `Julia` uses the start
1:10
```
But wait, nothing different printed? This is because `1:10` is efficiently stored. Basically, a recipe to generate the next number from the previous number is created and `1:10` just stores the start and end point and that recipe is used to generate the set of all values. To expand the values, you have to ask for them to be `collect`ed (though this typically isn't needed in practice):
But wait, nothing different printed? This is because `1:10` is efficiently stored. Basically, a recipe to generate the next number from the previous number is created and `1:10` just stores the start and end points (the step size is implicit in how this is stored) and that recipe is used to generate the set of all values. To expand the values, you have to ask for them to be `collect`ed (though this typically isn't needed in practice, as values are usually *iterated* over):
```{julia}
@@ -168,11 +168,13 @@ Now we concentrate on some more general styles to modify a sequence to produce a
### Filtering
The act of throwing out elements of a collection based on some condition is called *filtering*.
For example, another way to get the values between $0$ and $100$ that are multiples of $7$ is to start with all $101$ values and throw out those that don't match. To check if a number is divisible by $7$, we could use the `rem` function. It gives the remainder upon division. Multiples of `7` match `rem(m, 7) == 0`. Checking for divisibility by seven is unusual enough there is nothing built in for that, but checking for division by $2$ is common, and for that, there is a built-in function `iseven`.
The act of throwing out elements of a collection based on some condition is called *filtering*. The `filter` function does this in `Julia`; the basic syntax being `filter(predicate_function, collection)`. The "`predicate_function`" is one that returns either `true` or `false`, such as `iseven`. The output of `filter` consists of the new collection of values - those where the predicate returns `true`.
The `filter` function does this in `Julia`; the basic syntax being `filter(predicate_function, collection)`. The "`predicate_function`" is one that returns either `true` or `false`, such as `iseven`. The output of `filter` consists of the new collection of values - those where the predicate returns `true`.
To see it used, lets start with the numbers between `0` and `25` (inclusive) and filter out those that are even:
@@ -210,7 +212,7 @@ Let's return to the case of the set of even numbers between $0$ and $100$. We ha
* The set of numbers $\{2k: k=0, \dots, 50\}$.
While `Julia` has a special type for dealing with sets, we will use a vector for such a set. (Unlike a set, vectors can have repeated values, but as vectors are more widely used, we demonstrate them.) Vectors are described more fully in a previous section, but as a reminder, vectors are constructed using square brackets: `[]` (a special syntax for [concatenation](http://docs.julialang.org/en/latest/manual/arrays/#concatenation)). Square brackets are used in different contexts within `Julia`, in this case we use them to create a *collection*. If we separate single values in our collection by commas (or semicolons), we will create a vector:
While `Julia` has a special type for dealing with sets, we will use a vector for such a set. (Unlike a set, vectors can have repeated values, but, as vectors are more widely used, we demonstrate them.) Vectors are described more fully in a previous section, but as a reminder, vectors are constructed using square brackets: `[]` (a special syntax for [concatenation](http://docs.julialang.org/en/latest/manual/arrays/#concatenation)). Square brackets are used in different contexts within `Julia`, in this case we use them to create a *collection*. If we separate single values in our collection by commas (or semicolons), we will create a vector:
```{julia}
@@ -277,20 +279,24 @@ A typical pattern would be to generate a collection of numbers and then apply a
sum([2^i for i in 1:10])
```
Conceptually this is easy to understand, but computationally it is a bit inefficient. The generator syntax allows this type of task to be done more efficiently. To use this syntax, we just need to drop the `[]`:
Conceptually this is easy to understand: one step generates the numbers, the other adds them up. Computationally it is a bit inefficient. The generator syntax allows this type of task to be done more efficiently. To use this syntax, we just need to drop the `[]`:
```{julia}
sum(2^i for i in 1:10)
```
(The difference being no intermediate object is created to store the collection of all values specified by the generator.)
The difference being no intermediate object is created to store the collection of all values specified by the generator. Not all functions allow generators as arguments, but most common reductions do.
### Filtering generated expressions
Both comprehensions and generators allow for filtering through the keyword `if`. The following shows *one* way to add the prime numbers in $[1,100]$:
Both comprehensions and generators allow for filtering through the keyword `if`. The basic pattern is
`[expr for variable in collection if expr]`
The following shows *one* way to add the prime numbers in $[1,100]$:
```{julia}
@@ -299,6 +305,10 @@ sum(p for p in 1:100 if isprime(p))
The value on the other side of `if` should be an expression that evaluates to either `true` or `false` for a given `p` (like a predicate function, but here specified as an expression). The value returned by `isprime(p)` is such.
::: {.callout-note}
In these notes we primarily use functions rather than expressions for various actions. We will see creating a function is not much more difficult than specifying an expression, though there is additional notation necessary. Generators are one very useful means to use expressions, symbolic math will be seen as another.
:::
In this example, we use the fact that `rem(k, 7)` returns the remainder found from dividing `k` by `7`, and so is `0` when `k` is a multiple of `7`:
@@ -323,7 +333,7 @@ This example of Stefan Karpinski's comes from a [blog](http://julialang.org/blog
First, a simple question: using pennies, nickels, dimes, and quarters how many different ways can we generate one dollar? Clearly $100$ pennies, or $20$ nickels, or $10$ dimes, or $4$ quarters will do this, so the answer is at least four, but how much more than four?
Well, we can use a comprehension to enumerate the possibilities. This example illustrates how comprehensions and generators can involve one or more variable for the iteration.
Well, we can use a comprehension to enumerate the possibilities. This example illustrates how comprehensions and generators can involve one or more variable for the iteration. By judiciously choosing what is iterated over, the entire set can be described.
First, we either have $0,1,2,3$, or $4$ quarters, or $0$, $25$ cents, $50$ cents, $75$ cents, or a dollar's worth. If we have, say, $1$ quarter, then we need to make up $75$ cents with the rest. If we had $3$ dimes, then we need to make up $45$ cents out of nickels and pennies, if we then had $6$ nickels, we know we must need $15$ pennies.
@@ -333,22 +343,42 @@ The following expression shows how counting this can be done through enumeration
```{julia}
ways = [(q, d, n, p) for q = 0:25:100 for d = 0:10:(100 - q) for n = 0:5:(100 - q - d) for p = (100 - q - d - n)]
ways = [(q, d, n, p)
for q = 0:25:100
for d = 0:10:(100 - q)
for n = 0:5:(100 - q - d)
for p = (100 - q - d - n)]
length(ways)
```
We see $242$ cases, each distinct. The first $3$ are:
There are $242$ distinct cases. The first three are:
```{julia}
ways[1:3]
```
The generating expression reads naturally. It introduces the use of multiple `for` statements, each subsequent one depending on the value of the previous (working left to right). Now suppose, we want to ensure that the amount in pennies is less than the amount in nickels, etc. We could use `filter` somehow to do this for our last answer, but using `if` allows for filtering while the events are generating. Here our condition is simply expressed: `q > d > n > p`:
The generating expression reads naturally. It introduces the use of multiple `for` statements, each subsequent one depending on the value of the previous (working left to right).
The cashier might like to know the number of coins, not the dollar amount:
```{julia}
[amt ./ [25, 10, 5, 1] for amt in ways[1:3]]
```
There are various ways to get integer values, and not floating point values. One way is to call `round`. Here though, we use the integer division operator, `div`, through its infix operator `÷`:
```{julia}
[amt .÷ [25, 10, 5, 1] for amt in ways[1:3]]
```
Now suppose, we want to ensure that the amount in pennies is less than the amount in nickels, etc. We could use `filter` somehow to do this for our last answer, but using `if` allows for filtering while the events are generating. Here our condition is simply expressed: `q > d > n > p`:
```{julia}
[(q, d, n, p) for q = 0:25:100
[(q, d, n, p)
for q = 0:25:100
for d = 0:10:(100 - q)
for n = 0:5:(100 - q - d)
for p = (100 - q - d - n)
@@ -523,6 +553,24 @@ radioq(choices, answ)
###### Question
An arithmetic sequence ($a_0$, $a_1 = a_0 + h$, $a_2=a_0 + 2h, \dots,$ $a_n=a_0 + n\cdot h$) is specified with a starting point ($a_0$), a step size ($h$), and a number of points $(n+1)$. This is not the case with the colon constructor which take a starting point, a step size, and a suggested last value. This is not the case a with the default for the `range` function, with signature `range(start, stop, length)`. However, the documentation for `range` shows that indeed the three values ($a_0$, $h$, and $n$) can be passed in. Which signature (from the docs) would allow this:
```{julia}
#| echo: false
choices = [
"`range(start, stop, length)`",
"`range(start, stop; length, step)`",
"`range(start; length, stop, step)`",
"`range(;start, length, stop, step)`"]
answer = 3
explanation = """
This is a somewhat vague question, but the use of `range(a0; length=n+1, step=h)` will produce the arithmetic sequence with this parameterization.
"""
buttonq(choices, answer; explanation)
```
###### Question
Create the sequence $10, 100, 1000, \dots, 1,000,000$ using a list comprehension. Which of these works?
@@ -670,7 +718,7 @@ Let's see if `4137 8947 1175 5804` is a valid credit card number?
First, we enter it as a value and immediately break the number into its digits:
```{julia}
x = 4137_8947_1175_5904 # _ in a number is ignored by parser
x = 4137_8947_1175_5804 # _ in a number is ignored by parser
xs = digits(x)
```
@@ -688,7 +736,7 @@ for i in 1:2:length(xs)
end
```
Number greater than 9, have their digits added, then all the resulting numbers are added. This can be done with a generator:
Numbers greater than 9, have their digits added, then all the resulting numbers are added. This can be done with a generator:
```{julia}

View File

@@ -153,7 +153,7 @@ This decomposition breaks the rational expression into two pieces: $x-4$ and $40
plot(apart(h) - (x - 4), 10, 100)
```
Similarly, a plot over $[-100, -10]$ would show decay towards $0$, though in that case from below. Combining these two facts then, it is now no surprise that the graph of the rational function $f(x)$ should approach a straight line, in this case $y=x-4$ as $x \rightarrow \pm \infty$.
Similarly, a plot over $[-100, -10]$ would show decay towards $0$, though in that case from below. Combining these two facts then, it is now no surprise that the graph of the rational function $f(x)$ should approach a straight line, in this case $y=x-4$, as $x \rightarrow \pm \infty$.
We can easily do most of this analysis without needing a computer or algebra. First, we should know the four eventual shapes of a polynomial, that the graph of $y=mx$ is a line with slope $m$, the graph of $y = c$ is a constant line at height $c$, and the graph of $y=c/x^m$, $m > 0$ will decay towards $0$ as $x \rightarrow \pm\infty$. The latter should be clear, as $x^m$ gets big, so its reciprocal goes towards $0$.
@@ -358,8 +358,8 @@ We can avoid the vertical asymptotes in our viewing window. For example we could
```{julia}
𝒇(x) = (x-1)^2 * (x-2) / ((x+3)*(x-3) )
plot(𝒇, -2.9, 2.9)
f(x) = (x-1)^2 * (x-2) / ((x+3)*(x-3) )
plot(f, -2.9, 2.9)
```
This backs off by $\delta = 0.1$. As we have that $3 - 2.9$ is $\delta$ and $1/\delta$ is 10, the $y$ axis won't get too large, and indeed it doesn't.
@@ -373,7 +373,7 @@ We can also clip the `y` axis. The `plot` function can be passed an argument `yl
```{julia}
#| hold: true
plot(𝒇, -5, 5, ylims=(-20, 20))
plot(f, -5, 5, ylims=(-20, 20))
```
This isn't ideal, as the large values are still computed, just the viewing window is clipped. This leaves the vertical asymptotes still effecting the graph.
@@ -386,7 +386,7 @@ This was discussed in an earlier section where the `rangeclamp` function was int
```{julia}
plot(rangeclamp(𝒇, 30), -25, 25) # rangeclamp is in the CalculusWithJulia package
plot(rangeclamp(f, 30), -25, 25) # rangeclamp is in the CalculusWithJulia package
```
We can see the general shape of $3$ curves broken up by the vertical asymptotes. The two on the side heading off towards the line $x-4$ and the one in the middle. We still can't see the precise location of the zeros, but that wouldn't be the case with most graphs that show asymptotic behaviors. However, we can clearly tell where to "zoom in" were those of interest.
@@ -464,28 +464,28 @@ In the following, we import some functions from the `Polynomials` package. We av
import Polynomials: Polynomial, variable, lowest_terms, fromroots, coeffs
```
The `Polynomials` package has support for rational functions. The `//` operator can be used to create rational expressions:
The `Polynomials` package has support for rational functions. The `//` operator can be used to create rational expressions from polynomial expressions:
```{julia}
𝒙 = variable()
𝒑 = (𝒙-1)*(𝒙-2)^2
𝒒 = (𝒙-2)*(𝒙-3)
𝒑𝒒 = 𝒑 // 𝒒
x = variable()
p = (x-1)*(x-2)^2
q = (x-2)*(x-3)
pq = p // q
```
A rational expression is a formal object; a rational function the viewpoint that this object will be evaluated by substituting values for the indeterminate. Rational expressions made within `Polynomials` are evaluated just like functions:
```{julia}
𝒑𝒒(4) # p(4)/q(4)
pq(4) # p(4)/q(4)
```
The rational expressions are not in lowest terms unless requested through the `lowest_terms` method:
```{julia}
lowest_terms(𝒑𝒒)
lowest_terms(pq)
```
For polynomials as simple as these, this computation is not a problem, but there is the very real possibility that the lowest term computation may be incorrect. Unlike `SymPy` which factors symbolically, `lowest_terms` uses a numeric algorithm and does not, as would be done by hand or with `SymPy`, factor the polynomial and then cancel common factors.
@@ -512,7 +512,7 @@ Similarly, we can divide a polynomial by the polynomial $1$, which in `Julia` is
```{julia}
pp = 𝒑 // one(𝒑)
pp = p // one(p)
```
And as with rational numbers, `p` is recovered by `numerator`:
@@ -532,7 +532,7 @@ For the polynomial `pq` above, we have from observation that $1$ and $2$ will be
```{julia}
plot(𝒑𝒒)
plot(pq)
```
To better see the zeros, a plot over a narrower interval, say $[0,2.5]$, would be encouraged; to better see the slant asymptote, a plot over a wider interval, say $[-10,10]$, would be encouraged.
@@ -608,8 +608,8 @@ We can verify this does what we want through example with the previously defined
```{julia}
𝐩 = Polynomial([1, 2, 3, 4, 5])
𝐪 = mobius_transformation(𝐩, 4, 6)
p = Polynomial([1, 2, 3, 4, 5])
q = mobius_transformation(p, 4, 6)
```
As contrasted with
@@ -619,9 +619,9 @@ As contrasted with
#| hold: true
a, b = 4, 6
pq = 𝐩 // one(𝐩)
pq = p // one(p)
x = variable(pq)
d = Polynomials.degree(𝐩)
d = Polynomials.degree(p)
numerator(lowest_terms( (x + 1)^d * pq((a*x + b)/(x + 1))))
```
@@ -699,22 +699,22 @@ More challenging problems can be readily handled by this package. The following
```{julia}
𝒔 = Polynomial([0,1]) # also just variable(Polynomial{Int})
𝒖 = -1 + 254*𝒔 - 16129*𝒔^2 + 𝒔^15
s = Polynomial([0,1]) # also just variable(Polynomial{Int})
u = -1 + 254*s - 16129*s^2 + s^15
```
has three real roots, two of which are clustered very close to each other:
```{julia}
𝒔𝒕 = ANewDsc(coeffs(𝒖))
st = ANewDsc(coeffs(u))
```
and
```{julia}
refine_roots(𝒔𝒕)
refine_roots(st)
```
The SymPy package (`sympy.real_roots`) can accurately identify the three roots but it can take a **very** long time. The `Polynomials.roots` function from the `Polynomials` package identifies the cluster as complex valued. Though the implementation in `RealPolynomialRoots` doesn't handle such large polynomials, the authors of the algorithm have implementations that can quickly solve polynomials with degrees as high as $10,000$.

View File

@@ -54,7 +54,7 @@ ss = sin + sqrt
ss(4)
```
Doing this works, as Julia treats functions as first class objects, lending itself to [higher](https://en.wikipedia.org/wiki/Higher-order_programming) order programming. However, this definition in general is kind of limiting, as functions in mathematics and Julia can be much more varied than just the univariate functions we have defined addition for. We won't pursue this further.
Doing this works, as Julia treats functions as first class objects, lending itself to [higher](https://en.wikipedia.org/wiki/Higher-order_programming) order programming. However, this definition in general is kind of limiting, as functions in mathematics and Julia can be much more varied than just the univariate functions we have defined addition for. Further, users shouldn't be modifying base methods on types they don't control, as that can lead to really unexpected and undesirable behaviours. This is called *type piracy*. We won't pursue this possibility further. Rather we will define new function by what they do to their values, such as `h(x) = f(x) + g(x)`.
### Composition of functions
@@ -95,7 +95,7 @@ plot!(gf, label="g∘f")
:::{.callout-note}
## Note
Unlike how the basic arithmetic operations are treated, `Julia` defines the infix Unicode operator `\circ[tab]` to represent composition of functions, mirroring mathematical notation. This infix operations takes in two functions and returns an anonymous function. It can be useful and will mirror standard mathematical usage up to issues with precedence rules.
Unlike how the basic arithmetic operations are treated, `Julia` defines the infix Unicode operator `\circ[tab]` to represent composition of functions, mirroring mathematical notation. This infix operations takes in two functions and returns a composed function. It can be useful and will mirror standard mathematical usage up to issues with precedence rules.
:::
@@ -109,15 +109,16 @@ $$
(f \circ g)(x) = (e^x - x)^2 + 2(e^x - x) - 1.
$$
It can be helpful to think of the argument to $f$ as a "box" that gets filled in by $g$:
It can be helpful to think of the argument to $f$ as a "box" that gets filled in by $g(x)$:
$$
\begin{align*}
g(x) &=e^x - x\\
f(\square) &= (\square)^2 + 2(\square) - 1\\
f(g(x)) &= (g(x))^2 + 2(g(x)) - 1 = (e^x - x)^2 + 2(e^x - x) - 1.
\end{align*}
$$
Here we look at a few compositions:
@@ -171,46 +172,46 @@ To illustrate, let's define a hat-shaped function as follows:
```{julia}
𝒇(x) = max(0, 1 - abs(x))
f(x) = max(0, 1 - abs(x))
```
A plot over the interval $[-2,2]$ is shown here:
```{julia}
plot(𝒇, -2,2)
plot(f, -2,2)
```
The same graph of $f$ and its image shifted up by $2$ units would be given by:
```{julia}
plot(𝒇, -2, 2, label="f")
plot!(up(𝒇, 2), label="up")
plot(f, -2, 2, label="f")
plot!(up(f, 2), label="up")
```
A graph of $f$ and its shift over by $2$ units would be given by:
```{julia}
plot(𝒇, -2, 4, label="f")
plot!(over(𝒇, 2), label="over")
plot(f, -2, 4, label="f")
plot!(over(f, 2), label="over")
```
A graph of $f$ and it being stretched by $2$ units would be given by:
```{julia}
plot(𝒇, -2, 2, label="f")
plot!(stretch(𝒇, 2), label="stretch")
plot(f, -2, 2, label="f")
plot!(stretch(f, 2), label="stretch")
```
Finally, a graph of $f$ and it being scaled by $2$ would be given by:
```{julia}
plot(𝒇, -2, 2, label="f")
plot!(scale(𝒇, 2), label="scale")
plot(f, -2, 2, label="f")
plot!(scale(f, 2), label="scale")
```
Scaling by $2$ shrinks the non-zero domain, scaling by $1/2$ would stretch it. If this is not intuitive, the definition `x-> f(x/c)` could have been used, which would have opposite behaviour for scaling.
@@ -226,16 +227,16 @@ A shift right by $2$ and up by $1$ is achieved through
```{julia}
plot(𝒇, -2, 4, label="f")
plot!(up(over(𝒇,2), 1), label="over and up")
plot(f, -2, 4, label="f")
plot!(up(over(f,2), 1), label="over and up")
```
Shifting and scaling can be confusing. Here we graph `scale(over(𝒇,2),1/3)`:
Shifting and scaling can be confusing. Here we graph `scale(over(f,2),1/3)`:
```{julia}
plot(𝒇, -1,9, label="f")
plot!(scale(over(𝒇,2), 1/3), label="over and scale")
plot(f, -1,9, label="f")
plot!(scale(over(f,2), 1/3), label="over and scale")
```
This graph is over by $6$ with a width of $3$ on each side of the center. Mathematically, we have $h(x) = f((1/3)\cdot x - 2)$
@@ -245,8 +246,8 @@ Compare this to the same operations in opposite order:
```{julia}
plot(𝒇, -1, 5, label="f")
plot!(over(scale(𝒇, 1/3), 2), label="scale and over")
plot(f, -1, 5, label="f")
plot!(over(scale(f, 1/3), 2), label="scale and over")
```
This graph first scales the symmetric graph, stretching from $-3$ to $3$, then shifts over right by $2$. The resulting function is $f((1/3)\cdot (x-2))$.
@@ -265,9 +266,9 @@ We can view this as a composition of "scale" by $1/a$, then "over" by $b$, and
```{julia}
#| hold: true
a = 2; b = 5
𝒉(x) = stretch(over(scale(𝒇, 1/a), b), 1/a)(x)
plot(𝒇, -1, 8, label="f")
plot!(𝒉, label="h")
h(x) = stretch(over(scale(f, 1/a), b), 1/a)(x)
plot(f, -1, 8, label="f")
plot!(h, label="h")
```
(This transformation keeps the same amount of area in the triangles, can you tell from the graph?)
@@ -324,12 +325,6 @@ delta = (newyork(185) - datetime) * 60
This is off by a fair amount - almost $12$ minutes. Clearly a trigonometric model, based on the assumption of circular motion of the earth around the sun, is not accurate enough for precise work, but it does help one understand how summer days are longer than winter days and how the length of a day changes fastest at the spring and fall equinoxes.
##### Example: a growth model in fisheries
The von Bertalanffy growth [equation](https://en.wikipedia.org/wiki/Von_Bertalanffy_function) is $L(t) =L_\infty \cdot (1 - e^{k\cdot(t-t_0)})$. This family of functions can be viewed as a transformation of the exponential function $f(t)=e^t$. Part is a scaling and shifting (the $e^{k \cdot (t - t_0)}$) along with some shifting and stretching. The various parameters have physical importance which can be measured: $L_\infty$ is a carrying capacity for the species or organism, and $k$ is a rate of growth. These parameters may be estimated from data by finding the "closest" curve to a given data set.
##### Example: the pipeline operator
@@ -348,6 +343,44 @@ pi/2 |> g |> f
The output of the preceding expression is passed as the input to the next. This notation is especially convenient when the enclosing function is not the main focus. (Some programming languages have more developed [fluent interfaces](https://en.wikipedia.org/wiki/Fluent_interface) for chaining function calls. Julia has more powerful chaining macros provided in packages, such as `DataPipes.jl` or `Chain.jl`.)
##### Example: a growth model in fisheries
The von Bertalanffy growth [equation](https://en.wikipedia.org/wiki/Von_Bertalanffy_function) is $L(t) =L_\infty \cdot (1 - e^{k\cdot(t-t_0)})$. This family of functions can be viewed as a transformation of the exponential function $f(t)=e^t$. Part is a scaling and shifting (the $e^{k \cdot (t - t_0)}$) along with some shifting and stretching. The various parameters have physical importance which can be measured: $L_\infty$ is a carrying capacity for the species or organism, and $k$ is a rate of growth. These parameters may be estimated from data by finding the "closest" curve to a given data set.
##### Example: Representing data visually.
Suppose we have a data set like the following:^[Which comes from the "Palmer Penguins" data set]
|flipper length | bill length | body mass | gender | species |
|---------------|-------------|-----------|--------|:--------|
| 38.8 | 18.3 | 3701 | male | Adelie |
| 48.8 | 18.4 | 3733 | male | Chinstrap |
| 47.5 | 15.0 | 5076 | male | Gentoo |
We might want to plot on an $x-y$ axis flipper length versus bill length but also indicate body size with a large size marker for bigger sizes.
We could do so by transforming a marker: scaling by size, then shifting it to an `x-y` position; then plotting. Something like this:
```{julia}
flipper = [38.8, 48.8, 47.5]
bill = [18.3, 18.4, 15.0]
bodymass = [3701, 4733, 5076]
shape = Shape(:star5)
p = plot(; legend=false)
for (x,y,sz) in zip(flipper, bill, bodymass)
sz = (sz - 2000) ÷ 1000
new_shape = Plots.translate(Plots.scale(shape, sz, sz), x, y);
plot!(p, new_shape; fill=(:red, 0.25), stroke=(:black, 2))
end
p
```
While some of the commands in this example are unfamiliar and won't be explained further, the use of `translate` and `scale` for shapes is very similar to how transformations for functions are being described (Though this `translate` function combines `up` and `over`; and this `scale` function allows different values depending on direction.) In the above, the function names are qualified, as they are not exported by the `Plots.jl` package. More variables from the data set could be encoded through colors, different shapes etc. allowing very data-rich graphics.
### Operators
@@ -392,14 +425,14 @@ To see that it works, we take a typical function
```{julia}
𝐟(k) = 1 + k^2
f(k) = 1 + k^2
```
and check:
```{julia}
D(𝐟)(3), 𝐟(3) - 𝐟(3-1)
D(f)(3), f(3) - f(3-1)
```
That the two are the same value is no coincidence. (Again, pause for a second to make sure you understand why `D(f)(3)` makes sense. If this is unclear, you could name the function `D(f)` and then call this with a value of `3`.)
@@ -416,7 +449,7 @@ To check if this works as expected, compare these two values:
```{julia}
S(𝐟)(4), 𝐟(1) + 𝐟(2) + 𝐟(3) + 𝐟(4)
S(f)(4), f(1) + f(2) + f(3) + f(4)
```
So one function adds, the other subtracts. Addition and subtraction are somehow inverse to each other so should "cancel" out. This holds for these two operations as well, in the following sense: subtracting after adding leaves the function alone:
@@ -424,7 +457,7 @@ So one function adds, the other subtracts. Addition and subtraction are somehow
```{julia}
k = 10 # some arbitrary value k >= 1
D(S(𝐟))(k), 𝐟(k)
D(S(f))(k), f(k)
```
Any positive integer value of `k` will give the same answer (up to overflow). This says the difference of the accumulation process is just the last value to accumulate.
@@ -434,7 +467,7 @@ Adding after subtracting also leaves the function alone, save for a vestige of $
```{julia}
S(D(𝐟))(15), 𝐟(15) - 𝐟(0)
S(D(f))(15), f(15) - f(0)
```
That is the accumulation of differences is just the difference of the end values.
@@ -597,9 +630,11 @@ Consider this expression
$$
\left(f(1) - f(0)\right) + \left(f(2) - f(1)\right) + \cdots + \left(f(n) - f(n-1)\right) =
-f(0) + f(1) - f(1) + f(2) - f(2) + \cdots + f(n-1) - f(n-1) + f(n) =
f(n) - f(0).
\begin{align*}
\left(f(1) - f(0)\right) &+ \left(f(2) - f(1)\right) + \cdots + \left(f(n) - f(n-1)\right) \\
&= -f(0) + f(1) - f(1) + f(2) - f(2) + \cdots + f(n-1) - f(n-1) + f(n) \\
&=f(n) - f(0).
\end{align*}
$$
Referring to the definitions of `D` and `S` in the example on operators, which relationship does this support:

View File

@@ -44,14 +44,16 @@ annotate!([(.75, .25, "θ"), (4.0, 1.25, "opposite"), (2, -.25, "adjacent"), (1.
With these, the basic definitions for the primary trigonometric functions are
::: {.callout-note icon=false}
## Trigonometric definitions
$$
\begin{align*}
\sin(\theta) &= \frac{\text{opposite}}{\text{hypotenuse}} &\quad(\text{the sine function})\\
\cos(\theta) &= \frac{\text{adjacent}}{\text{hypotenuse}} &\quad(\text{the cosine function})\\
\tan(\theta) &= \frac{\text{opposite}}{\text{adjacent}}. &\quad(\text{the tangent function})
\tan(\theta) &= \frac{\text{opposite}}{\text{adjacent}} &\quad(\text{the tangent function})
\end{align*}
$$
:::
:::{.callout-note}
## Note
@@ -122,11 +124,12 @@ Julia has the $6$ basic trigonometric functions defined through the functions `s
Two right triangles - the one with equal, $\pi/4$, angles; and the one with angles $\pi/6$ and $\pi/3$ can have the ratio of their sides computed from basic geometry. In particular, this leads to the following values, which are usually committed to memory:
$$
\begin{align*}
\sin(0) &= 0, \quad \sin(\pi/6) = \frac{1}{2}, \quad \sin(\pi/4) = \frac{\sqrt{2}}{2}, \quad\sin(\pi/3) = \frac{\sqrt{3}}{2},\text{ and } \sin(\pi/2) = 1\\
\cos(0) &= 1, \quad \cos(\pi/6) = \frac{\sqrt{3}}{2}, \quad \cos(\pi/4) = \frac{\sqrt{2}}{2}, \quad\cos(\pi/3) = \frac{1}{2},\text{ and } \cos(\pi/2) = 0.
\end{align*}
$$
Using the circle definition allows these basic values to inform us of values throughout the unit circle.
@@ -144,10 +147,10 @@ The fact that $x^2 + y^2 = 1$ for the unit circle leads to the "Pythagorean iden
$$
\sin(\theta)^2 + \cos(\theta)^2 = 1.
\sin^2(\theta) + \cos^2(\theta) = 1.
$$
This basic fact can be manipulated many ways. For example, dividing through by $\cos(\theta)^2$ gives the related identity: $\tan(\theta)^2 + 1 = \sec(\theta)^2$.
This basic fact can be manipulated many ways. For example, dividing through by $\cos^2(\theta)$ gives the related identity: $\tan^2(\theta) + 1 = \sec^2(\theta)$.
`Julia`'s functions can compute values for any angles, including these fundamental ones:
@@ -157,8 +160,15 @@ This basic fact can be manipulated many ways. For example, dividing through by $
[cos(theta) for theta in [0, pi/6, pi/4, pi/3, pi/2]]
```
These are floating point approximations, as can be seen clearly in the last value. Symbolic math can be used if exactness matters:
To compute $\sin^2(\theta)$, the power is applied to the value of $\sin(\theta)$ and not the `sin` function. (Think of $\sin^2(\theta)$ as $(sin(\theta))^2$:
```{julia}
theta = pi/8
sin(theta)^2
```
These values are floating point approximations, as can be seen clearly in the computation of `sin(pi/2)`, which is mathematically $0$. Symbolic math can be usedby using `PI` for `pi` if exactness matters:
```{julia}
cos.([0, PI/6, PI/4, PI/3, PI/2])
@@ -167,7 +177,7 @@ cos.([0, PI/6, PI/4, PI/3, PI/2])
The `sincos` function computes both `sin` and `cos` simultaneously, which can be more performant when both values are needed.
```{juila}
```{julia}
sincos(pi/3)
```
@@ -266,11 +276,11 @@ $$
g(x) = a + b \sin((2\pi n)x)
$$
That is $g$ is shifted up by $a$ units, scaled vertically by $b$ units and has a period of $1/n$. We see a simple plot here where we can verify the transformation:
That is a graph of $g$ will be the sine curve shifted up by $a$ units, scaled vertically by $b$ units and has a period of $1/n$. We see a simple plot here where we can verify the transformation:
```{julia}
g(x; b=1,n=1) = b*sin(2pi*n*x)
g(x; b=1, n=1) = b*sin(2pi*n*x)
g1(x) = 1 + g(x, b=2, n=3)
plot(g1, 0, 1)
```
@@ -388,51 +398,181 @@ In `Julia`, the functions `sind`, `cosd`, `tand`, `cscd`, `secd`, and `cotd` are
Consider the point on the unit circle $(x,y) = (\cos(\theta), \sin(\theta))$. In terms of $(x,y)$ (or $\theta$) is there a way to represent the angle found by rotating an additional $\theta$, that is what is $(\cos(2\theta), \sin(2\theta))$?
More generally, suppose we have two angles $\alpha$ and $\beta$, can we represent the values of $(\cos(\alpha + \beta), \sin(\alpha + \beta))$ using the values just involving $\beta$ and $\alpha$ separately?
More generally, suppose we have two angles $\alpha$ and $\beta$, can we represent the values of $(\cos(\alpha + \beta), \sin(\alpha + \beta))$ using the values just involving $\beta$ and $\alpha$ separately? The sum formulas express the sine and cosine of $\alpha + \beta$ in terms of the sines and cosines of $\alpha$ and $\beta$. We show variations on the basic decomposition of a right triangle using sine and cosine to illustrate the resulting formula. According to [Wikipedia](https://en.wikipedia.org/wiki/Trigonometric_functions#Identities) this geometric derivation has ideas that date to Ptolemy.
According to [Wikipedia](https://en.wikipedia.org/wiki/Trigonometric_functions#Identities) the following figure (from [mathalino.com](http://www.mathalino.com/reviewer/derivation-of-formulas/derivation-of-sum-and-difference-of-two-angles)) has ideas that date to Ptolemy:
Suppose both $\alpha$ and $\beta$ are positive with $\alpha + \beta \leq \pi/2$. Then using right triangle geometry we can associate the sine and cosine of $\alpha + \beta$ with distances in this figure:
```{julia}
#| echo: false
# ImageFile(:precalc, "figures/summary-sum-and-difference-of-two-angles.jpg", "Relations between angles")
nothing
using Plots, LaTeXStrings
# two angles
α = pi/5
β = pi/6
## points
A = (0,0)
B = (cos(α + β), 0)
C = (cos(α)*cos(β), 0)
D = (cos(α + β), sin(α)cos(β))
E = (cos(α)*cos(β), sin(α)cos(β))
F = (cos(α + β), sin(α + β))
color1 = :royalblue
color2 = :forestgreen
color3 = :brown3
color4 = :mediumorchid2
canvas() = plot(axis=([],false), legend=false, aspect_ratio=:equal)
p1 = canvas()
plot!(Shape([A,B,F]), fill=(color4, 0.15))
r = 0.10
dae = sqrt(sum((A.-E).^2))
daf = sqrt(sum((A.-F).^2))
dbc = sqrt(sum((B.-C).^2))
dce = sqrt(sum((C.-E).^2))
def = sqrt(sum((E.-F).^2))
dde = sqrt(sum((D.-E).^2))
ddf = sqrt(sum((D.-F).^2))
Δ = 0.0
alphabeta = (r*cos(α/2 + β/2), r*sin(α/2 + β/2),
text("α + β",:hcenter; rotation=pi/2))
cosαβ = (B[1]/2, 0, text("cos(α + β)", :top))
sinαβ = (B[1], F[2]/2, text("sin(α + β)"))
txtpoints = (
one = (F[1]/2, F[2]/2, "1",:right),
beta=(r*cos(α + β/2), r*sin(α + β/2),
text("β", :hcenter)),
alpha = (r*cos(α/2), r*sin(α/2),
text("α",:hcenter)),
alphaa = (F[1] + r*sin(α/2), F[2] - r*cos(α/2) ,
text("α"),:hcenter),
cosβ = (dae/2*cos(α),dae/2*sin(α) + Δ,
text("cos(β)",:hcenter)),
sinβ = (B[1] + dbc/2 + Δ/2, D[2] + ddf/2 + Δ/2,
text("sin(β)",:bottom)),
cosαcosβ = (C[1]/2, 0 - Δ, text("cos(α)cos(β)", :top)),
sinαcosβ = (cos(α)*cos(β) - 0.1, dce/2 ,
text("sin(α)cos(β)", :hcenter)),
cosαsinβ = (D[1] - Δ, D[2] + ddf/2 ,
text("cos(α)sin(β)", :top)),
sinαsinβ = (D[1] + dde/2, D[2] + Δ ,
text("sin(α)sin(β)", :top)),
)
# Plot 1
p1 = canvas()
plot!(Shape([A,B,F]), fill=(color4, 0.15))
annotate!([txtpoints[:one], alphabeta, cosαβ, sinαβ])
plot!([A,B,F,A]; line=(5,:red, 0.25))
# Plot 2
p2 = canvas()
plot!(Shape([A,E,F]), fill=(color1, 0.15))
plot!([A,B,F,A]; line=(5,:red, 0.25))
annotate!(map(s ->getindex(txtpoints,s), [:one, :cosβ, :sinβ, :beta]))
# Plot 3
p3 = canvas()
plot!(Shape([A,E,F]), fill=(color1, 0.15))
plot!(Shape([A,C,E]), fill=(color2, 0.15))
annotate!(map(s ->getindex(txtpoints,s), [:alpha, :cosβ, :cosαcosβ, :sinαcosβ]))
plot!([A,B,F,A]; line=(5,:red, 0.25))
annotate!([txtpoints[:beta]])
# Plot 4
p4 = canvas()
plot!(Shape([A,E,D, F]), fill=(color1, 0.15))
plot!(Shape([A,C,E]), fill=(color2, 0.15))
plot!(Shape([D,E,F]), fill=(color3, 0.15))
annotate!(map(s ->getindex(txtpoints,s), [:alphaa, :sinβ, :sinαsinβ, :cosαsinβ]))
plot!([A,B,F,A]; line=(5,:red, 0.25))
# Plot 5
p5 = canvas()
plot!(Shape([A,E,D, F]), fill=(color1, 0.15))
plot!(Shape([A,C,E]), fill=(color2, 0.15))
plot!(Shape([D,E,F]), fill=(color3, 0.15))
plot!(Shape([F,B]), fill=(:black, 0.35))
annotate!(map(s ->getindex(txtpoints,s), collect(keys(txtpoints))))
p1
```
![Relations between angles](figures/summary-sum-and-difference-of-two-angles.jpg)
Another right triangle with hypotenuse of length $1$ can be made by isolating the angle $\beta$, as below:
To read this, there are three triangles: the bigger (green with pink part) has hypotenuse $1$ (and adjacent and opposite sides that form the hypotenuses of the other two); the next biggest (yellow) hypotenuse $\cos(\beta)$, adjacent side (of angle $\alpha$) $\cos(\beta)\cdot \cos(\alpha)$, and opposite side $\cos(\beta)\cdot\sin(\alpha)$; and the smallest (pink) hypotenuse $\sin(\beta)$, adjacent side (of angle $\alpha$) $\sin(\beta)\cdot \cos(\alpha)$, and opposite side $\sin(\beta)\sin(\alpha)$.
```{julia}
#| echo: false
p2
```
This figure shows the following sum formula for sine and cosine:
We can make two more right triangles one with hypotenuse $\cos(\beta)$ and one with hypotenuse $\sin(\beta)$; each having an angle $\alpha$, the latter using some geometry, for which we can apply right-triangle trigonometry to find the length of their sides.
```{julia}
#| echo: false
plot(p3, p4)
```
From the left figure and the initial triangle, by comparing the lengths along the $x$ direction, we can see the decomposition:
$$
\cos(\alpha)\cos(\beta) = \cos(\alpha + \beta) + \sin(\alpha)\sin(\beta)
$$
Similarly, this relationship comes from considering the vertical lengths:
$$
\sin(\alpha+\beta) = \sin(\alpha)\cos(\beta) + \cos(\alpha)\sin(\beta)
$$
These lead to:
::: {.callout-note icon=false}
## The *sum* formulas for sine and cosine
$$
\begin{align*}
\sin(\alpha + \beta) &= \sin(\alpha)\cos(\beta) + \cos(\alpha)\sin(\beta), & (\overline{CE} + \overline{DF})\\
\cos(\alpha + \beta) &= \cos(\alpha)\cos(\beta) - \sin(\alpha)\sin(\beta). & (\overline{AC} - \overline{DE})
\sin(\alpha+\beta) &= \sin(\alpha)\cos(\beta) + \cos(\alpha)\sin(\beta) \\
\cos(\alpha + \beta) &= \cos(\alpha)\cos(\beta) - \sin(\alpha)\sin(\beta)
\end{align*}
$$
:::
Taking $\alpha = \beta$ we immediately get
Using the fact that $\sin$ is an odd function and $\cos$ an even function, related formulas for the difference $\alpha - \beta$ can be derived.
Taking $\alpha = \beta$ we immediately get the "double-angle" formulas:
::: {.callout-note icon=false}
## The "double-angle" formulas
$$
\begin{align*}
\sin(2\alpha) &= 2\sin(\alpha)\cos(\alpha)\\
\cos(2\alpha) &= \cos(\alpha)^2 - \sin(\alpha)^2.
\cos(2\alpha) &= \cos^2(\alpha) - \sin^2(\alpha).
\end{align*}
$$
:::
The latter looks like the Pythagorean identify, but has a minus sign. In fact, the Pythagorean identify is often used to rewrite this, for example $\cos(2\alpha) = 2\cos^2(\alpha) - 1$ or $1 - 2\sin^2(\alpha)$.
The latter looks like the Pythagorean identify, but has a minus sign. In fact, the Pythagorean identify is often used to rewrite this, for example $\cos(2\alpha) = 2\cos(\alpha)^2 - 1$ or $1 - 2\sin(\alpha)^2$.
Applying the above with $\alpha = \beta/2$, we get that $\cos(\beta) = 2\cos(\beta/2)^2 -1$, which rearranged yields the "half-angle" formula: $\cos(\beta/2)^2 = (1 + \cos(\beta))/2$.
Applying the above with $\alpha = \beta/2$, we get that $\cos(\beta) = 2\cos^2(\beta/2) -1$. Similarly, using the Pythagorean identity a formula for sine can be done; when rearranged these yield the "half-angle" formulas:
::: {.callout-note icon=false}
## The "half-angle" formula
$$
\begin{align*}
\sin^2(\frac{\beta}{2}) &= \frac{1 - \cos(\beta)}{2}\\
\cos^2(\frac{\beta}{2}) &= \frac{1 + \cos(\beta)}{2}
\end{align*}
$$
:::
##### Example
@@ -440,18 +580,19 @@ Applying the above with $\alpha = \beta/2$, we get that $\cos(\beta) = 2\cos(\be
Consider the expressions $\cos((n+1)\theta)$ and $\cos((n-1)\theta)$. These can be re-expressed as:
$$
\begin{align*}
\cos((n+1)\theta) &= \cos(n\theta + \theta) = \cos(n\theta) \cos(\theta) - \sin(n\theta)\sin(\theta), \text{ and}\\
\cos((n-1)\theta) &= \cos(n\theta - \theta) = \cos(n\theta) \cos(-\theta) - \sin(n\theta)\sin(-\theta).
\end{align*}
$$
But $\cos(-\theta) = \cos(\theta)$, whereas $\sin(-\theta) = -\sin(\theta)$. Using this, we add the two formulas above to get:
$$
\cos((n+1)\theta) = 2\cos(n\theta) \cos(\theta) - \cos((n-1)\theta).
\cos((n+1)\theta) = 2 \cos(\theta) \cos(n\theta) - \cos((n-1)\theta).
$$
That is the angle for a multiple of $n+1$ can be expressed in terms of the angle with a multiple of $n$ and $n-1$. This can be used recursively to find expressions for $\cos(n\theta)$ in terms of polynomials in $\cos(\theta)$.
@@ -554,11 +695,11 @@ The approximation error is about $2.7$ percent.
##### Example
The AMS has an interesting column on [rainbows](http://www.ams.org/publicoutreach/feature-column/fcarc-rainbows) the start of which uses some formulas from the previous example. Click through to see a ray of light passing through a spherical drop of water, as analyzed by Descartes. The deflection of the ray occurs when the incident light hits the drop of water, then there is an *internal* deflection of the light, and finally when the light leaves, there is another deflection. The total deflection (in radians) is $D = (i-r) + (\pi - 2r) + (i-r) = \pi + 2i - 4r$. However, the incident angle $i$ and the refracted angle $r$ are related by Snell's law: $\sin(i) = n \sin(r)$. The value $n$ is the index of refraction and is $4/3$ for water. (It was $3/2$ for glass in the previous example.) This gives
The AMS has an interesting column on [rainbows](http://www.ams.org/publicoutreach/feature-column/fcarc-rainbows) the start of which uses some formulas from the previous example. Click through to see a ray of light passing through a spherical drop of water, as analyzed by Descartes. The deflection of the ray occurs when the incident light hits the drop of water, then there is an *internal* deflection of the light, and finally when the light leaves, there is another deflection. The total deflection (in radians) is $d = (i-r) + (\pi - 2r) + (i-r) = \pi + 2i - 4r$. However, the incident angle $i$ and the refracted angle $r$ are related by Snell's law: $\sin(i) = n \sin(r)$. The value $n$ is the index of refraction and is $4/3$ for water. (It was $3/2$ for glass in the previous example.) This gives
$$
D = \pi + 2i - 4 \arcsin(\frac{1}{n} \sin(i)).
d = \pi + 2i - 4 \arcsin(\frac{1}{n} \sin(i)).
$$
Graphing this for incident angles between $0$ and $\pi/2$ we have:
@@ -567,8 +708,8 @@ Graphing this for incident angles between $0$ and $\pi/2$ we have:
```{julia}
#| hold: true
n = 4/3
D(i) = pi + 2i - 4 * asin(sin(i)/n)
plot(D, 0, pi/2)
d(i) = pi + 2i - 4 * asin(sin(i)/n)
plot(d, 0, pi/2)
```
Descartes was interested in the minimum value of this graph, as it relates to where the light concentrates. This is roughly at $1$ radian or about $57$ degrees:
@@ -588,17 +729,17 @@ Consider again this equation derived with the sum-and-difference formula:
$$
\cos((n+1)\theta) = 2\cos(n\theta) \cos(\theta) - \cos((n-1)\theta).
\cos((n+1)\theta) = 2 \cos(\theta) \cos(n\theta) - \cos((n-1)\theta).
$$
Let $T_n(x) = \cos(n \arccos(x))$. Calling $\theta = \arccos(x)$ for $-1 \leq x \leq 1$ we get a relation between these functions:
Let $T_n(x) = \cos(n \arccos(x))$. Note $T_1(x) = \cos(x)$. By identifying $\theta$ with $\arccos(x)$ for $-1 \leq x \leq 1$, we get a relation between these functions:
$$
T_{n+1}(x) = 2x T_n(x) - T_{n-1}(x).
$$
We can simplify a few: For example, when $n=0$ we see immediately that $T_0(x) = 1$, the constant function. Whereas with $n=1$ we get $T_1(x) = \cos(\arccos(x)) = x$. Things get more interesting as we get bigger $n$, for example using the equation above we get $T_2(x) = 2xT_1(x) - T_0(x) = 2x\cdot x - 1 = 2x^2 - 1$. Continuing, we'd get $T_3(x) = 2 x T_2(x) - T_1(x) = 2x(2x^2 - 1) - x = 4x^3 -3x$.
We can simplify a few of the above : For example, when $n=0$ we see immediately that $T_0(x) = 1$, the constant function. We used above that for $n=1$ we get $T_1(x) = \cos(\arccos(x)) = x$. Things get more interesting as we get bigger $n$, for example using the equation above we get $T_2(x) = 2xT_1(x) - T_0(x) = 2x\cdot x - 1 = 2x^2 - 1$. Continuing, we'd get $T_3(x) = 2 x T_2(x) - T_1(x) = 2x(2x^2 - 1) - x = 4x^3 -3x$.
A few things become clear from the above two representations:
@@ -621,7 +762,7 @@ plot!(abs ∘ q, -1,1, label="|q|")
## Hyperbolic trigonometric functions
Related to the trigonometric functions are the hyperbolic trigonometric functions. Instead of associating a point $(x,y)$ on the unit circle with an angle $\theta$, we associate a point $(x,y)$ on the unit *hyperbola* ($x^2 - y^2 = 1$). We define the hyperbolic sine ($\sinh$) and hyperbolic cosine ($\cosh$) through $(\cosh(\theta), \sinh(\theta)) = (x,y)$.
Related to the trigonometric functions are the hyperbolic trigonometric functions. Instead of associating a point $(x,y)$ on the unit circle with an angle $\theta,$ we associate a point $(x,y)$ on the unit *hyperbola* ($x^2 - y^2 = 1$). We define the hyperbolic sine ($\sinh$) and hyperbolic cosine ($\cosh$) through $(\cosh(\theta), \sinh(\theta)) = (x,y)$.
```{julia}
@@ -671,11 +812,12 @@ end
These values are more commonly expressed using the exponential function as:
$$
\begin{align*}
\sinh(x) &= \frac{e^x - e^{-x}}{2}\\
\cosh(x) &= \frac{e^x + e^{-x}}{2}.
\end{align*}
$$
The hyperbolic tangent is then the ratio of $\sinh$ and $\cosh$. As well, three inverse hyperbolic functions can be defined.
@@ -791,7 +933,7 @@ numericq(val)
###### Question
For any positive integer $n$ the equation $\cos(x) - nx = 0$ has a solution in $[0, \pi/2]$. Graphically estimate the value when $n=10$.
For any positive integer $n$ the equation $\cos(x) - nx = 0$ has a solution in $[0, \pi/2].$ Graphically estimate the value when $n=10.$
```{julia}

View File

@@ -28,7 +28,7 @@ nothing
The Google calculator has a button `Ans` to refer to the answer to the previous evaluation. This is a form of memory. The last answer is stored in a specific place in memory for retrieval when `Ans` is used. In some calculators, more advanced memory features are possible. For some, it is possible to push values onto a stack of values for them to be referred to at a later time. This proves useful for complicated expressions, say, as the expression can be broken into smaller intermediate steps to be computed. These values can then be appropriately combined. This strategy is a good one, though the memory buttons can make its implementation a bit cumbersome.
With `Julia`, as with other programming languages, it is very easy to refer to past evaluations. This is done by *assignment* whereby a computed value stored in memory is associated with a name. The name can be used to look up the value later. Assignment does not change the value of the object being assigned, it only introduces a reference to it.
With `Julia`, as with other programming languages, it is very easy to refer to past evaluations. This is done by *assignment* whereby a computed value stored in memory is associated with a name (sometimes thought of as symbol or label). The name can be used to look up the value later. Assignment does not change the value of the object being assigned, it only introduces a reference to it.
Assignment in `Julia` is handled by the equals sign and takes the general form `variable_name = value`. For example, here we assign values to the variables `x` and `y`
@@ -49,7 +49,7 @@ x
Just typing a variable name (without a trailing semicolon) causes the assigned value to be displayed.
Variable names can be reused, as here, where we redefine `x`:
Variable names can be reused (or reassigned), as here, where we redefine `x`:
```{julia}
@@ -115,7 +115,7 @@ By defining a new variable `a` to represent a value that is repeated a few times
A [grass swale](https://stormwater.pca.state.mn.us/index.php?title=Design_criteria_for_dry_swale_(grass_swale)) is a design to manage surface water flow resulting from a storm. Swales detain, filter, and infiltrate runoff limiting erosion in the process.
![Swale cross section](precalc/figures/swale.png)
![Swale cross section](figures/swale.png)
There are a few mathematical formula that describe the characteristics of swale:
@@ -155,7 +155,7 @@ n, S = 0.025, 2/90
A = (b + d/tan(theta)) * d
P = b + 2d/sin(theta)
R = A / P
Q = R^(2/3) * S^(1/2) * A / n
Q = R^(2/3) * S^(1/2) / n * A
```
@@ -198,7 +198,7 @@ This is completely unlike the mathematical equation $x = x^2$ which is typically
##### Example
Having `=` as assignment is usefully exploited when modeling sequences. For example, an application of Newton's method might end up with this expression:
Having `=` as assignment is usefully exploited when modeling sequences. For example, an application of Newton's method might end up with this mathematical expression:
$$
@@ -208,7 +208,7 @@ $$
As a mathematical expression, for each $i$ this defines a new value for $x_{i+1}$ in terms of a known value $x_i$. This can be used to recursively generate a sequence, provided some starting point is known, such as $x_0 = 2$.
The above might be written instead with:
The above might be written instead using assignment with:
```{julia}
@@ -220,11 +220,19 @@ x = x - (x^2 - 2) / (2x)
Repeating this last line will generate new values of `x` based on the previous one - no need for subscripts. This is exactly what the mathematical notation indicates is to be done.
::: {.callout-note}
## Use of =
The distinction between ``=`` versus `=` is important and one area where common math notation and common computer notation diverge. The mathematical ``=`` indicates *equality*, and is often used with equations and also for assignment. Later, when symbolic math is introduced, the `~` symbol will be used to indicate an equation, though this is by convention and not part of base `Julia`. The computer syntax use of `=` is for *assignment* and *re-assignment*. Equality is tested with `==` and `===`.
:::
## Context
The binding of a value to a variable name happens within some context. For our simple illustrations, we are assigning values, as though they were typed at the command line. This stores the binding in the `Main` module. `Julia` looks for variables in this module when it encounters an expression and the value is substituted. Other uses, such as when variables are defined within a function, involve different contexts which may not be visible within the `Main` module.
The binding of a value to a variable name happens within some context. When a variable is assigned or referenced, the scope of the variable -- the region of code where it is accessible -- is taken into consideration.
For our simple illustrations, we are assigning values, as though they were typed at the command line. This stores the binding in the `Main` module. `Julia` looks for variables in this module when it encounters an expression and the value is substituted. Other uses, such as when variables are defined within a function, involve different contexts which may not be visible within the `Main` module.
:::{.callout-note}
@@ -235,14 +243,16 @@ The `varinfo` function will list the variables currently defined in the main wor
:::{.callout-warning}
## Warning
**Shooting oneselves in the foot.** `Julia` allows us to locally redefine variables that are built in, such as the value for `pi` or the function object assigned to `sin`. For example, this is a perfectly valid command `sin=3`. However, it will overwrite the typical value of `sin` so that `sin(3)` will be an error. At the terminal, the binding to `sin` occurs in the `Main` module. This shadows that value of `sin` bound in the `Base` module. Even if redefined in `Main`, the value in base can be used by fully qualifying the name, as in `Base.sin(pi)`. This uses the notation `module_name.variable_name` to look up a binding in a module.
**Shooting oneselves in the foot.** `Julia` allows us to locally redefine variables that are built in, such as the value for `pi` or the function object assigned to `sin`. This is called shadowing. For example, this is a perfectly valid command `sin=3`. However, it will overwrite the typical value of `sin` so that `sin(3)` will be an error. At the terminal, the binding to `sin` occurs in the `Main` module. This shadows that value of `sin` bound in the `Base` module. Even if redefined in `Main`, the value in base can be used by fully qualifying the name, as in `Base.sin(pi)`. This uses the notation `module_name.variable_name` to look up a binding in a module.
:::
## Variable names
`Julia` has a very wide set of possible [names](https://docs.julialang.org/en/stable/manual/variables/#Allowed-Variable-Names-1) for variables. Variables are case sensitive and their names can include many [Unicode](http://en.wikipedia.org/wiki/List_of_Unicode_characters) characters. Names must begin with a letter or an appropriate Unicode value (but not a number). There are some reserved words, such as `try` or `else` which can not be assigned to. However, many built-in names can be locally overwritten. Conventionally, variable names are lower case. For compound names, it is not unusual to see them squished together, joined with underscores, or written in camelCase.
`Julia` has a very wide set of possible [names](https://docs.julialang.org/en/stable/manual/variables/#Allowed-Variable-Names-1) for variables. Variables are case sensitive and their names can include many [Unicode](http://en.wikipedia.org/wiki/List_of_Unicode_characters) characters. Names must begin with a letter or an appropriate Unicode value (but not a number). There are some reserved words, such as `try` or `else` which can not be assigned to. However, many built-in names can be locally overwritten (shadowed).
Conventionally, variable names are lower case. For compound names, it is not unusual to see them squished together, joined with underscores, or written in camelCase.
```{julia}
@@ -272,18 +282,20 @@ For example, we could have defined `theta` (`\theta[tab]`) and `v0` (`v\_0[tab]`
θ = 45; v₀ = 200
```
:::{.callout-note}
## Unicode
These notes can be presented as HTML files *or* as `Pluto` notebooks. They often use Unicode alternatives to avoid the `Pluto` requirement of a single use of assigning to a variable name in a notebook without placing the assignment in a `let` block or a function body.
:::
:::{.callout-note}
## Emojis
There is even support for tab-completion of [emojis](https://github.com/JuliaLang/julia/blob/master/stdlib/REPL/src/emoji_symbols.jl) such as `\:snowman:[tab]` or `\:koala:[tab]`
:::
:::{.callout-note}
## Unicode
These notes often use Unicode alternatives for some variable. Originally this was to avoid a requirement of `Pluto` of a single use of assigning to a variable name in a notebook without placing the assignment in a `let` block or a function body. Now, they are just for clarity through distinction.
:::
##### Example
@@ -322,7 +334,7 @@ a, b = 1, 2
a, b = b, a
```
#### Example, finding the slope
### Example, finding the slope
Find the slope of the line connecting the points $(1,2)$ and $(4,6)$. We begin by defining the values and then applying the slope formula:
@@ -337,6 +349,7 @@ m = (y1 - y0) / (x1 - x0)
Of course, this could be computed directly with `(6-2) / (4-1)`, but by using familiar names for the values we can be certain we apply the formula properly.
## Questions

View File

@@ -87,12 +87,13 @@ For the motion in the above figure, the object's $x$ and $y$ values change accor
It is common to work with *both* formulas at once. Mathematically, when graphing, we naturally pair off two values using Cartesian coordinates (e.g., $(x,y)$). Another means of combining related values is to use a *vector*. The notation for a vector varies, but to distinguish them from a point we will use $\langle x,~ y\rangle$. With this notation, we can use it to represent the position, the velocity, and the acceleration at time $t$ through:
$$
\begin{align*}
\vec{x} &= \langle x_0 + v_{0x}t,~ -(1/2) g t^2 + v_{0y}t + y_0 \rangle,\\
\vec{v} &= \langle v_{0x},~ -gt + v_{0y} \rangle, \text{ and }\\
\vec{a} &= \langle 0,~ -g \rangle.
\end{align*}
$$
@@ -358,7 +359,13 @@ fibs = [1, 1, 2, 3, 5, 8, 13]
Later we will discuss different ways to modify the values of a vector to create new ones, similar to how scalar multiplication does.
As mentioned, vectors in `Julia` are comprised of elements of a similar type, but the type is not limited to numeric values. For example, a vector of strings might be useful for text processing, a vector of Boolean values can naturally arise, some applications are even naturally represented in terms of vectors of vectors (such as happens when plotting a collection points). Look at the output of these two vectors:
As mentioned, vectors in `Julia` are comprised of elements of a similar type, but the type is not limited to numeric values. Some examples:,
* a vector of strings might be useful for text processing, For example, the `WordTokenizers.jl` package takes text and produces tokens from the words.
* a vector of Boolean values can naturally arise and is widely used within Julia's `DataFrames.jl` package.
* some applications are even naturally represented in terms of vectors of vectors (such as happens when plotting a collection points).
Look at the output of these two vectors, in particular how the underlying type of the components is described on printing.
```{julia}
@@ -369,6 +376,8 @@ As mentioned, vectors in `Julia` are comprised of elements of a similar type,
[true, false, true] # vector of Bool values
```
Finally, we mention that if `Julia` has values of different types it will promote them to a common type if possible. Here we combine three types of numbers, and see that each is promoted to `Float64`:
@@ -441,7 +450,7 @@ vs[2] = 10
vs
```
The assignment `vs[2]` is different than the initial assignment `vs=[1,2]` in that, `vs[2]=10` **modifies** the container that `vs` points to, whereas `v=[1,2]` **replaces** the binding for `vs`. The indexed assignment is then more memory efficient when vectors are large. This point is also of interest when passing vectors to functions, as a function may modify components of the vector passed to it, though can't replace the container itself.
The assignment `vs[2]` is different than the initial assignment `vs=[1,2]` in that, `vs[2]=10` **modifies** the container that `vs` points to, whereas `vs=[1,2]` **replaces** any binding for `vs`. The indexed assignment is more memory efficient when vectors are large. This point is also of interest when passing vectors to functions, as a function may modify components of the vector passed to it, though can't replace the container itself.
## Some useful functions for working with vectors.
@@ -520,7 +529,7 @@ append!(v1, [6,8,7])
These two functions modify or mutate the values stored within the vector `𝒗` that passed as an argument. In the `push!` example above, the value `5` is added to the vector of $4$ elements. In `Julia`, a convention is to name mutating functions with a trailing exclamation mark. (Again, these do not mutate the binding of `𝒗` to the container, but do mutate the contents of the container.) There are functions with mutating and non-mutating definitions, an example is `sort` and `sort!`.
If only a mutating function is available, like `push!`, and this is not desired a copy of the vector can be made. It is not enough to copy by assignment, as with `w = 𝒗`. As both `w` and `𝒗` will be bound to the same memory location. Rather, you call `copy` to make a new container with copied contents, as in `w = copy(𝒗)`.
If only a mutating function is available, like `push!`, and this is not desired a copy of the vector can be made. It is not enough to copy by assignment, as with `w = 𝒗`. As both `w` and `𝒗` will be bound to the same memory location. Rather, you call `copy` (or sometimes `deepcopy`) to make a new container with copied contents, as in `w = copy(𝒗)`.
Creating new vectors of a given size is common for programming, though not much use will be made here. There are many different functions to do so: `ones` to make a vector of ones, `zeros` to make a vector of zeros, `trues` and `falses` to make Boolean vectors of a given size, and `similar` to make a similar-sized vector (with no particular values assigned).
@@ -610,7 +619,7 @@ This shows many of the manipulations that can be made with vectors. Rather than
:::{.callout-note}
## Note
The `map` function is very much related to broadcasting and similarly named functions are found in many different programming languages. (The "dot" broadcast is mostly limited to `Julia` and mirrors on a similar usage of a dot in `MATLAB`.) For those familiar with other programming languages, using `map` may seem more natural. Its syntax is `map(f, xs)`.
The `map` function is very much related to broadcasting and similarly named functions are found in many different programming languages. (The "dot" broadcast is mostly limited to `Julia` and mirrors a similar usage of a dot in `MATLAB`.) For those familiar with other programming languages, using `map` may seem more natural. Its syntax is `map(f, xs)`.
:::
@@ -631,18 +640,18 @@ Comprehension notation is similar. The above could be created in `Julia` with:
```{julia}
𝒙s = [1,2,3,4,5]
[x^3 for x in 𝒙s]
xs = [1,2,3,4,5]
[x^3 for x in xs]
```
Something similar can be done more succinctly:
```{julia}
𝒙s .^ 3
xs .^ 3
```
However, comprehensions have a value when more complicated expressions are desired as they work with an expression of `𝒙s`, and not a pre-defined or user-defined function.
However, comprehensions have a value when more complicated expressions are desired as they work with an expression of `xs`, and not a pre-defined or user-defined function.
Another typical example of set notation might include a condition, such as, the numbers divisible by $7$ between $1$ and $100$. Set notation might be:
@@ -680,21 +689,21 @@ The first task is to create the data. We will soon see more convenient ways to g
```{julia}
a, b, n = -1, 1, 7
d = (b-a) // (n-1)
𝐱s = [a, a+d, a+2d, a+3d, a+4d, a+5d, a+6d] # 7 points
xs = [a, a+d, a+2d, a+3d, a+4d, a+5d, a+6d] # 7 points
```
To get the corresponding $y$ values, we can use a compression (or define a function and broadcast):
```{julia}
𝐲s = [x^2 for x in 𝐱s]
ys = [x^2 for x in xs]
```
Vectors can be compared together by combining them into a separate container, as follows:
```{julia}
[𝐱s 𝐲s]
[xs ys]
```
(If there is a space between objects they are horizontally combined. In our construction of vectors using `[]` we used a comma for vertical combination. More generally we should use a `;` for vertical concatenation.)
@@ -711,8 +720,41 @@ The style generally employed here is to use plural variable names for a collecti
## Other container types
We end this section with some general comments that are for those interested in a bit more, but in general aren't needed to understand most all of what follows later.
Vectors in `Julia` are a container, one of many different types. Another useful type for programming purposes are *tuples*. If a vector is formed by placing comma-separated values within a `[]` pair (e.g., `[1,2,3]`), a tuple is formed by placing comma-separated values within a `()` pair. A tuple of length $1$ uses a convention of a trailing comma to distinguish it from a parenthesized expression (e.g. `(1,)` is a tuple, `(1)` is just the value `1`).
Vectors in `Julia` are a container for values. Vectors are
one of many different types of containers. The `Julia` manual uses the word "collection" to refer to a container of values that has properties like a vector.
Here we briefly review some alternate container types that are common in Julia and find use in these notes.
First, here are some of the properties of a *vector*:
* Vectors are *homogeneous*. That is, the container holding the vectors all have a common type. This type might be an abstract type, but for high performance, concrete types (like 64-bit floating point or 64-bit integers) are more typical.
* Vectors are $1$-dimensional.
* Vectors are ordered and *indexable* by their order. In Julia, the default indexing for vectors is $1$-based (starting) with one, with numeric access to the first, second, third, ..., last entries.
* Vectors are *mutable*. That is, their elements may be changed; the container may be grown or shrunk
* Vectors are *iterable*. That is, their values can be accessed one-by-one in various manners.
These properties may not all be desirable for one reason or the other and `Julia` has developed a large number of alternative container types of which we describe a few here.
### Arrays
Vectors are $1$-dimensional, but there are desires for other dimensions. Vectors are a implemented as a special case of a more general array type. Arrays are of dimension $N$ for various non-negative values of $N$. A common, and somewhat familiar, mathematical use of a $2$-dimensional array is a matrix.
Arrays can have their entries accessed by dimension and within that dimension their components. By default these are $1$-based, but other offsets are possible through the `OffetArrays.jl` package. A matrix can refer to its values either by row and column indices or, as a matrix has linear indexing by a single index.
For large collections of data with many entries being $0$ a sparse array is beneficial for less memory intensive storage. These are implemented in the `SparseArrays.jl` package.
There are numerous array types available. `Julia` has a number of *generic* methods for working with different arrays. An example would be `eachindex`, which provides an iterator interface to the underlying array access by index in an efficient manner.
### Tuples
Tuples are fixed-length containers where there is no expectation or enforcement of their having a common type. Tuples just combine values together in an *immutable* container. Like vectors they can be accessed by index (also $1$-based). Unlike vectors, the containers are *immutable* - elements can not be changed and the length of the container may not change. This has benefits for performance purposes. (For fixed length, mutable containers that have the benefits of tuples and vectors, the `StaticArrays.jl` package is available).
While a vector is formed by placing comma-separated values within a `[]` pair (e.g., `[1,2,3]`), a tuple is formed by placing comma-separated values within a `()` pair. A tuple of length $1$ uses a convention of a trailing comma to distinguish it from a parenthesized expression (e.g. `(1,)` is a tuple, `(1)` is just the value `1`).
:::{.callout-note}
## Well, actually...
@@ -721,10 +763,7 @@ Technically, the tuple is formed just by the use of commas, which separate diffe
:::
Tuples are used in programming, as they don't typically require allocated memory to be used so they can be faster. Internal usages are for function arguments and function return types. Unlike vectors, tuples can be heterogeneous collections. (When commas are used to combine more than one output into a cell, a tuple is being used.) (Also, a big technical distinction is that tuples are also different from vectors and other containers in that tuple types are *covariant* in their parameters, not *invariant*.)
Unlike vectors, tuples can have names which can be used for referencing a value, similar to indexing but possibly more convenient. Named tuples are similar to *dictionaries* which are used to associate a key (like a name) with a value.
There are *named tuples* where each component has an associated name. Like a tuple these can be indexed by number and unlike regular tuples also by name.
For example, here a named tuple is constructed, and then its elements referenced:
@@ -732,9 +771,76 @@ For example, here a named tuple is constructed, and then its elements referenced
```{julia}
nt = (one=1, two="two", three=:three) # heterogeneous values (Int, String, Symbol)
nt.one, nt[2], nt[end] # named tuples have name or index access
nt.one, nt[2], nt[end] # named tuples have name or index access
```
::: {.callout-note}
## Named tuple and destructuring
A *named* tuple is a container that allows access by index *or* by name. They are easily constructed. For example:
```{julia}
nt = (x0 = 1, x1 = 4, y0 = 2, y1 = 6)
```
The values in a named tuple can be accessed using the "dot" notation:
```{julia}
nt.x1
```
Alternatively, the index notation -- using a *symbol* for the name -- can be used:
```{julia}
nt[:x1]
```
Named tuples are employed to pass parameters to functions. To find the slope, we could do:
```{julia}
(nt.y1 - nt.y0) / (nt.x1 - nt.x0)
```
However, more commonly used is destructuring, where named variables are extracted by name when the left hand side matches the right hand side:
```{julia}
(;x0, x1) = nt # only extract what is desired
x1 - x0
```
(This works for named tuples and other iterable containers in `Julia`. It also works the other way, if `x0` and `x1` are defined then `(;x0, x1)` creates a named tuple with those values.)
:::
### Associative arrays
Named tuples associate a name (in this case a symbol) to a value. More generally an associative array associates to each key a value, where the keys and values may be of different types.
The `pair` notation, `key => value`, is used to make one association. A *dictionary* is used to have a container of associations. For example, this constructs a simple dictionary associating a spelled out name with a numeric value:
```{julia}
d = Dict("one" => 1, "two" => 2, "three" => 3)
```
The print out shows the keys are of type `String`, the values of type `Int64`, in this case. There are a number of different means to construct dictionaries.
The values in a dictionary can be accessed by name:
```{julia}
d["two"]
```
Named tuples are associative arrays where the keys are restricted to symbols. There are other types of associative arrays, specialized cases of the `AbstractDict` type with performance benefits for specific use cases. In these notes, dictionaries appear as output in some function calls.
Unlike vectors and tuples, named tuples and dictionaries are not currently supported by broadcasting. This causes no loss in usefulness, as the values can easily be iterated over, but the convenience of the dot notation is lost.
## Questions
@@ -881,6 +987,7 @@ From [transum.org](http://www.transum.org/Maths/Exam/Online_Exercise.asp?Topic=V
```{julia}
#| hold: true
#| echo: false
let
p = plot(xlim=(0,10), ylim=(0,5), legend=false, framestyle=:none)
for j in (-3):10
plot!(p, [j, j + 5], [0, 5*sqrt(3)], color=:blue, alpha=0.5)
@@ -906,7 +1013,8 @@ annotate!(p, [(2, 3/2*sqrt(3) -delta, L"a"),
])
p
p
end
```
The figure shows $5$ vectors.