Category Archives: Julia

Consonant Triads

We use a physiological interpretation of consonance and dissonant musical sound for human listeners and use a dissonance score to rate different possible triads (combinations of three notes in a scale). This is first done on the conventional scale of 12 equidistant tones in an octave, as well as the Bohlen-Pierce scale.

In [1]:
using Plots
gr()
Out[1]:
Plots.GRBackend()

Plomp-Levelt dissonance curve

As a basis of our investigation, we use the Plomp-Levelt dissonance curve as explained in this article by William Sethares.

The curve models the fact that similar frequencies appear dissonant because of “friction” between the adjacent sensors in our ears.

In [2]:
"Dissonance of sound with two sinusoidal sounds with given frequencies and amplitudes."
function diss(freq1, ampl1, freq2, ampl2)
    if freq2 < freq1 # swap args
        return diss(freq2, ampl2, freq1, ampl1)
    end
    s = 0.24 * (freq2 - freq1) / (0.0207 * freq1 + 18.96)
    5.0 * ampl1 * ampl2 * (exp(-3.51 * s) - exp(-5.75 * s))
end
Out[2]:
diss

Let’s draw the curve for a pair of sine waves, the first fixed at given base frequency and the other going through a range of relatively higher frequencies.

In [3]:
basefreq = 440.0
ratrange = range(1.0, stop=3.0, length=200)
otherfreq = basefreq * ratrange
disss = diss.(basefreq, 1.0, otherfreq, 1.0);
In [4]:
plot(otherfreq, disss, xlabel="Frequency", ylabel="Dissonance", legend=false)
Out[4]:

600 800 1000 1200 0.0 0.2 0.4 0.6 0.8 Frequency Dissonance

Dissonance with harmonics

Rather than analyzing dissonance between sinusoidal sounds, we also want to consider sounds with rich harmonics. These have sounds at multiple frequencies with declining amplitudes. When computing the dissonance between two pitches with harmonics, we have to add the dissonance values for all combinations of frequencies.

In [5]:
# Let's first give formulas for the amplitudes of common waveshapes.

enumrange(number::Int) = collect(range(1.0, stop=number, length=number))

abstract type Wave end

struct Sine <: Wave end
harm(::Sine, number::Int) = vcat([1.0], zeros(number - 1))

struct Sawtooth <: Wave end
harm(::Sawtooth, number::Int) = 1.0 ./ enumrange(number)

struct Triangle <: Wave end
harm(::Triangle, number::Int) = [(((i+2)%4)-2)*(i%2) for i=1:number] ./ enumrange(number).^2

struct Square <: Wave end
harm(::Square, number::Int) = [i%2 for i in 1:number] ./ enumrange(number)
Out[5]:
harm (generic function with 4 methods)
In [6]:
@show harm(Sine(), 9)
@show harm(Sawtooth(), 9)
@show harm(Square(), 9)
@show harm(Triangle(), 9);
harm(Sine(), 9) = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
harm(Sawtooth(), 9) = [1.0, 0.5, 0.333333, 0.25, 0.2, 0.166667, 0.142857, 0.125, 0.111111]
harm(Square(), 9) = [1.0, 0.0, 0.333333, 0.0, 0.2, 0.0, 0.142857, 0.0, 0.111111]
harm(Triangle(), 9) = [1.0, 0.0, -0.111111, 0.0, 0.04, 0.0, -0.0204082, 0.0, 0.0123457]

Let’s also quickly check how our approximated waveforms look:

In [7]:
xx = range(0, stop=4π, length=100)

function yy(wave, n)
    coefs = harm(wave, n)
    sum(coefs[f] * sin.(f*xx) for f=1:n)
end

plot(xx,[yy(w, 20) for w in [Sawtooth(), Square(), Triangle()]])
Out[7]:

0 2 4 6 8 10 12 -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5 y1 y2 y3

In [8]:
# We add a new type to couple frequency and amplitute of a harmonic overtone

struct Tone
    freq::Float64
    ampl::Float64
end

diss(t1::Tone, t2::Tone) = diss(t1.freq, t1.ampl, t2.freq, t2.ampl)
Out[8]:
diss (generic function with 2 methods)
In [9]:
# More functions to generate all overtones

function harmonics(wave::Wave, basefreq::Float64, number::Int)
    # frequencies with integer multiples
    freqs = basefreq .* enumrange(number)
    ampls = abs.(harm(wave, number))
    # keep tones with nonzero amplitude
    [Tone(tup...) for tup in zip(freqs, ampls) if tup[2] > 0.0]
