Demand optimization planning using PULP - python

Need help in solving a demand-optimiztion planning for factories.
Factories have Machines which can make one or more Products in it.
Each Product takes time to make 1 unit which is known as 'Cycle-Time'. So, to make 10 units of product/component on a machine with cycle-time of 5, it will take 5*10=50 seconds in total.
Not all products can be made in all machines.
So, we need to make products on valid machines and in the most effective manner.
Also, each machine has an availability limit (in seconds) and we can't go over it.
What we need to do is :
apply a "cost" of running a machine to make one or more products.
apply a "cost" in case the demand of a product is NOT met.
Objective is to Minimize this cost.
I'm also happy if we are able to solve this using equations as constraints (like model += ( x1 * 0.055555555555556 <= 10000, "material_300005551211-2" )). but unable to do so at the moment.
Sample data :
I tried optimizing PULP, but this approach isn't working correctly - for example, in case of demand being too high, it doens't max upto the limit of machine's availability but unsure where i'm going wrong.
import pandas as pd
import pulp
factories = pd.read_csv('factory_machines_small.csv', index_col=['Component', 'Machine'])
print(factories)
demand = pd.read_csv('component_demand_small.csv', index_col=['Component'])
print(demand)
production = pulp.LpVariable.dicts("production",
((component, machine) for component, machine in factories.index),
lowBound=0,
#upBound=1,
cat='Integer')
factory_status = pulp.LpVariable.dicts("factory_status",
((component, machine) for component, machine in factories.index),
cat='Binary')
model = pulp.LpProblem("Cost minimising scheduling problem", pulp.LpMinimize)
model += pulp.lpSum(
[production[component, machine] * factories.loc[(component, machine), 'Cycle_Time'] for component, machine in factories.index]
)
# Production in any month must be equal to demand
components = demand.index
for component in components :
model += production[(component, 'IP01')] + production[(component, 'IP02')] + production[(component, 'IP03')] \
+ production[(component, 'IP04')] + production[(component, 'IP05')] == demand.loc[component, 'Demand']
# Production in any month must be between minimum and maximum capacity, or zero.
for component, machine in factories.index:
min_production = factories.loc[(component, machine), 'Min_Capacity']
max_production = factories.loc[(component, machine), 'Max_Capacity']
model += production[(component, machine)] >= min_production * factory_status[component, machine]
model += production[(component, machine)] <= max_production * factory_status[component, machine]
model.solve()
print(pulp.LpStatus[model.status])
output = []
for component, machine in production:
var_output = {
'Component': component,
'Machine': machine,
'Production': production[(component, machine)].varValue,
'Machine Status': factory_status[(component, machine)].varValue
}
output.append(var_output)
#print(output)
output_df = pd.DataFrame.from_records(output).sort_values(['Component', 'Machine'])
output_df.set_index(['Component', 'Machine'], inplace=True)
print(output_df)
output_df.to_csv('OUTPUT.csv')
# Print our objective function value (Total Costs)
print (pulp.value(model.objective))

The first thing to do is to get rid of production[(component, 'IP01')]+production[(component, 'IP02')]+production[(component, 'IP03')]+production[(component, 'IP04')]+production[(component, 'IP05')]. I hope you realize how bad this type of hardcoding is. This only works if you only have machines IP01..IP05. Indeed for the example data, this is not the case. You need to make this data-driven. We have a sum construct for that.
To model shortage, the rhs of the supply=demand constraint needs to become
production == demand - shortage
where shortage is an additional nonnegative variable. You also need to add a cost term to the objective for this variable.

Related

Pulp Python linear programming problem seems to ignore my constraints

