Pyomo scheduling optimization problem with non-continuous objective function - python

I have the following optimization problem:
Assign a set of Users to a set of Shifts that minimizes labor cost. Each user has his own hourly wage but the caveat is that any hour worked above a certain overtime threshold needs to be counted with a wage multiplier. E.g. if the threshold if 5 hours and the shift is 8 hours then 5 hours would be paid with regular user wage and the remaining 3 with the wage multiplied by a predefined factor. And it carries over to shifts being worked later in the same date range (e.g. a week) so if a Monday shift may cause a shift on Friday to be counted for overtime.
Example:
threshold: 5
multiplier: 2
wage: 10,
shift duration: 8
cost = 5 * 10 + (8 - 5) * 10 * 2 = 110
I'm modelling my problem with pyomo library for python and I ran into an issue with
Evaluating Pyomo variables in a Boolean context, e.g.
Here is the complete example code that I'm trying to run:
import numpy
from datetime import datetime
from pyomo.environ import *
from pyomo.opt import SolverFactory
users = ['U1', 'U2', 'U3']
shifts = ['S1', 'S2'] # shifts in a chronological order
user_data = {
'U1': dict(wage=10),
'U2': dict(wage=20),
'U3': dict(wage=30),
}
shifts_data = {
'S1': dict(dtstart=datetime(2020, 2, 15, 9, 0), dtend=datetime(2020, 2, 15, 18, 0)),
'S2': dict(dtstart=datetime(2020, 2, 15, 19, 0), dtend=datetime(2020, 2, 15, 23, 0))
}
OVERTIME_THRESHOLD = 5 # hours
OVERTIME_MULTIPLIER = 2
model = ConcreteModel()
# (user, shift) binary pairs. If 1 then "user" works the given "shift"
model.assignments = Var(((user, shift) for user in users for shift in shifts), within=Binary, initialize=0)
def get_shift_hours(shift):
return (shifts_data[shift]['dtend'] - shifts_data[shift]['dtstart']).total_seconds() / 3600
def get_shift_cost(m, shift, shift_index, user):
shift_hours = get_shift_hours(shift)
all_hours_including_shift = sum(get_shift_hours(s) * m.assignments[user, s] for i, s in enumerate(shifts) if i <= shift_index)
# overtime hours are any hours above the OVERTIME_THRESHOLD threshold
ot_hours_including_shift = max(0, all_hours_including_shift - OVERTIME_THRESHOLD)
all_hours_excluding_shift = sum(get_shift_hours(s) * m.assignments[user, s] for i, s in enumerate(shifts) if i < shift_index)
ot_hours_excluding_shift = max(0, all_hours_excluding_shift - OVERTIME_THRESHOLD)
shift_ot_hours = ot_hours_including_shift - ot_hours_excluding_shift
shift_reg_hous = shift_hours - shift_ot_hours
return user_data[user]['wage'] * (shift_reg_hous + OVERTIME_MULTIPLIER * shift_ot_hours)
def obj_rule(m):
s = 0
# if a shift gets scheduled it has a negative impace on the objective function so it maximizes the number of scheduled shifts
s = s - sum(m.assignments[user, shift] * 1000000 for user in users for shift in shifts)
for user in users:
for shift_index, shift in enumerate(shifts):
# add the cost of a shift if the "user" was assigned to it (via the binary decision variable)
s = s + m.assignments[user, shift] * get_shift_cost(m, shift, shift_index, user)
return s
model.constraints = ConstraintList()
"""
Constraints that ensure the same user is not scheduled for overlapping shifts
"""
def shifts_overlap(shift_1, shift_2):
s1 = shifts_data[shift_1]
s2 = shifts_data[shift_2]
return s2['dtstart'] < s1['dtend'] and s2['dtend'] > s1['dtstart']
for shift_1 in shifts:
for shift_2 in shifts:
if shift_1 == shift_2 or not shifts_overlap(shift_1, shift_2):
continue
for user in users:
model.constraints.add(
1 >= model.assignments[user, shift_1] + model.assignments[user, shift_2]
)
"""
Constraints that a shift has only 1 assignee
"""
for shift in shifts:
model.constraints.add(
1 >= sum(model.assignments[user, shift] for user in users)
)
"""
End constraints
"""
model.obj = Objective(rule=obj_rule, sense=minimize)
opt = SolverFactory('cbc') # choose a solver
results = opt.solve(model) # solve the model with the selected solver
model.pprint()
I've been reading about disjunctions and piecewise constraints but I cannot find a way to apply these concepts to my problem. Any help would be much appreciated, thanks!

