JuMP.jl optimization which solver

I have an optimization problem, but I am unsure which solver to use.
I think JuMP.jl might work for this (the problem looks linear to me).

Either @objective(....) gives me an error or optimize!(model) errors.

The problem is to buy n items from different webshops with shipping costs.
Thus I have M[i,j] = {1,0} indicating that I buy item i in shop j
and P[j,i] the price of item i in shop j
additionally I have shipping costs (currently constant at 25 for each shop)

below is my code.
The top code is only data generation.
The model definition is rather simple.
See the line with Model(HiGHS.Optimizer)

Can someone tell me which solver to pick?
Or maybe if I should formulate the objective differently?

#using Revise; using BrickLink;import CSV; using DataFrames; using JSON3; using HTTP; using OAuth; using Dates ; using StatsBase
using CSV; using DataFrames
##################################################################################################
#optimize shopping
##################################################################################################
using JuMP #for optimization
#using HiGHS
#using Ipopt
using LinearAlgebra
@info("run ishop_fetch_data.jl to generate the data.")
data = CSV.read(download(raw"https://gist.githubusercontent.com/kafisatz/12ccf544a3924816be378cb785e0b04b/raw/c2baed05921e8f0e84ed0ccbdbdfb531ac7d90ab/dfsupply.csv"),DataFrame,types=Dict("setnowithdash"=>String));
#keep only one offer for each shop!
    sort!(data,[:setnowithdash,:strStorename,:price])
    unique!(data,[:setnowithdash,:strStorename])

#filter for debugging / development
#toy example: let us consider only 6 sets for now
setnolist = sort(unique(data.setnowithdash)[2:7])
filter!(x->x.setnowithdash in setnolist,data);
@show unique(data.name)

shoplist = sort(unique(data.strStorename))

nsets = length(setnolist)
nshops = length(shoplist)

#set shipping costs (constant for now)
shipping_cost_fixed_value = 25.0
shoplist_shippingcosts_di = Dict(shoplist .=> shipping_cost_fixed_value)
shippingcosts_vec = ones(nshops) .* shipping_cost_fixed_value

infinite_price = 9e20 #large number (if item is not available in a certain shop)

#export create_price_matrix
function create_price_matrix(setnolist,shoplist,data;infinite_price=9e20)
    #infinite_price to indicate that the shop has no offering for this set 
    #as we minimize the overall price, we set this to a large value ~ infinite
    #Float Matrix
    nsets = length(setnolist)
    nshops = length(shoplist)

    #checks
    for shop in shoplist
        dftmp = filter(x->x.strStorename==shop,data)
        @assert size(dftmp,1) > 0
    end
    for setno in setnolist
        dftmp = filter(x->x.setnowithdash==setno,data)
        @assert size(dftmp,1) > 0
    end

    pricedict = Dict()
    for i=1:size(data,1)
        #for i=1:11
        setno = data.setnowithdash[i]
        shop = data.strStorename[i]
        price = data.price[i]
        pricedict[(setno,shop)] = price
        #@show i,length(pricedict),i-length(pricedict)
    end

    p = zeros(nshops,nsets)
    for i=1:nshops 
        for j=1:nsets 
            setno = setnolist[j]
            shop = shoplist[i]
            if haskey(pricedict,(setno,shop))
                p[i,j] = pricedict[(setno,shop)]
            else
                p[i,j] = infinite_price
            end            
        end
    end

    #consistency checks
    @show sum(p .< infinite_price) , size(data,1), size(data,1) - sum(p .< infinite_price) 
    @assert sum(p .< infinite_price) == size(data,1)

    return p 
end

P = create_price_matrix(setnolist,shoplist,data);
one_vector = ones(Int,nsets)

#M_ij == 1 <-> we buy setnolist[i] from shoplist[j]
#M_init = zeros(Int,nsets,nshops)

#objective:
#We want to minimize Sum_ij (P * M) + shipping costs 
#shipping costs = Sum_j (shoplist_shippingcosts_di[j]) for each shop j where order at least one item (i.e. if the sum of column j of M is larger than zero)

#P -> price matrix, number of rows == nshops, number of columns == nsets
#M -> optimization variable, each entry must be 0 or 1, number of rows == nsets, number of columns == nshops
#M * P -> size is nsets X nsets
#(M * P) * one_vector is a vector of length nsets
#transpose(one_vector) * ((M * P) * one_vector) should be a float
shipping_costs = 0.0