end
Out[9]:
harmonics (generic function with 1 method)
In [10]:
@show harmonics(Sine(), 440.0, 4)
@show harmonics(Triangle(), 440.0, 4);
harmonics(Sine(), 440.0, 4) = Tone[Tone(440.0, 1.0)]
harmonics(Triangle(), 440.0, 4) = Tone[Tone(440.0, 1.0), Tone(1320.0, 0.111111)]
In [11]:
function diss(tones::AbstractArray{Tone})
    res = 0.0
    # iterate over all pairs
    n = length(tones)
    for i = 1:n
        for j=i+1:n
            res += diss(tones[i], tones[j])
        end
    end
    res
end
Out[11]:
diss (generic function with 3 methods)
In [12]:
function together(tones1::AbstractArray{Tone}, tones2::AbstractArray{Tone})
    # TODO: should do proper merge and match those tones with same frequency?
    vcat(tones1, tones2)
end

function diss(tones1::AbstractArray{Tone}, tones2::AbstractArray{Tone})
    diss(together(tones1, tones2))
end
Out[12]:
diss (generic function with 4 methods)

As a first example, we look at the square waveform approximated with 10 harmonices and draw the dissonance curve for two pitches played simultaneously.

Note the local maxima near frequency rations 1 and 2 and the minima at integer (or “simple”) ratios.

In [13]:
numharm = 10
basetone = harmonics(Square(), basefreq, numharm)
othertones = [harmonics(Square(), of, numharm) for of in otherfreq]
disss = [diss(basetone, ot) for ot in othertones]

plot(ratrange, disss, legend=false, xlabel="Freq ratio", ylabel="Dissonance", title="Square $basefreq")
Out[13]:

1.0 1.5 2.0 2.5 3.0 0.00 0.25 0.50 0.75 1.00 Square 440.0 Freq ratio Dissonance

Next, we repeat the experiment, now based on an approximation of the sawtooth waveform.
Because of the richer harmonices, there is more interaction and more dissonance (in absolute terms).

As a consequence, the dissonance curve shows more local minima, again corresponding to “simple” frequency ratios, that is, ratios of small integers.

In [14]:
numharm = 10
basetone = harmonics(Sawtooth(), basefreq, numharm)
othertones = [harmonics(Sawtooth(), of, numharm) for of in otherfreq]
disss = [diss(basetone, ot) for ot in othertones]

plot(ratrange, disss, legend=false, xlabel="Freq ratio", ylabel="Dissonance", title="Sawtooth $basefreq")
Out[14]:

1.0 1.5 2.0 2.5 3.0 0.00 0.25 0.50 0.75 1.00 1.25 Sawtooth 440.0 Freq ratio Dissonance

Triads

We will now compute the dissonaces for triads of tones in an analoguous fashion.
But this time, we will only evaluate a discrete set of pitches, based on a 12-tone scale, dividing the octave in equal steps.

In [15]:
# dissonance for triads
function diss(tones1::AbstractArray{Tone}, tones2::AbstractArray{Tone}, tones3::AbstractArray{Tone})
    diss(together(together(tones1, tones2), tones3))
end
Out[15]:
diss (generic function with 5 methods)
In [16]:
# range of one octave
halfstep = 2.0^(1.0/12.0)
@show halfstep
ratrange = [halfstep^i for i=0:12]

basefreq = 440.0
otherfreq = basefreq * ratrange;
halfstep = 1.0594630943592953

We start using the square waveform. In the heatmap below, we can see that dissonance is high whenever a single halfstep is present in the triad. This is true for the horizontal and vertical lines that are off by one from the origin, as well as the two diagonals off by one from the center. To a lesser extent, this pattern repeats for 11 halfsteps (one octave up, one halfstep down).

In [17]:
# start with square wave again
numharm = 10
basetone = harmonics(Square(), basefreq, numharm)
othertones = [harmonics(Square(), of, numharm) for of in otherfreq]

# compute dissonances of all triads
disss = [diss(basetone, ot1, ot2) for ot1 in othertones, ot2 in othertones]

heatmap(0:12, 0:12, disss, aspect_ratio=1.0, title="Square wave on 12-tone octave")
Out[17]:

0 2 4 6 8 10 12 0 2 4 6 8 10 12 Square wave on 12-tone octave 0.5 1.0 1.5 2.0 2.5

If we use the sawtooth wave instead, we see a richer structure in the dissonance heatmap.
Here, we should also be able to recognize familiar concepts such as the major or minor triads.