This can be modeled linearly and without binary variables.
Use something like:
totalHours = normalHours + overtimeHours
normalHours <= 40
cost = normalWage*normalHours + overtimePay*overtimeHours
We are lucky: overtime is more expensive so we will automatically first exhaust the normal hours.

Related

Geometric series: calculate quotient and number of elements from sum and first & last element

Creating evenly spaced numbers on a log scale (a geometric progression) can easily be done for a given base and number of elements if the starting and final values of the sequence are known, e.g., with numpy.logspace and numpy.geomspace. Now assume I want to define the geometric progression the other way around, i.e., based on the properties of the resulting geometric series. If I know the sum of the series as well as the first and last element of the progression, can I compute the quotient and number of elements?
For instance, assume the first and last elements of the progression are and and the sum of the series should be equal to . I know from trial and error that it works out for n=9 and r≈1.404, but how could these values be computed?
You have enough information to solve it:
Sum of series = a + a*r + a*(r^2) ... + a*(r^(n-1))
= a*((r^n)-1)/(r-1)
= a*((last element * r) - 1)/(r-1)
Given the sum of series, a, and the last element, you can use the above equation to find the value of r.
Plugging in values for the given example:
50 = 1 * ((15*r)-1) / (r-1)
50r - 50 = 15r - 1
35r = 49
r = 1.4
Then, using sum of series = a*((r^n)-1)/(r-1):
50 = 1*((1.4^n)-1)(1.4-1)
21 = 1.4^n
n = log(21)/log(1.4) = 9.04
You can approximate n and recalculate r if n isn't an integer.
We have to reconstruct geometric progesssion, i.e. obtain a, q, m (here ^ means raise into power):
a, a * q, a * q^2, ..., a * q^(m - 1)
if we know first, last, total:
first = a # first item
last = a * q^(m - 1) # last item
total = a * (q^m - 1) / (q - 1) # sum
Solving these equation we can find
a = first
q = (total - first) / (total - last)
m = log(last / a) / log(q)
if you want to get number of items n, note that n == m + 1
Code:
import math
...
def Solve(first, last, total):
a = first
q = (total - first) / (total - last)
n = math.log(last / a) / math.log(q) + 1
return (a, q, n);
Fiddle
If you put your data (1, 15, 50) you'll get the solution
a = 1
q = 1.4
n = 9.04836151801382 # not integer
since n is not an integer you, probably want to adjust; let last == 15 be exact, when total can vary. In this case q = (last / first) ^ (1 / (n - 1)) and total = first * (q ^ n - 1) / (q - 1)
a = 1
q = 1.402850552006674
n = 9
total = 49.752 # now n is integer, but total <> 50
You have to solve the following two equations for r and n:
a:= An / Ao = r^(n - 1)
and
s:= Sn / Ao = (r^n - 1) / (r - 1)
You can eliminate n by
s = (r a - 1) / (r - 1)
and solve for r. Then n follows by log(a) / log(r) + 1.
In your case, from s = 50 and a = 15, we obtain r = 7/5 = 1.4 and n = 9.048...
It makes sense to round n to 9, but then r^8 = 15 (r ~ 1.40285) and r = 1.4 are not quite compatible.

How to perform a ranking selection in this Genetic Algorithm