I have a Python script (Pulp library) for allocating funds among a number of clients depending on their current level of funding (gap/requirements) and their membership to priority groups. However, I am not receiving the expected results.
In particular, I want:
All allocations must be positive and their sum should be equal to the total available money I have.
I want to minimize the target funding gap for the most vulnerable group (group A) and then I want that the target gap % in the less vulnerable group increase of 10%: (for group B = funding gap A1.1, for group C = funding gap B1.1...).
I have tried this:
"""
DECISION VARIABLES
"""
# Create a continuous Decision Variable and Affine Expression for the amount of additional funding received by each
# project
allocation = {}
allocation_expr = LpAffineExpression()
for z in range(n):
if priority[z] == 'X' or (requirements[z] == 0 and skip_zero_requirements):
# Projects in Priority Group 'X' don't get any allocation
allocation[project_names[z]] = pulp.LpVariable(f'allocation_{project_names[z]}', lowBound=0, upBound=0)
else:
# allocation is non negative and cannot be greater than the initial gap
allocation[project_names[z]] = pulp.LpVariable(f'allocation_{project_names[z]}', lowBound=0, upBound=(gap[z]))
allocation_expr += allocation[project_names[z]]
# Create a continuous Decision Variable and Affine Expression for the maximum GAP% within each priority group
target_group_A_expr = LpAffineExpression()
target_group_A = pulp.LpVariable(f'allocation', lowBound=0 )
target_group_A_expr += target_group_A
"""
LINEAR PROGRAMMING PROBLEM
"""
# Create the linear programming problem object
lp_prob = pulp.LpProblem('Multi-Objective Optimization', pulp.LpMaximize)
"""
OBJECTIVE FUNCTIONS
"""
# Define the objective function as an LpAffineExpression
obj = LpAffineExpression()
# MAXIMIZE the portion of additional funding allocated to projects
obj += allocation_expr
# MINIMIZE the Max GAP% within each group [actually Maximizing the -(Max GAP%)]
obj += -target_group_A_expr
# Set the Objective Function
lp_prob += obj
"""
CONSTRAINTS
"""
# Additional funding allocations to individual projects must be non-negative and not greater than the project's gap
#for v in range(n):
# lp_prob += allocation[project_names[v]] <= gap[v]
# lp_prob += allocation[project_names[v]] >= 0
# The sum of allocations to individual projects cannot be greater than the additional funding
lp_prob += pulp.lpSum([allocation[project_names[u]] for u in range(n)]) <= additional_funding
# The Max GAP % within each group >= of the GAP % of all projects in the group (proxy for dynamic max calculation)
for i, (p, group) in enumerate(priority_groups.items()):
# Get the indices of the projects in the group
group_indices = priority_groups[p] #selects the indices matching with the rows of the projects belonging to that group
# Iterate over the indices of the projects in the group
for index in group:
# Create an LpAffineExpression for the GAP% of the project
project_gap_percentage = LpAffineExpression()
if requirements[index] == 0:
project_gap_percentage += 0
else:
project_gap_percentage += (gap[index] - allocation[project_names[index]]) / requirements[index]
# Add constraint to the model
lp_prob += target_group_A == (project_gap_percentage/pow(delta_gap, i))
"""
PROGRAMMING MODEL SOLVER
"""
# Solve the linear programming problem
lp_prob.solve()
delta_gap and the additional_funding are external parameters.
I receive even negative allocations and the constrains is not always meet, e.g. in group B, C I reach level of funding gap much lower than the level of group A- sometimes they randomly go to zero. How can this be possible?
I am considering to use another library, any suggestions?

Creating a conditional constraint for a specific Python Pulp Maximization problem

