STFC-Rutherford Appleton Laboratory, England
Thursday December 8, 2022
Alexis Montoison (alexis.montoison@polymtl.ca)
You can execute a julia script from the command line
julia script.jl
To pass command line arguments
julia script.jl arg1 arg2 arg3
Command-line arguments are accessed within julia via the variable ARGS
.
Julia is the "Ju" in "Jupyter"!
To run Julia in a notebook:
IJulia
package(if you skip the Jupyter installation, Julia will install a julia-only conda
distribution)
Booleans
true
true
typeof(true)
Bool
Integers
1 + 1
2
typeof(1)
Int64
Floating point
1.0 + 2.0
3.0
typeof(1.0)
Float64
Floating-point arithmetic has limited precision!
(1.0 + 1e-16) == 1.0
true
(1.0 + 1e-16) - 1e-16 == 1.0 + (1e-16 - 1e-16)
false
Type correspondence:
Julia | C |
---|---|
Int32 | int |
Int64 | long |
Float32 | float |
Float64 | double |
Strings are defined with double quotes
"Hello world!"
"Hello world!"
Concatenate strings with *
"Hello" * " " * "world!"
"Hello world!"
String interpolation eases the need for concatenation
x, y, z = "Alice", "Bob", "Charles"
"Hello $(x), $y and $(z)!"
"Hello Alice, Bob and Charles!"
Symbols are human-readable unique identifiers
:symbol
typeof(:symbol)
Symbol
Tuples are immutable collections of values
t1 = (1, 2, 3)
t2 = ("hello", 1, :x)
("hello", 1, :x)
typeof(t1)
Tuple{Int64, Int64, Int64}
typeof(t2)
Tuple{String, Int64, Symbol}
Arrays are collections of values of the same type that are stored contiguously in memory.
u = [1, 2, 3]
3-element Vector{Int64}: 1 2 3
typeof(u)
Vector{Int64} (alias for Array{Int64, 1})
/!\ Array indexing starts at 1 /!\
u[1]
1
Elements automatically get promoted to the same type if necessary
v = [1.0, 2, 3]
3-element Vector{Float64}: 1.0 2.0 3.0
typeof(v)
Vector{Float64} (alias for Array{Float64, 1})
If no common type can be inferred, the element type is Any
w = ["hello", 1, :x]
3-element Vector{Any}: "hello" 1 :x
typeof(w)
Vector{Any} (alias for Array{Any, 1})
Dictionnaries
d = Dict("a" => 1, "b" => 2, "c" => 3)
Dict{String, Int64} with 3 entries: "c" => 3 "b" => 2 "a" => 1
d["a"]
1
d["a"] = 2
d
Dict{String, Int64} with 3 entries: "c" => 3 "b" => 2 "a" => 2
d["d"] = 4
4
if x == 0
println("Hello 0!")
elseif x == 1
println("Hello 1!")
elseif x == -1
println("Hello -1!")
else
println("Hello x")
end
Hello x
for i in 1:5
println("Hello $(i)!")
end
Hello 1! Hello 2! Hello 3! Hello 4! Hello 5!
Iterate over arrays
u = [1, 4, 9, 16]
for x in u
println(x)
end
1 4 9 16
Iterate over dictionnaries
d = Dict("a" => 1, "b" => 2, "c" => 3)
for (key, val) in d
println("Key: $key, val: $val")
end
Key: c, val: 3 Key: b, val: 2 Key: a, val: 1
More generally: iterate over any collection
Build a list of increasing integers
u = [i for i in 1:4]
4-element Vector{Int64}: 1 2 3 4
v = [1 for _ in 1:4]
4-element Vector{Int64}: 1 1 1 1
Build a matrix
A = [i*j for i in 1:4, j in 1:4]
4×4 Matrix{Int64}: 1 2 3 4 2 4 6 8 3 6 9 12 4 8 12 16
Filter out some values
v = [i for i in 1:10 if i % 2 == 1] # odd integers between 1 and 10
5-element Vector{Int64}: 1 3 5 7 9
Dictionnaries
d = Dict("$i" => i^2 for i in 1:5)
Dict{String, Int64} with 5 entries: "4" => 16 "1" => 1 "5" => 25 "2" => 4 "3" => 9
To define a function:
function print_hello()
println("Hello world!")
end
print_hello()
Hello world!
Functions can have arguments
function print_it(x, y)
println(x)
println(y)
end
print_it("Hello", 1)
print_it([1, 2], 47.0)
Hello 1 [1, 2] 47.0
Optional arguments are possible
function print_info(x; prefix="Value : ")
println("$(prefix)$x")
end
print_info(3.1415)
print_info(3.1415, prefix="π = ")
Value : 3.1415 π = 3.1415
The return value of a function is specified with the keyword return
function my_mul(x, y)
return x * y
end
my_mul(1, 2)
2
Otherwise, the function returns the result of the last expression
Docstrings are written before the body of the function, and start/end with """
.
You can use Markdown syntax within docstrings.
"""
my_mul(x, y, z)
Compute the product `x*y*z`
"""
function my_mul(x, y, z)
return x * y * z
end
my_mul
Always write docstrings!
You can display a function's docstring by pre-prending ?
to its name
?my_mul
search: my_mul
my_mul(x, y, z)
Compute the product x*y*z
Everything in Julia has a type.
You don't need to specify types when declaring variables or function arguments... but sometimes it helps.
To avoid errors
"""
my_fact(n)
Compute the factorial of `n`, i.e., `1*2*...*(n-1)*n`.
"""
function my_fact(n)
if n == 1
return 1
else
return n * my_fact(n - 1)
end
end
my_fact(10)
3628800
my_fact(1.5)
StackOverflowError: Stacktrace: [1] my_fact(n::Float64) (repeats 79984 times) @ Main ./In[307]:10
abstract type AbstractFoo end
Abstract types
AbstractFoo()
MethodError: no constructors have been defined for AbstractFoo Stacktrace: [1] top-level scope @ In[310]:1 [2] eval @ ./boot.jl:368 [inlined] [3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String) @ Base ./loading.jl:1428
are called struct
in Julia (like C)
struct Foo
x
end
foo = Foo(1.0)
Foo(1.0)
You cannot modify the attributes of an immutable
type
foo.x = 2
setfield!: immutable struct of type Foo cannot be changed Stacktrace: [1] setproperty!(x::Foo, f::Symbol, v::Int64) @ Base ./Base.jl:39 [2] top-level scope @ In[313]:1 [3] eval @ ./boot.jl:368 [inlined] [4] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String) @ Base ./loading.jl:1428
are struct
s whose attributes can be modified.
mutable struct FooBar
x
end
foo = FooBar(1.0)
typeof(foo)
FooBar
foo.x = 2
2
Types can be parametrized by other types
Vector
Vector (alias for Array{T, 1} where T)
You can parametrize by multiple types
struct MyFoo{Ti<:Integer, Tv<:AbstractFloat}
i::Ti
v::Tv
end
typeof(MyFoo(1, 1.0))
MyFoo{Int64, Float64}
typeof(MyFoo(0x01, 1.0f0))
MyFoo{UInt8, Float32}
Functions are not "attached" to classes like in object-oriented languages.
Functions have methods that are dispatched based on the types of all arguments: it's called multiple dispatch
@which +(1.0, 1.0)
@which +(1.0, 1)
+
+ (generic function with 206 methods)
methods(+)
To use linear algebra functions, use the LinearAlgebra
package (part of the standard library)
using LinearAlgebra
A vector is a uni-dimensional array
Vector{Float32}
Vector{Float32} (alias for Array{Float32, 1})
x = [1.0, 2.0]
2-element Vector{Float64}: 1.0 2.0
A matrix is a two-dimensional array
Matrix{Float64}
Matrix{Float64} (alias for Array{Float64, 2})
A = [
1.0 2.0;
3.0 4.0
]
2×2 Matrix{Float64}: 1.0 2.0 3.0 4.0
Matrix-vector products work out of the box
A * x # mul!(y, A, x) for an in-place variant
2-element Vector{Float64}: 5.0 11.0
Solve linear systems
b = A * x
y = A \ b
2-element Vector{Float64}: 1.0 2.0
You can't multiply two vectors
x * x
MethodError: no method matching *(::Vector{Float64}, ::Vector{Float64}) Closest candidates are: *(::Any, ::Any, ::Any, ::Any...) at operators.jl:591 *(::StridedMatrix{T}, ::StridedVector{S}) where {T<:Union{Float32, Float64, ComplexF32, ComplexF64}, S<:Real} at ~/julia/julia-1.8.3/share/julia/stdlib/v1.8/LinearAlgebra/src/matmul.jl:49 *(::StridedVecOrMat, ::Adjoint{<:Any, <:LinearAlgebra.LQPackedQ}) at ~/julia/julia-1.8.3/share/julia/stdlib/v1.8/LinearAlgebra/src/lq.jl:269 ... Stacktrace: [1] top-level scope @ In[331]:1 [2] eval @ ./boot.jl:368 [inlined] [3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String) @ Base ./loading.jl:1428
But inner and outer product is OK
x' * x # inner product
5.0
x * x' # outer product
2×2 Matrix{Float64}: 1.0 2.0 2.0 4.0
For real/complex numbers in Float32
, Float64
precision:
You can also call BLAS/LAPACK functions directly
C = zeros(2, 2);
A = ones(2, 4);
BLAS.syrk!('U', 'N', 1.0, A, 0.0, C) # computes C = α A*A' + β C
2×2 Matrix{Float64}: 4.0 4.0 0.0 4.0
using SparseArrays
A = sparse([1, 2, 3], [1, 2, 3], [1.0, 1.0, 1.0])
3×3 SparseMatrixCSC{Float64, Int64} with 3 stored entries: 1.0 ⋅ ⋅ ⋅ 1.0 ⋅ ⋅ ⋅ 1.0
Matrix(A)
3×3 Matrix{Float64}: 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0
Sparse factorization call the CHOLMOD
module from SuiteSparse
.
Community wrappers to MUMPS, Pardiso, HSL linear solvers.
Julia's package manager is Pkg
(https://julialang.github.io/Pkg.jl/v1/).
using Pkg
Packages are organized into environments.
An environment is a set of packages.
Install a package with Pkg.add
Pkg.add("HSL")
Resolving package versions... Installed HSL ─ v0.3.5 Updating `~/Bureau/Angleterre/presentation/Project.toml` [34c5aeac] + HSL v0.3.5 Updating `~/Bureau/Angleterre/presentation/Manifest.toml` [34c5aeac] + HSL v0.3.5 [40b5814e] + METIS4_jll v400.0.301+0 ⌅ [656ef2d0] + OpenBLAS32_jll v0.3.17+0 Info Packages marked with ⌅ have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated -m` Building HSL → `~/.julia/scratchspaces/44cfe95a-1eb2-52ea-b672-e2afdf69b78f/7fecf9d7da2d5442a966b721952e0f4f3d8ad833/build.log` Precompiling project... ✓ HSL 1 dependency successfully precompiled in 2 seconds. 176 already precompiled.
Update a package
Pkg.update("HSL")
Updating registry at `~/.julia/registries/General.toml` No Changes to `~/Bureau/Angleterre/presentation/Project.toml` No Changes to `~/Bureau/Angleterre/presentation/Manifest.toml`
Remove a package
Pkg.rm("HSL")
Updating `~/Bureau/Angleterre/presentation/Project.toml` [34c5aeac] - HSL v0.3.5 Updating `~/Bureau/Angleterre/presentation/Manifest.toml` [34c5aeac] - HSL v0.3.5 [40b5814e] - METIS4_jll v400.0.301+0 [656ef2d0] - OpenBLAS32_jll v0.3.17+0
Project.toml
¶The project file describes the project on a high level, for example the package/project dependencies and compatibility constraints are listed in the project file.
The Project.toml
file contains the list of all packages in that environment and compatibility requirements (if any).
; cat Project.toml
[deps] DoubleFloats = "497a8b3b-efae-58df-a0af-a86822472b78" HarwellRutherfordBoeing = "ce388394-9b3f-5993-a911-eb95552e4f2e" ILUZero = "88f59080-6952-5380-9ea5-54057fb9a43f" IncompleteLU = "40713840-3770-5561-ab4c-a76e7d0d7895" Krylov = "ba0b0d4f-ebba-5204-a429-3ac8c609bfb7" LimitedLDLFactorizations = "f5a24dde-3ab7-510b-b81b-6a72c6098d3b" LinearOperators = "5c8ed15e-5a4c-59e4-a42b-c7e8811fb125" MAT = "23992714-dd62-5051-b70f-ba57cb901cac" MatrixMarket = "4d4711f2-db25-561a-b6b3-d35e7d4047d3" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Quadmath = "be4d8f0f-7fa4-5f49-b795-2f01399ab2dd" SuiteSparseMatrixCollection = "ac199af8-68bc-55b8-82c4-7abd6f96ed98" UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228"
Manifest.toml
¶The exact set of packages and versions in an environment is captured in a manifest file which can be checked into a project repository and tracked in version control
The Manifest.toml
file contains the exact version of every installed package, including all hidden dependencies.
If your Manifest.toml
file is the same, your package version are the same.
From Julia: use Pkg.activate
# activate environment located in current folder
Pkg.activate(".")
Activating project at `~/Bureau/Angleterre/presentation`
When using Julia from the command line
julia --project=. script.jl
First download and install all packages
Pkg.activate(".") # activate environment
Pkg.instantiate() # download and install packages
Activating project at `~/Bureau/Angleterre/presentation`
Good to go!
Project.toml
and Manifest.toml
--docs/
--src/
--test/
Project.toml
Manifest.toml
README.md
Code from a different file can be loaded
include("hello.jl")
Hello world! Current time is 1.670461640667626e9s
include("hello.jl")
Hello world! Current time is 1.670461642692941e9s
This causes the contents of the file to be evaluated in the global scope.
Your code gets executed every time you call include
.
You can load packages with import / using
import Krylov
cg
cg (generic function with 2 methods)
Krylov.cg
cg (generic function with 2 methods)
using
allows you to use exported names in the current namespace
using Krylov
cg
cg (generic function with 2 methods)
In Julia code is compiled just-in-time => some compilation happens while you execute your code.
Be careful when timing code execution!
"""Compute the sum of the elements in a vector."""
function my_sum(u::Vector{T}) where{T}
s = zero(T)
for x in u
s += x
end
return s
end
u = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
v = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
The first time a function is called, it is compiled. Subsequent calls are fast.
@time my_sum(u) # First time (with Int64): compilation happens
@time my_sum(u) # Second time: no compilation
0.028128 seconds (4.03 k allocations: 196.912 KiB, 99.86% compilation time) 0.000006 seconds
55
@time my_sum(v) # First time (with Float64): compilation happens
@time my_sum(v) # Second time: no compilation
0.024584 seconds (4.03 k allocations: 196.717 KiB, 99.84% compilation time) 0.000030 seconds (1 allocation: 16 bytes)
55.0
If you only care about the result => compilation doesn't impact you.
If you care about execution time => compilation does impact you.
Possible fix:
Everytime you run
julia --project=. hello.jl
Julia has to re-compile a part of your code.
Make sure what you're timing does not include compilation times!