work on better figures

This commit is contained in:
jverzani
2025-07-02 06:25:10 -04:00
parent 50cc2b2193
commit 5013211954
12 changed files with 1098 additions and 61 deletions

View File

@@ -2,6 +2,28 @@
#| output: false
#| echo: false
# Some style choices for `Plots.jl`
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35))
range_style = (; fill=(:blue, 0.35))
nothing
```
```{julia}
#| output: false
#| echo: false
## Formatting options are included here; not in CalculusWithJulia.WeaveSupport
using QuizQuestions
nothing

View File

@@ -158,7 +158,7 @@ format:
execute:
error: false
freeze: false
# freeze: auto
# freeze: false
freeze: auto
# cache: false
# enabled: true

View File

@@ -7,16 +7,19 @@
#alternatives/plotly_plotting.html
function _add_plotly(f)
lineno = 117
#lineno = 117
str = """
<script src="https://cdn.plot.ly/plotly-2.11.0.min.js"></script>
"""
r = readlines(f)
inserted = false
open(f, "w") do io
for (i,l) enumerate(r)
i == lineno && println(io, str)
if contains(l, "require.min.js")
!inserted && println(io, """
<script src="https://cdn.plot.ly/plotly-2.6.3.min.js"></script>
""")
inserted = true
end
println(io, l)
end
end
@@ -29,6 +32,7 @@ function (@main)(args...)
for fᵢ files
f = joinpath(root, fᵢ)
if endswith(f, ".html")
dirname(f) == "_book" && continue
_add_plotly(f)
end
end

View File

@@ -59,7 +59,7 @@ in a spirit similar to a section of a book. Just like a book, there
are try-it-yourself questions at the end of each page. All have a
limited number of self-graded answers. These notes borrow ideas from
many sources, for example @Strang, @Knill, @Schey, @Thomas,
@RogawskiAdams, several Wikipedia pages, and other sources.
@RogawskiAdams, @Angenent, several Wikipedia pages, and other sources.
These notes are accompanied by a `Julia` package `CalculusWithJulia`
that provides some simple functions to streamline some common tasks

View File