I can't manage to find a way to create a constraint that sounds like this: for example I have 2 variables, one is a regular product and the other one is a super rare product. In order to have a super rare product, you will need to have already 25 of the regular version of that product. This can be stackable (e.g. if the algorithm select 75 of that regular product, it can have 3 super rare). The reason for this is that the super rare is more profitable, so if I place it without any constraints, it will select only the super rare ones. Any ideas on how to write such a constraint?
Thanks in advance!
Part of the code:
hwProblem = LpProblem("HotWheels", LpMaximize)
# Variables
jImportsW_blister = LpVariable("HW J-Imports w/ blister", lowBound=20, cat=LpInteger) # regular product
jImportsTH = LpVariable("HW J-Imports treasure hunt", lowBound=None, cat=LpInteger) # super rare product
# Objective Function
hwProblem += 19 * jImportsW_blister + 350 * jImportsTH # profit for each type of product
# Constraints
hwProblem += jImportsW_blister <= 50, "HW J-Imports maximum no. of products"
hwProblem += jImportsTH <= jImportsW_blister / 25
# ^this is where the error is happening
There's a few "missing pieces" here regarding the structure of your model, but in general, you can limit the "super rare" (SR) by doing something like:
prob += SR <= R / 25

How to add a flag constraint in Pyomo?

I am trying to simulate a battery dispatch models with charging and discharging constraints. The BESS is charging from a Solar PV system. When I run the model currently, there are some time periods when the BESS is charging and discharging at the same time. How can I add a flag such that when Charge >, Discharge =0 and vice-versa.
def market_constraintx0(model, t):
return (model.Charge[t] <= df.loc[t,'PVGeneration']*stripeff)
model.market_rulex0 = Constraint(model.T, rule=market_constraintx0)
def market_constraintx1(model, t):
return (model.Charge[t] + model.RegDown[t] <= model.ChargeMax)
model.market_rulex1 = Constraint(model.T, rule=market_constraintx1)
def market_constraintx2(model, t):
return ( model.Discharge[t] + model.RegUp[t] <= model.DischargeMax)
model.market_rulex2 = Constraint(model.T, rule=market_constraintx2)
def charge_soc(model, t):
return model.RegUp[t] + model.Discharge[t] <= model.SoC[t] * stripeff ###Battery discharge and regup capacity is limited by SOC
model.charge_soc = Constraint(model.T, rule=charge_soc)
def discharge_soc(model, t):
return model.RegDown[t] + model.Charge[t] <= (model.SoCmax - model.SoC[t])/stripeff ### Battery can be charged by the amount of capacity left to charge.
model.discharge_soc = Constraint(model.T, rule=discharge_soc)
The constraint
x >= 0 or y >= 0
is sometimes called a complementarity condition. It can also be written as:
x * y = 0
(I assume x and y are non-negative variables). There are different ways to solve this:
complementarity solver. Some solvers support this kind of constraints directly. Complementarity constraints inside a math programming model is known as MPEC (Mathematical Programming with Equilibrium Constraints). So these solvers are sometimes called MPEC solvers.
nonlinear formulation. The constraint x*y=0 is not very easy, but a global solver should be able to handle this reliably. However these solvers only handle relatively small models (compared to local solvers).
discrete formulation. Formulate the OR condition using binary variables or a SOS1 construct. This is especially useful if the rest of the model is linear.
You may want to look into pyomo.mpec. For further information see link.
If you want to stick to (mixed-integer) linear formulations, you can also look for indicator constraints, which are discussed generally in this question. Some solvers like CPLEX and Gurobi seem to have specific constraint types for indicator constraints, but I'm not familiar with how to use those within Pyomo.
In general, you can get similar functionality by using a "Big M" formulation. In your case, something like:
model.Indicator = Var(model.T, within=Binary)
model.M = Param(initialize=1000)
def charge_indicator_constraint(model, t):
return model.M * model.Indicator[t] >= model.Charge[t]
...
def discharge_indicator_constraint(model, t):
return (1 - model.M) * model.Indicator >= model.Discharge[t]
...
As the discussed in the question I linked to, picking the right value of model.M is important to keep your model formulation "tight", and in your case, you would probably tie it directly to the power rating of your BESS.

How do I setup an objective function in CPLEX Python containing indicator functions?

