30  Julia 101

Welcome to Julia 101! Julia from zero to hero (for Scientific Computing)!

Important: the following course may assume you are using a redistributable version of Julia packaged as described here. This might be useful for instructors willing to work without internet access.

In this first course you will be introduced to the Julia programming languange. By the end of the working session (~4h) you will be able to:

Our goal in this course is not to master Julia or even pretend you have learned it; we will open the gates so that you can go out there and solve problems by yourself (or maybe open Pandora’s box…).

This is probably your first time learning Julia. If you got here by other means than the main index, please consider reading the introduction of this document. Next, you can launch a terminal and try running Julia by simply typing julia and pressing return. For this introductory course I assume you are using VS Code for which you might also wish to install the recommended extensions.

30.1 REPL basics

Julia’s REPL (read-eval-print loop) is an interactive environment where you execute code as you write. It’s main goal is to manage Julia, test code snippets, get help, and launch larger chunks of code.

To enter the package manager mode press ]m to get help for a function or module press ?. Shell mode (system dependent) is accessed through ;, but you are discouraged to do so here. To get back to Julia interpreted simply press backspace.

  • Below we inspect the status of our environment from the package manager:
(101) pkg> status
Status `D:\julia101\src\101\Project.toml`
  [13f3f980] CairoMakie v0.12.16
  [a93c6f00] DataFrames v1.7.0
  [c3e4b0f8] Pluto v0.20.3
  [6099a3de] PythonCall v0.9.23
  • In help mode you can check how to use the methods function:
help?> methods
search: methods Method methodswith hasmethod

  methods(f, [types], [module])

  Return the method table for f.

  If types is specified, return an array of methods whose types match. If module is specified, return an
  array of methods defined in that module. A list of modules can also be specified as an array.

  │ Julia 1.4
  │
  │  At least Julia 1.4 is required for specifying a module.

  See also: which, @which and methodswith.

Try by yourself and run add Polynomials from package manager mode (we will need it later); what do you get if you check the status again?

30.2 Julia as a calculator

The following example should speak by itself:

julia> 1 + 1
2

julia> 2 * 3
6

julia> 3 / 4
0.75

julia> 3 // 4
3//4

julia> 3^3
27

The representation of numbers in the computer is not perfect and round-off may be a problem in some cases. The most common numeric types in Julia are illustrated below with help of typeof.

julia> typeof(1)
Int64

julia> typeof(1.0)
Float64

julia> typeof(3/4)
Float64

julia> typeof(3//4)
Rational{Int64}

Being a language conceived for numerical purposes, Julia has built-in support to complex numbers:

julia> 1 + 1im
1 + 1im

julia> typeof(1 + 1im)
Complex{Int64}

Some transcendental constants are also available:

julia> pi
π = 3.1415926535897...

julia> π
π = 3.1415926535897...

julia> typeof(pi)
Irrational{:π}

julia> Base.MathConstants.

catalan     e           eulergamma
eval        golden      include
pi          γ           π
φ           ℯ

julia> Base.MathConstants.e
ℯ = 2.7182818284590...

julia> names(MathConstants)
10-element Vector{Symbol}:
 :MathConstants
 :catalan
 :e
 :eulergamma
 :golden
 :pi
 :γ
 :π
 :φ
 :ℯ

In some cases type conversion is handled automatically:

julia> 1 + 2.0
3.0

Trigonometric functions are also supported:

julia> sin(π/2)
1.0

julia> cos(π/2)
6.123233995736766e-17

julia> tan(π/2)
1.633123935319537e16

And also inverse trigonometric functions:

julia> asin(0)
0.0

julia> acos(0)
1.5707963267948966

julia> atan(Inf)
1.5707963267948966

Hyperbolic functions are not surprisingly built-in:

julia> sinh(1)
1.1752011936438014

Because of how floating point representation works, one must take care when performing equality comparisons:

julia> acos(0) == pi/2
true

julia> sin(2π / 3) == √3 / 2
false

Testing for approximately equal is a better practice here:

julia> sin(2π / 3) ≈ √3 / 2
true

julia> isapprox(sin(2π / 3), √3 / 2)
true

julia> isapprox(sin(2π / 3), √3 / 2; atol=1.0e-16)
false

The smallest number you can add to 1 and get a different result is often called the machine epsilon (although there are more formal definitions). It is a direct consequence of floating point representation and is somewhat related to the above:

julia> 1 + eps()/2
1.0

julia> 1 + eps()
1.0000000000000002

30.3 Vectors and matrices

Let’s start once again with no words:

julia> b = [5, 6]
2-element Vector{Int64}:
 5
 6

julia> A = [1.0 2.0; 3.0 4.0]
2×2 Matrix{Float64}:
 1.0  2.0
 3.0  4.0

julia> x = A \ b
2-element Vector{Float64}:
 -3.9999999999999987
  4.499999999999999

julia> A * x ≈ b
true

If you try to perform vector-vector multiplication you get an error:

julia> b * b
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})
The function `*` exists, but no method is defined for this combination of argument types.

