english improved

This commit is contained in:
2026-03-05 20:09:16 +01:00
parent c6609d15f5
commit 733fe8c290
21 changed files with 954 additions and 1042 deletions

View File

@@ -7,17 +7,34 @@ engine: julia
#| echo: false
#| output: false
using InteractiveUtils
import QuartoNotebookWorker
Base.stdout = QuartoNotebookWorker.with_context(stdout)
myactive_module() = Main.Notebook
Base.active_module() = myactive_module()
##import QuartoNotebookWorker
##Base.stdout = QuartoNotebookWorker.with_context(stdout)
##myactive_module() = Main.Notebook
##Base.active_module() = myactive_module()
#struct M a::Int end; x = M(22); @show x
#should not print "Main.Notebook.M(22)" but only "M(22)"
function Base.show(io::IO, x::T) where T
if parentmodule(T) == @__MODULE__
# Print "TypeName(fields...)" without module prefix
print(io, nameof(T), "(")
fields = fieldnames(T)
for (i, f) in enumerate(fields)
print(io, getfield(x, f))
i < length(fields) && print(io, ", ")
end
print(io, ")")
else
invoke(Base.show, Tuple{IO, Any}, io, x)
end
end
```
# Vectors, Matrices, Arrays
## General
Let us now turn to the probably most important containers for numerical mathematics:
We now turn to containers important for numerical computing:
- Vectors `Vector{T}`
- Matrices `Matrix{T}` with two indices
@@ -30,7 +47,7 @@ In fact, `Vector{T}` is an alias for `Array{T,1}` and `Matrix{T}` is an alias fo
Vector{Float64} === Array{Float64,1} && Matrix{Float64} === Array{Float64,2}
```
When created by an explicit element list, the 'greatest common type' for the type parameter `T` is determined.
When created from an explicit element list, Julia determines the greatest common type for the type parameter `T`.
```{julia}
v = [33, "33", 1.2]
@@ -59,8 +76,8 @@ for f ∈ (length, eltype, ndims, size)
end
```
- The strength of the 'classical' array for scientific computing lies in the fact that it is simply a contiguous memory segment in which components of the same length (e.g. 64 bits) are stored sequentially. This makes the memory requirement minimal and the access speed to a component, both when reading and when modifying, maximal. The location of the component `v[i]` can be calculated immediately from `i`.
- Julia's `Array{T,N}` (and therefore vectors and matrices) is implemented in this way for the usual numeric types `T`. The elements are stored *unboxed*. In contrast, for example, a `Vector{Any}` is implemented as a list of addresses of objects *(boxed)* and not as a list of the objects themselves.
- The strength of 'classical' arrays for scientific computing is that they are simply contiguous memory segments where same-length components (e.g., 64 bits) are stored sequentially. This minimizes memory requirements and maximizes access speed for both reading and modifying components. The location of `v[i]` can be calculated directly from `i`.
- Julia's `Array{T,N}` (including vectors and matrices) is implemented this way for standard numeric types `T`. Elements are stored *unboxed*. In contrast, `Vector{Any}` is implemented as a list of object addresses *(boxed)*, not as a list of the objects themselves.
- Julia's `Array{T,N}` stores its elements directly *(unboxed)*, when `isbitstype(T) == true`.
```{julia}
@@ -71,12 +88,12 @@ isbitstype(String)
## Vectors
### List-like Functions
### Stack-like Functions
- `push!(vector, items...)` --- appends elements at the end of the vector
- `pushfirst!(vector, items...)` --- prepends elements at the beginning of the vector
- `pop!(vector)` --- removes the last element and returns it as a result,
- `popfirst!(vector)` --- removes the first element and returns it
- `push!(vector, items...)` --- appends elements to the end
- `pushfirst!(vector, items...)` --- prepends elements to the beginning
- `pop!(vector)` --- removes and returns the last element
- `popfirst!(vector)` --- removes and returns the first element
```{julia}
v = Float64[] # empty Vector{Float64}
@@ -90,41 +107,41 @@ println("a= $a")
push!(v, 17)
```
A `push!()` can be very expensive, as new memory may need to be allocated and then the entire existing vector needs to be copied. Julia optimizes the memory management. In such a case, memory is allocated in advance, so that further `push!`s are very fast and one 'almost achieves O(1) speed'.
A `push!()` operation can be expensive, as it may require allocating new memory and copying the existing vector. Julia optimizes memory by preallocating space, so subsequent `push!` operations are very fast, almost achieving O(1) speed.
However, one should avoid operations like `push!()` or `resize()` in time-critical code and with very large arrays.
Avoid `push!()` or `resize()` in time-critical code with very large arrays.
### Further Constructors
One can create vectors with a given length and type uninitialized. This is fastest, the elements are random bit patterns.
You can create uninitialized vectors of a given length and type. This is the fastest method; elements contain random bit patterns.
```{julia}
# fixed length 1000, uninitialized
# Uninitialized vector of length 1000
v = Vector{Float64}(undef, 1000)
v[345]
```
- `zeros(n)` creates a `Vector{Float64}` of length `n` and initializes with zero.
- `zeros(n)` creates a `Vector{Float64}` of length `n`, initialized with zeros.
```{julia}
v = zeros(7)
```
- `zeros(T,n)` creates a zero vector of type `T`.
- `zeros(T,n)` creates a zero vector of type `T`:
```{julia}
v=zeros(Int, 4)
```
- `fill(x, n)` creates a `Vector{typeof(x)}` of length `n` and fills with `x`.
- `fill(x, n)` creates a `Vector{typeof(x)}` of length `n`, filled with `x`:
```{julia}
v = fill(sqrt(2), 5)
```
- `similar(v)` creates an uninitialized vector of the same type and size as `v`.
- `similar(v)` creates an uninitialized vector of the same type and size as `v`:
```{julia}
w = similar(v)
@@ -134,7 +151,7 @@ w = similar(v)
### Construction by Implicit Loop _(list comprehension)_
Implicit `for` loops are another method to create vectors.
Implicit `for` loops provide another method to create vectors.
```{julia}
v4 = [i for i in 1.0:8]
```
@@ -144,7 +161,7 @@ v4 = [i for i in 1.0:8]
v5 = [log(i^2) for i in 1:4 ]
```
One can even insert an `if` clause.
You can even include an `if` clause.
```{julia}
v6 = [i^2 for i in 1:8 if i%3 != 2]
@@ -152,24 +169,23 @@ v6 = [i^2 for i in 1:8 if i%3 != 2]
### Bit Vectors {#sec-bitvec}
Besides `Vector{Bool}`, there is also the special data type `BitVector` (and more generally `BitArray`) for storing arrays of truth values.
Besides `Vector{Bool}`, Julia provides the `BitVector` data type (and more generally `BitArray`) for storing boolean arrays.
While one byte is used for storing a `Bool`, storage in a BitVector is done bit by bit.
While a `Bool` uses one byte, `BitVector` stores values bit by bit.
The constructor converts a
`Vector{Bool}` into a `BitVector`.
The constructor converts a `Vector{Bool}` to a `BitVector`:
```{julia}
vb = BitVector([true, false, true, true])
```
For the reverse direction, there is `collect()`.
Use `collect()` for the reverse conversion:
```{julia}
collect(vb)
```
Bit vectors are produced, for example, as the result of element-wise comparisons (s. @sec-broadcast).
Bit vectors are produced, for example, by element-wise comparisons (see @sec-broadcast):
```{julia}
v4 .> 3.5
@@ -179,17 +195,17 @@ v4 .> 3.5
### Indexing
Indices are ordinal numbers. Therefore, __indexing starts at 1.__
Indices are ordinal numbers, so __indexing starts at 1.__
As an index, one can use:
Valid indices include:
- Integer
- Integer-valued range (same length or shorter)
- Integer vector (same length or shorter)
- Boolean vector or BitVector (same length)
- an integer
- an integer-valued range (same length or shorter)
- an integer vector (same length or shorter)
- a boolean vector or BitVector (same length)
With indices, one can read and write array elements/parts.
Using indices, we can read and write array elements/parts.
```{julia}
@@ -208,7 +224,7 @@ v
```
Exceeding the index limits leads to a `BoundsError`.
Exceeding index limits throws a `BoundsError`.
```{julia}
@@ -238,7 +254,7 @@ v
#### Indirect Indexing
The indirect indexing with a *Vector of Integers/Indices* follows the formula
Indirect indexing with a *vector of integers* follows the formula
$v[ [i_1,\ i_2,\ i_3,...]] = [\ v[i_1],\ v[i_2],\ v[i_3],...]$
@@ -248,7 +264,7 @@ $v[ [i_1,\ i_2,\ i_3,...]] = [\ v[i_1],\ v[i_2],\ v[i_3],...]$
v[ [1, 3, 4] ]
```
is therefore equal to
which is the same as
```{julia}
[ v[1], v[3], v[4] ]
```
@@ -259,18 +275,16 @@ is therefore equal to
#### Indexing with a Vector of Truth Values
As an index, one can also use a `Vector{Bool}` or `BitVector` (s. @sec-bitvec) **of the same length**.
You can also use a `Vector{Bool}` or `BitVector` (see @sec-bitvec) **of the same length** as an index.
```{julia}
v[ [true, true, false, false, true, false, true, true] ]
```
This is useful as one can, for example,
- broadcast tests (s. @sec-broadcast),
- these tests then produce a BitVector and
- Bit vectors can be combined with bitwise operators `&` and `|` as needed.
This is useful for:
- broadcast tests (see @sec-broadcast) which produce a `BitVector`, and
- combining `BitVector`s with bitwise operators `&` and `|`.
```{julia}
@@ -282,7 +296,7 @@ v[ (v .> 13) .& (v.<20) ]
## Matrices and Arrays
The methods presented so far for vectors also apply to higher-dimensional arrays.
Most methods for vectors also apply to higher-dimensional arrays.
One can create them uninitialized:
@@ -290,12 +304,12 @@ One can create them uninitialized:
A = Array{Float64,3}(undef, 6,9,3)
```
In most functions, the dimensions can also be passed as a tuple. The above instruction can also be written as:
In most functions, the dimensions can also be passed as a tuple; the above can be written as:
```julia
A = Array{Float64, 3}(undef, (6,9,3))
```
Functions like `zeros()` etc. of course also work.
Functions like `zeros()` etc. also work.
```{julia}
m2 = zeros(3, 4, 2) # or zeros((3,4,2))
@@ -315,7 +329,7 @@ M2 = similar(M, Float64)
### Construction by Explicit Element List
While vectors are noted in square brackets separated by commas, the notation for higher-dimensional objects is somewhat different.
While vectors are written in square brackets separated by commas, the notation for higher-dimensional objects is somewhat different.
- A matrix:
@@ -345,7 +359,7 @@ M3 = [2 3 -1
M2 = [2;4;; 3;5;; -1;-2]
```
In the last example, these rules apply:
Here, the following rules apply:
- The separator is the semicolon.
- A semicolon `;` increases the 1st index.
@@ -354,14 +368,14 @@ In the last example, these rules apply:
In the previous examples, the following syntactic enhancement (_syntactic sugar_) was applied:
- Spaces separate like 2 semicolons -- thus also increase the 2nd index: $\quad a_{12}\quad a_{13}\quad a_{14}\ ...$
- Newline separates like a semicolon -- thus also increases the 1st index.
- Spaces separate like two semicolons -- thus increasing the 2nd index: $\quad a_{12}\quad a_{13}\quad a_{14}\ ...$
- Newline separates like a semicolon -- thus increasing the 1st index.
:::{.callout-important}
- Vector notation with comma as separator only works for vectors, do not mix "semicolon, space, newline"!
- Vectors, $1\!\times\!n$-matrices, and $n\!\times\!1$-matrices are three different things!
- Vector notation with comma as separator only works for vectors -- do not mix "semicolon, space, newline".
- Vectors, $1\!\times\!n$-matrices, and $n\!\times\!1$-matrices are three different things.
```{julia}
@@ -398,7 +412,7 @@ v = [[2,3,4], [5,6,7,8]]
v[2][3]
```
One should only do this in special cases. The array language of Julia is usually more convenient and faster.
You should only do this in special cases. The array notation in Julia is usually more convenient and faster.
### Indices, Subarrays, Slices
@@ -415,7 +429,7 @@ A[2, 3] = 77.77777
A
```
One can use ranges to address subarrays:
You can use ranges to address subarrays:
```{julia}
B = A[1:2, 1:3]
@@ -436,10 +450,10 @@ C = A[:, 3]
E = A[3, :]
```
Of course, assignments are also possible:
Slicing can also used on the right hand side for assignments:
```{julia}
# One can also assign to slices and subarrays
# You can also assign to slices and subarrays
A[2, :] = [1,2,3,4,5,6]
A
@@ -473,7 +487,7 @@ B[3] = 300
```
This behavior saves a lot of time and memory, but is not always desired.
The function `copy()` creates a 'real' copy of the object.
The function `copy()` creates a true copy of the object.
```{julia}
A = [1, 2, 3]
@@ -483,7 +497,7 @@ A[1] = 100
```
The function
`deepcopy(A)` copies recursively. Copies are also created for the elements from which `A` consists.
`deepcopy(A)` copies recursively. It also creates copies of the elements that A contains.
As long as an array only contains primitive objects (numbers), `copy()` and `deepcopy()` are equivalent.
@@ -510,7 +524,7 @@ A[3].age = 199
### Views
When one assigns a piece of an array to a variable using *indices/ranges/slices*,
Julia fundamentally **constructs a new object**.
Julia **constructs a new object**.
```{julia}
@@ -525,11 +539,11 @@ A[1, 2] = 77
```
Sometimes, however, one wants exactly this reference semantics in the sense of: "Vector `v` should be the 2nd column vector of `A` and should also remain so (i.e., change if `A` changes)."
Sometimes, however, one wants reference semantics in the sense of: "Vector `v` should be the 2nd column vector of `A` and should also remain so (i.e., change if `A` changes)."
This is called *views* in Julia: We want the variable `v` to represent only an 'alternative view' of the matrix `A`.
This is called a *view* in Julia: We want the variable `v` to represent only an 'alternative view' of the matrix `A`.
This can be achieved with the `@view` macro:
It can be achieved with the `@view` macro:
```{julia}
A = [1 2 3
@@ -549,9 +563,8 @@ An example is the operator `'`, which delivers the adjoint matrix `A'` to a matr
- The adjoint matrix `A'` is the transposed and element-wise complex-conjugated matrix to `A`.
- The parser converts this to the function call `adjoint(A)`.
- For real matrices, the adjoint is equal to the transposed matrix.
- Julia implements `adjoint()` as a _lazy function_, i.e.,
- for efficiency reasons, no new object is constructed, but only an alternative 'view' of the matrix ("with swapped indices") and an alternative 'view' of the entries (with sign change in the imaginary part).
- From vectors, `adjoint()` makes a $1\!\times\!n$-matrix (a row vector).
- Julia implements `adjoint()` as a _lazy function_, i.e., for efficiency reasons no new object is constructed. The method provides an alternative 'view' of the matrix (with swapped indices) and an alternative 'view' of the entries (with sign change in the imaginary part).
- The adjoint of a vector produces a $1\!\times\!n$ matrix (row vector).
```{julia}
@@ -560,7 +573,7 @@ A = [ 1. 2.
B = A'
```
The matrix `B` is only a modified 'view' of `A`:
The matrix `B` is just a modified 'view' of `A`:
```{julia}
A[1, 2] =10
@@ -576,10 +589,10 @@ v = [1, 2, 3]
v'
```
Another such function, which provides an alternative 'view', a different indexing, of the same data
Another such function, which provides an alternative 'view', a different indexing of the same data
is `reshape()`.
Here, a vector with 12 entries is transformed into a 3x4 matrix.
Here, a vector with 12 entries is transformed into a 3x4 matrix:
```{julia}
A = [1,2,3,4,5,6,7,8,9,10,11,12]
@@ -635,22 +648,23 @@ using BenchmarkTools
### Locality of Memory Access and _Caching_
We have seen that the order of inner and outer loops makes a significant speed difference:
We have observed that the order of inner and outer loops significantly affects computational efficiency:
__It is more efficient when the innermost loop traverses the left index__, i.e., a column and not a row. The cause of this lies in the architecture of modern processors.
It is more efficient when the innermost loop iterates over the left index, i.e., a column rather than a row. This is due to the architecture of modern processors.
- Memory access involves multiple cache levels.
- A _cache miss_, which necessitates reloading from slower caches, slows down processing.
- To minimize the frequency of _cache misses_, processors reload larger memory blocks.
- Consequently, it is crucial to organize memory access as locally as possible.
- Memory access occurs over multiple cache levels.
- A _cache miss_, which triggers a reload from slower caches, slows down the process.
- Larger memory blocks are always reloaded to minimize the frequency of _cache misses_.
- Therefore, it is important to organize memory access as locally as possible.
::: {.content-visible when-format="html"}
![Memory hierarchy of Intel processors, from: Victor Eijkhout,_Introduction to High-Performance Scientific Computing_, [https://theartofhpc.com/](https://theartofhpc.com/)](../images/cache.png){width="75%"}
![Memory hierarchy of Intel processors, from: Victor Eijkhout, _Introduction to High-Performance Scientific Computing_, [https://theartofhpc.com/](https://theartofhpc.com/)](../images/cache.png){width="75%"}
:::
::: {.content-visible when-format="pdf"}
![Memory hierarchy of Intel processors, from: Victor Eijkhout,_Introduction to High-Performance Scientific Computing_, [https://theartofhpc.com/](https://theartofhpc.com/)](../images/cache.png){width="70%"}
::: {.content-visible when-format="typst"}
![Memory hierarchy of Intel processors, from: Victor Eijkhout, _Introduction to High-Performance Scientific Computing_, [https://theartofhpc.com/](https://theartofhpc.com/)](../images/cache.png){width="70%"}
:::
@@ -739,13 +753,12 @@ v * w'
## Broadcasting {#sec-broadcast}
- With _broadcasting_, operations or functions are applied __element-wise__ to arrays.
- The syntax for this is a dot _before_ an operator or _after_ a function name.
- The parser converts `f.(x,y)` to `broadcast(f, x, y)` and similarly for operators `x .⊙ y` to `broadcast(⊙, z, y)`.
- Operands that are missing one or more dimensions are virtually replicated in those dimensions.
- The *broadcasting* of assignments `.=`, `.+=`,... changes the semantics. No new object is created, but the values are entered into the object on the left side (which must have the correct dimension).
- With _broadcasting_, operations or functions are applied element-wise to arrays.
- Broadcasting is indicated by a dot preceding an operator or following a function name.
- The parser translates `f.(x,y)` into `broadcast(f, x, y)`, and similarly, `x .⊙ y` into `broadcast(⊙, x, y)`.
- Operands lacking one or more dimensions are virtually replicated in those dimensions.
- Broadcasting assignments such as `.=`, `.+=`, etc., alter the semantics by modifying values directly within the left-side object (which must have the appropriate dimensions) without creating a new object.
- With _broadcasting_, operations or functions are applied element-wise to arrays.
Some examples:
@@ -767,7 +780,7 @@ sqrt.(A)
A.^2
```
- For comparison, the result of the algebraic operations:
- For comparison, here the results of the algebraic operations:
```{julia}
@show A^2 A^(1/2);
```
@@ -784,10 +797,9 @@ hyp.(A, B)
When operands have different dimensions, the operand with missing dimensions is virtually
'grown' by replication in these dimensions.
When operands possess differing dimensions, the operand with fewer dimensions is effectively expanded through replication along those dimensions.
We add a scalar to a matrix:
Adding a scalar to a matrix:
```{julia}
A = [ 1 2 3
4 5 6]
@@ -798,15 +810,12 @@ A = [ 1 2 3
A .+ 300
```
The scalar was replicated to the same dimension as the matrix by replication. We let `broadcast()` show the form of the 2nd operand after *broadcasting*:
```{julia}
The scalar was replicated to match the matrix dimensions. Let `broadcast()` illustrate the form of the second operand after broadcasting:
```
broadcast( (x,y) -> y, A, 300)
```
(Of course, this replication only takes place virtually. This object is not really created for other operations.)
As another example: Matrix and (column-)vector
(This replication occurs solely in a virtual sense; the object is not actually instantiated in memory.)
```{julia}
A .+ [10, 20]
@@ -824,7 +833,7 @@ Matrix and row vector: The row vector is repeated row-wise:
A .* [1,2,3]' # adjoint vector
```
The 2nd operand is 'grown' by `broadcast()` through replication of rows.
The 2nd operand is grown by `broadcast()` through replication of rows.
```{julia}
broadcast((x,y)->y, A, [1,2,3]')
@@ -833,11 +842,11 @@ broadcast((x,y)->y, A, [1,2,3]')
#### _Broadcasting_ in Assignments
Assignments `=`, `+=`, `/=`,..., where a name is on the left side, work in Julia such that an object is constructed from the right side and assigned this new name.
Assignments such as `=`, `+=`, `/=`,... in Julia involve constructing an object on the right-hand side and assigning it to a variable on the left-hand side.
However, when working with arrays, one often wants to reuse an existing array for efficiency reasons. The values calculated on the right side should be entered into the already existing object on the left side.
When working with arrays, efficiency often requires reusing existing array objects. The values computed on the right-hand side are then stored directly into the pre-existing object on the left-hand side.
This is achieved with the broadcast variants `.=`, `.+=`,... of the assignment operators.
This is accomplished using broadcast variants of assignment operators: `.=`, `.+=`,....
```{julia}
A .= 3