Computational Analysis of Social Complexity
Fall 2025, Spencer Lyon
Prerequisites
- Laptop or personal computer with internet connection
- Julia intro lecture
Outcomes
- Creating julia modules
- Importing Julia source fode files with the
includefunction - Exporting types and functions to create
References
- Lecture notes
- Julia documentation on modules
Introduction¶
- Julia can be succesfully used for exploratory analysis and one-off scripts
- As projects grow, it is often useful to add structure and organization to the code
- In Julia, the main building blocks for code organization and re-use include
- Types/structs
- Functions/methods
.jlsource code files- Modules
- Packages
- In this not we’ll learn how to manage .
jlfiles and Modules - We’ll pick up with Packages in another lecture
.jl files¶
- Julia source code typically lives in a plain text file with a
.jlextension - While the extension is optional and not enforced by the Julia REPL or runtime, it is a strong convention followed by the community and by 3rd party tools like code editors and GitHub
- Suppose we have the following code for simulating Markov Chains in Julia
# Markov Chain code
struct MarkovChain{T}
P::Matrix{Float64}
initial_state::Vector{Float64}
state_values::Vector{T}
P_dists::Vector{Vector{Float64}}
end
function MarkovChain(P::Matrix{Float64}, initial_state::Vector{Float64}, state_values::Vector{T}) where T
P_dists = [cumsum(row) for row in eachrow(P)]
return MarkovChain{T}(P, initial_state, state_values, P_dists)
end
function simulate_indices(mc::MarkovChain, n_steps::Int)
init_dist = cumsum(mc.initial_state)
states = Vector{Int}(undef, n_steps)
states[1] = searchsortedfirst(init_dist, rand())
for i in 2:n_steps
states[i] = searchsortedfirst(mc.P_dists[states[i-1]], rand())
end
return states
end
function simulate_values(mc::MarkovChain{T}, n_steps::Int)::Vector{T} where T
states = simulate_indices(mc, n_steps)
return mc.state_values[states]
endsimulate_values (generic function with 1 method)P1 = [0.5 0.5; 0.5 0.5]
mc1 = MarkovChain(P1, [1.0, 0.0], ["A", "B"])
typeof(mc1)MarkovChain{String}mc1.P_dists2-element Vector{Vector{Float64}}:
[0.5, 1.0]
[0.5, 1.0]inds = simulate_indices(mc1, 10)10-element Vector{Int64}:
1
1
2
2
1
1
1
2
1
1vals = simulate_values(mc1, 10)
vals10-element Vector{String}:
"A"
"A"
"A"
"A"
"A"
"B"
"A"
"A"
"B"
"A"- As you can see, we can use this code from inside a Jupyter notebook by running the defining cell and then calling the routines
- However, what if we wanted to reuse the code for another notebook
- We have two options:
- Copy/paste it to the new notebook
- Store the code in a
.jlfile and import it from both places
- Let’s pick the latter
include¶
- I have created
markov.jlwith that code:
println(String(read("markov.jl")))# Markov Chain code
struct MarkovChain{T}
P::Matrix{Float64}
initial_state::Vector{Float64}
state_values::Vector{T}
P_dists::Vector{Vector{Float64}}
end
function MarkovChain(P::Matrix{Float64}, initial_state::Vector{Float64}, state_values::Vector{T}) where T
P_dists = [cumsum(row) for row in eachrow(P)]
return MarkovChain{T}(P, initial_state, state_values, P_dists)
end
function simulate_indices(mc::MarkovChain, n_steps::Int)
init_dist = cumsum(mc.initial_state)
states = Vector{Int}(undef, n_steps)
states[1] = searchsortedfirst(init_dist, rand())
for i in 2:n_steps
states[i] = searchsortedfirst(mc.P_dists[states[i-1]], rand())
end
return states
end
function simulate_values(mc::MarkovChain{T}, n_steps::Int)::Vector{T} where T
states = simulate_indices(mc, n_steps)
return mc.state_values[states]
end
function Base.rand(mc::MarkovChain{T}, n_steps::Int)::Vector{T} where T
simulate_values(mc, n_steps)
end
function stationary_distributions(mc::MarkovChain)
eig = eigen(mc.P')
out = Vector{Float64}[]
for i in 1:size(mc.P, 1)
if eig.values[i] == 1
vec = eig.vectors[:, i]
vec = vec ./ sum(vec)
push!(out, vec)
end
end
return out
end
- To make use of this code in julia I can run
include("markov.jl") - You can think of the
includefunction as copy/pasting AND evaluating the code from a file in whatever setting you are in
include("markov.jl")stationary_distributions (generic function with 1 method)- Notice that in the
markov.jlfile I added two extra functions (randandstationary_ditributions) - We can verify that these were defined for us when we ran
includeby calling them:
rand(mc1, 4)4-element Vector{String}:
"A"
"B"
"B"
"A"stationary_distributions(mc1)1-element Vector{Vector{Float64}}:
[0.5, 0.5]Modules¶
- Being able to define code in a
.jlfile and load it in a notebook (or other.jlfile!) is already a huge win for organization and reusability - But we can do better!
- One issue with our approach here is that all types, functions, and methods we define in our
included files will become part of our working Julia session. - This is not always wanted
- Suppose instead we really only wanted to make the
MarkovChain,rand, andstationary_distributionscode part of our session when loading our file - To do this, we will need to organize our code into a Module
- To create module we use the syntax
module NAME
# code here
end- Here we use the
modulekeyword, followed by the name of the module - Between the module name and
endwe insert any code we want to include in the module - By convention, the contents of a module are not indented (this is a rare exception to indenting code that comes before
end) - I’ve created a module for our Markov chain code in
module.jl:
println(String(read("module.jl")))module Markov
export MarkovChain, rand, stationary_distributions
include("markov.jl")
end
- Notice a few things:
- I used the
exportkeyword to list types/functions I want to be added to the caller’s namespace when someone runsusing Markov - I used
includeto add the actual source code in the module
- I used the
- This is a very common pattern in Julia and is one we will see throughout our course
Using Modules¶
- To use a module we first have to evaluate the code defining it
- To do that we can
include("module.jl")
include("module.jl")
Main.Markov- Notice the printout shows that we now have a
Main.Markovobject- NOTE:
Mainis the name of the default module the user executed code is evaluated in to
- NOTE:
- We can now access
Markov.<NAME>where<NAME>is any type or function in the module
P2 = [0.2 0.8; 0.5 0.5]
mc2 = Markov.MarkovChain(P2, [1.0, 0.0], [10, 20])Main.Markov.MarkovChain{Int64}([0.2 0.8; 0.5 0.5], [1.0, 0.0], [10, 20], [[0.2, 1.0], [0.5, 1.0]])Markov.rand(mc2, 2)2-element Vector{Int64}:
10
10- Notice that I had to use
Markov.to access members of the module - Now taht the module is defined I can call
using Markovto make it possible to use anyexported function without theMarkov.prefix
using Main.Markov
stationary_distributions(mc2)1-element Vector{Vector{Float64}}:
[0.38461538461538464, 0.6153846153846153]Exercise¶
- Now it is your turn
- Let’s create a module for working with geometric shapes
- Create two new
.jlfiles- One called
shapes.jl: here define the following:- An abstract type
Shape2D - Concrete types
Circle,Rectangle, andTrianglethat are subtypes ofShape2D - Methods for
areaandperimeterfor each shape type - A function
is_inside(shape, x, y)that checks if a point (x, y) is inside the shape
- An abstract type
- One called
GeometricShapes.jl: here wrap the code fromshapes.jlin a module namedGeometricShapes- Export the types:
Shape2D,Circle,Rectangle,Triangle - Export the functions:
area,perimeter,is_inside
- Export the types:
- One called
includeyour new module and test it by:- Creating instances of each shape
- Computing their areas and perimeters
- Testing if various points are inside or outside the shapes