In [18]:
# now use richer sawtooth wave
numharm = 10
basetone = harmonics(Sawtooth(), basefreq, numharm)
othertones = [harmonics(Sawtooth(), of, numharm) for of in otherfreq]

# compute dissonances of all triads
disss = [diss(basetone, ot1, ot2) for ot1 in othertones, ot2 in othertones]

heatmap(0:12, 0:12, disss, aspect_ratio=1.0, title="Sawtooth wave on 12-tone octave")
Out[18]:

0 2 4 6 8 10 12 0 2 4 6 8 10 12 Sawtooth wave on 12-tone octave 0.5 1.0 1.5 2.0 2.5 3.0 3.5

Bohlen-Pierce Scale

One way to view the conventional western scale is as a division of an octave (that is, a 2:1 ratio of frequency) in 12 (more or less) equal steps. Alternatively, one can start at the ratios 4:5:6 (appearing naturally as harmonics) and collect pitches that are reached from these recursively.

In the Bohlen-Pierce scale, we range over a tritave (a 3:1 ratio) in 13 steps. This can also be derived using ratios 3:5:7. The scale is well-suited for instruments that only feature odd harmonics, such as the clarinet or the pan flute.

In [19]:
# range of one tritave
bpstep = 3.0^(1.0/13.0)
@show bpstep
ratrange = [bpstep^i for i=0:13]

basefreq = 440.0
otherfreq = basefreq * ratrange;
bpstep = 1.0881822434633168
In [20]:
# start with square wave again
numharm = 10
basetone = harmonics(Square(), basefreq, numharm)
othertones = [harmonics(Square(), of, numharm) for of in otherfreq]

# compute dissonances of all triads
disss = [diss(basetone, ot1, ot2) for ot1 in othertones, ot2 in othertones]

heatmap(0:13, 0:13, disss, aspect_ratio=1.0, title="Square wave on BP tritave")
Out[20]:

0 2 4 6 8 10 12 0 2 4 6 8 10 12 Square wave on BP tritave 0.25 0.50 0.75 1.00 1.25 1.50 1.75 2.00

The heatmap above looks fairly boring, with not much dissonance apart from the obvious lines. Maybe this is related to the wider spacing of the steps in this scale.

We will also look at the sawtooth wave, even though we should expect high dissonance through-out.

In [21]:
# now use richer sawtooth wave
numharm = 10
basetone = harmonics(Sawtooth(), basefreq, numharm)
othertones = [harmonics(Sawtooth(), of, numharm) for of in otherfreq]

# compute dissonances of all triads
disss = [diss(basetone, ot1, ot2) for ot1 in othertones, ot2 in othertones]

heatmap(0:13, 0:13, disss, aspect_ratio=1.0, title="Sawtooth wave on BP tritave")
Out[21]:

0 2 4 6 8 10 12 0 2 4 6 8 10 12 Sawtooth wave on BP tritave 0.5 1.0 1.5 2.0 2.5

Larger Pitch Ranges

We will generate some more heatmaps covering two octaves or tritaves, each time based on sawtooth waves.

First the 12-tone octaves:

In [22]:
# range of two octaves
ratrange = [halfstep^i for i=0:24]
basefreq = 440.0
otherfreq = basefreq * ratrange

# sawtooth wave
numharm = 10
basetone = harmonics(Sawtooth(), basefreq, numharm)
othertones = [harmonics(Sawtooth(), of, numharm) for of in otherfreq]

# compute dissonances of all triads
disss = [diss(basetone, ot1, ot2) for ot1 in othertones, ot2 in othertones]

heatmap(0:24, 0:24, disss, aspect_ratio=1.0, title="Sawtooth wave on 12-tone octaves")
Out[22]:

0 5 10 15 20 0 5 10 15 20 Sawtooth wave on 12-tone octaves 0.5 1.0 1.5 2.0 2.5 3.0 3.5

Then the BP tritaves:

In [23]:
# range of two tritaves
ratrange = [bpstep^i for i=0:26]
basefreq = 440.0
otherfreq = basefreq * ratrange

# sawtooth wave
numharm = 10
basetone = harmonics(Sawtooth(), basefreq, numharm)
othertones = [harmonics(Sawtooth(), of, numharm) for of in otherfreq]

# compute dissonances of all triads
disss = [diss(basetone, ot1, ot2) for ot1 in othertones, ot2 in othertones]

heatmap(0:26, 0:26, disss, aspect_ratio=1.0, title="Sawtooth wave on BP tritaves")
Out[23]:

