Merge pull request #51 from jverzani/v0.14

V0.14
This commit is contained in:
john verzani 2022-11-04 10:51:11 -04:00 committed by GitHub
commit a7b8103ac1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 224 additions and 11 deletions

View File

@ -1,4 +1,4 @@
version: "0.13"
version: "0.14"
project:
type: book
@ -141,6 +141,7 @@ format:
# pdf:
# documentclass: scrbook
# classoption: [oneside]
# keep-tex: true
# geometry:
# - top=30mm
# - left=10mm

View File

@ -1532,6 +1532,43 @@ numericq(val)
###### Question
Compute the limit
$$
\lim_{x \rightarrow 0} \frac{x\sin(\sin(x)) - \sin^2(x)}{x^6}.
$$
```{julia}
#| hold: true
#| echo: false
f(x) = (x * sin(sin(x))- sin(x)^2)/x^6
val = N(limit(f(x), x => 0))
numericq(val)
```
###### Question
Compute the limit
$$
\lim_{x \rightarrow 0} \frac{\tan(x) - 24 \tan(x/2)}{4 \sin(x) - 5 x}.
$$
```{julia}
#| hold: true
#| echo: false
f(x) = (tan(x) - 24 * tan(x/2)) / (4 * sin(x) - 5 * x)
val = N(limit(f(x), x => 0))
numericq(val)
```
###### Question
Some limits involve parameters. For example, suppose we define `ex` as follows:
@ -1616,6 +1653,47 @@ Should `SymPy` have needed an assumption like
yesnoq("yes")
```
###### Question
The limit
$$
L= \lim_{x \rightarrow 0} \left(\frac{a^x - x \log(a)}{b^x - x\log(b)}\right)^{1/x^2}
$$
For $a=3$ and $b=2$
Can be computed symbolically *two* different ways:
```{julia}
@syms x
a, b = 3, 2
f(x) = ((a^x - x*log(a))/(b^x - x*log(b)))^(1/x^2)
limit(f(x), x=>0)
```
*or*
```{julia}
@syms x a b
f(x) = ((a^x - x*log(a))/(b^x - x*log(b)))^(1/x^2)
L = limit(f(x), x=>0)
L(a => 3, b=>2)
```
Which is correct?
```{julia}
#| echo: false
choices = ["The first one", "The second one"]
explanation = """
The first one is incorrect, as `log(3)` is evaluated numerically, and not symbolically. The difference between a floating point approximation and the symbolic expression is enough to make the first limit infinite, despite the actual limit being finite.
"""
buttonq(choices, 2; explanation=explanation)
```
###### Question: The squeeze theorem

View File

@ -1093,3 +1093,39 @@ choices = [
answ=1
radioq(choices, answ)
```
##### Question
(From [stackoverflow](https://discourse.julialang.org/t/the-speed-of-light-is-an-integer-why-should-we-care/40108/).)
Since 1983, the speed of light is defined to be exactly equal to 299,792,458 meters per second. This can be used to find the value of the Planck [length](https://en.wikipedia.org/wiki/Planck_units#Planck_length) using the formula
$$
l_p = \sqrt{\frac{\hbar\cdot G}{c^3}}
$$
Attempting to compute this, we have:
```{julia}
c = 299_792_458; # the speed of light
G = 6.67430e-11; # Gravitational constant
h = 6.62607015e-34; # Planck's contant
h_bar = h / (2*pi);
planck_length = sqrt(h_bar * G / c^3)
```
That all seems great, but the Wikipedia article says this value is around $10^{-35}$ **not** $10^{-32}$. What is a possible answer?
```{julia}
#| echo: false
choices = ["The Wikipedia article must be incorrect.",
"The difference is very *small*. It must be due to rounding errors",
"`Julia` computes this value incorrectly"
]
explanation = raw"""
Yes, this is computed incorrectly. The value `c^3` *overflows* as the actual value is around $10^{25}$, but the computed value is only around $10^{18}$. With `c` defined as above, it will be parsed as an integer, which by default (on most machines) will be stored with $64$ bits. Operations with 64-bit integers that mathematically produce values bigger than about $10^{18}$ will be incorrectly computed, as `Julia` uses a silent means to handle the [overflow](https://docs.julialang.org/en/v1/manual/integers-and-floating-point-numbers/#Overflow-behavior). (This allows integer computations to be faster.) The suggested approach is to use floating point values to define such values. Floating point can store integer values accurately up to values of $2^{53}$ (about $10^{15}$) and gracefully for larger values, such as $c^3$ in this problem. With `c` defined by `c = 299792458.0` the computed value for the Planck length is `1.6162550244237053e-35`, in agreement with the Wikipedia value.
"""
buttonq(choices, 3; explanation=explanation)
```

View File

@ -457,7 +457,7 @@ mxb(0)
So the `b` is found from the currently stored value. This fact can be exploited. we can write template-like functions, such as `f(x)=m*x+b` and reuse them just by updating the parameters separately.
How `Julia` resolves what a variable refers to is described in detail in the manual page [Scope of Variables](https://docs.julialang.org/en/v1/manual/variables-and-scoping/). In this case, the function definition finds variables in the context of where the function was defined, the main workspace. As seen, this context can be modified after the function definition and prior to the function call. It is only when `b` is needed, that the context is consulted, so the most recent binding is retrieved. Contexts (more formally known as environments) allow the user to repurpose variable names without there being name collision. For example, we typically use `x` as a function argument, and different contexts allow this `x` to refer to different values.
How `Julia` resolves what a variable refers to is described in detail in the manual page [Scope of Variables](https://docs.julialang.org/en/v1/manual/variables-and-scoping/). In this case, the function definition finds variables in the context of where the function was defined, the main workspace, and not where it is called. As seen, this context can be modified after the function definition and prior to the function call. It is only when `b` is needed, that the context is consulted, so the most recent binding is retrieved. Contexts allow the user to repurpose variable names without there being name collision. For example, we typically use `x` as a function argument, and different contexts allow this `x` to refer to different values.
Mostly this works as expected, but at times it can be complicated to reason about. In our example, definitions of the parameters can be forgotten, or the same variable name may have been used for some other purpose. The potential issue is with the parameters, the value for `x` is straightforward, as it is passed into the function. However, we can also pass the parameters, such as $m$ and $b$, as arguments. For parameters, one suggestion is to use [keyword](https://docs.julialang.org/en/v1/manual/functions/#Keyword-Arguments) arguments. These allow the specification of parameters, but also give a default value. This can make usage explicit, yet still convenient. For example, here is an alternate way of defining a line with parameters `m` and `b`:
@ -564,6 +564,23 @@ methods(log, (Number,))
(The arguments have *type annotations* such as `x::Float64` or `x::BigFloat`. `Julia` uses these to help resolve which method should be called for a given set of arguments. This allows for different operations depending on the variable type. For example, in this case, the `log` function for `Float64` values uses a fast algorithm, whereas for `BigFloat` values an algorithm that can handle multiple precision is used.)
##### 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`:
```{julia}
function twotox(x::Integer)
(2//1)^x
end
twotox(x::Real) = (2.0)^x
twotox(x::Complex) = (2.0 + 0.0im)^x
```
This is for illustration purposes -- the latter two are actually already done through `Julia`'s *promotion* mechanism -- but we see that `twotox` will return a rational number when `x` is an integer unlike `Julia` which, when `x` is non-negative will return an integer and will otherwise will error or return a float (when `x` is a numeric literal, like `2^(-3)`).
The key to reading the above is the type annotation acts like a gatekeeper allowing in only variables of that type.
For example, the number `2` is parsed as a 64-bit integer (typically) and has concrete type `Int64` which is a subtype of `Integer`. So `twotox(2)` will use the first definition, and return a rational number. Whereas, the number `2.0` is parsed as a floating point number with concrete type `Float64` which is a subtype of `Real`, not `Integer`, so `twotox(2.0)` will use the second method defined above.
##### Example: An application of composition and multiple dispatch
@ -612,15 +629,6 @@ plot(Area, 0, 10)
From the graph, we can see that that width for maximum area is $w=5$ and so $h=5$ as well.
## Function application
The typical calling pattern for a function simply follows *mathematical* notation, that is `f(x)` calls the function `f` with the argument `x`. There are times especially with function composition that an alternative *piping* syntax is desirable. `Julia` provides the *infix* operation `|>` for piping, defining it by `|>(x, f) = f(x)`. This allows composition to work left to right, instead of right to left. For example, these two calls produce the same answer:
```{julia}
exp(sin(log(3))), 3 |> log |> sin |> exp
```
## Other types of functions
@ -798,6 +806,30 @@ end
The name of the file to open appears, how the file is to be opened (`w` means write, `r` would mean read), and then a function with argument `io` which writes two lines to `io`.
## Function application
The typical calling pattern for a function simply follows *mathematical* notation, that is `f(x)` calls the function `f` with the argument `x`. There are times especially with function composition that an alternative *piping* syntax is desirable. `Julia` provides the *infix* operation `|>` for piping, defining it by `|>(x, f) = f(x)`. This allows composition to work left to right, instead of right to left. For example, these two calls produce the same answer:
```{julia}
exp(sin(log(3))), 3 |> log |> sin |> exp
```
While convenient, piping works most naturally with functions of a single variable. When more than one variable is needed for a function, say `max`, then things get a bit cumbersome. Using anonymous functions or "fixing" arguments is then useful. For example, here is one way to implement `clamp(x, lo, hi)` which returns `x` when `lo < x < hi` and otherwise `lo` or `hi`, depending:
```{julia}
x, lo, hi = -10, -5, 5 # clampl(x, lo, hi) is -5
x |> x -> max(x, lo) |> x -> min(x, hi)
```
Or
```{julia}
x |> Base.Fix2(max, lo) |> Base.Fix2(min, hi)
```
For heavy use of chaining through function application there are various packages to simplify the notation (e.g. `Chain.jl`, `Pipe.jl`, `DataPipes.jl`, among many others).
## Questions

View File

@ -486,6 +486,72 @@ For plotting points with `scatter`, or `scatter!` the markers can be adjusted vi
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
In plotting a primitive, like a line, some mapping of the mathematical object to a collection of pixels must be made. For the case of a line [Bresenhams's line algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm) can be used.
In the simplest case, let's assume a few things:
* we have a line with slope $-1 < m < 0$.
* the pixels have integer coordinates (e.g., the pixel $(1, -1)$ would cover the region $[1,2] \times [-1, -2]$ when lit.)
* we start at point $(x_0, y_0)$, $f(x_0) = y_0$, with integer coordinates and end a point $(x_1, y_1)$, also with integer coordinates. The pixel $(x_0,y_0)$ is lit.
With these assumptions, we have an initial decision to make:
> moving to the right, is the pixel $(x_0+1, y_0)$ or $(x_0 + 1, y_0 - 1)$ lit?
We re-express our equation $y=f(x)= mx+b$ in general form $f(x,y) = 0 = Ax + By + C$. Using the other point on the line $A=-(y_1-y_0)$, $B=(x_1-x_0)$, and $C = -x_1y_0 + x_0 y_1$. In particular, by assumption both $A$ and $B$ are positive.
With this, we have $f(x_0,y_0) = 0$. But moreover, any point with $y>y_0$ will have $f(x_0,y)>0$ and if $y < y_0$ the opposite. That is this equation divides the plane into two pieces depending on whether $f$ is positive, the line is the dividing boundary.
For the algorithm, we start at $(x_0, y_0)$ and ask if the pixel $(x_0 + 1, y_0)$ or $(x_0 + 1, y_0 - 1)$ will be lit, then we continue to the right.
To check, we ask if $f(x_0 + 1, y_0 - 1/2)$ is positive. If so, then the actual line is below this value so the pixel below is chosen. Otherwise, the pixel above is chosen.
This last check can be done a bit more efficiently, but for now let's see it in action:
```{julia}
f(x) = -(1/3) * x + 1
x₀, x₁ = 0, 14
y₀, y₁ = f(x₀), f(x₁)
A,B,C = -(y₁ - y₀), (x₁-x₀), -x₁*y₀ + x₀*y₁
f(x,y) = A*x + B*y + C
xs = [(x₀, y₀)]
for i ∈ 1:(x₁ - 1)
xᵢ, yᵢ = xs[end]
xᵢ₊₁ = xᵢ + 1
Δ = f(xᵢ+1, yᵢ-1/2) > 0 ? 1 : 0
yᵢ₊₁ = yᵢ - Δ
push!(xs, (xᵢ₊₁, yᵢ₊₁))
end
xs
```
We can visualize with the following:
```{julia}
p = plot(f, x₀, x₁; legend=false, aspect_ratio=:equal,
xticks=0:x₁, yticks = (floor(Int, f(x₁))-1):(1 + ceil(Int, f(x₀))))
col = RGBA(.64,.64,.64, 0.25)
for xy ∈ xs
x, y = xy
scatter!([x], [y]; markersize=5)
scatter!([x+1], [y - 1/2], markersize=5, markershape=:star7)
plot!(Shape(x .+ [0, 1, 1, 0], y .+ [0, 0, -1, -1]), color=col)
end
p
```
We see a number of additional arguments used: different marker sizes and shapes and a transparent color. As well, the `Shape` primitive is used to represent a pixel.
Of course, generalizations for positive slope and slope with magnitude greater than $1$ are needed. As well, this basic algorithm could be optimized, especially if it is part of a lower-level drawing primitive. But this illustrates the considerations involved.
## Graphs of parametric equations