That should be obvious because multiplying a pair of row-vectors makes no sense (in Python NumPy would allow that mathematical horror); in Julia we make things more formally by transposing the first vector to get a column-row pair:

julia> b' * b
61

Element-wise multiplication can also be achieved with .* (same is valid for other operators):

julia> b .* b
2-element Vector{Int64}:
 25
 36

There are a few ways to create equally spaced vectors that might be useful:

julia> 0:1.0:100
0.0:1.0:100.0

julia> collect(0:1.0:100)'
1×101 adjoint(::Vector{Float64}) with eltype Float64:
 0.0  1.0  2.0  3.0  4.0  5.0  6.0  7.0  8.0  9.0  10.0  …  93.0  94.0  95.0  96.0  97.0  98.0  99.0  100.0

julia> LinRange(0, 100, 101)
101-element LinRange{Float64, Int64}:
 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, …, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0

30.4 Representing text

The uninformed think the numerical people need not to know how to manipulate text; that is completely wrong. An important amount of time in the conception of a simulation code or data analysis is simply string manipulation.

In Julia we use double quotes to enclose strings and single quotes for characters. Let’s see a few examples:

julia> "Some text here"
"Some text here"

julia> println("Some text here")
Some text here

julia> print("Some text here")
Some text here
julia>

During a calculation, to write the value of a variable to the standard output one might do the following:

julia> x = 2.3
2.3

julia> println("The value of x = $(x)")
The value of x = 2.3

julia> @info("""
             Strings may be composed of several lines!

             You can also display the value of x = $(x)
             """)
┌ Info: Strings may be composed of several lines!
│
└ You can also display the value of x = 2.3

30.5 Tuples and dictionaries

Tuples are immutable data containers; they consist their own type.

julia> t = ("x", 1)
("x", 1)

julia> typeof(t)
Tuple{String, Int64}

A variant called NamedTuple is also available in Julia:

julia> t = (variable = "x", value = 1)
("x", 1)

julia> typeof(t)
@NamedTuple{variable::String, value::Int64}

On the other hand, dictionaries are mutable mappings:

julia> X = Dict("CH4" => 0.95, "CO2" => 0.03)
Dict{String, Float64} with 2 entries:
  "CH4" => 0.95
  "CO2" => 0.03

julia> sum(values(X))
0.98

julia> X["N2"] = 1.0 - sum(values(X))
0.020000000000000018

julia> X
Dict{String, Float64} with 3 entries:
  "CH4" => 0.95
  "CO2" => 0.03
  "N2"  => 0.02

30.6 A first function

A function can be created in a single line:

julia> normal(x) = (1 / sqrt(2pi)) * exp(-x^2/2)
normal (generic function with 1 method)

julia> normal(1)
0.24197072451914337

julia> normal(3)
0.0044318484119380075

The same name can be used to create different interfaces thanks to multiple dispatch:

julia> normal(x, mu, sigma) =
           exp(-(x - mu)^2 / (2 * sigma^2)) /
           sqrt(2pi * sigma)
normal (generic function with 2 methods)

julia> normal(3, 0, 1)
0.0044318484119380075

For larger functions using the keyword function is the way to go; notice below the introduction of optional keyword arguments after the semicolon:

julia> function normal(x; mu = 0, sigma = 1)
           return exp(-(x - mu)^2 / (2 * sigma^2)) / sqrt(2pi * sigma)
       end
normal (generic function with 2 methods)

julia> normal(3)
0.0044318484119380075

