Comparing model solutions - Validating solutions

Hi!
I’m working on an MINLP model and solving it with KNITRO.
Since it’s an MINLP, the solver solution heavily depends on solver settings and I’m looking for new ways to validate it.
I was trying to take the same model and solve it with Juniper + Ipopt but it doesn’t seem to work…
Any other solver recommendations? Or how to improve the Juniper performance? I don’t know if SCIP works with my problem formulation.

MINLP is highly dependent on the model formulation. If you post it here, people may have suggestions for improvements.

Well it took me a while but this is a first draft of a MWE:

using JuMP, InfiniteOpt

function solvePolicies(optimizer, # model optimizer
    data::Dict, # information for the model (mainly parameters), these are the devices (EV, BESS, PV, etc.), the costs (interests, capex, etc) and the revenues
    )
    # Sets
    Dt = 0:0.25:24; # time discretization
    tend=Dt[end]
    t0=Dt[1]; # initial time, can´t divide by 0
    model = InfiniteModel(optimizer) # create model

    # define cont t
    @infinite_parameter(model, t ∈ [t0, tend], supports = collect(Dt),
                        derivative_method = FiniteDifference(Backward()))
    # Electrical
    # BESS
    PbessMin = -12.5; # Min power [kW]
    PbessMax = 12.5; # Max power [kW]
    SoCbessMin = 0.2; # Min State of Charge [p.u.]
    SoCbessMax = 1.0; # Max State of Charge [p.u.]
    vmin = 2.8; vmax = 4.2; # voltage limits
    Nsbess = 100; # number of cells in series
    Npbess = 7; # number of cells in parallel
    # EV
    SoCevMin = 0.2; # Min State of Charge [p.u.]
    SoCevMax = 1.0; # Max State of Charge [p.u.]
    aOCV = 3.2; bOCV = 0.7;
    SoCev0 = 0.5;
    Qev0 = 5.2; # [Ah/cell]
    Npev = 25; # number of cells in parallel
    Nsev = 100; # number of cells in series
    # Thermal
    # ST
    ηST = 0.6; # conversion factor from Electric PV to thermal
    PhpRated = 5; # rated power of the heat pump
    ηHP = 3.; # conversion factor from Electric to thermal heat pump
    # Extract data
    Qtess = 200.; # Capacity [kWh]
    PtessMin = -5.; # Min power [kW]
    PtessMax = 5.; # Max power [kW]
    SoCtessMin = 0.2; # Min State of Charge [p.u.]
    SoCtessMax = 0.9; # Max State of Charge [p.u.]
    SoCtess0 = 0.5; # Initial State of Charge [p.u.]
    ηtess = 0.95; # thermal efficiency
    # grids and balances
    PgMin = -17.5; # Min power going out
    PgMax = 17.5; # Max power coming in
    ηg = 0.9; # converter efficiency
    # Electrical
    # PV
    MPPTmeas = data["SPV"].MPPTData[it0:itend];
    loadTh = data["grid"].loadTh[it0:itend];
    loadElec = data["grid"].loadE[it0:itend];
    [γ[n,:] = data["EV"][n].driveInfo.γ[it0:itend] for n ∈ 1:sets.nEV];
    γ_interp = linear_interpolation((1:nEV, Dt), γ)
    priceBuy=hcat(data["grid"].λ[it0:itend, 1].*1e-3/3600) # convert from €/MWh to €/kWs
    priceSell=hcat(data["grid"].λ[it0:itend, 2].*1e-3/3600) # convert from €/MWh to €/kWs
    # simulated forecast
    @parameter_function(model, PpvMPPT == (t) -> MPPTmeas[zero_order_time_index(Dt, t)])
    @parameter_function(model, Pst == (t) -> ηST*MPPTmeas[zero_order_time_index(Dt, t)])
    @parameter_function(model, Plt == (t) -> loadTh[zero_order_time_index(Dt, t)])
    @parameter_function(model, Ple == (t) -> loadElec[zero_order_time_index(Dt, t)])
    @parameter_function(model, γ_cont[n in 1:nEV] == (t) -> γ_interp(n, t))
    @parameter_function(model, λbuy == (t) -> priceBuy[zero_order_time_index(Dt, t)])
    @parameter_function(model, λsell == (t) -> priceSell[zero_order_time_index(Dt, t)])

    OCVev0 = [aOCV[n]+bOCV[n]*SoCev0[n] for n in 1:nEV];
    Pdrive = [rand(truncated(Normal(3.5, 1); lower = 0.01)) for n in 1:nEV]; # Gaussian distribution
    Ereq=[sum(Pdrive[n].*(1 .-γ[n,:])*Δt)./3600 for n in 1:nEV];
    # check if the driving power is greater than the energy in the battery pack.
    while  any(Ereq .> Qev0.*Npev.*Nsev.* vmax./1000*0.8)   
        Pdrive = [rand(truncated(Normal(3.5, 1); lower = 0.01)) for n in 1:nEV]; # Gaussian distribution
        Ereq=[sum(Pdrive[n].*(1 .-γ[n,:])*Δt)./3600 for n in 1:nEV];
    end  

    @variables(model, begin
        # Grid connection
        Pg, Infinite(t)  # grid power
        bPg, Infinite(t), Bin # Binary variable for Grid power
        0 ≤ PgPos, Infinite(t) # Pg^+ out/buy power
        PgNeg ≤ 0, Infinite(t), (start=0) # Pg^- in/sell power
        # BESS
        Pbess, Infinite(t) # output power
        bPbess, Infinite(t), Bin # Binary variable for output power
        SoCbessMin ≤ SoCbess ≤ SoCbessMax, Infinite(t) # State of Charge
        0 ≤ PbessPos, Infinite(t) # Pbess^+ out power
        PbessNeg ≤ 0, Infinite(t) # Pbess^- in power
        vmin ≤ OCVbess ≤ vmax, Infinite(t), (start=OCVbess0) # open circuit voltage of the cell
        ibess, Infinite(t), (start=1e3*Pbess0/Npbess/Nsbess/OCVbess0) # current per branch
        # EV
        Pev[n ∈ 1:nEV], Infinite(t)  # EV charger power 
        bPev[n ∈ 1:nEV], Infinite(t), Bin  # EV charger power 
        PevTot[n ∈ 1:nEV], Infinite(t)  # total power of each EV, driving+V2G
        SoCevMin[n] .≤ SoCev[n ∈ 1:nEV] .≤ SoCevMax[n], Infinite(t) # State of Charge
        0 ≤ PevPos[n ∈ 1:nEV], Infinite(t) # Pev^+ out power   
        PevNeg[n ∈ 1:nEV] ≤ 0, Infinite(t) # Pev^- in power
        vmin[n] ≤ OCVev[n ∈ 1:nEV] ≤ vmax[n], Infinite(t),(start=OCVev0[n]) # open circuit voltage of the cell
        iev[1:nEV], Infinite(t)  # current per branch
        # HP    
        0 ≤ Phpe ≤ PhpRated, Infinite(t) # Heat pump power
        # TESS
        SoCtess ≥ SoCtessMin, Infinite(t) # State of Charge
        auxTess ≥ 0, Infinite(t) # slack variable for SoCtess
        Ptess, Infinite(t) # Thermal power
        bPtess, Infinite(t), Bin # Binary variable for TESS power
        0 ≤ PtessPos, Infinite(t) # Ptess^+ out power
        PtessNeg ≤ 0, Infinite(t) # Ptess^- in power
    end)
    
    t1 = t0 + 6*3600; OCVbess0 = aOCV+bOCV*SoCbess0;
    @constraints(model, begin
        # Grid connection
        PgPos ≤ (1-bPg)*PgMax
        bPg*PgMin ≤ PgNeg
        PgNeg * (1/ηg) + PgPos * ηg .== Pg
        # BESS
        PbessNeg * (1/ηbess) + PbessPos * ηbess .== Pbess
        bPbess*PbessMin ≤ PbessNeg
        PbessPos ≤ (1-bPbess)*PbessMax
        OCVbess == aOCV+bOCV*SoCbess # Linear voltage model
        ibess == 1e3*Pbess/Npbess/Nsbess/OCVbess # current per branch. 1e3 to convert kW->W    
        SoCbess(t0) ==  SoCbess0 # Initial conditions
        SoCbess(t1) ==  SoCbess(t1+24*3600) # periodic condition
        ∂.(SoCbess, t) .== -(ηbess * bPbess + (1-bPbess))*ibess/Qbess0/3600 # Static Qbess
        # EV
        [n ∈ 1:nEV], PevNeg[n] * (1/ηev[n]) + PevPos[n] * ηev[n] == Pev[n]
        [n ∈ 1:nEV], bPev[n]*PevMin[n] ≤ PevNeg[n]
        [n ∈ 1:nEV], PevPos[n] ≤ (1-bPev[n])*PevMax[n]
        [n ∈ 1:nEV], SoCev[n](t0) == SoCev0[n] # Initial conditions
        availability[n ∈ 1:nEV], γ_cont[n].*Pev[n] + (1-γ_cont[n]).*Pdrive[n] - PevTot[n] .== 0 # power balance
        [n ∈ 1:nEV], OCVev[n] .== aOCV[n]+bOCV[n]*model[:SoCev][n] # linear voltage model
        [n ∈ 1:nEV], iev[n] .== 1e3*model[:PevTot][n]/Npev[n]/Nsev[n]/OCVev[n] # current per branch
        [n ∈ 1:nEV], ∂.(SoCev[n], t) .== -(ηev[n] * bPev[n] + (1-bPev[n]))*iev[n]/Qev0[n]/3600
        # TESS
        bPtess*PtessMin ≤ PtessNeg
        PtessNeg * (1/ηtess) + PtessPos * ηtess .== Ptess
        PtessPos ≤ (1-bPtess)*PtessMax
        auxTess ≥ SoCtess - SoCtessMax # slack variable soft constraint
        SoCtess(t0) .== SoCtess0 # Initial condition
        ∂.(SoCtess, t) .== -Ptess/Qtess/3600 # Bucket model
    end)
    

    # CHECK ALL OF THIS tDep are the departure times from γ
    model[:ϵSoC]=[]; # assign name in the model
    for day in eachindex(tDep[1,:]) # if there's more than one day loop over them
        for n in 1:nEV # loop over the EVs
            td = tDep[n, day] # pick value
            td = td * 3600 + (24 * 3600 * (day - 1)) # change to secs and add days
            # find the nearest td inside supports(t)
            td = findmin(abs.(td .- supports(t)))[1]
            # Check if the time index is within the bounds of SoCev
            if td >= t0 && td <= tend
                ϵSoCexpr = SoCev[n](td) - SoCdep[n]
                push!(model[:ϵSoC], ϵSoCexpr) # push to the expression vector
            end
        end
    end

    # Thermal balance
    model[:thBalance]=@constraint(model, Pst+Phpe.*ηHP+Ptess .==  Plt);   
    # Power balance DC busbar
    Pev_sum = nEV != 1 ? sum(γ_cont[n].*Pev[n] for n in 1:nEV) : sum(γ_cont.*Pev[n] for n in 1:nEV)
    model[:powerBalance]=@constraint(model, PpvMPPT + Pbess + Pev_sum + Pg .== Ple + Phpe)
    set_start_value_function(Ptess, t -> loadTh[zero_order_time_index(Dt, t)])
    set_start_value_function(Pg, t -> loadElec[zero_order_time_index(Dt, t)] .- MPPTmeas[zero_order_time_index(Dt, t)])
    # Add objective function
    costFunction!(model, sets, data)
    # Grid costs
    Wgrid = 1; # regularization factor for grid cost. max(λ)*max(P)
    Wlims = 1000; # penalty for TESS overcharging
    WSoCDep = 1000; # penalty for EVs not reaching the desired SoC
    cgrid = Wgrid .* (PgPos*λbuy + PgNeg*λsell);
    pDep = WSoCDep*sum(ϵSoC[n]^2 for n ∈ eachindex(ϵSoC))
    @objective(model, Min, ∫(cgrid,t)/tend + pDep + Wlims*∫(auxTess,t)/tend)
    return model