0 5 10 15 20 25 0 5 10 15 20 25 Sawtooth wave on BP tritaves 0.5 1.0 1.5 2.0 2.5

The cutting stock problem: part 2, solving with column generation

By: Julia on μβ

Re-posted from: https://mbesancon.github.io/post/2018-05-25-colgen2/


In the previous post, we explored a well-known integer optimization situation
in manufacturing, the cutting stock problem. After some details on the
decisions, constraints and objectives, we implemented a naive model in JuMP.

One key thing to notice is the explosion of number of variables and constraints
and the fact that relaxed solutions (without constraining variables to be
integers) are very far from actual feasible solutions.

We will now use an other way of formulating the problem, using a problem
decomposition and an associated solution method (column generation).

Re-stating the cutting stock problem

In the last post, we used two decisions: $Y_i$ stating if the big roll $i$ is
used and $X_{ij}$ expressing the number of cuts $j$ made in the roll $i$.
To minimize the number of rolls, it makes sense to put as many small cuts
as possible on a big roll. We could therefore identify saturating patterns,
that is, a combination of small cuts fitting on a big roll, such that no
additional cut can be placed, and then find the smallest combination of the
pattern satisfying the demand.

One problem remains: it is impossible to compute, or even to store in memory all
patterns, their number is exponentially big with the number of cuts, so we will
try to find the best patterns and re-solve the problem, using the fact that not
all possible patterns will be necessary.

This is exactly what the Dantzig-Wolfe decomposition does, it splits the problem
into a Master Problem MP and a sub-problem SP.

  • The Master Problem, provided a set of patterns, will find the best combination
    satisfying the demand.
  • The sub-problem, given an “importance” of each cut provided by the master
    problem, will find the best cuts to put on a new pattern.

This is an iterative process, we can start with some naive patterns we can think
of, compute an initial solution for the master problem, which will be feasible
but not optimal, move on to the sub-problem to try to find a new pattern
(or column in the optimization jargon, hence the term of column generation).

How do we define the “importance” of a cut $j$? The value of the dual variable
associated with this constraint will tell us that. This is not a lecture in
duality theory, math-eager readers can check out further documentation on the
cutting stock problem and duality in linear optimization.

Moreover, we are going to add one element to our model: excess cuts can be sold
at a price $P_j$, so that we can optimize by minimizing the net cost (production
cost of the big rolls minus the revenue from excess cuts).

New formulation

As in the previous post, we’re going to formulate the decisions and constraints
for the new version of the problem.

Decisions

At the master problem level, given a pattern $p$, the decision will be
$\theta_p$ (theta, yes Greek letters are awesome), the number of big rolls which
will be used with this pattern. $\theta_p$ is a positive integer.

The decision at the sub-problem level will be to find how many of each cut $j$
to fit onto one big roll, $a_j$.

For a pattern $p$, the number of times a cut $j$ appears is given by $a_{jp}$.

Constraints

The big roll size constraint is kept in the sub-problem, a pattern built
has to respect this constraint:
$$ \sum_j a_{j} \cdot W_j \leq L $$

The demand $D_j$ is met with all rolls of each pattern so it is kept at the master
level. The number of cuts of type $j$ produced is the sum of the number of this
cut on each patterns times the number of the pattern in a solution:

$$ NumCuts_j = \sum_p a_{jp} \cdot \theta_p \geq D_j$$

Objective formulation

At the master problem, we minimize the number of rolls, which is simply:
$$ \sum_{p} \theta_p $$

At the sub-problem, we are trying to maximize the gain associated with the need
for the demand + the residual price of the cuts. If we can find a worth using
producing compared to its production cost, it is added.

Implementation

As before, we will formulate the master and sub-problem using Julia with JuMP.
Just like the previous post, we use the Clp and Cbc open-source solvers.
We read the problem data (prices, sizes, demand) from a JSON file.

using JuMP
using Cbc: CbcSolver
using Clp: ClpSolver
import JSON

const res = open("data0.json", "r") do f
    data = readstring(f)
    JSON.Parser.parse(data)
end

const maxwidth = res["maxwidth"]
const cost = res["cost"]
const prices = Float64.(res["prices"])
const widths = Float64.(res["widths"])
const demand = Float64.(res["demand"])
const nwidths = length(prices)

cost is the production cost of a big roll.

Sub-problem

The subproblem is a function taking reduced costs of each cut and maximizing
the utility of the pattern it creates:

"""
    subproblem tries to find the best feasible pattern
    maximizing reduced cost and respecting max roll width
    corresponding to a multiple-item knapsack
"""
function subproblem(reduced_costs, sizes, maxcapacity)
    submodel = Model(solver = CbcSolver())
    n = length(reduced_costs)
    xs = @variable(submodel, xs[1:n] >= 0, Int)
    @constraint(submodel, sum(xs. * sizes) <= maxcapacity)
    @objective(submodel, Max, sum(xs. * reduced_costs))
    solve(submodel)
    return round.(Int,getvalue(xs)), round(Int,getobjectivevalue(submodel))
end

Initial master problem

We saw that the master problem finds a solution and then requires a new pattern
from the sub-problem. This is therefore preferable to start from an initial
feasible, otherwise we fall into a special case we’re not discussing here.
One initial solution would be to build one pattern per cut, with as many cuts as
we can, which is $floor(L/w_j)$.

function init_master(maxwidth, widths, rollcost, demand, prices)
    n = length(widths)
    ncols = length(widths)
    patterns = spzeros(UInt16,n,ncols)
    for i in 1:n
        patterns[i,i] = min(floor(Int,maxwidth/widths[i]),round(Int,demand[i]))
    end
    m = Model(solver = ClpSolver())
    θ = @variable(m, θ[1:ncols] >= 0)
    @objective(m, Min,
        sum(θ[p] * (rollcost - sum(patterns[j,p] * prices[j] for j=1:n)) for p in 1:ncols)
    )
    @constraint(m, demand_satisfaction[j=1:n], sum(patterns[j,p] * θ[p] for p in 1:ncols)>=demand[j])
    if solve(m) != :Optimal
        warn("No optimal")
    end
    return (m, getvalue(θ), demand_satisfaction, patterns)
end

We can compute the reduced costs from the dual values associated with the
demand and the prices of cuts

# getting the model and values
(m, θ, demand_satisfaction, patterns) = init_master(maxwidth, widths, cost, demand, prices);

# compute reduced costs
reduced_costs = getdual(demand_satisfaction)+prices;

# ask sub-problem for new pattern
newcol, newobj = subproblem(reduced_costs, widths, maxwidth)

Putting it all together

We can now build a column generation function putting all elements together and
performing the main iteration:

function column_generation(maxwidth, widths, rollcost, demand, prices; maxcols = 5000)
    (m, θ, demand_satisfaction, patterns) = init_master(maxwidth, widths, rollcost, demand, prices)
    ncols = nwidths
    while ncols <= maxcols
        reduced_costs = getdual(demand_satisfaction) + prices
        newcol, newobj = subproblem(reduced_costs, widths, maxwidth)
        netcost = cost - sum(newcol[j] * (getdual(demand_satisfaction)[j]+prices[j]) for j in 1:nwidths)
        println("New reduced cost: $netcost")
        if netcost >= 0
            return (:Optimal, patterns, getvalue(θ))
        end
        patterns = hcat(patterns, newcol)
        ncols += 1
        m = Model(solver = ClpSolver())
        θ = @variable(m, θ[1:ncols] >= 0)
        @objective(m, Min,
            sum(θ[p] * (rollcost - sum(patterns[j,p] * prices[j] for j=1:nwidths)) for p in 1:ncols)
        )
        @constraint(m, demand_satisfaction[j=1:nwidths], sum(patterns[j,p] * θ[p] for p in 1:ncols)>=demand[j])
        if solve(m) != :Optimal
            warn("No optimal")
            return (status(m), patterns, getvalue(θ))
        end
    end
    return (:NotFound, patterns, :NoVariable)
end

We’ve printed information along the computation to see what’s going on more
clearly, now launching it:

status, patterns, θ = column_generation(maxwidth, widths, cost, demand, prices, maxcols = 500);
New reduced cost: -443.18181818181824
New reduced cost: -375.0
New reduced cost: -264.0
New reduced cost: -250.0
New reduced cost: -187.5
New reduced cost: -150.0
New reduced cost: -150.0
New reduced cost: -107.14285714285711
New reduced cost: -97.5
New reduced cost: -107.14285714285734
New reduced cost: -72.0
New reduced cost: -53.571428571428555
New reduced cost: -53.125
New reduced cost: -50.0
New reduced cost: -43.40625
New reduced cost: -36.0
New reduced cost: -34.625
New reduced cost: -41.5
New reduced cost: -21.8515625
New reduced cost: -22.159090909090878
New reduced cost: -20.625
New reduced cost: -16.304347826086314
New reduced cost: -16.304347826086996
New reduced cost: -20.310344827586277
New reduced cost: -18.0
New reduced cost: -8.837209302325732
New reduced cost: -6.060606060606119
New reduced cost: 0.0