The following is the objective function:
The idea is that a mean-variance optimization has already been done on a universe of securities. This gives us the weights for a target portfolio. Now suppose the investor already is holding a portfolio and does not want to change their entire portfolio to the target one.
Let w_0 = [w_0(1),w_0(2),...,w_0(N)] be the initial portfolio, where w_0(i) is the fraction of the portfolio invested in
stock i = 1,...,N. Let w_t = [w_t(1), w_t(2),...,w_t(N)] be the target portfolio, i.e., the portfolio
that it is desirable to own after rebalancing. This target portfolio may be constructed using quadratic optimization techniques such as variance minimization.
The objective is to decide the final portfolio w_f = [w_f (1), w_f (2),..., w_f(N)] that satisfies the
following characteristics:
(1) The final portfolio is close to our target portfolio
(2) The number of transactions from our initial portfolio is sufficiently small
(3) The return of the final portfolio is high
(4) The final portfolio does not hold many more securities that our initial portfolio
An objective function which is to be minimized is created by summing together the characteristic terms 1 through 4.
The first term is captured by summing the absolute difference in weights from the final and the target portfolio.
The second term is captured by the sum of an indicator function multiplied by a user specified penalty. The indicator function is y_{transactions}(i) where it is 1 if the weight of security i is different in the initial portfolio and the final portfolio, and 0 otherwise.
The third term is captured by the total final portfolio return multiplied by a negative user specified penalty since the objective is minimization.
The final term is the count of assets in the final portfolio (ie. sum of an indicator function counting the number of positive weights in the final portfolio), multiplied by a user specified penalty.
Assuming that we already have the target weights as target_w how do I setup this optimization problem in docplex python library? Or if anyone is familiar with mixed integer programming in NAG it would be helpful to know how to setup such a problem there as well.
`
final_w = [0.]*n
final_w = np.array(final_w)
obj1 = np.sum(np.absolute(final_w - target_w))
pen_trans = 1.2
def ind_trans(final,inital):
list_trans = []
for i in range(len(final)):
if abs(final[i]-inital[i]) == 0:
list_trans.append(0)
else:
list_trans.append(1)
return list_trans
obj2 = pen_trans*sum(ind_trans(final_w,initial_w))
pen_returns = 0.6
returns_np = np.array(df_secs['Return'])
obj3 = (-1)*np.dot(returns_np,final_w)
pen_count = 1.
def ind_count(final):
list_count = []
for i in range(len(final)):
if final[i] == 0:
list_count.append(0)
else:
list_count.append(1)
return list_count
obj4 = sum(ind_count(final_w))
objective = obj1 + obj2 + obj3 + obj4
The main issue in your code is that final_w is not a an array of variables but an array of data. So there will be nothing to optimize. To create an array of variables in docplex you have to do something like this:
from docplex.mp.model import Model
with Model() as m:
final = m.continuous_var_list(n, 0.0, 1.0)
That creates n variables that can take values between 0 and 1. With that in hand you can start things. For example:
obj1 = m.sum(m.abs(initial[i] - final[i]) for i in range(n))
For the next objective things become harder since you need indicator constraints. To simplify definition of these constraints first define a helper variable delta that gives the absolute difference between stocks:
delta = m.continuous_var_list(n, 0.0, 1.0)
m.add_constraints(delta[i] == m.abs(initial[i] - final[i]) for i in range(n))
Next you need an indicator variable that is 1 if a transaction is required to adjust stock i:
needtrans = m.binary_var_list(n)
for i in range(n):
# If needtrans[i] is 0 then delta[i] must be 0.
# Since needtrans[i] is penalized in the objective, the solver will
# try hard to set it to 0. It will only set it to 1 if delta[i] != 0.
# That is exactly what we want
m.add_indicator(needtrans[i], delta[i] == 0, 0)
With that you can define the second objective:
obj2 = pen_trans * m.sum(needtrans)
once all objectives have been defined, you can add their sum to the model:
m.minimize(obj1 + obj2 + obj3 + obj4)
and then solve the model and display its solution:
m.solve()
print(m.solution.get_values(final))
If any of the above is not (yet) clear to you then I suggest you take a look at the many examples that ship with docplex and also at the (reference) documentation.

Finding minimum value of a function wit 11,390,625 variable combinations

I am working on a code to solve for the optimum combination of diameter size of number of pipelines. The objective function is to find the least sum of pressure drops in six pipelines.
As I have 15 choices of discrete diameter sizes which are [2,4,6,8,12,16,20,24,30,36,40,42,50,60,80] that can be used for any of the six pipelines that I have in the system, the list of possible solutions becomes 15^6 which is equal to 11,390,625
To solve the problem, I am using Mixed-Integer Linear Programming using Pulp package. I am able to find the solution for the combination of same diameters (e.g. [2,2,2,2,2,2] or [4,4,4,4,4,4]) but what I need is to go through all combinations (e.g. [2,4,2,2,4,2] or [4,2,4,2,4,2] to find the minimum. I attempted to do this but the process is taking a very long time to go through all combinations. Is there a faster way to do this ?
Note that I cannot calculate the pressure drop for each pipeline as the choice of diameter will affect the total pressure drop in the system. Therefore, at anytime, I need to calculate the pressure drop of each combination in the system.
I also need to constraint the problem such that the rate/cross section of pipeline area > 2.
Your help is much appreciated.
The first attempt for my code is the following:
from pulp import *
import random
import itertools
import numpy
rate = 5000
numberOfPipelines = 15
def pressure(diameter):
diameterList = numpy.tile(diameter,numberOfPipelines)
pressure = 0.0
for pipeline in range(numberOfPipelines):
pressure += rate/diameterList[pipeline]
return pressure
diameterList = [2,4,6,8,12,16,20,24,30,36,40,42,50,60,80]
pipelineIds = range(0,numberOfPipelines)
pipelinePressures = {}
for diameter in diameterList:
pressures = []
for pipeline in range(numberOfPipelines):
pressures.append(pressure(diameter))
pressureList = dict(zip(pipelineIds,pressures))
pipelinePressures[diameter] = pressureList
print 'pipepressure', pipelinePressures
prob = LpProblem("Warehouse Allocation",LpMinimize)
use_diameter = LpVariable.dicts("UseDiameter", diameterList, cat=LpBinary)
use_pipeline = LpVariable.dicts("UsePipeline", [(i,j) for i in pipelineIds for j in diameterList], cat = LpBinary)
## Objective Function:
prob += lpSum(pipelinePressures[j][i] * use_pipeline[(i,j)] for i in pipelineIds for j in diameterList)
## At least each pipeline must be connected to a diameter:
for i in pipelineIds:
prob += lpSum(use_pipeline[(i,j)] for j in diameterList) ==1
## The diameter is activiated if at least one pipelines is assigned to it:
for j in diameterList:
for i in pipelineIds:
prob += use_diameter[j] >= lpSum(use_pipeline[(i,j)])
## run the solution
prob.solve()
print("Status:", LpStatus[prob.status])
for i in diameterList:
if use_diameter[i].varValue> pressureTest:
print("Diameter Size",i)
for v in prob.variables():
print(v.name,"=",v.varValue)
This what I did for the combination part which took really long time.
xList = np.array(list(itertools.product(diameterList,repeat = numberOfPipelines)))
print len(xList)
for combination in xList:
pressures = []
for pipeline in range(numberOfPipelines):
pressures.append(pressure(combination))
pressureList = dict(zip(pipelineIds,pressures))
pipelinePressures[combination] = pressureList
print 'pipelinePressures',pipelinePressures
I would iterate through all combinations, I think you would run into memory problems otherwise trying to model ALL combinations in a MIP.
If you iterate through the problems perhaps using the multiprocessing library to use all cores, it shouldn't take long just remember only to hold information on the best combination so far, and not to try and generate all combinations at once and then evaluate them.
If the problem gets bigger you should consider Dynamic Programming Algorithms or use pulp with column generation.

Categories

Resources