Author Archives: Tamás K. Papp

Making dotted graph paper in Julia with Luxor.jl

By: Tamás K. Papp

Re-posted from: https://tamaspapp.eu/post/dotted-paper-luxor/

I needed some graph paper with subdivisions at centimeters and millimetres. There are some generators online, but I found their output too heavy, visually cluttered, or simply not ideal (yes, I am very picky about these things).

The obvious solution was to generate a PDF or SVG using a program. Before learning Julia, I would have just started in pgf/tikz, and spent hours searching online to do the thing I want. pgf/tikz are fine tools, but I generally try to avoid programming anything nontrivial in LaTeX, so I always have to spend a lot of time debugging my code.

However, being a Julia user, this was a great opportunity to try Luxor.jl, the “Cairo for tourists!”. This means that I don’t have to immerse myself in the intricacies of its syntax and concepts; I can just dip my toes and get the results.

I was not disappointed: within 15 minutes of Pkg.add("Luxor"), I had arrived at a solution I like. I am very satisfied with Luxor.jl and will probably use it in the future when I need graphics built up from primitives.

download as dotted_paper.jl

using Luxor

#######################
# customize script here
#######################
paper = "A4"                       # paper size
major_unit = 1cm                   # gridlines at each
subdivisions = 10                  # dots on gridlines in between
radius = 0.1mm                     # radius for the tiny dots
margins = (5mm, 5mm)               # minimum margins (may be more, centered)
color = "black"                    # color for the tiny dots
filename = "dotted_paper.pdf"      # output goes here, OVERWRITTEN

#######################################################
# runtime code - you probably don't need to change this
#######################################################
pagesize = paper_sizes[paper]
nx, ny = @. floor(Int, (pagesize - (2 * margins)) / major_unit)
offsetx, offsety = @. margins + (pagesize % major_unit) / 2

major_grid_x = offsetx .+ major_unit * (0:nx)
major_grid_y = offsety .+ major_unit * (0:ny)
minor_grid_x = offsetx .+ (major_unit / subdivisions) * (0:(nx*subdivisions))
minor_grid_y = offsety .+ (major_unit / subdivisions) * (0:(ny*subdivisions))