Notice that a better alternative in this case would be to wrap the 3-argument version of normal in the key-word interface as follows:

julia> normal(x; mu = 0, sigma = 1) = normal(x, mu, sigma)
normal (generic function with 2 methods)

julia> normal(3)
0.0044318484119380075

To identify the possible interfaces of a given function, one can inspect its methods:

julia> methods(normal)
# 2 methods for generic function "normal" from Main:
 [1] normal(x; mu, sigma)
     @ REPL[108]:1
 [2] normal(x, mu, sigma)
     @ REPL[106]:1

The macro @which provides the functionality for a given function call:

julia> @which normal(3)
normal(x; mu, sigma)
     @ Main REPL[108]:1

30.7 Using modules

One can import all functionalities from a given module with using ModuleName; that is generally not what we want in complex programs (because of name clashes). If only a few functionalities of a given module will be used, then proceed as follows:

julia> using Statistics: mean

julia> mean(rand(10))
0.4922029043061601

julia> mean(rand(100))
0.5081883767210799

julia> mean(rand(1000))
0.5176285712019737

Alias imports are also supported:

julia> import Statistics as ST

julia> ST.mean(rand(1000))
0.5123135802316784

By the way, we used built-in random number generation in the above example.

30.8 Calling Python

julia> using PythonCall
    CondaPkg Found dependencies: D:\julia101\src\101\CondaPkg.toml
    CondaPkg Found dependencies: D:\julia101\bin\julia-1.11.1-win64\depot\packages\PythonCall\Nr75f\CondaPkg.toml
    CondaPkg Dependencies already up to date

julia> ct = pyimport("cantera")
Python: <module 'cantera' from 'D:\\julia101\\bin\\julia-1.11.1-win64\\CondaPkg\\Lib\\site-packages\\cantera\\__init__.py'>

julia> gas = ct.Solution("gri30.yaml")
Python: <cantera.composite.Solution object at 0x000001659DFC4510>

julia> gas.TPX = 1000, ct.one_atm, "CH4: 1.0, O2: 2.0"
(1000, <py 101325.0>, "CH4: 1.0, O2: 2.0")

julia> gas.equilibrate("HP")
Python: None

julia> println(gas.report())

  gri30:

       temperature   3126.8 K
          pressure   1.0132e+05 Pa
           density   0.080809 kg/m^3
  mean mol. weight   20.734 kg/kmol
   phase of matter   gas

                          1 kg             1 kmol
                     ---------------   ---------------
          enthalpy        1.1826e+05        2.4519e+06  J
   internal energy       -1.1356e+06       -2.3546e+07  J
           entropy             13721         2.845e+05  J/K
    Gibbs function       -4.2786e+07       -8.8711e+08  J
 heat capacity c_p            2171.8             45029  J/K
 heat capacity c_v            1770.8             36715  J/K

                      mass frac. Y      mole frac. X     chem. pot. / RT
                     ---------------   ---------------   ---------------
                H2         0.0078306          0.080534           -23.513
                 H         0.0031683          0.065169           -11.757
                 O          0.039194          0.050793           -16.443
                O2           0.13498          0.087465           -32.886
                OH          0.085304             0.104             -28.2
               H2O           0.30666           0.35294           -39.956
               HO2        8.2121e-05        5.1586e-05           -44.643
              H2O2        3.5871e-06        2.1865e-06           -56.399
                 C        2.5216e-11        4.3529e-11           -18.927
                CH        2.8497e-12        4.5383e-12           -30.684
               CH2        1.5294e-12        2.2607e-12            -42.44
            CH2(S)        1.6465e-13        2.4338e-13            -42.44
               CH3         1.086e-12        1.4977e-12           -54.197
               CH4         9.411e-14        1.2163e-13           -65.953
                CO           0.22248           0.16468            -35.37
               CO2           0.20029          0.094361           -51.813
               HCO        1.0254e-06        7.3267e-07           -47.127
              CH2O        7.9233e-09        5.4713e-09           -58.883
             CH2OH        5.3103e-13        3.5478e-13            -70.64
              HCCO        2.3319e-14        1.1784e-14           -66.054
     [  +33 minor]        3.0931e-14        1.9711e-14

30.9 Other things to learn

