Category Archives: Julia

Introduction to Julia

By: Jaafar Ballout

Re-posted from: https://www.supplychaindataanalytics.com/introduction-to-julia/

After having introduced in one of my previous posts optimization and linear programming, I explain the basics of the Julia programming language in this article. The article represents a tutorial and is based on the official Julia documentation. My tutorial covers the key aspects that I will later use, in upcoming blog posts, for solving supply chain and operation research problems (e.g. network problems). My tutorial also highlights some major syntactic and functional differences when compared to other popular programming languages (namely Python and Matlab).

Defining variables in Julia

A variable is a name associated with a value and saved in the computer memory. Assigning a value for the variable is done using the = operator. Unicode can be used as a variable name. Most Julia editors support LaTeX syntax which can be used to create Unicode characters. The double quotes ” are used for strings while the single quote ‘ is used for a character.

Here are some examples demonstrating the above explanation.

In[]:
x = 7 # variable name: x; variable value = 7

Out[]:
7
In[]:
y = 10 # variable name: y; variable value = 10

Out[]:
10

Variable names are case-sensitive and have no semantic meaning.

In[]:
Y = 100 

Out[]:
100
In[]:
n = "Jaafar" # variable name: n; variable value = "Jaafar" which is  a string

Out[]:
"Jaafar"
In[]:
Letter = 'a' # character

Out[]:
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
In[]:
α = 1 # unicode is easier in Julia: write \alpha and press tab 

Out[]:
1
In[]:
😠=0 # \:angry: and then press <tab>

Out[]: 
0

Integer and floating point numbers

Integers and floating points are the building blocks of mathematical operations. Using the typeof command in Julia I can find the type of any pre-defined variable.

In[]:
x = 1; #semicolon is to avoid printing the variables
y = 10000;
z = 1.1;
d = 5e-3

println("x is ", typeof(x))
println("y is ", typeof(y))
println("z is ", typeof(z))
println("d is ", typeof(d))

Out[]:
x is Int64
y is Int64
z is Float64
d is Float64

Using the Sys.WORD_SIZE, Julia’s internal variable, I can indicate whether the targetted system is 32-bit or 64-bit.

In[]:
# 64-bit system
Sys.WORD_SIZE

Out[]:
64

Main mathematical operations in Julia

The arithmetic operations are similar to Python except for the power. For power operations, Julia uses ^ instead of ** (as known from Python).

Boolean operations are as follows:

  • a && b: a and b
  • a || b: a or b
  • !a: negation
In[]:
a = 1;
b = 3;

# Addition
a + b;

# Subtraction
a - b;

# times
a*b;

# divison
a/b;

# Power 
a^b; # this is different from python where ** is used to raise a to the bth power

#Updating operators 
a+=1; # a = a + 1
b*=2; # b = b * 2

Vectorized operators are very important in linear algebra. Dot operators are used for arrays where elementary operations are performed.

In[]:
[1,2,3].^1 # [1^1,2^1,3^1]

Out[]:
3-element Vector{Int64}:
 1
 2
 3

Basic collections

Tuples, named tuples, and dictionaries

  • Tuples: Ordered immutable collections of elements
  • NamedTuples: Exactly like tuples but also assign a name for each variable
  • Dictionaries: Unordered mutable collections of pairs: key-value
In[]:
# Tuple 
favoritesongs = ("outnumbered", "Power Over Me", "Bad Habits") # elements have the same type

# Tuple
favoritethings= ("yellow", 'j', pi) # elements with different types

# NamedTuple
favoritesongs_named = (a = "outnumbered", b = "Power Over Me", c = "Bad Habits") # it is between a Tuple and Dictionary

# Dictionary
myDict = Dict("name" => "Jaafar", "age" => "twenty", "hobby"=> "biking")

Out[]:
Dict{String, String} with 3 entries:
  "name"  => "Jaafar"
  "hobby" => "biking"
  "age"   => "twenty"
In[]:
# Tuple access
favoritesongs[1] # indexing starts by 1 not 0 (unlike Python)

# NamedTuple access
favoritesongs_named[1] # accessed by index
favoritesongs_named.a # accessed by key

# Dictionary access
myDict["name"] # call the kay to output the value in the dictionary 

Out[]:
"Jaafar"

