Arrays

Arrays are a good example to get some idea of the Julian way of defining interfaces.

Defining arrays

Arrays can be defined by explicitly providing their content:

In [ ]:
i = [1, 2, 3 , 4]    # An integer array
In [ ]:
f = [1.2, 3.4, 2.3, π]  # A float array
In [ ]:
s = ["abc", "def", "ghi"]  # A string array

Notice, that the type of arrays is Array{T, N}, where T is a type and N is the number of dimensions. This type is an example of a parametric type, i.e. a type, which itself is parametrised by other values or types.

Arrays do not need to be of one type only, for example:

In [ ]:
m = [1, 3.4, "abc"]   # A mixed array

In some cases, Julia tries to be smart, however, and converts the types of the elements. For example:

In [ ]:
m = [1, 3.4, 4]    # As soon as there is one float element, it's a float array

One can influence this by explictly denoting the type:

In [ ]:
m = Number[1, 3.4, 4]
In [ ]:
# or
m = Float32[1, 3.4, 4]

Even though all above arrays present as Array, their data layout in memory differs. Arrays with pointer-free element types such as Array{Float64, 1} or Array{Int64, 1} are stored contiguously in memory, but Arrays with mutable types (such as Array{String, 1} and arrays containing abstract types are stored boxed, which means that only a pointer to a different memory location is stored inside the array.

On the interface level Julia does not distinguish between both kind of arrays. Since operations on boxed elements are a lot slower, arrays with abstract types should be avoided in critical parts of a program.

Multidimensional arrays can be defined similarly:

In [ ]:
A = [[1 2 3];    # Define row-wise
      [5 6 7]]
In [ ]:
A = [[1, 5] [2, 6] [3, 7]]   # Define column-wise

Notice the subtle difference to an Array of Arrays

In [ ]:
A = [[1, 5], [2, 6], [3, 7]]

Yet another option is to explicitly filled arrays:

In [ ]:
A = zeros(3, 4, 2)
In [ ]:
@show randn(1, 2)  # Float64
A = randn(Float32, 3, 4, 2)

Vector operations and vectorised operations

Array addition (+, -) and scalar multiplication are directly available on arrays (of any dimension):

In [ ]:
x = [1,2,3]
y = [4,5,6]
In [ ]:
x + 2.0y

For element-wise operations the vectorisation syntax is used:

In [ ]:
x .* y  # elementwise mulitplication
In [ ]:
x .^ y  # Elementwise exponentiation

Note, that the .-syntax continues to functions of the standard library ...

In [ ]:
sqrt.(cos.(2π * x) .+ sin.(2π * x))
In [ ]:
@. sqrt(cos(2π * x) + sin(2π * x))

... custom functions ...

In [ ]:
myrand(x) = rand() * x
myrand.(y)

... and may be easily chained

In [ ]:
@. exp(cos(x^2))

Exercise

Create the following arrays using Julia code: $$\left(\begin{array}{ccccc} 2&2&2&2&2 \\ 2&2&2&2&2 \\ 2&2&2&2&2 \\ \end{array}\right) \qquad \left(\begin{array}{cccc} 0.1&0.5&0.9&1.3\\ 0.2&0.6&1.0&1.4\\ 0.3&0.7&1.1&1.5\\ 0.4&0.8&1.2&1.6\\ \end{array}\right) $$

Accessing array elements

Array elements can be indexed individually ...

In [ ]:
A[1, 2, 1] = 15
@show A[1, 2, 1]
@show A[2, 2, 2];

... or using ranges

In [ ]:
A[2:end, 1, :] = ones(2, 2)   # : is full range, 2:end means ignore 1st
@show A[2:end, 1, :]
@show A[1, 1, :];

It should be noted that Arrays in Julia are stored in column-major order (like in MATLAB, FORTRAN and R) and that indices start with 1:

In [ ]:
vec = collect(1:6)     # Collect all numbers 1 to 6 in a Vector
@show vec
A = reshape(vec, 2, 3) # Reshape the result
In [ ]:
for i in 1:length(A)
    # Iterate through the storage in memory order
    println(i, " ", A[i])
end

Basic functions

In [ ]:
A = randn(Float32, 4, 5)
In [ ]:
ndims(A)    # Get the number of dimensions
In [ ]:
eltype(A)   # Get the type of the array elements
In [ ]:
length(A)   # Return the number of elements
In [ ]:
size(A)     # Get the size of the array
In [ ]:
size(A, 1)  # Get the size along an axis
In [ ]:
reshape(A, 2, 5, 2)   # Return an array with the shape changed

Exercise: Propagating a particle in 2D

We want improve our capabilities to perform simulations to two dimensions using the 2D harmonic potential $$ V\left( \begin{array}{c} x_1 \\ x_2 \end{array}\right) = x_1^2 + x_2^2 $$ and the acceleration map $$ \vec{A}_V = - \nabla V. $$ The adapted forward-Euler scheme is $$\left\{\begin{array}{l} \vec{v}^{(n+1)} = \vec{v}^{(n)} + \vec{A}_V(\vec{x}^{(n)}) \Delta t\\ \vec{x}^{(n+1)} = \vec{x}^{(n)} + \vec{v}^{(n)} \Delta t\\ \end{array}\right. .$$

Change the euler function we introduced in 02_Functions_Types_Dispatch.ipynb accordingly (Hint: Suprisingly few changes are needed) and run the dynamics for five steps using $$x_n = \left( \begin{array}{c} 0 \\ 0 \end{array}\right) \qquad v_n = \left( \begin{array}{c} 1 \\ 0 \end{array}\right) .$$ Check against your previous implementation.

In [ ]:
# You're code here

Adding elements to arrays

Julia provides the push! and append! functions to add additional elements to an existing array. For example:

In [ ]:
A = Vector{Float64}()  # Create an empty Float64 array
In [ ]:
push!(A, 4.)
In [ ]:
append!(A, [5, 6, 7])

Notice, that the ! is part of the name of the function. In Julia the ! is a convention to indicate that the respective function mutates the content of at least one of the passed arrays.

Automatically deducing element types

Very helpful functions for type-generic code in Julia are:

  • zero, which allocates an array of zeros of the same element type
In [ ]:
A = randn(Float32, 3, 4)
zero(A)
  • similar, which returns an uninitialised array, which is similar to the passed array. This means that by default array type, element type and size are all kept.
In [ ]:
similar(A)
  • One may also change these parameters easily:
In [ ]:
similar(A, (3, 2))            # Keep element type and array type
In [ ]:
similar(A, Float64)           # Change element type
In [ ]:
similar(A, Float64, (1, 2))   # Change element type and shape

Comprehensions

A syntax Julia borrowed from Python are comprehensions. For example:

In [ ]:
arr = randn(5)
In [ ]:
[e for e in arr if e > 0]