While the cost of a new pattern is negative, we can add it to the master and
keep running. This seems to make sense. Now, one thing to note, we have not
yet specified the integrality constraints, meaning that we don’t have integer
number of patterns. We can see that on the $\theta$ variable:

println(θ)
[0.0, 0.0, 0.0, ... 70.0, 0.0, 0.0, 0.0, 12.56, 46.86, 0.0, 0.0, 0.0, 0.0,
3.98, 0.0, 0.0, 21.5, 5.0, 31.12, 61.12, 33.58, 0.0, 0.0, 32.2, 44.0,
46.88, 19.0, 1.88, 16.42]
println(sum(θ))
446.1000000000001

We saw in the last post that the problem without integrality constraints is
a relaxation and therefore, can only yield a better result. This means that we
cannot have an integer solution using 446 big rolls or less, the minimum will
be 447 rolls. Let’s solve the problem with the same patterns, but adding the
integrality:

# compute initial integer solution:
# take worse case from linear solution, round up
intial_integer = ceil.(Int,θ);


"""
    From patterns built in the column generation phase, find an integer solution
"""function branched_model(patterns, demand, rollcost, prices; npatts = size(patterns)[2], initial_point = zeros(Int,npatts))
    npatts = size(patterns)[2]
    m = Model(solver = CbcSolver())
    θ = @variable(m, θ[p = 1:npatts] >= 0, Int, start = initial_point[p])
    @objective(m, Min,
        sum(θ[p] * (rollcost - sum(patterns[j,p] * prices[j] for j=1:nwidths)) for p in 1:npatts)
    )
    @constraint(m, demand_satisfaction[j=1:nwidths], sum(θ[p] * patterns[j,p] for p in 1:npatts) >= demand[j])
    status = solve(m)
    return (status, round.(Int,(getvalue(θ))))
end

Let’s see what the results look like:

status, θ_final = branched_model(patterns, demand, cost, prices; initial_point = intial_integer)
println(status)
:Optimal
println(sum(θ_final))
447

Given that we cannot do better than 447, we know we have the optimal
number of rolls.

Conclusion

After seeing what a mess integer problems can be in the first part, we used a
powerful technique called Dantzig-Wolfe decomposition, spliting the problem into
master and sub-problem, each handling a subset of the constraints.

Column generation is a technique making this decomposition usable in practice,
by adding only one or few columns (patterns) at each iteration, we avoid
an exponentially growing number of variables. The fact that JuMP is built as
an embedded Domain Specific Language in Julia makes it a lot easier to specify
problems and play around them. Most optimization specific modeling languages
are built around declarative features and get messy very quickly when
introducing some logic (like column generation iterations). Developers
could relate this technique to lazy value computation: we know all values are
there, but we just compute them whenever needed.

Hope you enjoyed reading this second post on the cutting stock problem. A
Jupyter notebook summing up all code snippets can be found at
this repository.
Feel free to ping me for feedback!

Note on performance

The column generation approach we just saw scales well to huge problems, but
this particular implementation can feel a bit slow at first. One recommended
thing is to do in such case is “warm-starting” the solver: give it a good
initial solution to start from. Since we built both the master and subproblem
as stateless functions, the model is being re-built from scratch each time.
The advantage is that any solver can be used, since some of them don’t support
warm starts.


Image source: https://www.flickr.com/photos/30478819@N08/38272827564

Optimizing your diet with JuMP

By: oxinabox.github.io

Re-posted from: https://white.ucc.asn.au/2018/05/28/Optimizing-your-diet-with-JuMP.html

I’ve been wanting to do a JuMP blog post for a while.
JuMP is a Julia mathematical programing library.
It is to an extent a DSL for describing constrained optimisation problems.

A while ago, a friend came to me who was looking to get “buff”,
what he wanted to do, was maximise his protein intake, while maintaining a generally healthy diet.
He wanted to know what foods he should be eating.
To devise a diet.

If one thinks about this,
this is actually a Linear Programming problem – constrained linear optimisation.
The variables are how much of each food to eat,
and the contraints are around making sure you have enough (but not too much) of all the essential vitamins and minerals.

Note: this is a bit of fun, in absolutely no way do I recommend using the diets the code I am about to show off generates.
I am in no way qualified to be giving dietry or medical advice, etc.
But this is a great way to play around with optimisation.
Continue reading