Vectors, arrays, and matrices

Like any numerical computation language, Julia provides an easy way to handle matrices and their corresponding operations. Unlike Matlab, Julia arrays are indexed with square brackets, A[i,j]. However, similarly to Matlab, indexing starts using one, not zero, which makes it more convenient especially in loops later by using f(i) instead of f(i-1).

  • array: ordered and mutable collection of items of the same type
  • vector: array of dimension one
  • matrix: array of dimension two
  • tensor: array of n-dimension (usually 3 and above)
In[]:
#vector
array_V = [1, 2, 3, 4, 5, 6, 7, 8, 9] # acts as a vector of one-dimension
typeof(array_V)

Out[]:
Vector{Int64} (alias for Array{Int64, 1})
In[]:
#Matrix
array_M = [1 2 3; 4 5 6; 7 8 9] # acts as a matrix of two-dimension

Out[]:
3×3 Matrix{Int64}:
 1  2  3
 4  5  6
 7  8  9
In[]:
# Random vector
vec = rand(9)

Out[]:
9-element Vector{Float64}:
 0.7130265942088201
 0.9545688377050932
 0.7878361868436774
 0.4973658015754845
 0.44265779030703434
 0.01870528656705095
 0.010563833645745424
 0.8906392694739755
 0.5416448302194592
In[]:
# Random matrix 
mat = rand(3,3)

Out[]:
3×3 Matrix{Float64}:
 0.412231  0.0180507  0.862113
 0.534452  0.711949   0.541887
 0.52126   0.894952   0.443401
In[]:
# Random tensor
ten = rand(3,3,3) # three-dimenisonal 

Out[]:
3×3×3 Array{Float64, 3}:
[:, :, 1] =
 0.517095    0.976259  0.114393
 0.00295048  0.759259  0.302369
 0.988611    0.688391  0.438473

[:, :, 2] =
 0.163933  0.138108  0.770564
 0.899507  0.109004  0.577751
 0.63999   0.280642  0.751499

[:, :, 3] =
 0.361409  0.575224  0.525733
 0.858351  0.586987  0.638436
 0.101579  0.447222  0.364909

It is important to note that ranges act like vector. However, specifying a range is easier to code. The syntax is as follows: range_name = start:step:end

In[]:
# Range
r = 1:1:9 

Out[]:
1:1:9
In[]:
collect(r) # transofrm the range output to a vector output (better output)

Out[]:
9-element Vector{Int64}:
 1
 2
 3
 4
 5
 6
 7
 8
 9

More on indices and ranges in Julia

Working with matrices and arrays, in general, requires good command of indexing and slicing operations in Julia. This can be done more easily using the ranges. This is due to their compact code.

In[]:
# Define an array
name_letters = ['j','a','a','f','a','r']

# Index the array
name_letters[1] # returns j

# slice the array using a range: start:end
name_letters[1:3] # returns jaa

# slice the array using a range with step of 2: start:step:end
name_letters[1:3:6] # returns jf

Out[]:
2-element Vector{Char}:
 'j': ASCII/Unicode U+006A (category Ll: Letter, lowercase)
 'f': ASCII/Unicode U+0066 (category Ll: Letter, lowercase)

Printing output in Julia

Although printing the output of the code is simple it is important for e.g. debugging the code. Print commands are also helpful to readers that are trying to understand the function and purpose of the code.

In[]:
# Print on new line
println("Jaafar") ;
println("Ballout");

# Print one same line
print("Jaafar");
print(" Ballout");

Out[]:
Jaafar
Ballout
Jaafar Ballout

Specifying loops and conditions in Julia

Both loops and conditions in Julia require an end command, unlike Python where indentation is enough to end the if-condition, loop, or function-definition.

In[]:
#If condition
if length("Jaafar") > length("Ballout")
    print("Your first name is bigger than your last name")
elseif length("Jaafar") == length("Ballout")
    println("First name and last name have same number of characters")
else
    println("Your first name is smaller than your last name")
end

Out[]:
Your first name is smaller than your last name

The code block below prints each character in my name into a new line.

In[]:
name_letters = ['j','a','a','f','a','r']
# For loop
for i in 1:length(name_letters)
    println(name_letters[i])
end

Out[]:
j
a
a
f
a
r

The code below finds the location or index of a character in my name.

