Box-Muller method to Generate normal distribution

using NiLang

In this tutorial, we introduce using Box-Muller method to transform a uniform distribution to a normal distribution. The transformation and inverse transformation of Box-Muller method could be found in this blog

@i function boxmuller(x::T, y::T) where T
    @routine @invcheckoff begin
        @zeros T θ logx _2logx
        θ += 2π * y
        logx += log(x)
        _2logx += -2 * logx
    end

    # store results
    z1 ← zero(T)
    z2 ← zero(T)
    z1 += _2logx ^ 0.5
    ROT(z1, z2, θ)
    ~@routine

    SWAP(x, z1)
    SWAP(y, z2)

    # arithmetic uncomputing: recomputing the original values of `x` and `y` to deallocate z1 and z2
    @routine @invcheckoff begin
        @zeros T at sq _halfsq
        at += atan(y, x)
        if (y < 0, ~)
            at += T(2π)
        end
        sq += x ^ 2
        sq += y ^ 2
        _halfsq -= sq / 2
    end
    z1 -= exp(_halfsq)
    z2 -= at / (2π)
    @invcheckoff z1 → zero(T)
    @invcheckoff z2 → zero(T)
    ~@routine
end

One may wonder why this implementation is so long, should't NiLang generate the inverse for user? The fact is, although Box-Muller is arithmetically reversible. It is not finite precision reversible. Hence we need to "uncompute" it manually, this trick may introduce reversibility error.

using Plots
N = 5000
x = rand(2*N)

Plots.histogram(x, bins = -3:0.1:3, label="uniform",
    legendfontsize=16, xtickfontsize=16, ytickfontsize=16)

forward

@instr boxmuller.(x[1:N], x[N+1:end])
Plots.histogram(x, bins = -3:0.1:3, label="normal",
    legendfontsize=16, xtickfontsize=16, ytickfontsize=16)

backward

@instr (~boxmuller).(x[1:N], x[N+1:end])
Plots.histogram(x, bins = -3:0.1:3, label="uniform",
    legendfontsize=16, xtickfontsize=16, ytickfontsize=16)

Check the probability distribution function

using LinearAlgebra, Test

normalpdf(x) = sqrt(1/2π)*exp(-x^2/2)
normalpdf (generic function with 1 method)

obtain log(abs(det(jacobians)))

@i function f(x::Vector)
    boxmuller(x[1], x[2])
end
jac = NiLang.AD.jacobian(f, [0.5, 0.5], iin=1)
ladj = log(abs(det(jac)))
2.5310242469692907

check if it matches the log(p/q).

z1, z2 = boxmuller(0.5, 0.5)
@test ladj ≈ log(1.0 / (normalpdf(z1) * normalpdf(z2)))
Test Passed
  Expression: ladj ≈ log(1.0 / (normalpdf(z1) * normalpdf(z2)))
   Evaluated: 2.5310242469692907 ≈ 2.5310242469692907

To obtaining Jacobian - a simpler approach

We can define a function that exactly reversible from the instruction level, but costs more space for storing output.

@i function boxmuller2(x1::T, x2::T, z1::T, z2::T) where T
    @routine @invcheckoff begin
        @zeros T θ logx _2logx

        θ += 2π * x2
        logx += log(x1)
        _2logx += -2 * logx
    end

    # store results
    z1 += _2logx ^ 0.5
    ROT(z1, z2, θ)
    ~@routine
end

However, this is not a bijector from that maps x to z, because computing the backward just erases the content in z. However, this function can be used to obtain log(abs(det(jacobians)))

@i function f2(x::Vector, z::Vector)
    boxmuller2(x[1], x[2], z[1], z[2])
end
jac = NiLang.AD.jacobian(f2, [0.5, 0.5], [0.0, 0.0], iin=1, iout=2)
ladj = log(abs(det(jac)))
2.5310242469692907

check if it matches the log(p/q).

_, _, z1, z2 = boxmuller2(0.5, 0.5, 0.0, 0.0)
@test ladj ≈ log(1.0 / (normalpdf(z1) * normalpdf(z2)))
Test Passed
  Expression: ladj ≈ log(1.0 / (normalpdf(z1) * normalpdf(z2)))
   Evaluated: 2.5310242469692907 ≈ 2.5310242469692907

This page was generated using Literate.jl.