#testing Linear Algebra
    Mtest = rand(nsets,nshops);
    transpose(one_vector) * ((Mtest * P) * one_vector) + shipping_costs

    oneM = ones(Int,size(P))
    fill!(Mtest,0.0)
    Mtest[1,1] = 1; Mtest[2,2] = 1; Mtest[3,2] = 1; Mtest[4,2] = 1;  Mtest[5,3] = 1; Mtest[6,4] = 1;
    oneM * Mtest
    shipping_costs = dot(shippingcosts_vec , (transpose(ones(nshops)) * (oneM * Mtest) .> 0))
    shipping_costs = dot(shippingcosts_vec , map(x->min(1,x),(transpose(ones(nshops)) * (oneM * Mtest))[:] ))
    objective_costs = transpose(one_vector) * ((Mtest * P) * one_vector) + shipping_costs

    sum(Mtest,dims=1)
    transpose(one_vector) * ((Mtest * P) * one_vector) + shipping_costs

#using JuMP;using Alpine;using Ipopt;using HiGHS
using HiGHS; model = Model(HiGHS.Optimizer)
#using Ipopt; model = Model(Ipopt.Optimizer)
#using Alpine;using Ipopt;using HiGHS;ipopt = optimizer_with_attributes(Ipopt.Optimizer, "print_level" => 0) ; highs = optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false); model = Model(optimizer_with_attributes(Alpine.Optimizer,"nlp_solver" => ipopt,"mip_solver" => highs,),)
#using MadNLP; model = Model(()->MadNLP.Optimizer(print_level=MadNLP.INFO, max_iter=100))
#using Juniper;using Ipopt; ipopt = optimizer_with_attributes(Ipopt.Optimizer, "print_level"=>0); optimizer = optimizer_with_attributes(Juniper.Optimizer, "nl_solver"=>ipopt); model = Model(optimizer)

#M_ij == true <-> we buy setnolist[i] from shoplist[j]
@variable(model, M[i = 1:nsets, j = 1:nshops],integer = true)
#M_ij must be 0 or 1
@constraint(model, [i = 1:nsets, j = 1:nshops], 0 <= M[i,j] <= 1)

#buy each item once
for i in 1:nsets
    @constraint(model, sum(M[i,j] for j in 1:nshops) == 1)
end

#set initial values
for i=1:nsets
    for j=1:nshops
        set_start_value(M[i,j],0)
    end
end

#@objective(model,Min, transpose(one_vector) * ((M * P) * one_vector) + dot(shippingcosts_vec , map(x->min(1,x),(transpose(ones(nshops)) * (oneM * M))[:] )) ) #Matrix notation
#@objective(model,Min, sum(M[i,j] * P[j,i] for i in 1:nsets,j in 1:nshops) + dot(shippingcosts_vec , map(x->min(1,x),(transpose(ones(nshops)) * (oneM * M))[:] )) )
@objective(model,Min, sum(M[i,j] * P[j,i] for i in 1:nsets,j in 1:nshops) + sum(ifelse(sum(M,dims=1)[i] > 0 , shippingcosts_vec[i] , 0) for i = 1:nshops))
print(model)
@time optimize!(model)

primal_status(model)
dual_status(model)
objective_value(model)
is_solved_and_feasible(model)

Hi @bernhard, you can use HiGHS for this problem, but you need to reformulate the objective. You must formulate the problem as a MIP, which means you cannot use nonlinear functions like ifelse in the objective.

A standard trick for these fixed-cost issues is to introduce a new decision variable which is 1 if there is flow and 0 otherwise.

Here’s how I would write your model:

using JuMP
import CSV
import DataFrames
import HiGHS
data = CSV.read(
    download(
        raw"https://gist.githubusercontent.com/kafisatz/12ccf544a3924816be378cb785e0b04b/raw/c2baed05921e8f0e84ed0ccbdbdfb531ac7d90ab/dfsupply.csv",
    ),
    DataFrames.DataFrame;
    types = Dict("setnowithdash" => String),
)
sort!(data, [:setnowithdash, :strStorename, :price])
unique!(data, [:setnowithdash, :strStorename])
setnolist = sort(unique(data.setnowithdash)[2:7])
filter!(x -> x.setnowithdash in setnolist, data);
shoplist = sort(unique(data.strStorename))
nsets, nshops = length(setnolist), length(shoplist)
shippingcosts_vec = fill(25.0, nshops)

function create_price_matrix(setnolist, shoplist, data)
    nsets, nshops = length(setnolist), length(shoplist)
    for shop in shoplist
        @assert size(filter(x -> x.strStorename == shop, data), 1) > 0
    end
    for setno in setnolist
        @assert size(filter(x -> x.setnowithdash == setno, data), 1) > 0
    end
    pricedict = Dict(
        (data.setnowithdash[i], data.strStorename[i]) => data.price[i]
        for i in 1:size(data,1)
    )
    p = [
        get(pricedict, (setnolist[i], shoplist[j]), Inf)
        for i in 1:nsets, j in 1:nshops
    ]
    @assert sum(p .< Inf) == size(data, 1)
    return p 