In[]:
#If condition in For Loop
for i in 1:length(name_letters)
    if name_letters[i] == 'a'
        println(i)
    end
end

Out[]:
2
3
5

Defining functions in Julia

Functions, e.g. routines or methods, can be defined in Julia. The linear optimization model, below, is from my previous blog post. In the coding example that follows, I define the objective function as a function in Julia.

Here is how I can define the objective function in Julia:

In[]:
function max(x,y)
    return 5x + 4y # no need to write the multiplication * sign; Julia will understand
end

Out[]:
max (generic function with 1 method)
In[]:
# Optimal soultion of the constrained problem above from previous post 
z = max(3.75,1.25) # optimal value
print(z)

Out[]:
23.75

Import files into Julia

Importing files is very import especially in supply chain management and logistics problems. Because I might use .csv files in future posts, explaining how to import these files in Julia is necessary at this stage. I will be using Pandas.jl which is a Julia interface to the excellent Pandas package in Python.

In[]:
using Pandas
In[]:
df_list = Pandas.read_csv("https://gist.githubusercontent.com/brooksandrew/e570c38bcc72a8d102422f2af836513b/raw/89c76b2563dbc0e88384719a35cba0dfc04cd522/edgelist_sleeping_giant.csv");

I will introduce useful packages like DataFrames.jl and PyPlots in Julia in future work. These packages are very useful for solving network problems as known from supply chain management and operations research.

The post Introduction to Julia appeared first on Supply Chain Data Analytics.

Advent of Code 2021 – Day 24

By: Julia on Eric Burden

Re-posted from: https://www.ericburden.work/blog/2022/01/05/advent-of-code-2021-day-24/

It’s that time of year again! Just like last year, I’ll be posting my solutions to the Advent of Code puzzles. This year, I’ll be solving the puzzles in Julia. I’ll post my solutions and code to GitHub as well. I had a blast with AoC last year, first in R, then as a set of exercises for learning Rust, so I’m really excited to see what this year’s puzzles will bring. If you haven’t given AoC a try, I encourage you to do so along with me!

Day 24 – Arithmetic Logic Unit

Find the problem description HERE.

The Puzzle – The Whole Enchilada

MONAD imposes additional, mysterious restrictions on model numbers, and legend says the last copy of the MONAD documentation was eaten by a tanuki. You’ll need to figure out what MONAD does some other way.

Today’s puzzle took a bit of effort on my part just to figure out exactly what it was we were trying to do. Essentially, we are given a set of instructions (with some explanation about how to parse them), then informed that somehow this set of instructions can be used to validate a 14 digit number. 14 digits, eh? You know, after looking at that input program, it looks like there are 14 different chunks there, as well…

Table comparing the 14 chunks of the puzzle input

In fact, among those 14 chunks, there are only three lines that differ from one chunk to the next. Those must be the important bits! Let’s give each of them a name. I opted for optype, correction, and offset, for reasons that might make sense as we continue. Working through each chunk, we get the following pseudocode which expresses the operation of each ALU code chunk:

x = ((z mod 26) + correction) != w
z = floor(z / optype)
z = z * ((25 * x) + 1)
z = z + ((w + offset) * x)

You’ll either need to trust me on that or work through one of the chunks yourself (it’s a bit tedious, but not too bad). As you can see, y ends up being unnecessary to keep track of (mul x 0 and mul y 0 reset x and y to zero, respectively) and we only need w to determine whether x is a 1 or 0, a boolean value. The last line modifies z depending on whether or not w is true or false. If that sounds a bit like a conditional expression, then you’re right! The chunk above can then be simplified to:

if (z mod 26) + correction != w
    z = floor(z / optype)
    z = z * 26
    z = z + (w + offset)
else
    z = floor(z / optype)
end

Another look at our input program reveals that optype can only be 1 or 26, which will determine what type of operation we perform (optype, get it?).

optype == 1                             |     optype == 26
if (z mod 26) + correction != w         |     if (z mod 26) + correction != w
    z = z * 26                          |          z = floor(z / 26) 
    z = z + (w + offset)                |          z = z * 26
end                                     |          z = z + (w + offset)
                                        |     else
                                        |          z = floor(z / 26)
                                        |     end

Ok, so what does this mean, exactly? For one, we were able to drop the else clause from the optype == 1 case, since z == floor(z / 1) for all integers. For another, let’s consider our input again, particularly chunks 3 and 4, which have an optype of 1 and 26, respectively. What, then, do those chunks do? Let’s try them and see! We’ll assume, for the sake of experimentation, that z is 0 going into chunk 3.

chunk 3: optype => 1;  correction => 14; offset => 8

Assume z == 0 (it doesn't really matter, as you'll see, but it does simplify 
the expressions a bit. For fun, try working through this with any positive 
integer value for z and see what happens)

(0 mod 26) + 14 -> 14 != w  # Since 1 <= w <= 9, w _can't_ == 14
    z = 0 * 26 -> 0
    z = (w + 8)
chunk 4: optype => 26; correction => -5; offset => 5

We'll use `w3` as a standin for the w from chunk 3, to differentiate. I'm 
leaving the variable in to demonstrate what's really being compared. So, to 
start chunk 4, z = (w3 + 8)

(z mod 26)   -> (w3 + 8)
(w3 + 8) - 5 -> (w3 + 3)

There are two scenarios here. Either w3 + 3 == w4, or it doesn't...

w3 + 3 != w4                            |     w3 + 3 == w4
    # w3 + 8 is less than 26            |         z = floor((w3 + 8) / 26) -> 0
    z = floor((w3 + 8) / 26) -> 0       |
    z = 0 * 26 -> 0                     |
    z = (w4 + 5)                        |

So, the only thing chunk 3 can do here is multiply z by 26 and add w<3> + offset<3>. Chunk 4 is going to remove w<3> + offset<3> from z regardless, but if w<4> != w<3> + offset<3> + correction<4>, then chunk 4 will add w<4> + offset<4> back to z. This means that z is a base-26 number, and each time optype == 1, z is shifted to the left and the value of w + offset is tacked on to the end. Each time optype = 26, depending on the w (input) values, either the last value tacked on to z is erased, or the last value is erased and replaced with a different w + offset. Since our goal is to have z == 0 at the end of this process, we should then strive to identify values of w that can be ‘canceled out’ from one chunk to the next. This also means that, for every optype == 1 chunk, there should be a corresponding optype == 26 chunk. And, wouldn’t you know, there are 7 of each kind of chunk, for 14 total digits. Let’s try to pair them off:

chunk  1: optype == 1  ──────┐
chunk  2: optype == 1  ────┐ │
chunk  3: optype == 1  ┐   │ │
chunk  4: optype == 26 ┘   │ │
chunk  5: optype == 1  ──┐ │ │
chunk  6: optype == 1  ─┐│ │ │
chunk  7: optype == 1  ┐││ │ │     ________________
chunk  8: optype == 26 ┘││ │ │    < That's a stack! >
chunk  9: optype == 26 ─┘│ │ │     ----------------
chunk 10: optype == 1  ┐ │ │ │            \   ^__^
chunk 11: optype == 26 ┘ │ │ │             \  (oo)\_______
chunk 12: optype == 26 ──┘ │ │                (__)\       )\/\
chunk 13: optype == 26 ────┘ │                    ||----w |
chunk 14: optype == 26 ──────┘                    ||     ||

You know what, cow, you’re right! That does look exactly like a stack. And, based on what we learned earlier, it’s a stack that gets pushed to every time optype == 1, and that get’s popped (and optionally pushed) every time optype == 26. Based on these pairings, we can now deduce the relationships between each of the digits in our final number. I’ll be filling in values for offset and correction from the table above.

offset<1>  + correction<14> == -1
w<1>  - 1 == w<14> <|> w<14> + 1 == w<1>

offset<2>  + correction<13> == -6
w<2>  - 6 == w<13> <|> w<13> + 6 == w<2>

offset<3>  + correction<4>  == +3
w<3>  + 3 == w<4>  <|> w<4>  - 3 == w<3>

offset<5>  + correction<12> == +8
w<5>  + 8 == w<12> <|> w<12> - 8 == w<5>

offset<6>  + correction<9>  == +1
w<6>  + 1 == w<9>  <|> w<9>  - 1 == w<6>

offset<7>  + correction<8>  == -8
w<7>  - 8 == w<8>  <|> w<8>  + 8 == w<7>