end;

it should be MIQCP/MINLP.

So what’s the question, specifically? We can’t run your problem because you haven’t provided the input data.

Also, since this is really an InfiniteOpt model, I don’t know if the usual JuMP suggestions directly apply. @pulsipher may have suggestions.

I am happy to help. Could you explain more of the problem you are running into? Does it throw an error? MINLPs like this are very difficult to solve, so some solvers might not be able to converge in any reasonable amount of time.

One potential issue with models that use a lot of parameter functions is the final constraint structure might not be what you expect. For instance, suppose p is a parameter function and y is a variable which are used in the following constraint:

@constraint(model, p*y^2 <= 42)

Even though we know that this a a quadratic constraint, it will be stored as a general nonlinear constraint since it has to store the references for p and y^2. So, if you call a solver like Gurobi that supports MIQCPs, it will throw an error since it will think you have a general nonlinear constraint.

JuMP has a similar limitation with the use of Parameter sets.

Developing an intelligent way of automatically detecting and converting such expressions down to quadratic or linear expressions during transcription is on the TODO list, but I haven’t had time to look into it much yet.

Depending on the inputs KNITRO can solve it quite fast (seconds) and the same instance on Juniper worked in hours. Other instances it didn’t even solved.
If this a matter of structure the problem should only be a MIQCP. I’m not using Parameters or something like that.
I’m trying to look for alternatives for KNITRO to validate results and the settings I’m using in Juniper don’t seem to be working.

  1. The general question is, what good MINLP solvers are out there?
  2. The particular question is, what heuristic strategies do we have available in Juniper? or you can only use the ones from the linear solver being used, like HiGHS?

So long and none of the denominators in your model involve parameter functions or variables, then this should be recognized as an MIQCP by InfiniteOpt/JuMP. In which case, Gurobi is the way to go, though it only provides global solutions. For heuristic MINLPs, KNITRO is a good choice. Juniper does tend to struggle in my experience. If you have a GAMS license, you could try using DICOPT even though it is primarily intended for convex problems which your model is not.

MINLPs are really hard to solve, especially with general purpose solvers. The state-of-the-art typically involves making tailored solution techniques that exploit the structure of the problem to more efficiently find a solution.

2 Likes