Pulp Integer Programming Constraint ignored - python

I am trying to solve a LpProblem with only boolean variables and Pulp seems to be ignoring some constraints. To give some context about the problem:
I want to find an optimal solution to the problem schools face when trying to create classroom groups. In this case, students are given a paper to write at most 5 other students and the school guarantees them that they will be together with at least one of those students. To see how I modeled this problem into an integer programming problem please refer to this question.
In that link you will see that my variables are defined as x_ij = 1 if student i will be together with student j, and x_i_j = 0 otherwise. Also, in that link I ask about the constraint that I am having trouble implementing with Pulp: if x_i_j = 1 and x_j_k = 1, then by transitive property, x_i_k = 1. In other words, if student i is with student j, and student j is with student k, then, student i will inherently be together with student k.
My objective is to maximize the sum of all the elements of the matrix obtained when performing a Hadamard product between the input matrix and the variables matrix. In other words, I want to contemplate as many of the student's requests as possible.
I will now provide some code snippets and screen captures that should help visualize the problem:
Inputs (just a sample: the real matrix is 37x37)
Output
As you can see in this last image, x_27 = 1 and x_37 = 1 but x_23 = 0 which doesn't make sense.
Here is how I define my variables
def define_variables():
variables = []
for i in range(AMOUNT_OF_STUDENTS):
row = []
for j in range(AMOUNT_OF_STUDENTS):
row.append(LpVariable(f"x_{i}_{j}", lowBound=0, upBound=1, cat='Integer'))
variables.append(row)
return variables
Here is how I define the transitive constraints
for i in range(len(variables)):
for j in range(i, len(variables)):
if i != j:
problem += variables[i][j] == variables[j][i] # Symmetry
for k in range(j, len(variables)):
if i < j < k < len(variables):
problem += variables[i][j] + variables[j][k] - variables[i][k] <= 1 # Transitive
problem += variables[i][j] + variables[i][k] - variables[j][k] <= 1
problem += variables[j][k] + variables[i][k] - variables[i][j] <= 1
When printing the LpProblem I see the constraint that is apparently not working:
As you can see in the output: x_2_7 = 1 and x_3_7 = 1. Therefore, to satisfy this constraint, x_2_3 should also be 1, but as you can also see in the output, it is 0.
Any ideas about what could be happening? I've been stuck for days and the problem seems to be modeled fine and it worked when I only had 8 students (64 variables). Now that I have 37 students (1369 variables) it seems to be behaving oddly. The solver arrives to a solution but it seems to be ignoring some constraints.
Any help is very much appreciated! Thank you in advance.

