Gekko - infeasible solution to optimal scheduling, comparison w/ gurobi - optimization

I am somewhat familiar with Gurobi, but transitioning to Gekko since the latter appears to have some advantages. I am running into one issue though, which I will illustrate using my imaginary apple orchard. The 5-weeks harvest period (#horizon: T=5) is upon us, and my - very meagre - produce will be:
[3.0, 7.0, 9.0, 5.0, 4.0]
Some apples I keep for myself [2.0, 4.0, 2.0, 4.0, 2.0], the remaining produce I will sell in the farmer's market at the following prices: [0.8, 0.9, 0.5, 1.2, 1.5]. I have storage space with room for 6 apples, so I can plan ahead and sell apples at the most optimal moments, hence maximizing my revenue. I try to determine the optimal schedule with the following model:
m = GEKKO()
m.time = np.linspace(0,4,5)
orchard = m.Param([3.0, 7.0, 9.0, 5.0, 4.0])
demand = m.Param([2.0, 4.0, 2.0, 4.0, 2.0])
price = m.Param([0.8, 0.9, 0.5, 1.2, 1.5])
### manipulated variables
# selling on the market
sell = m.MV(lb=0)
sell.DCOST = 0
sell.STATUS = 1
# saving apples
storage_out = m.MV(value=0, lb=0)
storage_out.DCOST = 0
storage_out.STATUS = 1
storage_in = m.MV(lb=0)
storage_in.DCOST = 0
storage_in.STATUS = 1
### storage space
storage = m.Var(lb=0, ub=6)
### constraints
# storage change
m.Equation(storage.dt() == storage_in - storage_out)
# balance equation
m.Equation(sell + storage_in + demand == storage_out + orchard)
# Objective: argmax sum(sell[t]*price[t]) for t in [0,4]
m.Maximize(sell*price)
m.options.IMODE=6
m.options.NODES=3
m.options.SOLVER=3
m.options.MAX_ITER=1000
m.solve()
For some reason this is unfeasible (error code = 2). Interestingly, if set demand[0] to 3.0, instead of 2.0 (i.e. equal to orchard[0], the model does produce a succesful solution.
Why is this the case?
Even the "succesful" output values are bit weird: the storage space is not used a single time, and storage_out is not properly constrained in the last timestep. Clearly, I am not formulating the constraints correctly. What should I do to get realistic results, which are comparable to the gurobi output (see code below)?
output = {'sell' : list(sell.VALUE),
's_out' : list(storage_out.VALUE),
's_in' : list(storage_in.VALUE),
'storage' : list(storage.VALUE)}
df_gekko = pd.DataFrame(output)
df_gekko.head()
> sell s_out s_in storage
0 0.0 0.000000 0.000000 0.0
1 3.0 0.719311 0.719311 0.0
2 7.0 0.859239 0.859239 0.0
3 1.0 1.095572 1.095572 0.0
4 26.0 24.124924 0.124923 0.0
Gurobi model solved for with demand = [3.0, 4.0, 2.0, 4.0, 2.0]. Note that gurobi also produces a solution with demand = [2.0, 4.0, 2.0, 4.0, 2.0]. This only has a trivial impact on the outcome: n apples sold at t=0 becomes 1.
T = 5
m = gp.Model()
### horizon (five weeks)
### supply, demand and price data
orchard = [3.0, 7.0, 9.0, 5.0, 4.0]
demand = [3.0, 4.0, 2.0, 4.0, 2.0]
price = [0.8, 0.9, 0.5, 1.2, 1.5]
### manipulated variables
# selling on the market
sell = m.addVars(T)
# saving apples
storage_out = m.addVars(T)
m.addConstr(storage_out[0] == 0)
storage_in = m.addVars(T)
# storage space
storage = m.addVars(T)
m.addConstrs((storage[t]<=6) for t in range(T))
m.addConstrs((storage[t]>=0) for t in range(T))
m.addConstr(storage[0] == 0)
# storage change
#m.addConstr(storage[0] == (0 - storage_out[0]*delta_t + storage_in[0]*delta_t))
m.addConstrs(storage[t] == (storage[t-1] - storage_out[t] + storage_in[t]) for t in range(1, T))
# balance equation
m.addConstrs(sell[t] + demand[t] + storage_in[t] == (storage_out[t] + orchard[t]) for t in range(T))
# Objective: argmax sum(a_sell[t]*a_price[t] - b_buy[t]*b_price[t])
obj = gp.quicksum((price[t]*sell[t]) for t in range(T))
m.setObjective(obj, gp.GRB.MAXIMIZE)
m.optimize()
output:
sell storage_out storage_in storage
0 0.0 0.0 0.0 0.0
1 3.0 0.0 0.0 0.0
2 1.0 0.0 6.0 6.0
3 1.0 0.0 0.0 6.0
4 8.0 6.0 0.0 0.0

You can get a successful solution with:
m.options.NODES=2
The issue is that it is solving the balance equation in between the primary node points with NODES=3. Your differential equation has a linear solution so NODES=2 should be sufficiently accurate.
Here are a couple other ways to improve the solution:
Set a small penalty on moving inventory into or out of storage. Otherwise the solver can find large arbitrary values with storage_in = storage_out.
I used m.Minimize(1e-6*storage_in) and m.Minimize(1e-6*storage_out).
Because the initial condition is typically fixed, I used zero values at the beginning just to make sure that the first point is calculated.
I also switched to integer variables if they are sold and stored in integer units. You need to switch to the APOPT solver if you want an integer solution with SOLVER=1.
Successful solution
---------------------------------------------------
Solver : APOPT (v1.0)
Solution time : 0.058899999999999994 sec
Objective : -17.299986
Successful solution
---------------------------------------------------
Sell
[0.0, 0.0, 4.0, 1.0, 1.0, 8.0]
Storage Out
[0.0, 0.0, 1.0, 0.0, 0.0, 6.0]
Storage In
[0.0, 1.0, 0.0, 6.0, 0.0, 0.0]
Storage
[0.0, 1.0, 0.0, 6.0, 6.0, 0.0]
Here is the modified script.
from gekko import GEKKO
import numpy as np
m = GEKKO(remote=False)
m.time = np.linspace(0,5,6)
orchard = m.Param([0.0, 3.0, 7.0, 9.0, 5.0, 4.0])
demand = m.Param([0.0, 2.0, 4.0, 2.0, 4.0, 2.0])
price = m.Param([0.0, 0.8, 0.9, 0.5, 1.2, 1.5])
### manipulated variables
# selling on the market
sell = m.MV(lb=0, integer=True)
sell.DCOST = 0
sell.STATUS = 1
# saving apples
storage_out = m.MV(value=0, lb=0, integer=True)
storage_out.DCOST = 0
storage_out.STATUS = 1
storage_in = m.MV(lb=0, integer=True)
storage_in.DCOST = 0
storage_in.STATUS = 1
### storage space
storage = m.Var(lb=0, ub=6, integer=True)
### constraints
# storage change
m.Equation(storage.dt() == storage_in - storage_out)
# balance equation
m.Equation(sell + storage_in + demand == storage_out + orchard)
# Objective: argmax sum(sell[t]*price[t]) for t in [0,4]
m.Maximize(sell*price)
m.Minimize(1e-6 * storage_in)
m.Minimize(1e-6 * storage_out)
m.options.IMODE=6
m.options.NODES=2
m.options.SOLVER=1
m.options.MAX_ITER=1000
m.solve()
print('Sell')
print(sell.value)
print('Storage Out')
print(storage_out.value)
print('Storage In')
print(storage_in.value)
print('Storage')
print(storage.value)

Related

Julia Jump : Getting all feasible solutions to mip

I would like to have instead of only the vector of optimal solution to a mip , all the feasible (suboptimal) vectors.
I found some old questions here, but I am not sure how they work.
First of all, is there any new library tool/way to do that automatically ?
I tried this but, it did nothing:
if termination_status(m) == MOI.FEASIBLE_POINT
println(x)
end
optimize!(m);
If not, what's the easiest way?
I thought of scanning the optimal solution till I find the first non -zero decision variable, then constraint this variable to be zero and solving the model again.
for i in 1:active_variables
if value.(z[i])==1
#constraint(m, x[i] == 0)
break
end
end
optimize!(m);
But I see this problem with this method** :
Ιf I constraint x[i] to be zero, in the next step I will want maybe to drop again this constraint? This comes down to whether there can exist two(or more) different solutions in which x[i]==1
JuMP supports returning multiple solutions.
Documentation: https://jump.dev/JuMP.jl/stable/manual/solutions/#Multiple-solutions
The workflow is something like:
using JuMP
model = Model()
#variable(model, x[1:10] >= 0)
# ... other constraints ...
optimize!(model)
if termination_status(model) != OPTIMAL
error("The model was not solved correctly.")
end
an_optimal_solution = value.(x; result = 1)
optimal_objective = objective_value(model; result = 1)
for i in 2:result_count(model)
#assert has_values(model; result = i)
println("Solution $(i) = ", value.(x; result = i))
obj = objective_value(model; result = i)
println("Objective $(i) = ", obj)
if isapprox(obj, optimal_objective; atol = 1e-8)
print("Solution $(i) is also optimal!")
end
end
But you need a solver that supports returning multiple solutions, and to configure the right solver-specific options.
See this blog post: https://jump.dev/tutorials/2021/11/02/tutorial-multi-jdf/
The following is an example of all-solution finder for a boolean problem. Such problems are easier to handle since the solution space is easily enumerated (even though it can still grow exponentially big).
First, let's get the packages and define the sample problem:
using Random, JuMP, HiGHS, MathOptInterface
function example_knapsack()
profit = [5, 3, 2, 7, 4]
weight = [2, 8, 4, 2, 5]
capacity = 10
minprofit = 10
model = Model(HiGHS.Optimizer)
set_silent(model)
#variable(model, x[1:5], Bin)
#objective(model, FEASIBILITY_SENSE, 0)
#constraint(model, weight' * x <= capacity)
#constraint(model, profit' * x >= minprofit)
return model
end
(it is a knapsack problem from the JuMP docs).
Next, we use recursion to explore the tree of all possible solutions. The tree does not go down branches with no solution (so the running time is not always exponential):
function findallsol(model, x)
perm = shuffle(1:length(x))
res = Vector{Float64}[]
_findallsol!(res, model, x, perm, 0)
return res
end
function _findallsol!(res, model, x, perm, depth)
n = length(x)
depth > n && return
optimize!(model)
if termination_status(model) == MathOptInterface.OPTIMAL
if depth == n
push!(res, value.(x))
return
else
idx = perm[depth+1]
v = value(x[idx])
newcon = #constraint(model, x[idx] == v)
_findallsol!(res, model, x, perm, depth + 1)
delete(model, newcon)
newcon = #constraint(model, x[idx] == 1 - v)
_findallsol!(res, model, x, perm, depth + 1)
delete(model, newcon)
end
end
return
end
Now we can:
julia> m = example_knapsack()
A JuMP Model
Maximization problem with:
Variables: 5
...
Names registered in the model: x
julia> res = findallsol(m, m.obj_dict[:x])
5-element Vector{Vector{Float64}}:
[1.0, 0.0, 0.0, 1.0, 1.0]
[0.0, 0.0, 0.0, 1.0, 1.0]
[1.0, 0.0, 1.0, 1.0, 0.0]
[1.0, 0.0, 0.0, 1.0, 0.0]
[0.0, 1.0, 0.0, 1.0, 0.0]
And we get a vector with all the solutions.
If the problem in question is a boolean problem, this method might be used, as is. In case it has non-boolean variables, the recursion will have to split the feasible space in some even fashion. For example, choosing a variable and cutting its domain in half, and recursing to each half with a smaller domain on this variable (to ensure termination).
P.S. This is not the optimal method. This problem has been well studied. Possible terms to search for are 'model counting' (especially in the boolean domain).
(UPDATE: Changed objective to use FEASIBLE)

How to choose the mesh for phonon calculation with PyIron

I would like to calculate phonon density of states and band structure with pyiron, using the phononpy package.
I created a job, following the tutorial:
phono = pr.create_job(pr.job_type.PhonopyJob,"pDOS")
I can run this job, but it takes a lot of time because the mesh is too dense. Is there a way of choosing the mesh I would like to work with ?
Also, I would like to calculate phonon band structure for a given path, is it possible with pyiron ?
You can specify the input in:
phono.input
Here you can set the mesh as:
phono.input["dos_mesh"]
Best,
Jan
To address the comment regarding the band structure - you can use the phonopy API directly:
bands = []
q_start = np.array([0.5, 0.5, 0.0])
q_end = np.array([0.0, 0.0, 0.0])
band = []
for i in range(51):
band.append(q_start + (q_end - q_start) / 50 * i)
bands.append(band)
q_start = np.array([0.0, 0.0, 0.0])
q_end = np.array([0.5, 0.0, 0.0])
band = []
for i in range(51):
band.append(q_start + (q_end - q_start) / 50 * i)
bands.append(band)
phon.phonopy.set_band_structure(bands)
phon.phonopy.plot_band_structure().show()

Running sum of complicated functions using pandas data frame values

In this simplified example I have three lists of the same length, list a, list b, and list c. I want to find the following running summations
from math import exp
a = [1.3, 4.5, 7.8, 9.2, 4.1]
b = [2.1, 1.1, 1.0, 1.0, -2.0]
c = [3.1, 4.0, 5.0, 6.0, 7.0]
# This simple, but SLOW method
sum1 = 0.0
for i in range(0, 3):
sum1 += a[i] ** b[i] + 3.1 * c[i]
sum2 = 0.0
for i in range(2,4):
sum2 += (b[i] / a[i]) * exp(-c[i])
total = sum1 + sum2
print(total) # yields 52.27644
The above code works just fine; however, for examples with MUCH larger lists it runs very slow. If I were to combine the lists in a pandas data frame, is there some built-in and vectorized capability to conduct this same running summations with the data frame? Something like below.
import pandas as pd
df_dict = {'A': [1.3, 4.5, 7.8, 9.2, 4.1],
'B': [2.1, 1.1, 1.0, 1.0, -2.0],
'C': [3.1, 4.0, 5.0, 6.0, 7.0]}
df = pd.DataFrame(df_dict)
# Some version of a running summation here!
I do not think you need a dataframe here, just use numpy's functions :
step1 = np.power(a[:3], b[:3])
step2 = np.multiply(c[:3], 3.1)
sum1 = np.add(step1, step2).sum()
step3 = np.divide(b[2:4], a[2:4])
step4 = np.exp(np.multiply(c[2:4], -1))
sum2 = np.multiply(step3, step4).sum()
result = sum1 + sum2
result
52.27644589942484
This should be significantly faster as the list size grows; plus you can optimize it further.

Math.Net Multiple Regression Is Wrong After The 4th Independent Variables

I am able to generate correct intercept and coefficients for a multiple regression (Math.Net) adding up to three independent variables. However, once a fourth independent variable is added the returned values are nowheres near close.
Using this code:
Dim i As Integer
Dim g(5)() As Double
g(0) = {1.0, 4.0, 3.2}
g(1) = {2.0, 5.0, 4.1}
g(2) = {3.0, 2.0, 2.5}
g(3) = {4.0, 3.0, 1.6}
g(4) = {4.0, 3.0, 1.6}
Dim d As Double() = {3.5, 5.6, 1.2, 15.2, 3.4, 4.2}
Dim p As Double() = MultipleRegression.QR(Of Double)(g, d, intercept:=True)
For i = 0 To UBound(p)
Debug.WriteLine(p(i))
Next
I get:
-2.45972222222223
1.13194444444445
3.11805555555555
-2.38888888888889
These are correct.
However, if I run the same code, but add a 4th independent variable as such:
Dim i As Integer
Dim g(5)() As Double
g(0) = {1.0, 4.0, 3.2, 5.3}
g(1) = {2.0, 5.0, 4.1, 2.4}
g(2) = {3.0, 2.0, 2.5, 3.6}
g(3) = {4.0, 3.0, 1.6, 2.1}
g(4) = {4.0, 3.0, 1.6, 2.1}
g(5) = {4.0, 3.0, 1.6, 2.1}
Dim d As Double() = {3.5, 5.6, 1.2, 15.2, 3.4, 4.2}
Dim p As Double() = MultipleRegression.QR(Of Double)(g, d, intercept:=True)
For i = 0 To UBound(p)
Debug.WriteLine(p(i))
Next
I get:
6.88018203734109E+17
-9.8476516475107E+16
-3.19472310972754E+16
-4.61094057074081E+16
-5.92835216238101E+16
These number are nowhere close to being correct.
If anyone can provide any direction as to what I am doing wrong, I would be very appreciative. TIA
I have not worked out the math details, but looking intuitively at your problem, of the six observations, three (g(3),g(4),g(5)) have identical independent variables, and the corresponding values of the dependent variable have the highest, median, and lowest values. So these observations don't have any real predictive value. In effect, you are trying to estimate 5 values based on three observations. That's not going to work well, and results in instability in the math.
I've changed your data very slightly, and it returns better values. (I use C#). The problem is with the data, not the program.
double[][] g = new double [6][];
g[0] = new double[4] { 1.0, 4.0, 3.2, 5.3};
g[1] = new double[4] { 2.0, 5.0, 4.1, 2.4};
g[2] = new double[4] { 3.0, 2.0, 2.5, 3.6};
g[3] = new double[4] { 4.0, 3.0, 1.6, 2.12};
g[4] = new double[4] { 4.0, 3.0, 1.6, 2.11};
g[5] = new double[4] { 4.0, 3.0, 1.6, 2.1};
double[] d = new double[6] { 3.5, 5.6, 1.2, 15.2, 3.4, 4.2 };
var p = MultipleRegression.QR(g, d, true);
for (int i = 0; i < p.Length; i++) Console.WriteLine(p[i].ToString());
This returns:
-6386.81388888898
913.902777777791
297.597222222225
428.444444444452
550.000000000007

transform pandas dataframe column via Interpolation

i am looking to apply a 1d interpolation on a df and am not sure how to this in an efficient way. Here goes:
In [8]: param
Out[8]:
alpha beta rho nu
0.021918 0.544953 0.5 -0.641566 6.549623
0.041096 0.449702 0.5 -0.062046 5.047923
0.060274 0.428459 0.5 -0.045312 3.625387
0.079452 0.424686 0.5 -0.049508 2.790139
0.156164 0.423139 0.5 -0.071106 1.846614
0.232877 0.414887 0.5 -0.040070 1.334070
0.328767 0.415757 0.5 -0.042071 1.109897
I would like the new index (but dont mind to reset_index() if needed) to look like this:
np.array([0.02, 0.04, 0.06, 0.08, 0.1, 0.15, 0.2, 0.25])
So the corresponding values for alpha, beta, rho, nu need to be interpolated.
Came up with the following which only works for one column and only if x and y have the same dimensions:
x = np.array([0.02, 0.04, 0.06, 0.08, 0.1, 0.15, 0.2, 0.25])
y = np.array(param.alpha)
f = interp1d(x, y, kind='cubic', fill_value='extrapolate')
f(x)
Appreciate any pointer towards an efficient solution. Thanks.
You could try using reindex and interpolate then index selection with loc:
param.reindex(new_idx.tolist()+param.index.values.tolist())\
.sort_index()\
.interpolate(method='cubic')\
.bfill()\
.loc[new_idx]
Output:
alpha beta rho nu
0.02 0.544953 0.5 -0.641566 6.549623
0.04 0.452518 0.5 -0.073585 5.138333
0.06 0.428552 0.5 -0.044739 3.641854
0.08 0.424630 0.5 -0.049244 2.772958
0.10 0.423439 0.5 -0.047119 2.294109
0.15 0.423326 0.5 -0.069473 1.873499
0.20 0.419130 0.5 -0.060861 1.573724
0.25 0.412985 0.5 -0.029573 1.221732