I'm building a Genetic Algorithm to maximize this function: x^5 - 10x^3 + 30x - y^2 + 21y.
The code must be in binary and the bounds for x and y are [-2.5, 2.5]. To generate the initial population I made a 16 bit string for both x and y where:
The first bit represents the signal [0 or 1]
The the second and third bit represents the integer part [00, 01 or 10]
The rest represents the float part
This is the function that generates the initial population:
def generate_population(n_pop):
population = list()
for _ in range(n_pop):
aux = list()
for _ in range(2):
signal = bin(randint(0, 1))[2:]
int_part = bin(randint(0, 2))[2:].zfill(2)
float_part = bin(randint(0, 5000))[2:].zfill(13)
aux.append((signal+int_part+float_part))
population.append(aux)
return population
I also made a function that returns the binary number into float:
def convert_float(individual):
float_num = list()
for i in range(2):
signal = int(individual[i][0])
int_part = int(individual[i][1:3], 2)
float_part = int(individual[i][3:], 2) * (10 ** -4)
value = round(int_part + float_part, 4)
if value > 2.5:
value = 2.5
if signal == 1:
value = value * (-1)
float_num.append(value)
return float_num
And lastly this function that calculate the fitness of each individual:
def get_fitness(individual):
x = individual[0]
y = individual[1]
return x ** 5 - 10 * x ** 3 + 30 * x - y ** 2 + 21 * y
This is my main function:
def ga(n_pop=10, n_iter=10):
population = generate_population(n_pop)
best_fitness_id, best_fitness = 0, get_fitness(convert_float(population[0]))
for i in range(n_iter):
float_population = [convert_float(x) for x in population]
fitness_population = [get_fitness(x) for x in float_population]
for j in range(n_pop):
if fitness_population[j] > best_fitness:
best_fitness_id, best_fitness = j, fitness_population[j]
print(f'--> NEW BEST FOUND AT GENERATION {i}:')
print(f'{float_population[j]} = {fitness_population[j]}')
selected_parents = rank_selection()
# childrens = list()
# childrens = childrens + population[best_fitness_id] # ELITE
After running the program I have something like this:
The population looks like: [['0000001100110111', '0000110111110101'], ['0010011111101110', '1000100101001001'], ...
The float population: [[0.0823, 0.3573], [1.203, -0.2377], ...
And the fitness values: [9.839066068044746, 16.15145434928624, ...
I need help to build the rank_selection() function, I've been stuck in this selection for 2 days. I know is something 1/N, 2/N etc and I've seen tons of examples in multiple languages but I could not apply any of them to this particular algorithm and it MUST be rank selecion.
I already know how to perform crossover and mutation.

Implementing binary constraint in PuLP Objective Function

I'm trying to determine the maximum revenue that can be earned from a battery connected to the grid using linear programming. The battery can earn revenues in two markets, the energy market and the frequency market. My model is throwing an error when I include a binary constraint in the objective function (TypeError: Non-constant expressions cannot be multiplied).
My Objective function is:
N is the time horizon of the optimisation
is the energy price at time t
are the allocated discharge and charge power at time t
is the frequency price at time t
is the allocated frequency power at time t
The battery should only be active in one market (energy or frequency) at each time period t. So needs a constraint that looks something like this:
where is a binary variable that activates activity x.
Ok, so that's what I trying to achieve. I'm struggling to create such a constraint in pulp that essentially switches off participation in one of the markets if the value in the other is higher (all other constraints being met). In my battery class, I've created decision variables for each of the power activities and also for their on/off status.
self.charge = \
pulp.LpVariable.dicts(
"charging_power",
('c_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_charge_power_capacity,
cat='Continuous')
self.discharge = \
pulp.LpVariable.dicts(
"discharging_power",
('d_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_discharge_power_capacity,
cat='Continuous')
self.freq = \
pulp.LpVariable.dicts(
"freq_power",
('f_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_freq_power_capacity,
cat='Continuous')
self.charge_status = \
pulp.LpVariable.dicts(
"charge_status",
('c_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
self.discharge_status = \
pulp.LpVariable.dicts(
"discharge_status",
('d_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
self.freq_status = \
pulp.LpVariable.dicts(
"freq_status",
('ds3_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
In my objective function, I included these binary variables.
self.model = pulp.LpProblem("Max Profit", pulp.LpMaximize)
self.model +=\
pulp.lpSum(
[self.charge['c_t_' + str(i)]*-1*prices[i] *
self.charge_status['c_status_t_' + str(i)] for i in range(0,self.time_horizon)]
+ [self.discharge['d_t_' + str(i)]*prices[i] *
self.discharge_status['d_status_t_' + str(i)] for i in range(0,self.time_horizon)]
+ [self.freq['f_t_' + str(i)]*freq_prices[i] *
self.freq_status['freq_status_t_' + str(i)] for i in range(0,self.time_horizon)]
)
The constraint for these binary variables, I set up as follows:
for hour_of_sim in range(1,self.time_horizon+1):
self.model += \
pulp.lpSum([self.charge_status['c_status_t_' + str(i)] for i in range(0,self.time_horizon)] +\
[self.discharge_status['d_status_t_' + str(i)] for i in range(0,self.time_horizon)] +\
[self.freq_status['freq_status_t_' + str(i)] for i in range(0,self.time_horizon)]
) <= 1
When I try to solve, I get a
TypeError: Non-constant expressions cannot be multiplied
on the objective function. Doesn't like my binary variables, runs if they are removed. There must be an alternative way of setting this up which is escaping me?
The comment is correct... you are violating "linearity" by multiplying 2 variables together. Fortunately, this is easy to linearize. You have a binary variable controlling the mode, so the key element you are looking for (google it) is a Big-M constraint, where you use the binary variable multiplied by a max value (or just something sufficiently large) to limit the other variable either to the max, or clamp it to zero.
An example below. I also re-arranged things a bit. You might find this style more readable. Two main things on style:
You are constantly re-creating the indices you are using which is really painful to read and error prone. Just make them and re-use them...and you don't need to get complicated with the index set values
You can easily double-index this model, which I think is more clear than making multiple sets of variables. You essentially have 2 sets you are working with: Time periods, and Op modes. Just make those sets and double index.
Example
# battery modes
import pulp
# some data
num_periods = 3
rate_limits = { 'energy' : 10,
'freq' : 20}
price = 2 # this could be a table or double-indexed table of [t, m] or ....
# SETS
M = rate_limits.keys() # modes. probably others... discharge?
T = range(num_periods) # the time periods
TM = {(t, m) for t in T for m in M}
model = pulp.LpProblem('Batts', pulp.LpMaximize)
# VARS
model.batt = pulp.LpVariable.dicts('batt_state', indexs=TM, lowBound=0, cat='Continuous')
model.op_mode = pulp.LpVariable.dicts('op_mode', indexs=TM, cat='Binary')
# Constraints
# only one op mode in each time period...
for t in T:
model += sum(model.op_mode[t, m] for m in M) <= 1
# Big-M constraint. limit rates for each rate, in each period.
# this does 2 things: it is equivalent to the upper bound parameter in the var declaration
# It is a Big-M type of constraint which uses the binary var as a control <-- key point
for t, m in TM:
model += model.batt[t, m] <= rate_limits[m] * model.op_mode[t, m]
# OBJ
model += sum(model.batt[t, m] * price for t, m in TM)
print(model)
# solve...
Yields:
Batts:
MAXIMIZE
2*batt_state_(0,_'energy') + 2*batt_state_(0,_'freq') + 2*batt_state_(1,_'energy') + 2*batt_state_(1,_'freq') + 2*batt_state_(2,_'energy') + 2*batt_state_(2,_'freq') + 0
SUBJECT TO
_C1: op_mode_(0,_'energy') + op_mode_(0,_'freq') <= 1
_C2: op_mode_(1,_'energy') + op_mode_(1,_'freq') <= 1
_C3: op_mode_(2,_'energy') + op_mode_(2,_'freq') <= 1
_C4: batt_state_(2,_'freq') - 20 op_mode_(2,_'freq') <= 0
_C5: batt_state_(2,_'energy') - 10 op_mode_(2,_'energy') <= 0
_C6: batt_state_(1,_'freq') - 20 op_mode_(1,_'freq') <= 0
_C7: batt_state_(1,_'energy') - 10 op_mode_(1,_'energy') <= 0
_C8: batt_state_(0,_'freq') - 20 op_mode_(0,_'freq') <= 0
_C9: batt_state_(0,_'energy') - 10 op_mode_(0,_'energy') <= 0
VARIABLES
batt_state_(0,_'energy') Continuous
batt_state_(0,_'freq') Continuous
batt_state_(1,_'energy') Continuous
batt_state_(1,_'freq') Continuous
batt_state_(2,_'energy') Continuous
batt_state_(2,_'freq') Continuous
0 <= op_mode_(0,_'energy') <= 1 Integer
0 <= op_mode_(0,_'freq') <= 1 Integer
0 <= op_mode_(1,_'energy') <= 1 Integer
0 <= op_mode_(1,_'freq') <= 1 Integer
0 <= op_mode_(2,_'energy') <= 1 Integer
0 <= op_mode_(2,_'freq') <= 1 Integer

Barrier Option Pricing in Python

We have a barrier call option of European type with strike price K>0 and a barrier value
0 < b< S0 ,
where S_0 is the starting price.According to the contract, the times 0<t_1<...<t_k<T the price must be checked S(t_k)>b for every k.
Assuming the S(t) is described with the binomial option model with u=1.1 and d = 0.9,r=0.05,T=10, and t_1=2,t_2=4 and t-3=7 the times that the asset must be checked.Also consider the S_0=100,K=125 and the barrier b=60.
My attempt is the following :
# Initialise parameters
S0 = 100 # initial stock price
K = 125 # strike price
T = 10 # time to maturity in years
b = 60 # up-and-out barrier price/value
r = 0.05 # annual risk-free rate
N = 4 # number of time steps
u = 1.1 # up-factor in binomial models
d = 0.9 # ensure recombining tree
opttype = 'C' # Option Type 'C' or 'P'
def barrier_binomial(K,T,S0,b,r,N,u,d,opttype='C'):
#precompute values
dt = T/N
q = (1+r - d)/(u-d)
disc = np.exp(-r*dt)
# initialise asset prices at maturity
S = S0 * d**(np.arange(N,-1,-1)) * u**(np.arange(0,N+1,1))
# option payoff
if opttype == 'C':
C = np.maximum( S - K, 0 )
else:
C = np.maximum( K - S, 0 )
# check terminal condition payoff
C[S >= b] = 0
# backward recursion through the tree
for i in np.arange(N-1,-1,-1):
S = S0 * d**(np.arange(i,-1,-1)) * u**(np.arange(0,i+1,1))
C[:i+1] = disc * ( q * C[1:i+2] + (1-q) * C[0:i+1] )
C = C[:-1]
C[S >= H] = 0
return C[0]
barrier_binomial(K,T,S0,b,r,N,u,d,opttype='C')
I receive nothing because something is wrong and I don’t know what
But is it a simulation ?
Any help from someone ?
In your loop you are using C[S >= H] = 0, but your barrier param is defined as b. Also you are filling the array C with 0s only, so check the payoff condition. In general, I find it much easier looping through matrices when working with tree models.

Python Simulation Inconsistency

I have made a simulator using Python for a game called Final Fantasy XII. The purpose of this simulator is to see how much "gil" (in-game currency) you can get from one particular enemy.
This should output around 23000 from our experiance, but what happens with this is, it outputs 24000 usually, but sometimes 29000. Considering it takes 1000 samples, it is impossible for that to be coincident. I think this code has problems with random generator, but I have no idea.
Are there reasons python simulator sometimes goes wrong?
import random
# defining variables, dictionaries and functions here
for t in range (1, test + 1):
for n in range (1, dustia + 1):
c = c + 1 # first, increase the chain counter
r = random.random()
# deciding a thing using "if r < [probability]" here
# ....
# apparently this part caused the problem
r = random.random()
if m == 1 and r < 0.4:
m = 0
elif m == 0 and r < 0.05:
m = 1
r1 = random.random() # RNG for book
r2 = random.random() # RNG for staff
rr1 = random.random() # amount of books
rr2 = random.random() # amount of staff
# deciding another thing using r < [probability] here
#summing up samples and calculating average here

Categories

Resources