30.10 Pluto environment

Before going further, learn some Markdown! Please notice that Markdown has no standard and not all of GitHub’s markdown works in Pluto (although most of it will be just fine). Here you find some recommendations:

For entering equations, Julia Markdown supports LaTeX typesetting; you can learn some of it here but we won’t enter in the details during this session.

Also notice that you can enter variable names in Unicode. This helps keeping consistency between mathematical formulation and code, what is pretty interesting. Please do not use Unicode mixed with LaTeX (only Pluto supports that), otherwise you won’t be able to copy your equations directly to reports. Partial support of autocompletion works in Pluto for Unicode, simply write a backslash and press tab, a list of available names will be shown.

Generally speaking, you should follow its documentation to run Pluto, i.e. using Pluto; Pluto.run(). In some cases because of local system configuration that might fail. Some corporate browsers will not be able to access localhost and configuring Pluto might be required. The following snippet provides a pluto function that could be useful in those cases:

using Pluto

function pluto(; port = 2505)
    session = Pluto.ServerSession()
    session.options.server.launch_browser = false
    session.options.server.port = port
    session.options.server.root_url = "http://127.0.0.1:$(port)/"
    Pluto.run(session)
end

Your task:

  • Read media/newton_cooling.dat as a data frame
  • Clean data as required and establish a proper visualization
  • Propose a model to fit the data and identify a package to find its parameters

Your tools:

Your goal:

newton-cooling

Newton Cooling

30.11 Going further

  • Julia Documentation: in this page you find the whole documentation of Julia.

    • Generally after taking this short course you should be able to navigate and understand a good part of the Manual.

    • It is also interesting to check-out Base to explore what is built-in to Julia and avoid reinventing the wheel!

    • Notice that not everything built-in to Julia is available under Base and some functionalities are available in the Standard Library which consists of built-in packages that are shipped with Julia.

  • Julia Academy: you can find a few courses conceived by important Julia community members in this page. The Introduction to Julia is a longer version of what you learned here.

  • Python: Julia does not replace Python (yet) because it is much younger. We have seen how to call Python from Julia and the computational scientist should be well versed in both languages as of today.

  • Julia Packages: the ecosystem of Julia is reaching a good maturity level and you can explore it to find packages suiting your needs in this page.

  • Exercism Julia Track: this page proposes learning through exercises, it is good starting point to get the fundamentals of algorithmic thinking.

  • Julia Data Science: a very straightforward book on Data Science in Julia; you can learn more about tabular data and review some plotting with Makie here.

  • Introduction to Computational Thinking: this MIT course by Julia’s creator Dr. Alan Edelman gives an excellent overview of how powerful the language can be in several scientific domains.

  • SciML Book: this course enters more avanced topics and might be interesting as a final learning resource for those into applied mathematics and numerical simulation.

  • JuliaHub: this company proposes a cloud platform (non-free) for performing computations with Julia. In its page you find a few useful and good quality learning materials.

30.12 A few Julia organizations

  • SciML: don’t be tricked by its name, its field of application goes way beyond what we understand by Machine Learning. SciML is probably the best curated set of open source scientific tools out there. Take a look at the list of available packages here. This last link is probably the best place to search for a generic package for Scientific Computing.

  • JuMP: it is a modeling language for optimization problems inside Julia. It is known to work well with several SciML packages and allows to state problems in a readable way. Its documentation has its own Getting started with Julia that you may use to review this module.

  • JuliaData: all about tabular data and related manipulations. The parents of DataFrames.jl.

  • Julia Math: several math-related packages. Most basic users coming from spreadsheet hell might be interested in Polynomials.jl from this family.

  • JuliaMolsim: molecular simulation in Julia, from DFT to Molecular dynamics.

  • JuliaGeometry: computational geometry (meshes, ray tracing) packages.

30.13 Getting help in the wild

  • Julialang Discourse: instead of Stackoverflow, most Julia discussion threads are hosted in this page.

  • Zulipchat Julialang: some discussion (specially regarding development) is hold at Zulipchat.

30.14 Selected Julia packages

30.14.1 General

30.14.2 Plotting and graphics

30.14.3 Numerical methods

30.14.4 Kinetics and Thermodynamics

30.14.5 Machine learning

30.14.6 GPU Programming