end

P = create_price_matrix(setnolist, shoplist, data);
model = Model(HiGHS.Optimizer)
@variable(
    model,
    0 <= M[i in 1:nsets, j in 1:nshops] <= ifelse(P[i, j] == Inf, 0, 1),
    Bin,
)
@variable(model, M_fixed_cost[1:nshops], Bin)
@constraint(model, [i in 1:nsets], sum(M[i, :]) == 1)
@constraint(model, [j in 1:nshops], sum(M[:, j]) <= nshops * M_fixed_cost[j])
@objective(
    model,
    Min,
    sum(M[i,j] * P[i,j] for i in 1:nsets, j in 1:nshops if P[i,j] < Inf) +
    sum(M_fixed_cost[j] * shippingcosts_vec[j] for j in 1:nshops),
)
optimize!(model)
2 Likes

There’s also an option to use a formulation that is much closer to the structure of the data:

julia> using JuMP

julia> import CSV

julia> import DataFrames

julia> import HiGHS

julia> begin
           data = CSV.read(
               download(
                   raw"https://gist.githubusercontent.com/kafisatz/12ccf544a3924816be378cb785e0b04b/raw/c2baed05921e8f0e84ed0ccbdbdfb531ac7d90ab/dfsupply.csv",
               ),
               DataFrames.DataFrame;
               types = Dict("setnowithdash" => String),
           )
           sort!(data, [:setnowithdash, :strStorename, :price])
           unique!(data, [:setnowithdash, :strStorename])
           setnolist = sort(unique(data.setnowithdash)[2:7])
           filter!(x -> x.setnowithdash in setnolist, data)
           model = Model(HiGHS.Optimizer)
           set_silent(model)
           data.x_buy = @variable(model, x_buy[1:size(data, 1)], Bin)
           for df in DataFrames.groupby(data, "setnowithdash")
               @constraint(model, sum(df.x_buy) == 1)
           end
           fixed_shipping = zero(AffExpr)
           for df in DataFrames.groupby(data, "strStorename")
               x_fixed_cost = @variable(model, binary = true)
               @constraint(model, sum(df.x_buy) <= size(df, 1) * x_fixed_cost)
               add_to_expression!(fixed_shipping, 25.0 * x_fixed_cost)
           end
           @objective(model, Min, data.price' * data.x_buy + fixed_shipping)
           optimize!(model)
           @assert is_solved_and_feasible(model)
           data.x_decision = round.(Bool, value.(data.x_buy))
           filter(row -> row.x_decision, data)
       end
6Γ—26 DataFrame
 Row β”‚ setnowithdash  name                               price     price_currency  codeComplete  itemid  strStorename            mMinBuy    strSellerCountryName  idInv      strDesc   β‹―
     β”‚ String         String                             Float64   String3         String1       Int64   String                  String15   String31              Int64      String?   β‹―
─────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1 β”‚ 7261-1         Clone Turbo Tank (with Light-Up …  285.757   CHF             C              52663  Bontis Bunte Bausteine  ILS 18.66  Germany               335285863  No box, n β‹―
   2 β”‚ 75005-1        Rancor Pit                         180.884   CHF             C             112755  AlandBricks             ILS 37.32  Finland               430759849  100% comp
   3 β”‚ 75091-1        Flash Speeder                       30.004   CHF             C             136019  Brickland_officiel      None       France                452858454  Complete,
   4 β”‚ 75100-1        First Order Snowspeeder             23.8118  CHF             C             138395  Sixth Brick             None       Finland               448975350  With inst
   5 β”‚ 75151-1        Clone Turbo Tank                    97.0365  CHF             C             145081  Story of my brick       ILS 35.87  South Korea           433323581  no boxes  β‹―
   6 β”‚ 75156-1        Krennic's Imperial Shuttle         152.404   CHF             C             148007  Sixth Brick             None       Finland               448194436  With inst  

I’ll point you in the direction of tutorials like The network multi-commodity flow problem Β· JuMP

1 Like

thank you @odow!
Much appreciated.
The solutions seem to be working.
I will try a more detailed look later today (or tomorrow)!

thank you again @odow.
Just a quick follow up question:

I think the constraint:
@constraint(model, [j in 1:nshops], sum(M[:, j]) <= nshops * M_fixed_cost[j])
should be
@constraint(model, [j in 1:nshops], sum(M[:, j]) <= nsets * M_fixed_cost[j])

As one solution might be to buy all sets from a single shop.

I understand the purpose of this constraint is to force M_fixed_cost[j] to be true if I order at least one item from shop j

1 Like

Oops yes you are correct

1 Like