offset<10> + correction<11> == +2
w<10> + 2 == w<11> <|> w<11> - 2 == w<10>

Well, now, I daresay we know everything we need to start filling in digits, and solving this puzzle! Again, looking at the pair between chunks 3 and 4, we see that w<3> + 3 == w<4>. The largest digits we can place into w<3> and w<4> that will satisfy this constraint (while keeping each digit in the range from 1 -> 9), are w<3> = 6 and w<4> = 9. Do this for the rest of the digit pairs, and you have your answer to part one of the puzzle!

Since part two asks for the smallest valid model number, we don’t need to deduce anything new. Instead, we just need to fill in the smallest digits that will meet our constraints. For the chunk 3/chunk 4 pairing, that works out to w<3> = 1 and w<4> = 4. Fill out the rest of the digits, and we’ve earned ourselves another gold star.

Some Code – You Complete Me

There wasn’t any way that I was going to finish up this blog post without any code, however. While I solved the puzzle without any code, I did write the function below which could be used to validate model numbers using the method we described above. Fun fact: there are only two valid model numbers anyway! (Can you tell why that is? Hint: check out chunks 5/12 and chunks 7/8).

#(optype, correction, offset)
const PARAMS = [
    ( 1,  13,  0),  # d[1]  +  0 -  1 == d[14] -> d[1] -  1 == d[14]   | 9  | 2
    ( 1,  11,  3),  # d[2]  +  3 -  9 == d[13] -> d[2] -  6 == d[13]   | 9  | 7
    ( 1,  14,  8),  # d[3]  +  8 -  5 == d[4]  -> d[3] +  3 == d[4]    | 6  | 1
    (26,  -5,  5),  # d[4]  -  8 +  5 == d[3]  -> d[4] -  3 == d[3]    | 9  | 4
    ( 1,  14, 13),  # d[5]  + 13 -  5 == d[12] -> d[5] +  8 == d[12]   | 1  | 1
    ( 1,  10,  9),  # d[6]  +  9 -  8 == d[9]  -> d[6] +  1 == d[9]    | 8  | 1
    ( 1,  12,  6),  # d[7]  +  6 - 14 == d[8]  -> d[7] -  8 == d[8]    | 9  | 9
    (26, -14,  1),  # d[8]  -  6 + 14 == d[7]  -> d[8] +  8 == d[7]    | 1  | 1
    (26,  -8,  1),  # d[9]  -  9 +  8 == d[6]  -> d[9] -  1 == d[6]    | 9  | 2
    ( 1,  13,  2),  # d[10] +  2 -  0 == d[11] -> d[10] + 2 == d[11]   | 7  | 1
    (26,   0,  7),  # d[11] -  2 +  0 == d[10] -> d[11] - 2 == d[10]   | 9  | 3
    (26,  -5,  5),  # d[12] - 13 +  5 == d[5]  -> d[12] - 8 == d[5]    | 9  | 9
    (26,  -9,  8),  # d[13] -  3 +  9 == d[2]  -> d[13] + 6 == d[2]    | 3  | 1
    (26,  -1, 15)   # d[14] -  0 +  1 == d[1]  -> d[14] + 1 == d[1]    | 8  | 1
] 


# This function can be used to check whether a given number is a valid model
# number, according to the MONAD program.
function isvalid(n::Int64)
    ndigits = reverse(digits(n))

    # Valid model numbers don't contain `0` as a digit
    0  ndigits && return false
    stack = []; sizehint!(stack, length(ndigits))

    for (w, (optype, correction, offset)) in zip(ndigits, PARAMS)
        if optype == 1
            push!(stack, w + offset)
            continue
        end
        stack[end] + correction == w && pop!(stack)
    end

    # In a valid model number, half the digits will be "canceled" out by the
    # other half, leaving an empty stack
    return isempty(stack)
end

Wrap Up

At first, I wasn’t sure how to feel about this puzzle. On the one hand, I do AoC primarily to practice my coding skills (in Julia, this year). On the other hand, this required a fair bit of deduction and mental flexing to suss out, which is massively satisfying once the puzzle is complete. You know what? I had fun. I suppose that’s pretty important, too, now and again. Only one more day (and one more puzzle) left to go!

If you found a different solution (or spotted a mistake in one of mine), please drop me a line!