updates
This commit is contained in:
@@ -13,4 +13,5 @@ PlotlyKaleido = "f2990250-8cf9-495f-b13a-cce12b45703c"
|
||||
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
|
||||
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
|
||||
Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
|
||||
ScatteredInterpolation = "3f865c0f-6dca-5f4d-999b-29fe1e7e3c92"
|
||||
SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6"
|
||||
|
||||
@@ -15,6 +15,7 @@ using SymPy
|
||||
using Roots
|
||||
using QuadGK
|
||||
using JSON
|
||||
using ScatteredInterpolation
|
||||
```
|
||||
|
||||
Also, these methods from the `Contour` package:
|
||||
@@ -173,7 +174,7 @@ surface(xs, ys, 𝒇)
|
||||
The `surface` function will generate the surface.
|
||||
|
||||
|
||||
:::{.callout-note}
|
||||
::: {.callout-note}
|
||||
## Note
|
||||
Using `surface` as a function name is equivalent to `plot(xs, ys, f, seriestype=:surface)`.
|
||||
|
||||
@@ -524,12 +525,97 @@ The filled contour layers on the contour lines to a heatmap:
|
||||
```{julia}
|
||||
#| hold: true
|
||||
f(x,y) = exp(-(x^2 + y^2)/5) * sin(x) * cos(y)
|
||||
xs= ys = range(-pi, pi, length=100)
|
||||
xs = ys = range(-pi, pi, length=100)
|
||||
contourf(xs, ys, f)
|
||||
```
|
||||
|
||||
This function has a prominent peak and a prominent valley, around the middle of the viewing window. The nested contour lines indicate this, and the color key can be used to identify which is the peak and which the valley.
|
||||
|
||||
##### Example
|
||||
|
||||
The description of a function for the contour function is in terms of a grid of $x-y$ values and a function $f$ which gives the height, $z$. In other situations, it might make more sense to have a stream of $x-y-z$ values describing a surface. This might be the case, say, with trying to piece together a topography using a series of GPS track. To do so, one way is to take a regular grid of points and then *interpolate* $z$ values from the existing values.
|
||||
|
||||
The `ScatteredInterpolation.jl` package can be used to create a structure that can be used to interpolate points. The necessary pieces are the points, the sampled heights, and a method for the interpolation.
|
||||
|
||||
|
||||
A simple example follows (inspired by a [discourse post](https://discourse.julialang.org/t/plots-contourf-and-plotlyjs-contour-behaviour-with-regard-to-the-x-y-z-input/122897/2)) where the true surface is known so that a comparison can be made is given. The two figures show the contour described by 4 paths through the space, is not as detailed but captures the general shape reasonably well.
|
||||
|
||||
```{julia}
|
||||
f(x,y) = 3*(1-x)^2*exp(-(x^2) - (y+1)^2) -
|
||||
10*(x/5 - x^3 - y^5)*exp(-x^2-y^2) -
|
||||
1/3*exp(-(x+1)^2 - y^2)
|
||||
|
||||
r(t, a=2) = [a*cbrt(sinpi(t)), a * cbrt(cospi(t))]
|
||||
ts = range(0, 2, 30)[1:end-1]
|
||||
pts = vcat([[r(t,a) for t in ts] for a in [1/2, 1, 3/2, 2]]...)
|
||||
samples = [f(pt...) for pt in pts]
|
||||
first(zip(pts, samples),5)
|
||||
```
|
||||
|
||||
```{julia}
|
||||
using ScatteredInterpolation
|
||||
itp = interpolate(Multiquadratic(), stack(pts), samples)
|
||||
|
||||
# make a grid
|
||||
(xm,xM), (ym,yM) = extrema.(eachrow(stack(pts)))
|
||||
n, m = 25, 40
|
||||
xg, yg = range(xm,xM,n), range(ym, yM, m)
|
||||
X = [s for t in yg, s in xg] #size(X) is (m,n)
|
||||
Y = [t for t in yg, s in xg] # size(Y) is also (m,n)
|
||||
gridP = stack(vec([[x, y] for (x,y) in zip(X, Y)]))
|
||||
gridP = stack(vec([[x, y] for (x,y) in zip(X, Y)])) #2 x m*n - matrix; each column is a grid point
|
||||
interpolated = evaluate(itp, gridP)
|
||||
zg = reshape(interpolated, m, n)
|
||||
|
||||
p = Plots.contourf(xg, yg, zg; levels=6)
|
||||
q = Plots.contourf(xg, yg, f)
|
||||
plot(p,q)
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```{julia}
|
||||
using Plots
|
||||
```
|
||||
##### Example
|
||||
|
||||
The arrangement of the data in a heatmap or contour plot depends on the underlying plotting package. A [discourse post](https://discourse.julialang.org/t/wrong-heatmap-orientation-with-plots-jl/124822/9) used this example to illustrate.
|
||||
|
||||
The data is a matrix
|
||||
|
||||
```{julia}
|
||||
xy = [1 2; 3 4]
|
||||
```
|
||||
|
||||
which has these colors mapped to their values:
|
||||
|
||||
```{julia}
|
||||
cmap =[:red, :green, :blue, :orange]
|
||||
cmap[xy]
|
||||
```
|
||||
|
||||
@fig-plots-makie-heatmap shows on the left the image created by this command in `Plots`, and on the right the image created with the same command using `Makie`:
|
||||
|
||||
|
||||
|
||||
```{julia}
|
||||
#| eval: false
|
||||
heatmap(xy; colormap = cols, title="Plots", legend=false)
|
||||
```
|
||||
|
||||
```{julia}
|
||||
#| echo: false
|
||||
#| layout-ncol: 2
|
||||
#| label: fig-plots-makie-heatmap
|
||||
#| fig-cap: "Orientation of heatmap may vary by plotting package."
|
||||
p = heatmap(xy; colormap = cmap, title="Plots",
|
||||
legend=false)
|
||||
q = heatmap(xy'; colormap = cmap, title="Makie",
|
||||
legend=false)
|
||||
plot(p,q,layout=(1,2))
|
||||
```
|
||||
|
||||
`Makie` uses the first dimension for `x` and the second for `y` (the first dimension is down the columns, then across); and `Plots` plots `x` values on the `x` axis etc. with values rising upwards and towards the left.
|
||||
|
||||
## Limits
|
||||
|
||||
@@ -785,7 +871,7 @@ Another alternative would be to hold one variable constant, and use the `derivat
|
||||
partial_x(f, y) = x -> ForwardDiff.derivative(u -> f(u,y), x)
|
||||
```
|
||||
|
||||
:::{.callout-note}
|
||||
::: {.callout-note}
|
||||
## Note
|
||||
For vector-valued functions, we can override the syntax `'` using `Base.adjoint`, as `'` is treated as a postfix operator in `Julia` for the `adjoint` operation. The symbol `\\nabla` is also available in `Julia`, but it is not an operator, so can't be used as mathematically written `∇f` (this could be used as a name though). In `CalculusWithJulia` a definition is made so essentially `∇(f) = x -> ForwardDiff.gradient(f, x)`. It does require parentheses to be called, as in `∇(f)`.
|
||||
|
||||
|
||||
@@ -224,6 +224,208 @@ The two dimensions are different so for each value of `xs` the vector of `ys` is
|
||||
|
||||
At times using the "apply" notation: `x |> f`, in place of using `f(x)` is useful, as it can move the wrapping function to the right of the expression. To broadcast, `.|>` is available.
|
||||
|
||||
## Aside: simplifying calculations using containers and higher-order functions
|
||||
|
||||
Storing data in a container, be it a vector or a tuple, can seem at first more complicated, but in fact can lead to much simpler computations.
|
||||
|
||||
A simple example might be to add up a sequence of numbers. A direct way might be:
|
||||
|
||||
```{julia}
|
||||
x1, x2, x3, x4, x5, x6 = 1, 2, 3, 4, 5, 6
|
||||
x1 + x2 + x3 + x4 + x5 + x6
|
||||
```
|
||||
|
||||
Someone doesn't need to know `Julia`'s syntax to guess what this computes, save for the idiosyncratic tuple assignment used, which could have been bypassed at the cost of even more typing.
|
||||
|
||||
A more efficient means to do, as each componenent isn't named, this would be to store the data in a container:
|
||||
|
||||
```{julia}
|
||||
xs = [1, 2, 3, 4, 5, 6] # as a vector
|
||||
sum(xs)
|
||||
```
|
||||
|
||||
Sometimes tuples are used for containers. The difference to `sum` is not noticeable (though a different code path for the computation is taken behind the scenes):
|
||||
|
||||
```{julia}
|
||||
xs = (1, 2, 3, 4, 5, 6)
|
||||
sum(xs)
|
||||
```
|
||||
|
||||
(Tuples and vectors are related, but tuples don't have built-in arithmetic defined. Several popular packages, such as `Plots`, draw a distinction between the two basic containers, but most generic functions just need to be able to iterate over the values.)
|
||||
|
||||
The `sum` function has a parallel `prod` function for finding the product of the entries:
|
||||
|
||||
```{julia}
|
||||
prod(xs)
|
||||
```
|
||||
|
||||
Both `sum` and `prod` will error if the container is empty.
|
||||
|
||||
These two functions are *reductions*. There are others, such as `maximum` and `minimum`. They reduce the dimensionality of the container, in this case from a vector to a scalar. When applied to higher-dimensional containers, dimenensions to reduce over are specified. The higher-order `reduce` function can be used as a near alternate to `sum`:
|
||||
|
||||
```{julia}
|
||||
reduce(+, xs; init=0) # sum(xs)
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```{julia}
|
||||
reduce(*, xs; init=1) # prod(xs)
|
||||
```
|
||||
|
||||
The functions (`+` and `*`) are binary operators and are serially passed the running value (or `init`) and the new term from the iterator.
|
||||
|
||||
The initial value above is the unit for the operation (which could be found programatically by `zero(eltype(xs))` or `one(eltype(xs))` where the type is useful for better performance).
|
||||
|
||||
The `foldl` and `foldr` functions are similar to `reduce` only left (and right) associativity is guaranteed. This example uses the binary, infix `Pair` operator, `=>`, to illustrate the difference:
|
||||
|
||||
```{julia}
|
||||
foldl(=>, xs)
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```{julia}
|
||||
foldr(=>, xs)
|
||||
```
|
||||
|
||||
Next, we do a slighlty more complicated problem.
|
||||
|
||||
Recall the distance formula between two points, also called the *norm*. It is written here with the square root on the other side: $d^2 = (x_1-y_1)^2 + (x_0 - y_0)^2$. This computation can be usefully generalized to higher dimensional points (with $n$ components each).
|
||||
|
||||
This first example shows how the value for $d^2$ can be found using broadcasting and `sum`:
|
||||
|
||||
```{julia}
|
||||
xs = [1, 2, 3, 4, 5]
|
||||
ys = [1, 3, 5, 7, 3]
|
||||
|
||||
sum((xs - ys).^2)
|
||||
```
|
||||
|
||||
|
||||
This formula is a sum after applying an operation to the paired off values. Using a geneator that sum would look like:
|
||||
|
||||
```{julia}
|
||||
sum((xi - yi)^2 for (xi, yi) in zip(xs, ys))
|
||||
```
|
||||
|
||||
The `zip` function, used above, produces an iterator over tuples of the paired off values in the two (or more) containers passed to it.
|
||||
|
||||
|
||||
This pattern -- where a reduction follows a function's application to the components -- is implemented in `mapreduce`.
|
||||
|
||||
|
||||
```{julia}
|
||||
f(xy) = (xy[1] - xy[2])^2
|
||||
mapreduce(f, +, zip(xs, ys))
|
||||
```
|
||||
|
||||
In the generator example above, the components of the tuple are destructured into `(xi, yi)`; in the function `f` above $1$-based indexing is used to access the first and second components.
|
||||
|
||||
The `mapreduce` function can take more than one iterator to reduce over, When used this way, the function takes multiple arguments. Unlike the above example, where `f` was first defined and then used, we use an anonymous function below, to make the example a one-liner:
|
||||
|
||||
```{julia}
|
||||
mapreduce((xi,yi) -> (xi-yi)^2, +, xs, ys)
|
||||
```
|
||||
|
||||
|
||||
(The `mapreduce` form is more performant than broadcasting where the vectors are traversed more times.)
|
||||
|
||||
|
||||
### Extracting pieces of a container
|
||||
|
||||
At times, extracting all but the first or last value can be of interest. For example, a polygon comprised of $n$ points (the vertices), might be stored using a vector for the $x$ and $y$ values with an additional point that mirrors the first. Here are the points:
|
||||
|
||||
```{julia}
|
||||
xs = [1, 3, 4, 2]
|
||||
ys = [1, 1, 2, 3]
|
||||
pts = zip(xs, ys) # recipe for [(x1,y1), (x2,y2), (x3,y3), (x4,y4)]
|
||||
```
|
||||
|
||||
To *add* the additional point we might just use `push!`:
|
||||
|
||||
```{julia}
|
||||
push!(xs, first(xs))
|
||||
push!(ys, first(ys))
|
||||
```
|
||||
|
||||
(Though this approach won't work with `pts`, only with mutable containers.)
|
||||
|
||||
The `first` and `last` methods refer to the specific elements in the indexed collection. To get the rest of the values can be done a few ways. For example, this pattern peels off the first and leaves a new container to hold the rest:
|
||||
|
||||
```{julia}
|
||||
a, bs... = xs
|
||||
a, bs
|
||||
```
|
||||
|
||||
The splatting operation for `bs....` is usually seen inside a function, so this is a bit unusual. The `Iterators.peel` method also can also do this task (the `Iterators` module and its methods are not exported, so `peel` is necessarily qualified if not imported):
|
||||
|
||||
```{julia}
|
||||
a, bs = Iterators.peel(xs)
|
||||
bs
|
||||
```
|
||||
|
||||
|
||||
The `bs` are represented with an iterator and can be collected to yield the values, though often this is unecessary and possibly a costly step:
|
||||
|
||||
```{julia}
|
||||
collect(bs), sum(bs)
|
||||
```
|
||||
|
||||
The iterators shown here are *lazy* and only construct a recipe to produce the points one after another. Reductions like `sum` and `prod` can use this recipe to produce an answer without needing to realize in memory at one time the entire collection of values being represented.
|
||||
|
||||
The `Iterators.rest` method can be used to take the rest of the container starting a given index. This command also finds the `bs` above:
|
||||
|
||||
```{julia}
|
||||
bs = Iterators.rest(xs, 2)
|
||||
collect(bs)
|
||||
```
|
||||
|
||||
The `Iterators.take` method can be used to take values at the beginning of a container. This command takes all but the last value of `xs`:
|
||||
|
||||
```{julia}
|
||||
as = Iterators.take(xs, length(xs) - 1)
|
||||
collect(as)
|
||||
```
|
||||
|
||||
The `take` method could be used to remove the padded value from the `xs` and `ys`. Between `Iterators.take` and `Iterators.rest` the iterable object can be split into a head and tail.
|
||||
|
||||
##### Example: Riemann sums
|
||||
|
||||
In the computation of a Riemann sum, the interval $[a,b]$ is partitioned using $n+1$ points $a=x_0 < x_1 < \cdots < x_{n-1} < x_n = b$.
|
||||
|
||||
```{julia}
|
||||
a, b, n = 0, 1, 4
|
||||
xs = range(a, b, n+1) # n + 1 points gives n subintervals [xᵢ, xᵢ₊₁]
|
||||
```
|
||||
|
||||
To grab these points as adjacent pairs can be done by combining the first $n$ points and the last $n$ points, as follows:
|
||||
|
||||
```{julia}
|
||||
partitions = zip(Iterators.take(xs, n), Iterators.rest(xs, 1))
|
||||
collect(partitions)
|
||||
```
|
||||
|
||||
A left-hand Riemann sum for `f` could then be done with:
|
||||
|
||||
```{julia}
|
||||
f(x) = x^2
|
||||
sum(f ∘ first, partitions)
|
||||
```
|
||||
|
||||
This uses a few things: like `mapreduce`, `sum` allows a function to
|
||||
be applied to each element in the `partitions` collection. (Indeed, the default method to compute `sum(xs)` for an arbitrary container resolves to `mapreduce(identity, add_sum, xs)` where `add_sum` is basically `+`.)
|
||||
|
||||
In this case, the
|
||||
values come as tuples to the function to apply to each component.
|
||||
The function above uses `first` to find the left-endpoint value and then calls `f`. The composition (through `∘`) implements this.
|
||||
|
||||
Alternatively, `zip` can be avoided with:
|
||||
|
||||
```{julia}
|
||||
mapreduce((l, r) -> l^2, +, Iterators.take(xs, n), Iterators.rest(xs, 1))
|
||||
```
|
||||
|
||||
|
||||
## The dot product
|
||||
|
||||
|
||||
Reference in New Issue
Block a user