Drawing(paper, filename)
background("white")
sethue(color)
@. circle(Point(major_grid_x, minor_grid_y'), radius, :fill);
@. circle(Point(minor_grid_x, major_grid_y'), radius, :fill);
finish()
preview()

The final PDF is here.

Common random variables: an optimization case study

By: Tamás K. Papp

Re-posted from: https://tamaspapp.eu/post/common-random-variables/

Code used in this post was written for Julia v0.6.2 and, when applicable, reflects the state of packages used on 2018-03-23. You may need to modify it for different versions.

Motivation

When simulating models with random components, it is frequently advantageous to decompose the structure into

  1. parameters \(\theta\) that we need to characterize structural relationships,

  2. noise \(\varepsilon\) that is independent of parameters,

  3. a function \(f(\theta, \varepsilon)\) that generates observed data or moments.

Doing this allows us to use common random variables (aka common random numbers), which is a technique which simply keeps \(\varepsilon\) fixed for different \(\theta\). This can help with making \(f\) differentiable, which allows the use of derivative-based optimization algorithms (eg for maximum likelihood or MAP) or derivative-based MCMC methods. It is also used to reduce the variance of simulated moments.

When implementing this technique in Julia, I frequently create a wrapper structure that saves the \(\varepsilon\), allocating an Array which can be updated in place. Since a redesign of DynamicHMC.jl is coming up which will accommodate simulated likelihood methods in a more disciplined manner, and I wanted to explore other options, most importantly StaticArrays.jl.

Here I benchmark the alternatives for Julia v0.6.2 using a simple toy model. TL;DR for the impatient: StaticArrays.jl is 150x faster, and this does not depend on using immutable or mutable StaticArrays, or even reallocating every time new \(\varepsilon\)s are generated.

Problem setup

The setup is simple: we draw \(M\) observations, and our noise is

\[
\varepsilon_{i,j} \sim N(0, 1)
\qquad
\text{for } i = 1, \dots, M; j = 1, \dots, 7.
\]

Our parameters are \(\mu\) and \(\sigma\), and for each \(i\) we calculate

\[
A_i = \frac17 \sum_{j=1}^7 \exp(\mu + \sigma \varepsilon_{i,j})
\]

which is the sample average after a nonlinear transformation. The \(7\) is a bit accidental, it comes from simplifying an actual problem I am working on. We are interested in the sample average for \(A_i\). I deliberately refrain micro-optimizing each version, to reflect how I would approach a real-life problem.

We code the common interface as

using BenchmarkTools
using StaticArrays
using Parameters

# common interface

"Dimension of noise ``ϵ`` for each observation."
const EDIM = 7

"""
Common random variables. The user needs to define

1. `observation_moments`, which should use `observation_moment`,
2. `newcrv = update!(crv)`, which returns new common random variables,
potentially (but not necessarily) overwriting `crv`.
"""
abstract type CRVContainer end

observation_moment(ϵ, μ, σ) = mean(@. exp(μ + σ * ϵ))

average_moment(crv::CRVContainer, μ, σ) = mean(observation_moments(crv, μ, σ))

"Calculate statistics, making `N` draws, updating every `L`th time."
function stats(crv, μ, σ, N, L)
    _stat() = (N % L == 0 && (crv = update!(crv)); average_moment(crv, μ, σ))
    [_stat() for _ in 1:N]
end

The way I wrote stats is representative of how I use HMC/NUTS: simulated moments on the same trajectory are calculated with the same \(\varepsilon\)s, which are updated for each trajectory. Of course, the parameters would change along the trajectory; here they don’t, but this does not affect the benchmarks.

The semantics of update! allows both in-place modifications and a functional style.

Using a preallocated matrix

This is the standard way I would write this.1 \(\varepsilon\)s are in columns of a matrix, which is preferable for mapping them as slices, then they are mapped using observation_moment.

update! overwrites the contents.

"""
Common random variables are stored in columns of a matrix.
"""
struct PreallocatedMatrix{T} <: CRVContainer
    ϵ::Matrix{T}
end

PreallocatedMatrix(M::Int) = PreallocatedMatrix(randn(EDIM, M))

update!(p::PreallocatedMatrix) = (randn!(p.ϵ); p)

observation_moments(p::PreallocatedMatrix, μ, σ) =
    vec(mapslices(ϵ -> observation_moment(ϵ, μ, σ), p.ϵ, 1))

Using static vectors

We share the following between various static vector implementations:

"Common random variables as a vector of vectors, in the `ϵs`."
abstract type CRVVectors <: CRVContainer end

observation_moments(p::CRVVectors, μ, σ) =
    map(ϵ -> observation_moment(ϵ, μ, σ), p.ϵs)

I find the use of map more intuitive here than mapslices above.

Static vectors, container preallocated

In the first version using static vectors, we keep SVector in a Vector, and update in place.

struct PreallocatedStaticCRV{K, T} <: CRVVectors
    ϵs::Vector{SVector{K, T}}
end

PreallocatedStaticCRV(M::Int) =
    PreallocatedStaticCRV([@SVector(randn(EDIM)) for _ in 1:M])

function update!(p::PreallocatedStaticCRV)
    @unpack ϵs = p
    @inbounds for i in indices(ϵs, 1)
        ϵs[i] = @SVector(randn(EDIM))
    end
    p
end

Mutable static vectors, overwritten

We modify this to use mutable vectors — this should not make a difference.

struct MutableStaticCRV{K, T} <: CRVVectors
    ϵs::Vector{MVector{K, T}}
end

MutableStaticCRV(M::Int) =
    MutableStaticCRV([@MVector(randn(EDIM)) for _ in 1:M])

function update!(p::MutableStaticCRV)
    @unpack ϵs = p
    @inbounds for i in indices(ϵs, 1)
        randn!(ϵs[i])
    end
    p
end

Static vectors, allocated each time

Finally, for the third solution,

struct GeneratedStaticCRV{K, T} <: CRVVectors
    ϵs::Vector{SVector{K, T}}
end

GeneratedStaticCRV(M::Int) =
    GeneratedStaticCRV([@SVector(randn(EDIM)) for _ in 1:M])

update!(p::GeneratedStaticCRV{K, T}) where {K, T} =
    GeneratedStaticCRV([@SVector(randn(T, K)) for _ in indices(p.ϵs, 1)])

Results

Running

@btime mean(stats(PreallocatedMatrix(100), 1.0, 0.1, 100, 10))
@btime mean(stats(PreallocatedStaticCRV(100), 1.0, 0.1, 100, 10))
@btime mean(stats(MutableStaticCRV(100), 1.0, 0.1, 100, 10))
@btime mean(stats(GeneratedStaticCRV(100), 1.0, 0.1, 100, 10))

we obtain

solution time allocations
PreallocatedMatrix 230 ms 22 MiB
PreallocatedStaticCRV 1.5 ms 102 KiB
MutableStaticCRV 1.5 ms 104 KiB
GeneratedStaticCRV 1.5 ms 666 KiB

As a preview of future improvements, I tried PreallocatedMatrix on current master (which will become Julia v0.7, obtaining 3.5 ms (2.46 MiB), which is really promising.2

The conclusion is that StaticArrays simplifies and speeds up my code at the same time. I especially like the last version (GeneratedStaticCRV), because it obviates the need to think about types in advance. While here the example is simple, in practice I would use automatic differentiation, which makes it more challenging to determine buffer types in advance. I expect I will transition to a more “buffer-free” style in the future, and design the interface for DynamicHMC.jl accordingly.

download code as code.jl

PS: From now on, my blog posts with Julia code will have a banner about version information.


  1. Which will change following this blog post 😁 [return]
  2. The other 3 options are slow because of deprecation warnings. [return]

Local test coverage in Julia

By: Tamás K. Papp

Re-posted from: https://tamaspapp.eu/post/julia-local-coverage/

While codecov.io and coveralls.io are fine services and very easy to set up for Github repositories with Julia packages, they become more difficult to use under some circumstances. For example, if testing requires large binaries you don’t want to build every time, you need to build a Docker image to run testing and obtain coverage information. Also, if your test scripts take a long time and you have access to a powerful computer, you might want to just run coverage locally when working on the code.

Naturally, Julia has all the facilities for generating and collecting coverage information locally. To make their application even easier, I packaged up a small set of scripts in LocalCoverage.jl that I use to generate and visualize coverage information for Julia packages on my machine.

This is how it works:

  1. run generate_coverage(pkg) to

    a. run Pkg.test(pkg; coverage = true),

    b. use Coverage.jl for collecting information to a single file in lcov format,

    c. unless disabled, run the external program genhtml to format everything as a nice HTML file.

  2. if you have generated the HTML files, open_coverage(pkg) opens them for you in your default browser.

  3. if you want to tidy up, run clean_coverage(pkg).

This is what it looks like for this package: