Lecture 3: Optimization 1

Creating a Julia Package for your work

  • Creating a package is a great way to ensure reproducibility of your work.
  • It helps to make your work shareable.
  • It helps to test the code which makes up your work.
  • 👉 Let’s do it!

Cookbook

  1. Start julia
  2. two equivalent options:
    1. enter Pkg mode (hit ]), then generate path/to/new/package, or
    2. say using Pkg, then Pkg.generate("path/to/new/package")

Here is an example:


]    # this jumps into Pkg mode

(@v1.11) pkg> generate Mypkg
  Generating  project Mypkg:
    Mypkg/Project.toml
    Mypkg/src/Mypkg.jl

(@v1.11) pkg> 

Depending on where you started your julia session, there is now a new folder Mypkg:

shell> ls Mypkg/
Project.toml  src/

It’s a good idea to start a new VSCode window at that folder location. Doing so, you would see this:

Our julia package
  • great. Start a julia repl in the usual way.
  • Notice how the bottom bar in VSCode indicates that we are in Mypkg env - VScode asked me whether I wanted to change into this. If this is not the case, you won’t be able to load our package:
julia> using Mypkg
ERROR: ArgumentError: Package Mypkg not found in current path.
- Run `import Pkg; Pkg.add("Mypkg")` to install the Mypkg package.
  • We need to switch into the environment of this package before we can load it locally. This is called activate an environment:
]  # in Pkg mode
(@v1.11) pkg> activate .  # `.` for current directory
  Activating project at `~/Mypkg`

(Mypkg) pkg>  # hit backspace

julia> using Mypkg
[ Info: Precompiling Mypkg [c4d85591-a952-48fb-b3d1-49a9454516b2] 

Alternatively, just click on the env indicator in VSCode and choose the current folder.

Great, now we can use the functions contained in the package. Let’s see:

julia> Mypkg.greet()
Hello World!
Code Loading

There are two ways in which we can load code into a running julia session:

  1. By includeing code - equivalent to copy and pasting code into the REPL, and what happens when we say execute active file in REPL in VSCode. In practice, those mechanisms execute the function include("some_file.jl").
  2. Via package loading: We import a set of functions contained in a package, via the using or import statements.

Notice how we did not have to say run current file in REPL or similar commands. Saying using Mypkg immediately made our code available in the current session.

Revise.jl

Next question: How can we now work on the code and investigate it’s changes in the REPL?

  1. We can obviously execute the current file in the REPL (basically copy and paste the code into the REPL). Again, copy and paste, or include("file.jl"). But that’s cumbersome.
  2. There is a great alternative - Revise.jl. Loading this package before you import your package means that Revise will track changes in your source code and expose them immediately in what you see in the REPL. Revise tracks all changes in our code. Let’s go and look the package documentation.

Good, let’s try this out. Restart the REPL in the Mypkg project. First, we add Revise to our package’s environment, so we can always load it.

] # for pkg mode
(Mypkg) pkg> add Revise

Next, let’s load Revise before we import any other code we want to work on:

using Revise
using Mypkg

see again if that works now:

julia> Mypkg.greet()
Hello World!
  • Great! Now let’s open VSCode in that location and make some changes. Like, let’s just change the greet function slightly and save the Mypkg.jl file:
greet() = print("Hello Earthlings!")
  • Execute again in the REPL (notice no code loading action necessary on our behalf!)
julia> Mypkg.greet()
Hello Earthlings!
  • Awesome! So we can change our code and immediately try out it’s effects. Notice that a limitation of Revise tracking are changes to type definitions and removal of exports. In early stages of development, when you change the content of your types frequently, that can be an issue. Either restart the REPL after each change of types, or rename them, as illustrated here.
  • Let us add some more functionality to our package now.
module Mypkg

greet() = print("Hello Earthlings!")

mutable struct MPoint
    x::Number
    y::Number
end
# want to add a `+` method: must import all known `+` first
import Base.:+
+(a::MPoint,b::MPoint) = MPoint(a.x + b.x, a.y + b.y)

end # module Mypkg
  • We added a custom data type MPoint, and our version of the + function for it. Let’s try it out in the REPL!
julia> a = Mypkg.MPoint(2,3)
Mypkg.MPoint(2, 3)

julia> b = Mypkg.MPoint(3,1)
Mypkg.MPoint(3, 1)

julia> a + b
Mypkg.MPoint(5, 4)
  • Ok, seems to work. Isn’t it a bit annoying that we always have to type Mypkg in front of our functions, though? Does it even work without typing this? What’s the deal here?
Module Namespace and Export
  • By default, none of the objects (functions, variables, etc) contained in a Module are visible from outside of it.
  • The keyword export xyz will export the name xyz from your package into the scope where it was loaded, hence, make it visible to the outside.
  • Let’s add export MPoint in our module definition and try again:
julia> a = MPoint(2,3)
MPoint(2, 3)

julia> b = MPoint(3,1)
MPoint(3, 1)

julia> a + b
MPoint(5, 4)

🎉

Unit Testing

Let’s take a quick moment to appreciate what we have done just now:

  1. We added a new feature to our package (added MPoint and +).
  2. We (or rather, Revise.jl) updated the loaded code in our REPL.
  3. We checked that it works (by typing a series of commands, see above).

With some imagination, we could call this process unit testing: We added one new aspect (a feature, a unit, a piece,…) to our project, and we tested whether it works as we intended it to work.

Automate Testing!

In a more complex environment, we will forget how to establish our check of this works. There will be interdepencies between different parts of our code, which we fail to see, and other reasons. We may simple not remember what the setting was when we test this piece of code when we wrote it.

👉 We should write the test itself down as a piece of code which we regularly execute. Better still: which someone else executes for us.

Testing

  • Julia has extensive testing capabilities built in. We need to load the built-in Test library to access the tools. See here in the manual.
  • There is a variety of addon packages which smooth the experience somewhat. I recommend the TestItemRunner.jl package, which nicely integrates with the VSCode environment:
]  # pkg
add Test  
add TestItemRunner
  • you have now access to a basic macro called @test which checks a boolean outcome:
julia> using Test

julia> @test true
Test Passed

julia> @test false
Test Failed at REPL[19]:1
  Expression: false

ERROR: There was an error during testing
  • Ok, let’s import the TestItemRunner into our package (not Test!), and let’s write our first TestItem!
module Mypkg

greet() = print("Hello Earthlings!")

using TestItemRunner  # allows using @testitem

mutable struct MPoint
    x::Number
    y::Number
end

import Base.:+
+(a::MPoint,b::MPoint) = MPoint(a.x + b.x, a.y + b.y)

@testitem "Test MPoint +" begin
    x = [rand(1:10) for i in 1:4]
    A = MPoint(x[1],x[2])
    B = MPoint(x[3],x[4])
    C = A + B 
    @test C isa MPoint
    @test C.x == A.x + B.x
    @test C.y == A.y + B.y
    @test C.x == x[1] + x[3]
    @test C.y == x[2] + x[4]
end

export MPoint
end # module Mypkg
  • Notice the green play symbol which appears in our VSCode next to the line where the testitem starts. Click it! 😉

Organizing Files

  • Our package is starting to look a bit cluttered by now.
  • You can freely arrange your code over multiple files, which you then include("file1.jl") into your module. Also, let’s move the tests to a dedicated directory. Let’s try to arrange everything into this view in VSCode:

Growing our package: Here we see the main Module definition including the code for MPoint, we see in the left file browser the structure of the package, and we illustrate how the Project.toml file has evolved so far, keeping track of our dependencies.

Debugging

  • With debugging we generally mean the ability to step through our code in an interactive fashion to repair bugs 🐛 as they appear in our code. General concepts to know are a debugger (a program which knows how to attach to our actual program), a breakpoint (a location in our code where the program will stop - ideally before an error occurs), and stepping in various forms.
  • Debugging simple scripts or packages is the same workflow.
  • Let’s add another function to our package now at the bottom of mpoint.jl maybe? An economic model of sorts:
function econ_model(; startval = 1.0)
    # make an Mpoint
    x = MPoint(startval, startval-0.5)
    # ... and evaluate a utility function
    MPoint(log(x.x),log(x.y))
end
  • Make sure to try out that it works.
julia> Mypkg.econ_model()
Mypkg.MPoint(0.0, -0.6931471805599453)
  • Ok great. Now what about that? Try it out!
julia> Mypkg.econ_model(startval = 0.3)
  • Error. Good. 😜 Let’s pretend we don’t know what’s going on and we need to investigate this function more in detail.

Debugging Strategies

  1. Add println statements: simplest is to just print output along the way, before an error occurs.

  2. Use the Logging module. Add @debug statements. This is preferable, because you can leave the @debug statements in your code without any performance implication. Logging works as follows:

    1. insert debug statements in your code: @info, @warn, @debug etc
    2. create a logger at a certain logging level
    3. run code
    julia> using Logging  # loads the standard logger at Info level
    
    julia> @info "just for info"
    [ Info: just for info
    
    julia> @debug "whoaa, this looks suspicious! 😬"

    Notice that this prints nothing! Let’s use debug logger instead for this one:

    julia> with_logger(ConsoleLogger(stdout, Logging.Debug)) do
               @debug "whoaa, this looks suspicious! 😬"
           end
    ┌ Debug: whoaa, this looks suspicious! 😬
    └ @ Main REPL[30]:2

    We can set the global_logger to capture all messages like this:

    global_logger(ConsoleLogger(stdout, Logging.Debug)) # Logging.Debug sets level to `Debug`
    old_log = global_logger(debug_logger)  # returns previous logger, so can set back later.
  3. Use an actual debugger to step through our code.

    1. VSCode exports by default the @enter macro. type: @enter Mypkg.econ_model(startval = -0.3)
    2. click on the play symbol. program hits an error.
    3. set a break point just before
    4. click on replay.

Some Julia-Bootcamp stuff

Topic Notebook
Intro to Macros click for notebook
Intro to Differential Equations click for notebook
Plotting with Plots.jl click for notebook
Plotting with Makie.jl click for website
Interactive click for notebook

Optimization, Finally!

Topic Notebook
Review of Optimization Algorithms download notebook

© Florian Oswald, 2025