The constraint is working correctly. Find below the analysis:
(crossposted from github: https://github.com/coin-or/pulp/issues/377)
import pulp as pl
import pytups as pt
path = 'debugSolution.txt'
# import model
_vars, prob = pl.LpProblem.from_json(path)
# get all variables with non-zero value
vars_value = pt.SuperDict(_vars).vfilter(pl.value)
# export the lp
prob.writeLP('debugSolution.lp')
# the constraint you show in the SO problem is:
# _C3833: - x_2_3 + x_2_7 + x_3_7 <= 1
'x_2_7' in vars_value
# True, so x_2_7 has value 1
'x_3_7' in vars_value
# False, so x_3_7 has value 0
'x_2_3' in vars_value
# False, so x_2_3 has value 0
So -0 + 1 + 0 <= 1 means the constraint is respected. There must be a problem with bringing back the value of x_3_7 somewhere because you think is 1 when in pulp it's 0.

This is called a set partitioning problem and PuLP has an example in their documentation here.
In essence, instead of modeling your variables as indicators of whether student A is in the same class as student B, you'll define a mapping between a set of students and a set of classrooms. You can then apply your student preferences as either constraints or part of a maximization objective.

Related

Can you create an array of unknown values with CVXPY?

I am attempting to find an array of optimum values to help with selecting the best runtime for some motors in a factory.
At the moment I have created a simple version of what I would like to do, and it works for one.
# Create variables
running_time = cp.Variable(name='running_time')
max_running_time = 240 # in minutes
pump_flow_per_minute = 9.6*3600
required_volume = 2073600 # in litres
# Find the optimal solution
objective = cp.Minimize(running_time)
# Constraints
runtime_constraint = running_time <= max_running_time
volume_contraint = pump_flow_per_minute * running_time >= required_volume
constraints = [runtime_constraint, volume_contraint]
prob = cp.Problem(objective, constraints)
prob.solve()
print("The optimal solution is:", prob.value)
The issue I am having is translating this to multiple areas, where I would like to look at an array of max_running_times which could equate to a 24 hour period, and likewise with the required volumes.
Is there such a way to complete this trick with the use of CVXPY, I ultimately think the use of one problem is best as in the future adding onto this problem, the solution will directly cause effect on the other times of day selected. Is the solution best solved with the liked of loops?

Python: Why do I not need 2 variables when unpacking a dictionary?

movie_dataset = {'Avatar': [0.01940156245995175, 0.4812286689419795, 0.9213483146067416], "Pirates of the Caribbean: At World's End": [0.02455894456664483, 0.45051194539249145, 0.898876404494382], 'Spectre': [0.02005646812429373, 0.378839590443686, 0.9887640449438202], ... }
movie_ratings = {'Avatar': 7.9, "Pirates of the Caribbean: At World's End": 7.1, 'Spectre': 6.8, ...}
def distance(movie1, movie2):
squared_difference = 0
for i in range(len(movie1)):
squared_difference += (movie1[i] - movie2[i]) ** 2
final_distance = squared_difference ** 0.5
return final_distance
def predict(unknown, dataset, movie_ratings, k):
distances = []
#Looping through all points in the dataset
for title in dataset:
movie = dataset[title]
distance_to_point = distance(movie, unknown)
#Adding the distance and point associated with that distance
distances.append([distance_to_point, title])
distances.sort()
#Taking only the k closest points
neighbors = distances[0:k]
total_rating = 0
for i in neighbors[1]:
total_rating += movie_ratings[i] <----- Why is this an error?
return total_rating / len(neighbors) <----- Why can I not divide by total rating
#total_rating = 0
#for i in neighbors:
# title = neighbors[1]
#total_rating += movie_ratings[title] <----- Why is this not an error?
#return total_rating / len(neighbors)
print(movie_dataset["Life of Pi"])
print(movie_ratings["Life of Pi"])
print(predict([0.016, 0.300, 1.022], movie_dataset, movie_ratings, 5))
Two questions here. First, why is this an error?
for i in neighbors[1]:
total_rating += movie_ratings[i]
It seems to be the same as
for i in neighbors:
title = neighbors[1]
total_rating += movie_ratings[title]
Second, why can I not divide by len(total_rating)?
Second question first, because it's more straightforward:
Second, why can I not divide by len(total_rating)?
You're trying to compute an average, right? So you want the sum of the ratings divided by the number of ratings?
Okay. So, you're trying to figure out how many ratings there are. What's the rule that tells you that? It seems like you're expecting to count up the ratings from where they are stored. Where are they stored? It is not total_rating; that's where you stored the numerical sum. Where did the ratings come from? They came from looking up the names of movies in the movie_ratings. So the ratings were not actually stored at all; there is nothing to measure the len of. Right? Well, not quite. What is the rule that determines the ratings we are adding up? We are looking them up in the movie_ratings by title. So how many of them are there? As many as there are titles. Where were the titles stored? They were paired up with distances in the neighbors. So there are as many titles as there are neighbors (whatever "neighbor" is supposed to mean here; I don't really understand why you called it that). So that is what you want the len() of.
Onward to fixing the summation.
total_rating = 0
for i in neighbors[1]:
total_rating += movie_ratings[i]
First, this computes neighbors[1], which will be one of the [distance_to_point, title] pairs that was .appended to the list (assuming there are at least two such values, to make the [1] index valid).
Then, the loop iterates over that two-element list, so it runs twice: the first time, i is equal to the distance value, and the second time it is equal to the title. An error occurs because the title is a string and you try to do math with it.
total_rating = 0
for i in neighbors:
title = neighbors[1]
total_rating += movie_ratings[title]
This loop makes i take on each of the pairs as a value. The title = neighbors[1] is broken; now we ignore the i value completely and instead always use a specific pair, and also we try to use the pair (which is a list) as a title (we need a string).
What you presumably wanted is:
total_rating = 0
for neighbor in neighbors:
title = neighbor[1]
total_rating += movie_ratings[title]
Notice I use a clearer name for the loop variable, to avoid confusion. neighbor is one of the values from the neighbors list, i.e., one of the distance-title pairs. From that, we can get the title, and then from the ratings data and the title, we can get the rating.
I can make it clearer, by using argument unpacking:
total_rating = 0
for neighbor in neighbors:
distance, title = neighbor
total_rating += movie_ratings[title]
Instead of having to understand the reason for a [1] index, now we label each part of the neighbor value, and then use the one that's relevant for our purpose.
I can make it simpler, by doing the unpacking right away:
total_rating = 0
for distance, title in neighbors:
total_rating += movie_ratings[title]
I can make it more elegant, by not trying to explain to Python how to do sums, and just telling it what to sum:
total_rating = sum(movie_ratings[title] for distance, title in neighbors)
This uses a generator expression along with the built-in sum function, which does exactly what it sounds like.
distances is generated in the form:
[
[0.08565491616637051, 'Spectre'],
[0.1946446017955758, "Pirates of the Caribbean: At World's End"],
[0.20733104650812437, 'Avatar']
]
which is what neighbors is derived from, and the names are in position 1 of each list.
neighbors[1] would just retrieve [0.1946446017955758, "Pirates of the Caribbean: At World's End"], or a single element, which doesn't look like is what you want. It would try to use 0.19... and Pirates... as keys in dict movie_ratings.
I'm guessing you want this, to average all the ratings of the closest by the extracted distance values from dataset?:
for dist, name in neighbors:
total_rating += movie_ratings[name]
return total_rating / len(neighbors)

PuLP: Stuck with objective function definition

I am a first time user of PuLP and I the last time I did linear programming, Python did not exist.
I can solve this problem with LibreOffice's Solve extension (which does LP)
But I need to do it in code.
I want to optimise a stock picking problem.
We need to pick a certain quantity of screws, say 98.
Screws are packed in packs of 25 and 100. I name those pack sizes '25' and '100'.
The cost of the pick needs to be minimised.
There is a cost to pick each pack, and there is a cost to the excess quantity picked.
The constraint is that the quantity picked >= target_qty
For example, if the cost to each unit of excess was 0.1 and the cost to pick the '25' pack was 1 and the cost to pack the '100' pack is 1.1., the cost of picking is 1 x 100 pack is
(100 - 98) *.1 + 0*1 + 1*1.1
This is cheaper than picking 4*'25' pack.
Assuming that there are dicts pack_cost{} and pack_capacity{} which both have the key pack_name,
e.g. pack_cost = {'25':1,'100':1.1} and therefore list_of_pack_names = ['25','100']
I try this:
lp_prob = pulp.LpProblem('PackSizes', pulp.LpMinimize)
packs_used = pulp.LpVariable.dicts("Packs",list_of_pack_names,lowBound=0,cat="Integer")
pack_cost = [pack_costs[pack_name]*packs_used[pack_name] for pack_name in list_of_pack_names]
excess_cost = cost_per_unit * ( sum([pack_sizes[pack_name]*packs_used[pack_name] for pack_name in list_of_pack_names])- original_qty)
lp_prob += pulp.lpSum(pack_cost) + pulp.lpSum(excess_cost) #objective function
# and constraint: total picked >= needed
lp_prob += pulp.lpSum(sum([pack_sizes[pack_name]*packs_used[pack_name] for pack_name in list_of_pack_names]) >= target_qty)
Results:
print("Status:",pulp.LpStatus[lp_prob.status])
shows Optimal
lp_prob.objective is 10*Packs_10 + 15*Packs_15 + 30*Packs_30 - 16.5
but the solution is 0 of each pack size
You may check your problem with
print(lp_prob)
You do not add any essential constraint that prevents all vars from becoming zero.
Probably, you misprinted in the constraint statement. This constraint makes the problem not trivial (check brackets):
lp_prob += pulp.lpSum(sum([pack_sizes[pack_name]*packs_used[pack_name] for pack_name in list_of_pack_names])) >= target_qty

Constraining Pulp Binary Variable Selection Based on Another Variable

I'm creating an optimisation script for Fantasy Football. It starts off quite easily- loading in players & their relevant details.
The key in this game is that 15 players can be selected in your squad but only 11 can be fielded per week.
What I would like to do is have 2 variables- one defining that the player is in your squad and a sub-variable that determines whether you put the player in your starting 11.
I have tried a few things- one broad solution is that have 2 unrelated variables. 1 that selects 11 starters and a second that selects 4 subs. This works well for 1 week, but for example one week Player A from your squad might be best starting and the next he's better on the bench. Therefore I would get a more optimal solution if I can make the starting 11 variable a subset of the squad variable.
I've attached the code defining the variables and my attempt at creating a constraint that would link them together. (there are other constraints that all successfully work. For example I can pick a starting 11 or a squad of 15 to maximize expected results without issue, but I cannot pick a starting 11 within a squad of 15.
#VECTORS OF BINARY DECISIONS VARIABLES
squad_variables = []
for rownum in ID:
variable = str('x' + str(rownum))
variable = pulp.LpVariable(str(variable), lowBound = 0, upBound = 1, cat= 'Integer')
squad_variables.append(variable)
xi_variables = []
for rownum in ID:
bariable = str('y' + str(rownum))
bariable = pulp.LpVariable(str(bariable), lowBound = 0, upBound = 1, cat= 'Integer')
xi_variables.append(bariable)
The code below is not working for this task and is the root of the problem..
#ID CONSTRAINTS (ie. only 15 unique id selection across both systems)
id_usage = ""
for rownum in ID:
for i, player in enumerate(squad_variables):
if rownum == i:
formula = max(1*xi_variables[rownum],(1*player))
id_usage += formula
prob += (id_usage ==15)
Any help would be greatly appreciated- perhaps this is simply a non-linear problem. Thank you :)
You want a constraint that says "if x[i] = 0 then y[i] = 0". The typical way to do this is through the constraint y[i] <= x[i]. Note that this only works if both variables are binary; otherwise a modified approach is necessary. I can't quite follow your PuLP code so I won't try to give you the code for this constraint, but I assume you'll be able to implement it once you understand the logic.

Decision Tree Learning

I want to implement the decision-tree learning alogorithm.
I am pretty new to coding so I know it's not the best code, but I just want it to work. Unfortunately i get the error: e2 = b(pk2/(pk2 + nk2))
ZeroDivisionError: division by zero
Can someone explain to me what I am doing wrong?
Lets assume after some splits you are left with two records with 3 features/attributes (last column being the truth label)
1 1 1 2
2 2 2 1
Now you are about to select the next best feature to split on, so you call this method remainder(examples, attribute) as part of selection which internally calls nk1, pk1 = pk_nk(1, examples, attribute).
The value returned by pk_nk for the above mentioned rows and features will be 0, 0 which will result in divide by zero exception for e1 = b(pk1/(pk1 + nk1)). This is a valid scenario based on how you coded the DT and you should be handling the case.
(pk2 + nk2) at some point equals zero. If we step backwards through your code, we see they are assigned here:
nk2, pk2 = pk_nk(2, examples, attribute)
def pk_nk(path, examples, attribute):
nk = 0
pk = 0
for ex in examples:
if ex[attribute] == path and ex[7] == NO:
nk += 1
elif ex[attribute] == path and ex[7] == YES:
pk += 1
return nk, pk
As such, for the divisor to equal zero nk and pk must remain zero through the function, i.e. either:
examples is empty, or
neither if/elif condition is satisfied

Categories

Resources