@@ -64,7 +64,88 @@ $$
#### Examples
Find the area between
$$
\begin{align*}
f(x) &= \frac{x^3 \cdot (2-x)}{2} \text{ and } \\
g(x) &= e^{x/3} + (1-\frac{x}{1.7})^6 - 0.6
\end{align*}
$$
over the interval $[0.2, 1.7]$. The area is illustrated in the figure below.
```{julia}
f(x) = x^3*(2-x)/2
g(x) = exp(x/3) + (1 - (x/1.7))^6 - 0.6
a, b = 0.2, 1.7
h(x) = g(x) - f(x)
answer, _ = quadgk(h, a, b)
answer
```
::: {#fig-area-between-f-g}
```{julia}
#| echo: false
p = let
gr()
# area between graphs
# https://github.com/SigurdAngenent/WisconsinCalculus/blob/master/figures/221/09areabetweengraphs.pdf
f(x) = 1/6+x^3*(2-x)/2
g(x) = 1/6+exp(x/3)+(1-x/1.7)^6-0.6
a,b =0.2, 1.7
A, B = 0, 2
A, B = A + .1, B - .1
n = 20
plot(; empty_style..., aspect_ratio=:equal, xlims=(A,B))
plot!(f, A, B; fn_style...)
plot!(g, A, B; fn_style...)
xp = range(a, b, n)
marked = n ÷ 2
for i in 1:n-1
x0, x1 = xp[i], xp[i+1]
mpt = (x0 + x1)/2
R = Shape([x0,x1,x1,x0], [f(mpt),f(mpt),g(mpt),g(mpt)])
color = i == marked ? :gray : :white
plot!(R; fill=(color, 0.5), line=(:black, 1))
end
# axis
plot!([(A,0),(B,0)]; axis_style...)
# hightlight
x0, x1 = xp[marked], xp[marked+1]
_style = (;line=(:gray, 1, :dash))
plot!([(a,0), (a, f(a))]; _style...)
plot!([(b,0), (b,f(b))]; _style...)
plot!([(x0,0), (x0, f(x0))]; _style...)
plot!([(x1,0), (x1, f(x1))]; _style...)
annotate!([
(B, f(B), text(L"f(x)", 10, :left,:top)),
(B, g(B), text(L"g(x)", 10, :left, :bottom)),
(a, 0, text(L"a=x_0", 10, :top, :left)),
(b, 0, text(L"b=x_n", 10, :top, :left)),
(x0, 0, text(L"x_i", 10, :top)),
(x1, 0, text(L"x_{i+1}", 10, :top,:left))
])
current()
end
plotly()
p
```
Illustration of a Riemann sum approximation to estimate the area between $f(x)$ and $g(x)$ over an interval $[a,b]$. (Figure follows one by @Angenent.)
:::
##### Example
Find the area bounded by the line $y=2x$ and the curve $y=2 - x^2$.
@@ -970,4 +1051,3 @@ choices = ["The two enclosed areas should be equal",
"The two enclosed areas are clearly different, as they do not overap"]
radioq(choices, 1)
```

View File

@@ -20,6 +20,7 @@ using SymPy
#| echo: false
#| results: "hidden"
import LinearAlgebra: norm
using SplitApplyCombine
nothing
```
@@ -122,6 +123,145 @@ The formula is for a rotation around the $x$-axis, but can easily be generalized
:::
::: {#fig-solid-of-revolution}
```{julia}
#| echo: false
plt = let
gr()
# Follow lead of # https://github.com/SigurdAngenent/WisconsinCalculus/blob/master/figures/221/09surf_of_rotation2.py
# plot surface of revolution around x axis between [0, 3]
# best if r(t) descreases
rad(x) = 2/(1 + exp(x))#2/(2.0+x)
viewp = [2,-2,1]
##
unitize(x) = x / norm(x)
"""Orthogonal projection along the vector viewp"""
function make_Pmat(viewp)
a = unitize( [-viewp[2], viewp[1], 0] )
b = unitize( [-viewp[3]*viewp[1],
-viewp[3]*viewp[2],
viewp[1]^2 + viewp[2]^2] )
collect(zip(a,b))
end
#linear projection of R^3 onto R^2
function proj(X, viewp)
Pmat = make_Pmat(viewp)
x=sum([Pmat[i][1]*X[i] for i in 1:3])
y=sum([Pmat[i][2]*X[i] for i in 1:3])
(x, y) # a point
end
proj(X) = proj(X, viewp)
# discrete determinant of Jacobian; area multiplier?
function jac(X, u, v)
ϵ = 0.000001
A = map((p,q) -> (p-q)/ϵ, X(u+ϵ/2, v), X(u-ϵ/2, v))
B = map((p,q) -> (p-q)/ϵ, X(u, v+ϵ/2), X(u, v-ϵ/2))
return A[1]*B[2]-A[2]*B[1]
end
# surface of revolution
surf(t, z) = [t, rad(t)*cos(z), rad(t)*sin(z)]
# project the surface at (t, a=theta)
psurf(t,z) = proj(surf(t,z))
bisect(f, a, b) = find_zero(f, (a,b), Bisection())
_fold(t, zmin, zmax) = bisect(z -> jac(psurf, t, z), zmin, zmax)
# create shape holding project disc
drawdiscF(t) = Shape(invert([psurf(t, 2*i*pi/100) for i in 1:101])...)
# project a line between two points
pline!(p,q; kwargs...) = plot!([proj(p),proj(q)];
line_style..., kwargs...)
α = 1.0
line_style = (; line=(:black, 1))
plot(; empty_style..., aspect_ratio=:equal)
# by layering, we get x-axis as desired
pline!([-1,0,0], [0,0,0])
plot!(drawdiscF(0); fill =(:lightgray, α))
pline!([0,0,0], [1,0,0])
plot!(drawdiscF(1); fill =(:black, α)) # black to lightgray gives thickness
plot!(drawdiscF(1.1); fill=(:lightgray, α))
pline!([1.1,0,0], [2,0,0])
plot!(drawdiscF(2); fill=(:lightgray, α))
pline!([2,0,0], [3,0,0])
plot!(drawdiscF(3); fill=(:lightgray, α))
pline!([3,0,0], [4,0,0]; arrow=true, side=:head)
pline!([0,0,0], [0,0,1.25]; arrow=true, side=:head)
tt = range(0, pi, 30)
curve = [psurf(t, pi/2) for t in tt]
plot!(curve; line=(:black, 2))
f1 = [[t, _fold(t, 0, pi)] for t in tt]
curve = [psurf(f[1], f[2]) for f in f1]
plot!(curve; line=(:black,))
f2 = [[t, _fold(t, pi, 2*pi)] for t in tt]
curve = [psurf(f[1], f[2]) for f in f2]
plot!(curve; line=(:black,))
tt= [0.025*i for i in 1:121]
f1 = [[t, _fold(t, pi, 2*pi)] for t in tt]
for f in f1
plot!([psurf(f[1], f[2]-k*0.01*(4-f[1])) for k in 1:21];
line=(:black,1))
end
tt= [0.05*i for i in 1:61]
f1 = [[t, _fold(t, pi, 2*pi)] for t in tt]
for f in f1
plot!([psurf( f[1], f[2]-k*0.01*(6-f[1]) )
for k in 1:21]; line=(:black, 1))
end
#=
ts = 0:.1:2.95
θs = range(pi + pi/4, 3pi/2, 25)
for ti in ts
plot!([psurf(ti,θ) for θ in θs]; line=(:black, 1))
end
θs = range(pi + pi/6, 3pi/2, 25)
for ti in ts
plot!([psurf(ti +0.05,θ) for θ in θs]; line=(:black, 1))
end
=#
current()
#ts = range(0, 3, 100)
#θs = range(0, 2pi, 100)
#contour(ts, θs, (t,z) -> jac(psurf,t,z), levels=[0])
end
plt
```
```{julia}
#| echo: false
plotly()
nothing
```
Illustration of a figure being rotated around the $x$-axis. The discs have approximate volume given by the area of the base times the height or $\pi r(x)^2 \Delta x$. (Figure ported from @Angenent.)
:::
For a numeric example, we consider the original Red [Solo](http://en.wikipedia.org/wiki/Red_Solo_Cup) Cup. The dimensions of the cup were basically: a top diameter of $d_1 = 3~ \frac{3}{4}$ inches, a bottom diameter of $d_0 = 2~ \frac{1}{2}$ inches and a height of $h = 4~ \frac{3}{4}$ inches.
@@ -352,6 +492,136 @@ $$
V = \int_a^b \pi \cdot (R(x)^2 - r(x)^2) dx.
$$
::: {#fig-washer-illustration}
```{julia}
#| echo: false
plt = let
gr()
# Follow lead of # https://github.com/SigurdAngenent/WisconsinCalculus/blob/master/figures/221/09surf_of_rotation2.py
# plot surface of revolution around x axis between [0, 3]
# best if r(t) descreases
rad(x) = 2/(1 + exp(x))#2/(2.0+x)
viewp = [2,-2,1]
##
unitize(x) = x / norm(x)
"""Orthogonal projection along the vector viewp"""
function make_Pmat(viewp)
a = unitize( [-viewp[2], viewp[1], 0] )
b = unitize( [-viewp[3]*viewp[1],
-viewp[3]*viewp[2],
viewp[1]^2 + viewp[2]^2] )
collect(zip(a,b))
end
#linear projection of R^3 onto R^2
function proj(X, viewp)
Pmat = make_Pmat(viewp)
x=sum([Pmat[i][1]*X[i] for i in 1:3])
y=sum([Pmat[i][2]*X[i] for i in 1:3])
(x, y) # a point
end
proj(X) = proj(X, viewp)
# discrete determinant of Jacobian; area multiplier?
function jac(X, u, v)
ϵ = 0.000001
A = map((p,q) -> (p-q)/ϵ, X(u+ϵ/2, v), X(u-ϵ/2, v))
B = map((p,q) -> (p-q)/ϵ, X(u, v+ϵ/2), X(u, v-ϵ/2))
return A[1]*B[2]-A[2]*B[1]
end
# surface of revolution
surf(t, z) = [t, rad(t)*cos(z), rad(t)*sin(z)]
surf2(t, z) = (t, rad(t)*cos(z)/2, rad(t)*sin(z)/2)
# project the surface at (t, a=theta)
psurf(t,z) = proj(surf(t,z))
psurf2(t, z) = proj(surf2(t,z))
bisect(f, a, b) = find_zero(f, (a,b), Bisection())
_fold(t, zmin, zmax) = bisect(z -> jac(psurf, t, z), zmin, zmax)
# create shape holding project disc
drawdiscF(t) = Shape(invert([psurf(t, 2*i*pi/100) for i in 1:101])...)
drawdiscI(t) = Shape([psurf2(t, 2*i*pi/100) for i in 1:101])
# project a line between two points
pline!(p,q; kwargs...) = plot!([proj(p),proj(q)];
line_style..., kwargs...)
α = 1.0
line_style = (; line=(:black, 1))
plot(; empty_style..., aspect_ratio=:equal)
# by layering, we get x-axis as desired
pline!([-1,0,0], [0,0,0])
plot!(drawdiscF(0); fill =(:lightgray, α))
plot!(drawdiscI(0); fill=(:white, .5))
pline!([0,0,0], [1,0,0])
plot!(drawdiscF(1); fill =(:black, α)) # black to lightgray gives thickness
plot!(drawdiscI(1); fill=(:white, .5))
plot!(drawdiscF(1.1); fill=(:lightgray, α))
plot!(drawdiscI(1.1); fill=(:white, .5))
pline!([1.1,0,0], [2,0,0])
plot!(drawdiscF(2); fill=(:lightgray, α))
plot!(drawdiscI(2); fill=(:white, .5))
pline!([2,0,0], [3,0,0])
plot!(drawdiscF(3); fill=(:lightgray, α))
plot!(drawdiscI(3); fill=(:white, .5))
pline!([3,0,0], [4,0,0]; arrow=true, side=:head)
pline!([0,0,0], [0,0,1.25]; arrow=true, side=:head)
tt = range(0, pi, 30)
curve = [psurf(t, pi/2) for t in tt]
plot!(curve; line=(:black, 2))
f1 = [[t, _fold(t, 0, pi)] for t in tt]
curve = [psurf(f[1], f[2]) for f in f1]
plot!(curve; line=(:black,))
f2 = [[t, _fold(t, pi, 2*pi)] for t in tt]
curve = [psurf(f[1], f[2]) for f in f2]
plot!(curve; line=(:black,))
tt= [0.025*i for i in 1:121]
f1 = [[t, _fold(t, pi, 2*pi)] for t in tt]
for f in f1
plot!([psurf(f[1], f[2]-k*0.01*(4-f[1])) for k in 1:21];
line=(:black,1))
end
tt= [0.05*i for i in 1:61]
f1 = [[t, _fold(t, pi, 2*pi)] for t in tt]
for f in f1
plot!([psurf( f[1], f[2]-k*0.01*(6-f[1]) )
for k in 1:21]; line=(:black, 1))
end
current()
end
plt
```
```{julia}
#| echo: false
plotly()
nothing
```
Modification of earlier figure to show washer method. The interior volumn would be given by $\int_a^b \pi r(x)^2 dx$, the entire volume by $\int_a^b \pi R(x)^2 dx$. The difference then is the volume computed by the washer method.
:::
##### Example
@@ -412,19 +682,21 @@ Let $h$ be the distance from the apex to the base. Consider cones with the prope
```{julia}
#| hold: true
#| echo: false
h = 5
R, r, rho = 1, 1/4, 1/4
f(t) = (R-r) * cos(t) + rho * cos((R-r)/r * t)
g(t) = (R-r) * sin(t) - rho * sin((R-r)/r * t)
ts = range(0, 2pi, length=100)
plt = let
h = 5
R, r, rho = 1, 1/4, 1/4
f(t) = (R-r) * cos(t) + rho * cos((R-r)/r * t)
g(t) = (R-r) * sin(t) - rho * sin((R-r)/r * t)
ts = range(0, 2pi, length=100)
p = plot(f.(ts), g.(ts), zero.(ts), legend=false)
for t ∈ range(0, 2pi, length=25)
plot!(p, [0,f(t)], [0,g(t)], [h, 0], linecolor=:red)
plot(f.(ts), g.(ts), zero.(ts), legend=false)
for t ∈ range(0, 2pi, length=25)
plot!([0,f(t)], [0,g(t)], [h, 0], linecolor=:red)
end
current()
end
p
plot
```
A right circular cone is one where this shape is a circle. This definition can be more general, as a square-based right pyramid is also such a cone. After possibly reorienting the cone in space so the base is at $u=0$ and the apex at $u=h$ the volume of the cone can be found from:
@@ -450,6 +722,72 @@ $$
This gives a general formula for the volume of such cones.
::: {#fig-cross-sections}
```{julia}
#| echo: false
plt = let
gr()
# sections
# https://github.com/SigurdAngenent/WisconsinCalculus/blob/master/figures/221/09Xsections.py
x(h,z) = 0.3*h^2+(0.6-0.2*h)*cos(z)
y(h,z) = h+(0.3-0.2*h)*sin(z)+0.05*sin(4*z)
r(h,z) = (x(h,z), y(h,z))
r1(h,z) = (2,0) .+ r(h,z)
Nh=30
heights = range(-1/2, 1/2, Nh)
h0=heights[Nh ÷ 2]
h1=heights[Nh ÷ 2 + 1]
hs = [heights[1], h0, h1, heights[end]]
ts = range(0, 2pi, 300)
plot(; empty_style..., aspect_ratio=:equal)
# stack the curves
for h in heights
curve = r.(h, ts)
plot!(Shape(curve); fill=(:white, 1.0), line=(:black, 1))
end
# shape pull outs; use black to give thickness
for (h, color) in zip(hs, (:white, :black, :white, :white))
curve = r1.(h,ts)
plot!(Shape(curve); fill=(color,1.0), line=(:black, 1,))
end
# axis with marked points
plot!([(-1,-1), (-1, 1)]; axis_style...)
pts = [(-1, y(h, pi)) for h in hs]
scatter!(pts, marker=(5, :circle))
# connect with dashes
for h in hs
plot!([(-1, y(h, pi)), r(h,pi)]; line=(:black, 1, :dash))
plot!([r(h,0), r1(h,pi)]; line=(:black, 1, :dash))
end
current()
end
plt
```
This figure shows the volume of a figure being comprised of slices. A discrete approximation would be found by estimating the volume of each slice by the cross sectional area times a small $\Delta h$.
(This figure was ported from @Angenent.)
:::
```{julia}
#| echo: false
plotly()
nothing
```
### Cavalieri's method
@@ -457,6 +795,47 @@ This gives a general formula for the volume of such cones.
[Cavalieri's](http://tinyurl.com/oda9xd9) Principle is "Suppose two regions in three-space (solids) are included between two parallel planes. If every plane parallel to these two planes intersects both regions in cross-sections of equal area, then the two regions have equal volumes." (Wikipedia).
::: {#fig-Cavalieris-first}
```{julia}
#| echo: false
plt = let
gr()
x(h,z) = (0.6-0.2*h) * cos(z)
y(h,z) = h + (0.2-0.15*h) * sin(z) + 0.01 * sin(4*z)
xa(h,z) = 2 + 0.1 * cos(7*pi*h) + (0.6-0.2*h)*cos(z)
heights = range(-1/2, 1/2, 50)
ts = range(0, 2pi, 300)
h0 = heights[25]
h1 = heights[26]
plot(; empty_style..., aspect_ratio=:equal)
for h in heights
curve=[(x(h, t), y(h, t)) for t in ts]
plot!(Shape(curve); fill=(:white,), line=(:black,1))
curve=[(xa(h, t), y(h, t)) for t in ts]
plot!(Shape(curve); fill=(:white,), line=(:black,1))
end
current()
end
plt
```
```{julia}
#| echo: false
plotly()
nothing
```
Illustration of Cavalieri's first principle. The discs from the left are moved around to form the left volume, but as the volumes of each cross-sectional disc remains the same, the two valumes are equally approximated. (This figure ported from @Angenent.)
:::
With the formula for the volume of solids based on cross sections, this is a trivial observation, as the functions giving the cross-sectional area are identical. Still, it can be surprising. Consider a sphere with an interior cylinder bored out of it. (The [Napkin](http://tinyurl.com/o237v83) ring problem.) The bore has height $h$ - for larger radius spheres this means very wide bores.

View File

@@ -32,12 +32,69 @@ If $f$ is continuous on $[a,b]$ with, say, $f(a) < f(b)$, then for any $y$ with
:::
::: {#fig-IVT}
```{julia}
#| hold: true
#| echo: false
#| cache: true
### {{{IVT}}}
gr()
plt = let
gr()
# IVT
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35), line=nothing)
range_style = (; fill=(:blue, 0.35), line=nothing)
f(x) = x + sinpi(3x) + 5sin(2x) + 3cospi(2x)
a, b = -1, 5
xs = range(a, b, 251)
ys = f.(xs)
y0, y1 = extrema(ys)
plot(; empty_style...)
plot!(f, a, b; fn_style...)
plot!([a-.2, b + .2],[0,0]; axis_style...)
plot!([a-.1, a-.1], [y0-2, y1+2]; axis_style...)
plot!([(a,0),(a,f(a))]; line=(:black, 1, :dash))
plot!([(b,0),(b,f(b))]; line=(:black, 1, :dash))
m = f(a/2 + b/2)
plot!([a, b], [m,m]; line=(:black, 1, :dashdot))
δx = 0.03
plot!(Shape([a,b,b,a], 4*δx*[-1,-1,1,1]);
domain_style...)
plot!(Shape((a-.1) .+ 2δx * [-1,1,1,-1], [f(a),f(a),f(b), f(b)]);
range_style...)
plot!(Shape((a-.1) .+ δx/2 * [-1,1,1,-1], [y0,y0,y1,y1]);
range_style...)
zs = find_zeros(f, (a,b))
c = zs[2]
plot!([(c,0), (c,f(c))]; line=(:black, 1, :dashdot))
annotate!([
(a, 0, text(L"a", 12, :bottom)),
(b, 0, text(L"b", 12, :top)),
(c, 0, text(L"c", 12, :top)),
(a-.1, f(a), text(L"f(a)", 12, :right)),
(a-.1, f(b), text(L"f(b)", 12, :right)),
(b, m, text(L"y", 12, :left)),
])
end
plt
#=
function IVT_graph(n)
f(x) = sin(pi*x) + 9x/10
a,b = [0,3]
@@ -76,7 +133,17 @@ with $f(x)=y$.
plotly()
ImageFile(imgfile, caption)
=#
```
```{julia}
#| echo: false
plotly()
nothing
```
Illustration of the intermediate value theorem. The theorem implies that any randomly chosen $y$ value between $f(a)$ and $f(b)$ will have at least one $c$ in $[a,b]$ with $f(c)=y$. This graphic shows one of several possible values for the given choice of $y$.
:::
In the early years of calculus, the intermediate value theorem was intricately connected with the definition of continuity, now it is a consequence.

View File

@@ -218,35 +218,64 @@ This bounds the expression $\sin(x)/x$ between $1$ and $\cos(x)$ and as $x$ gets
The above bound comes from this figure, for small $x > 0$:
::: {#fig-sin-cos-bound}
```{julia}
#| hold: true
#| echo: false
gr()
p = plot(x -> sqrt(1 - x^2), 0, 1, legend=false, aspect_ratio=:equal,
linewidth=3, color=:black)
θ = π/6
y,x = sincos(θ)
col=RGBA(0.0,0.0,1.0, 0.25)
plot!(range(0,x, length=2), zero, fillrange=u->y/x*u, color=col)
plot!(range(x, 1, length=50), zero, fillrange = u -> sqrt(1 - u^2), color=col)
plot!([x,x],[0,y], linestyle=:dash, linewidth=3, color=:black)
plot!([x,1],[y,0], linestyle=:dot, linewidth=3, color=:black)
plot!([1,1], [0,y/x], linewidth=3, color=:black)
plot!([0,1], [0,y/x], linewidth=3, color=:black)
plot!([0,1], [0,0], linewidth=3, color=:black)
Δ = 0.05
annotate!([(0,0+Δ,"A"), (x-Δ,y+Δ/4, "B"), (1+Δ/2,y/x, "C"),
(1+Δ/2,0+Δ/2,"D")])
annotate!([(.2*cos(θ/2), 0.2*sin(θ/2), "θ")])
imgfile = tempname() * ".png"
savefig(p, imgfile)
caption = "Triangle ``ABD`` has less area than the shaded wedge, which has less area than triangle ``ACD``. Their respective areas are ``(1/2)\\sin(\\theta)``, ``(1/2)\\theta``, and ``(1/2)\\tan(\\theta)``. The inequality used to show ``\\sin(x)/x`` is bounded below by ``\\cos(x)`` and above by ``1`` comes from a division by ``(1/2) \\sin(x)`` and taking reciprocals.
"
plotly()
ImageFile(imgfile, caption)
plt = let
gr()
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35), line=nothing)
range_style = (; fill=(:blue, 0.35), line=nothing)
plot(; empty_style..., aspect_ratio=:equal)
plot!(x -> sqrt(1 - x^2), 0, 1; line=(:black, 2))
θ = π/6
y,x = sincos(θ)
col=RGBA(0.0,0.0,1.0, 0.25)
plot!(range(0,x, length=2), zero, fillrange=u->y/x*u, color=col)
plot!(range(x, 1, length=50), zero, fillrange = u -> sqrt(1 - u^2), color=col)
plot!([x,x],[0,y], line=(:dash, 2, :black))
plot!([x,1],[y,0], line=(:dot, 2, :black))
plot!([1,1], [0,y/x], line=(2, :black))
plot!([0,1], [0,y/x], line=(2, :black))
plot!([0,1], [0,0], line=(2, :black))
Δ = 0.05
annotate!([(0,0+Δ, text(L"A", 10)),
(x-Δ,y+Δ/4, text(L"B",10)),
(1+Δ/2,y/x, text(L"C", 10)),
(1+Δ/2,0+Δ/2, text(L"D", 10)),
(0.2*cos(θ/2), 0.2*sin(θ/2), text(L"\theta", 12))
])
current()
end
plt
```
```{julia}
#| echo: false
plotly()
nothing
```
Triangle $\triangle ABD$ has less area than the shaded wedge, which has less area than triangle $\triangle ACD$. Their respective areas are $(1/2)\sin(\theta)$, $(1/2)\theta$, and $(1/2)\tan(\theta)$. The inequality used to show $\sin(x)/x$ is bounded below by $\cos(x)$ and above by $1$ comes from a division by $(1/2) \sin(x)$ and taking reciprocals.
:::
To discuss the case of $(1+x)^{1/x}$ it proved convenient to assume $x = 1/m$ for integer values of $m$. At the time of Cauchy, log tables were available to identify the approximate value of the limit. Cauchy computed the following value from logarithm tables:

View File

@@ -42,6 +42,101 @@ For these examples, the domain of both $f(x)$ and $g(x)$ is all real values of $
In general the range is harder to identify than the domain, and this is the case for these functions too. For $f(x)$ we may know the $\cos$ function is trapped in $[-1,1]$ and it is intuitively clear than all values in that set are possible. The function $h(x)$ would have range $[0,\infty)$. The $s(x)$ function is either $-1$ or $1$, so only has two possible values in its range. What about $g(x)$? It is a parabola that opens upward, so any $y$ values below the $y$ value of its vertex will not appear in the range. In this case, the symmetry indicates that the vertex will be at $(1/2, -1/4)$, so the range is $[-1/4, \infty)$.
::: {#fig-domain-range layout-nrow=2}
```{julia}
#| echo: false
plt = let
gr()
# domain/range shade
λ = 1.2
a, b = .1, 3
f(x) = (x-1/2) + sin((x-1)^2)
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35))
range_style = (; fill=(:blue, 0.35))
xs = range(a,b, 1000)
y0,y1 = extrema(f.(xs))
Δy = (y1-y0)/60
Δx = (b - a)/75
plot(; aspect_ratio=:equal, empty_style...)
plot!([-.25,3.25],[0,0]; axis_style...)
plot!([0,0], [min(-2Δy, y0 - Δy), y1 + 4Δy]; axis_style... )
plot!(f, a, b; fn_style...)
plot!(Shape([a,b,b,a], Δy * [-1,-1,1,1] ); domain_style...)
plot!(Shape(Δx*[-1,1,1,-1], [y0,y0,y1,y1]); range_style...)
plot!([a,a], [0, f(a)]; mark_style...)
plot!([b,b], [0, f(b)]; mark_style...)
plot!([a, b], [f(a), f(a)]; mark_style...)
plot!([a, b], [f(b), f(b)]; mark_style...)
end
plot
```
```{julia}
#| echo: false
plt = let
a, b = 0, 2pi
λ = 1.1
Δx, Δy = .033, .1
δx = 0.05
f(x) = sec(x)
g(x) = abs(f(x)) < 5 ? f(x) : NaN
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35))
range_style = (; fill=(:blue, 0.35))
plot(; empty_style...)
plot!(g, a+0.1, b; fn_style...)
plot!(λ*[a,b],[0,0]; axis_style...)
plot!([0,0], λ*[-5,5+1/2]; axis_style...)
vline!([pi/2, 3pi/2]; line=(:gray, :dash))
plot!(Shape([a,a+pi/2-δx,a+pi/2-δx,a], Δy*[-1,-1,1,1]); domain_style...)
plot!(Shape([a+pi/2+δx, a+pi/2+pi-δx,a+pi/2+pi-δx,a+pi/2+δx],
Δy*[-1,-1,1,1]); domain_style...)
plot!(Shape([3pi/2 + δx, 2pi, 2pi, 3pi/2+δx],
Δy*[-1,-1,1,1]); domain_style...)
plot!(Shape(Δx*[-1,1,1,-1], [-5, -5,-1,-1]); range_style...)
plot!(Shape(Δx*[-1,1,1,-1], [ 5, 5,1,1]); range_style...)
end
plot
```
```{julia}
#| echo: false
plotly()
nothing
```
The top figure shows the domain and range of the function as highlighted intervals. The bottom figure shows that the domain may be a collection of intervals. (In this case the $\sec$ function is not defined at $\pi/2 + k \pi$ for integer $k$) and the range may be a collection of intervals. (In this case, the $\sec$ function never have a value in $(-1,1)$.
:::
:::{.callout-note}
## Note
@@ -90,7 +185,7 @@ Whereas function definitions and usage in `Julia` mirrors standard math notation
:::
### The domain of a function
### The domain of a function in Julia
Functions in `Julia` have an implicit domain, just as they do mathematically. In the case of $f(x)$ and $g(x)$, the right-hand side is defined for all real values of $x$, so the domain is all $x$. For $h(x)$ this isn't the case, of course. Trying to call $h(x)$ when $x < 0$ will give an error:
@@ -347,10 +442,27 @@ In our example, we see that in trying to find an answer to $f(x) = 0$ ( $\sqrt{2
```{julia}
#| echo: false
plot(q, a, b, linewidth=5, legend=false)
plot!(zero, a, b)
plot!([a, b], q.([a, b]))
scatter!([c], [q(c)])
plt = let
gr()
plt = plot(q, a, b, linewidth=5, legend=false)
plot!(plt, zero, a, b)
plot!(plt, [a, b], q.([a, b]))
scatter!(plt, [c], [q(c)]; marker=(:circle,))
scatter!(plt, [a,b], [q(a), q(b)]; marker=(:square,))
annotate!(plt, [
(a, 0, text(L"a", 10,:top)),
(b, 0, text(L"b", 10, :top)),
(c, 0, text(L"c", 10, :bottom))
])
end
plt
```
```{julia}
#| echo: false
plotly()
nothing
```
Still, `q(c)` is not really close to $0$:
@@ -398,6 +510,7 @@ c = secant_intersection(f, a, b)
p = plot(f, a, b, linewidth=5, legend=false)
plot!(p, zero, a, b)
scatter!([a,b], [f(a), f(b)]; marker=(:square,))
plot!(p, [a,b], f.([a,b]));
scatter!(p, [c], [f(c)])
@@ -918,6 +1031,86 @@ answ = 1
radioq(choices, answ)
```
##### Question
::: {#fig-floor-function}
```{julia}
#| echo: false
plt = let
gr()
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35), line=nothing)
range_style = (; fill=(:blue, 0.35), line=nothing)
ts = range(0, 2pi, 100)
xys = sincos.(ts)
xys = [.1 .* xy for xy in xys]
plot(; empty_style..., aspect_ratio=:equal)
plot!([-4.25,4.25], [0,0]; axis_style...)
plot!([0,0], [-4.25, 4.25]; axis_style...)
for k in -4:4
P,Q = (k,k),(k+1,k)
plot!([P,Q], line=(:black,1))
S = Shape([k .+ xy for xy in xys])
plot!(S; fill=(:black,))
S = Shape([(k+1,k) .+ xy for xy in xys])
plot!(S; fill=(:white,), line=(:black,1))
end
current()
end
plotly()
plt
```
The `floor` function rounds down. For example, any value in $[k,k+1)$ rounds to $k$ for integer $k$.
:::
The figure shows the `floor` function which is useful in programming. It rounds down to the first integer value.
From the graph, what is the domain of the function?
```{julia}
#| hold: true
#| echo: false
choices = [
"The entire real line",
"The entire real line except for integer values",
"The integers"
]
answer = 1
radioq(choices, answ)
```
From the graph, what is the range of the function?
```{julia}
#| hold: true
#| echo: false
choices = [
"The entire real line",
"The entire real line except for integer values",
"The integers"
]
answer = 3
radioq(choices, answ)
```
(This graphic uses the convention that a filled in point is present, but an open point is not, hence each bar represents some $[k, k+1)$.)
###### Question

View File

@@ -30,20 +30,57 @@ Why is this useful? When available, it can help us solve equations. If we can wr
Let's explore when we can "solve" for an inverse function.
Consider the graph of the function $f(x) = 2^x$:
Consider this graph of the function $f(x) = 2^x$
```{julia}
#| hold: true
f(x) = 2^x
plot(f, 0, 4, legend=false)
plot!([2,2,0], [0,f(2),f(2)])
#| echo: false
p = let
gr()
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
domain_style = (;fill=(:orange, 0.35), line=nothing)
range_style = (; fill=(:blue, 0.35), line=nothing)
f(x) = 2^x
a, b = 0, 2
plot(; empty_style...)
xs = range(a, b, 200)
ys = f.(xs)
plot!( xs, ys; fn_style...)
plot!([a-1/4, b+.2], [0,0]; axis_style...)
plot!([0, 0], [-.1, f(2.1)]; axis_style...)
x = 1
y = (f(b)+f(a))/2
plot!([x,x,0],[0,f(x),f(x)]; line=(:black, 1, :dash), arrow=true, side=:head)
plot!([0,log2(y), log2(y)], [y,y,0]; line=(:black,1,:dash), arrow=true, side=:head)
annotate!([
(x, 0, text(L"c", 10, :top)),
(0,f(x), text(L"f(c)", 10, :right)),
(0, y, text(L"y=f(d)", 10, :right)),
(log2(y), 0, text(L"d", 10, :top))
])
end
plotly()
p
```
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 $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)$.
If we were to *reverse* the direction, starting at $y = f(d)$ on the $y$ axis and then moving horizontally to the graph, and then vertically to the $x$-axis we end up at a value $d$ with the correct $f(d)$. 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.
@@ -309,6 +346,131 @@ What do we see? In blue, we can see the familiar square root graph along with a
This is reminiscent of the formula for the slope of a perpendicular line, $-1/m$, but quite different, as this formula implies the two lines have either both positive slopes or both negative slopes, unlike the relationship in slopes between a line and a perpendicular line.
::: {#fig-inverse-normal layout-ncol=1}
```{julia}
#| echo: false
# inverse function slope
gr()
p1 = let
f(x) = x^2
df(x) = 2x
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
plot(; aspect_ratio=:equal, empty_style...)
xs = range(0, 1.25, 100)
plot!(xs,f.(xs); fn_style...)
plot!(f.(xs), xs; fn2_style...)
plot!(identity, -1/4, 2; line=(:gray, 1, :dot))
#plot!([-.1, 1.35],[0,0]; axis_style...)
#plot!([0,0], [-0.1, f(1.3)]; axis_style...)
c = .4
m = df(c)
tl(x) = f(c) + df(c)*(x-c)
plot!(tl; line=(:black, 1, :dash))
d = c + .6
p1, p2, p3 = (c, tl(c)), (d, tl(c)), (d, tl(d))
q1, q2, q3 = (tl(c),c), (tl(c),d), (tl(d), d)
plot!([p1, p2, p3]; line=(:black, 1, :dot))
tl1(x) = c + (1/m)*(x - f(c))
plot!(tl1; line=(:red, 1, :dash))
plot!([q1, q2, q3]; line=(:red, 1, :dot))
annotate!([
((c+d)/2, f(c), text(L"\Delta x", 10, :top, :black)),
(d, (tl(c)+tl(d))/2, text(L"\Delta y", 10, :left, :black)),
(f(c), (c+d)/2, text(L"\Delta x", 10, :right, :red)),
((tl(c)+tl(d))/2, d, text(L"\Delta y", 10, :bottom, :red)),
(d, tl(d),
text(L"rise/run = $m = \Delta y / \Delta x$", 10, :top, :left,
rotation= rad2deg(atan(m)))),
(tl(d), d,
text(L"rise/run = $\Delta x / \Delta y = 1/m$", 10, :bottom, :left, rotation=rad2deg(atan(1/m)))),
(1.9, 1.9, text(L"y=x", 10, :top, rotation=45))
])
current()
end
# normal line
p2 = let
f(x) = 4 - (x-2)^2
df(x) = -2*(x-2)
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
plot(; aspect_ratio=:equal, empty_style...,
xlims=(1, 2.5), ylims=(3, 4.5))
xs = range(.99, 2.01, 100)
plot!(xs,f.(xs); fn_style...)
c = 1.5
tl(x) = f(c) + df(c)*(x-c)
nl(x) = f(c) - (x-c)/df(c)
xs = range(1, 2, 10)
plot!(xs, tl.(xs); fn2_style...)
xs = range(.9, 2, 10)
plot!(xs, nl.(xs); fn2_style...)
ylims!((f(.85), nl(.95)))
o = 1/3
plot!([c,c+o, c+o], [tl(c), tl(c), tl(c+o)]; mark_style...)
m = (tl(c+o) - tl(c))
plot!([c,c,c+m], [nl(c),nl(c + m),nl(c+m)]; mark_style...)
theta = rad2deg(atan(tl(c+o)-tl(c), o))
annotate!([
(c + o/2, f(c), text(L"1", :top, 10)),
(c + o, (f(c)+f(c+o))/2, text(L"m", :right, 10)),
(c, (nl(c) + nl(c+m))/2, text(L"-1", :right, 10)),
(c+m/2, nl(c+m), text(L"m", :top, 10)),
(c + o/2, tl(c+o), text(L"rise/run $=m/1$", 10, :top,
rotation=theta)),
(c + 1.1*o, nl(c+1.1*o), text(L"rise/run $=(-1)/m$", 10, :bottom,
rotation=theta-90))
])
current()
end
plot(p1, p2)
```
The inverse function has slope at a corresponding point that is the *reciprocal*; the "normal line" for a function at a point has slope that is the *negative reciprocal* of the "tangent line" at a point.
:::
```{julia}
#| echo: false
plotly()
nothing
```
The key here is that the shape of $f(x)$ near $x=c$ is somewhat related to the shape of $f^{-1}(x)$ at $f(c)$. In this case, if we use the tangent line as a fill in for how steep a function is, we see from the relationship that if $f(x)$ is "steep" at $x=c$, then $f^{-1}(x)$ will be "shallow" at $x=f(c)$.

View File

@@ -108,11 +108,6 @@ Plotting a function is then this simple: `plot(f, xmin, xmax)`.
:::{.callout-note}
## Note
The time to first plot can feel sluggish, but subsequent plots will be speedy. See the technical note at the end of this section for an explanation.
:::
Let's see some other graphs.
@@ -193,7 +188,70 @@ Some types we will encounter, such as the one for symbolic values or the special
:::
---
:::{.callout-note}
## Viewing window
The default style for `Plots.jl` is to use a frame style where the viewing window is emphasized. This is a rectangular region, $[x_0, x_1] \times [y_0, y_1]$, which is seen through the tick labeling, the bounding scales on the left and bottom, and emphasized through the grid.
This choices does *not* show the $x-y$ axes. As such, we might layer on the axes when these are of interest.
To emphasize concepts, we may stylize a function graph, rather than display the basic graphic. For example, in this graphic highlighting the amount the function goes up as it moves from $1$ to $x$:
```{julia}
gr()
#| echo: false
let
f(x) = x^2
empty_style = (xaxis=([], false),
yaxis=([], false),
framestyle=:origin,
legend=false)
axis_style = (arrow=true, side=:head, line=(:gray, 1))
text_style = (10,)
fn_style = (;line=(:black, 3))
fn2_style = (;line=(:red, 4))
mark_style = (;line=(:gray, 1, :dot))
plot(; empty_style..., aspect_ratio=:equal)
a, b = 0, 1.25
x = 1.15
plot!(f, a, b; fn_style...)
plot!([-.1, 1.5], [0,0]; axis_style...)
plot!([0,0], [-.1, f(1.35)]; axis_style...)
plot!([1,x,x], [f(1),f(1),f(x)]; line=(:black, 1))
plot!([1,1],[0,f(1)]; mark_style...)
plot!([x,x],[0,f(1)]; mark_style...)
plot!([0,1],[f(1),f(1)]; mark_style...)
plot!([0,x],[f(x),f(x)]; mark_style...)
annotate!([
(1, 0, text(L"1", 10, :top)),
(x, 0, text(L"x", 10, :top)),
(0, f(1), text(L"1", 10, :right)),
(0, f(x), text(L"x^2", 10, :right)),
(1, f(1), text(L"P", 10, :right, :bottom)),
(x, f(x), text(L"Q", 10, :right, :bottom)),
((1 + x)/2, f(1), text(L"\Delta x", 10, :top)),
(x, (f(1) + f(x))/2, text(L"\Delta y", 10, :left))
])
current()
end
```
```{julia}
#| echo: false
plotly()
nothing
```
:::
----
Making a graph with `Plots` is easy, but producing a graph that is informative can be a challenge, as the choice of a viewing window can make a big difference in what is seen. For example, trying to make a graph of $f(x) = \tan(x)$, as below, will result in a bit of a mess - the chosen viewing window crosses several places where the function blows up:
@@ -699,6 +757,40 @@ plot(f, g, 0, max((R-r)/r, r/(R-r))*2pi)
In the above, one can fix $R=1$. Then different values for `r` and `rho` will produce different graphs. These graphs will be periodic if $(R-r)/r$ is a rational. (Nothing about these equations requires $\rho < r$.)
## Points, lines, polygons
Two basic objects to graph are points and lines.
A point in two-dimensional space has two coordinates, often denoted by $(x,y)$. In `Julia`, the same notation produces a `tuple`. Using square brackets, as in `[x,y]`, produces a vector. Vectors are usually used, as we have seen there are algebraic operations defined for them. However, tuples have other advantages and are how `Plots` designates a point.
The plot command `plot(xs, ys)` plots the points $(x_1,y_1), \dots, (x_n, y_n)$ and then connects adjacent points with with lines. The command `scatter(xs, ys)` just plots the points.
However, the points might be more naturally specified as coordinate pairs. If tuples are used to pair them off, then `Plots` will plot a vector of tuples as a sequence of points:
```{julia}
pts = [(1, 0), (1/4, 1/4), (0, 1), (-1/4, 1/4), (-1, 0),
(-1/4, -1/4), (0, -1), (1/4, -1/4)]
scatter(pts; legend=false)
```
A line segment simply connects two points. While these can be specified as vectors of $x$ and $y$ values, again it may be more convenient to use coordinate pairs to specify the points. Continuing the above, we can connect adjacent points with line segments:
```{julia}
plot!(pts; line=(:gray, 0.5, :dash))
```
This uses the shorthand notation of `Plots` to specify `linecolor=:gray, linealpha=0.5, linestyle=:dash`. To plot just a line segment, just specifying two points suffices.
The four-pointed star is not closed off, as there isn't a value from the last point to the first point. A polygon closes itself off. The `Shape` function can take a vector of points or a pair of `xs` and `ys` to specify a polygon. When these are plotted, the arguments to `fill` describe the interior of the polygon, the arguments to `line` the boundary:
```{julia}
plot(Shape(pts); fill=(:gray, 0.25), line=(:black, 2), legend=false)
scatter!(pts)
```
## Questions

View File

@@ -18,6 +18,15 @@
@Misc{Angenent,
key = {WisconsinCalculus},
author = {Sigurd Angenent},
title = {Wisconsin Calculus},
howpublished = {https://github.com/SigurdAngenent/WisconsinCalculus/tree/master},
year = 2012,
note = {GNU Free Documentation License, Version 1.2}
}
@Book{Schey,
author = {H.M. Schey},
title = {Div, Grad, Curl, and all that},