Minimise set size in task assignation problem - python

I have to create a solution for assigning tasks to users according to some rules and I wanted to give linear programming a try.
I have a list of tasks that require a certain skill and belong to a specific team, and I have a list of available users, their assigned team for the day, and their skill set:
# Creating dummies
task = pd.DataFrame({
'id': [n for n in range(25)],
'skill': [random.randint(0,3) for _ in range(25)]
})
task['team'] = task.apply(lambda row: 'A' for row.skill in (1, 2) else 'B', axis=1)
user_list = pd.DataFrame({
'user': [''.join(random.sample(string.ascii_lowercase, 4)) for _ in range(10)],
'team': [random.choice(['A', 'B']) for _ in range(10)]
})
user_skill = {user_list['user'][k]: random.sample(range(5), 3) for k in range(len(user_list))}
The constraints I have to implement are the following:
All tasks must be assigned
A task can only be assigned to one user
A user can not do a task for which he or she isn't skilled
A user can not do a task for another team than his or hers
The amount of tasks per user should be as low as possible inside a team
I struggled a lot to write this in PuLP but thanks to this post I managed to get some results.
# Create the problem
task_assignment = pulp.LpProblem('task_assignment', pulp.LpMaximize)
# Create model vars
pair = pulp.LpVariable.dicts("Pair", (user_list.user, task.id), cat=pulp.LpBinary)
task_covered = pulp.LpVariable.dicts('Covered', task.id, cat=pulp.LpBinary)
# Set objective
task_assignment += pulp.lpSum(task_covered[t] for t in task.id) + \
0.05 * pulp.lpSum(pair[u][t] for u in user_list.user for t in task.id)
# Constraint
# A task can only be done by one user
for t in task.id:
task_assignment+= pulp.lpSum([pair[u][t] for u in user_list.user]) <= 1
# A user must be skilled for the task
for u in user_list.user:
for t in task.id:
if not task[task.id == t].skill.values[0] in user_skill[u]:
task_assignment += pair[u][t] == 0
# A user can not do a task for another team
for u in user_list.user:
for t in task.id:
if not (task[task.id == t].team.values[0] == user_list[user_list.user == u].team.values[0]):
task_assignment+= pair[u][t] == 0
task_assignment.solve()
My problem is that I have absolutely no idea on how to implement the last constraint (i.e. the amount of tasks per user should be as low as possible inside a team)
Does someone have any idea how to do this ?

First of all, your dummy data set isn't valid python code since it misses some brackets.
One way to minimize the number of tasks per user inside a team is to minimize the maximal number of tasks per user inside a team. For this end, we just include a non-negative variable eps for each team and add the following constraints:
teams = user_list.team.unique()
# Create the problem
task_assignment = pulp.LpProblem('task_assignment', pulp.LpMaximize)
# Create model vars
pair = pulp.LpVariable.dicts("Pair", (user_list.user, task.id), cat=pulp.LpBinary)
task_covered = pulp.LpVariable.dicts('Covered', task.id, cat=pulp.LpBinary)
eps = pulp.LpVariable.dicts("eps", teams, cat=pulp.LpContinuous, lowBound=0.0)
# Set objective
task_assignment += pulp.lpSum(task_covered[t] for t in task.id) + \
0.05 * pulp.lpSum(pair[u][t] for u in user_list.user for t in task.id) - \
0.01 * pulp.lpSum(eps[team] for team in teams)
# Constraint
# ... your other constraints here ...
# the amount of tasks per user should be as low as possible inside a time
for team in teams:
# for all users in the current team
for u in user_list[user_list["team"] == team].user:
task_assignment += pulp.lpSum(pair[u][t] for t in task.id) <= eps[team]
task_assignment.solve()
Because you have a maximization problem, we need to subtract the sum of the eps in the objective.

Related

set costs for orders in or-tools

We minimize the fuel consumed by the vehicles in terms of cost. Some customers may sometimes need an extra staff member while purchasing their products. These extra staffs have a fixed daily cost. This cost + fuel consumption should be minimized.
In the products you see in the figure, those with a code of 1 need extra staff in the car.
If those with 1 code go more in the same car and if there is no 1 code in other cars, they do not need staff and no extra money is paid for that person. This is not a necessity of course, but the aim is to minimize the total cost spent.
Note: If there is 1 product that needs even one extra person in the car, an extra fee will be paid. (extra $50 cost will be if there's an extra person in the car)
Here is how I calculate vehicle costs.
data['costs'] = [0.2314,0.158,0.132,0.201]
number_of_vehicles = 4
for vehicle_id in range(number_of_vehicles):
routing.SetFixedCostOfVehicle(data['costs'][vehicle_id], vehicle_id)
So basically if a vehicle visit a node which require a staff member you want to add a cost of 50 to the objective ?
Proposal in 3 steps:
Count number of staff needed locations which have been visited;
Transform this value in range [0, 1] at each end node.
Add a penalty cost to the objective if this value is 1.
first I would add a dimension "staff_count" which is increased by 1 each time you visit a location which need a staff.
(see capacity example: https://developers.google.com/optimization/routing/cvrp#python_1)
staff_cost = [...., 1, 0, 1, 0, 1, 0, 0, 0, 1, 1] # from 0..., 101 to 110
def staff_counter_callback(from_index):
"""Returns if staff is needed for this node."""
# Convert from routing variable Index to demands NodeIndex.
node = manager.NodeToIndex(from_index)
return staff_cost[node]
staff_counter_callback_id = routing.RegisterUnaryTransitCallback(
staff_counter_callback)
dimension_name = "staff_counter"
routing.AddDimension(
staff_counter_callback_id,
0, # no slack
N, # don't care just big enough so we won't reach it
True, # force it to zero at start
dimension_name
)
staff_counter_dimension = routing.GetDimensionOrDie(dimension_name)
Second I'll create a second dimension "staff_used" (between 0,1) whose end node is 1 iif
end node of the "staff" dimension is > zero.
note: you can see it as a boolean set to 1 if we need a staff along the route, 0 otherwise
dimension_name = "staff_used"
routing.AddConstantDimensionWithSlack(
0, # transit is 0 everywhere
1, # capacity will be 0 everywhere and maybe 1 on end node
1, # need slack to allow transition to 1 in end node
True, # force it to zero at start
dimension_name
)
staff_dimension = routing.GetDimensionOrDie(dimension_name)
solver = routing.solver()
for vehicle_id in range(manager.GetNumberOfVehicles()):
index = routing.End(vehicle_id)
# the following expr will resolve to 1 iff staff has been required along the route
expr = staff_counter_dimension.CumulVar(index) > 0
solver.Add(expr == staff_dimension.CumulVar(index))
note: please notice that AddConstantDimensionWithSlack() has slack and capacity parameter swapped, I'm sorry for this API consistent issue...
ref: https://github.com/google/or-tools/blob/b37d9c786b69128f3505f15beca09e89bf078a89/ortools/constraint_solver/routing.h#L457-L467
Third, use RoutingDimension::SetCumulVarSoftUpperBound, with upper bound 0 and penalty 50 on each end node of this second dimension.
note: Idea pay 50 if staff_used_dimension.CumulVar(end_node) == 1
for vehicle_id in range(manager.GetNumberOfVehicles()):
index = routing.End(vehicle_id)
staff_dimension.SetCumulVarSoftUpperBound(index, 0, 50)
ref: https://github.com/google/or-tools/blob/b37d9c786b69128f3505f15beca09e89bf078a89/ortools/constraint_solver/routing.h#L2514-L2523
Annexe
If you want to limit the total number of vehicles having an extra worker to N you can use:
solver = routing.solver()
staff_at_end = []
for vehicle_id in range(manager.GetNumberOfVehicles()):
  index = routing.End(vehicle_id)
  staff_at_end.append(staff_dimension.CumulVar(index))
solver.Add(solver.Sum(staff_at_end) <= N)

Pulp Integer Programming Constraint ignored

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.

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.

Genetic Algorithm - creatures in 2d world are not learning

Goal -
I am trying to implement a genetic algorithm to optimise the fitness of a
species of creatures in a simulated two-dimensional world. The world contains edible foods, placed at random, and a population of monsters (your basic zombies). I need the algorithm to find behaviours that keep the creatures well fed and not dead.
What i have done -
So i start off by generating a 11x9 2d array in numpy, this is filled with random floats between 0 and 1. I then use np.matmul to go through each row of the array and multiply all of the random weights by all of the percepts (w1+p1*w2+p2....w9+p9) = a1.
This first generation is run and I then evaluate the fitness of each creature using (energy + (time of death * 100)). From this I build a list of creatures who performed above the average fitness. I then take the best of these "elite" creatures and put them back into the next population. For the remaining space I use a crossover function which takes two randomly selected "elite" creatures and mixes their genes. I have tested two different crossover functions one which does a two point crossover on each row and one which takes a row from each parent until the new child has a complete chromosome. My issue is that the creatures just don't really seem to be learning, at 75 turns I will only get 1 survivor every so often.
I am fully aware this might not be enough to go off but I am truly stuck on this and cannot figure out how to get these creatures to learn even though I think I am implementing the correct procedures. Occasionally I will get a 3-4 survivors rather than 1 or 2 but it appears to occur completely randomly, doesn't seem like there is much learning happening.
Below is the main section of code, it includes everything I have done but none of the provided code for the simulation
#!/usr/bin/env python
from cosc343world import Creature, World
import numpy as np
import time
import matplotlib.pyplot as plt
import random
import itertools
# You can change this number to specify how many generations creatures are going to evolve over.
numGenerations = 2000
# You can change this number to specify how many turns there are in the simulation of the world for a given generation.
numTurns = 75
# You can change this number to change the world type. You have two choices - world 1 or 2 (described in
# the assignment 2 pdf document).
worldType=2
# You can change this number to modify the world size.
gridSize=24
# You can set this mode to True to have the same initial conditions for each simulation in each generation - good
# for development, when you want to have some determinism in how the world runs from generation to generation.
repeatableMode=False
# This is a class implementing you creature a.k.a MyCreature. It extends the basic Creature, which provides the
# basic functionality of the creature for the world simulation. Your job is to implement the AgentFunction
# that controls creature's behaviour by producing actions in response to percepts.
class MyCreature(Creature):
# Initialisation function. This is where your creature
# should be initialised with a chromosome in a random state. You need to decide the format of your
# chromosome and the model that it's going to parametrise.
#
# Input: numPercepts - the size of the percepts list that the creature will receive in each turn
# numActions - the size of the actions list that the creature must create on each turn
def __init__(self, numPercepts, numActions):
# Place your initialisation code here. Ideally this should set up the creature's chromosome
# and set it to some random state.
#self.chromosome = np.random.uniform(0, 10, size=numActions)
self.chromosome = np.random.rand(11,9)
self.fitness = 0
#print(self.chromosome[1][1].size)
# Do not remove this line at the end - it calls the constructors of the parent class.
Creature.__init__(self)
# This is the implementation of the agent function, which will be invoked on every turn of the simulation,
# giving your creature a chance to perform an action. You need to implement a model here that takes its parameters
# from the chromosome and produces a set of actions from the provided percepts.
#
# Input: percepts - a list of percepts
# numAction - the size of the actions list that needs to be returned
def AgentFunction(self, percepts, numActions):
# At the moment the percepts are ignored and the actions is a list of random numbers. You need to
# replace this with some model that maps percepts to actions. The model
# should be parametrised by the chromosome.
#actions = np.random.uniform(0, 0, size=numActions)
actions = np.matmul(self.chromosome, percepts)
return actions.tolist()
# This function is called after every simulation, passing a list of the old population of creatures, whose fitness
# you need to evaluate and whose chromosomes you can use to create new creatures.
#
# Input: old_population - list of objects of MyCreature type that participated in the last simulation. You
# can query the state of the creatures by using some built-in methods as well as any methods
# you decide to add to MyCreature class. The length of the list is the size of
# the population. You need to generate a new population of the same size. Creatures from
# old population can be used in the new population - simulation will reset them to their
# starting state (not dead, new health, etc.).
#
# Returns: a list of MyCreature objects of the same length as the old_population.
def selection(old_population, fitnessScore):
elite_creatures = []
for individual in old_population:
if individual.fitness > fitnessScore:
elite_creatures.append(individual)
elite_creatures.sort(key=lambda x: x.fitness, reverse=True)
return elite_creatures
def crossOver(creature1, creature2):
child1 = MyCreature(11, 9)
child2 = MyCreature(11, 9)
child1_chromosome = []
child2_chromosome = []
#print("parent1", creature1.chromosome)
#print("parent2", creature2.chromosome)
for row in range(11):
chromosome1 = creature1.chromosome[row]
chromosome2 = creature2.chromosome[row]
index1 = random.randint(1, 9 - 2)
index2 = random.randint(1, 9 - 2)
if index2 >= index1:
index2 += 1
else: # Swap the two cx points
index1, index2 = index2, index1
child1_chromosome.append(np.concatenate([chromosome1[:index1],chromosome2[index1:index2],chromosome1[index2:]]))
child2_chromosome.append(np.concatenate([chromosome2[:index1],chromosome1[index1:index2],chromosome2[index2:]]))
child1.chromosome = child1_chromosome
child2.chromosome = child2_chromosome
#print("child1", child1_chromosome)
return(child1, child2)
def crossOverRows(creature1, creature2):
child = MyCreature(11, 9)
child_chromosome = np.empty([11,9])
i = 0
while i < 11:
if i != 10:
child_chromosome[i] = creature1.chromosome[i]
child_chromosome[i+1] = creature2.chromosome[i+1]
else:
child_chromosome[i] = creature1.chromosome[i]
i += 2
child.chromosome = child_chromosome
return child
# print("parent1", creature1.chromosome[:3])
# print("parent2", creature2.chromosome[:3])
# print("crossover rows ", child_chromosome[:3])
def newPopulation(old_population):
global numTurns
nSurvivors = 0
avgLifeTime = 0
fitnessScore = 0
fitnessScores = []
# For each individual you can extract the following information left over
# from the evaluation. This will allow you to figure out how well an individual did in the
# simulation of the world: whether the creature is dead or not, how much
# energy did the creature have a the end of simulation (0 if dead), the tick number
# indicating the time of creature's death (if dead). You should use this information to build
# a fitness function that scores how the individual did in the simulation.
for individual in old_population:
# You can read the creature's energy at the end of the simulation - it will be 0 if creature is dead.
energy = individual.getEnergy()
# This method tells you if the creature died during the simulation
dead = individual.isDead()
# If the creature is dead, you can get its time of death (in units of turns)
if dead:
timeOfDeath = individual.timeOfDeath()
avgLifeTime += timeOfDeath
else:
nSurvivors += 1
avgLifeTime += numTurns
if individual.isDead() == False:
timeOfDeath = numTurns
individual.fitness = energy + (timeOfDeath * 100)
fitnessScores.append(individual.fitness)
fitnessScore += individual.fitness
#print("fitnessscore", individual.fitness, "energy", energy, "time of death", timeOfDeath, "is dead", individual.isDead())
fitnessScore = fitnessScore / len(old_population)
eliteCreatures = selection(old_population, fitnessScore)
print(len(eliteCreatures))
newSet = []
for i in range(int(len(eliteCreatures)/2)):
if eliteCreatures[i].isDead() == False:
newSet.append(eliteCreatures[i])
print(len(newSet), " elites added to pop")
remainingRequired = w.maxNumCreatures() - len(newSet)
i = 1
while i in range(int(remainingRequired)):
newSet.append(crossOver(eliteCreatures[i], eliteCreatures[i-1])[0])
if i >= (len(eliteCreatures)-2):
i = 1
i += 1
remainingRequired = w.maxNumCreatures() - len(newSet)
# Here are some statistics, which you may or may not find useful
avgLifeTime = float(avgLifeTime)/float(len(population))
print("Simulation stats:")
print(" Survivors : %d out of %d" % (nSurvivors, len(population)))
print(" Average Fitness Score :", fitnessScore)
print(" Avg life time: %.1f turns" % avgLifeTime)
# The information gathered above should allow you to build a fitness function that evaluates fitness of
# every creature. You should show the average fitness, but also use the fitness for selecting parents and
# spawning then new creatures.
# Based on the fitness you should select individuals for reproduction and create a
# new population. At the moment this is not done, and the same population with the same number
# of individuals is returned for the next generation.
new_population = newSet
return new_population
# Pygame window sometime doesn't spawn unless Matplotlib figure is not created, so best to keep the following two
# calls here. You might also want to use matplotlib for plotting average fitness over generations.
plt.close('all')
fh=plt.figure()
# Create the world. The worldType specifies the type of world to use (there are two types to chose from);
# gridSize specifies the size of the world, repeatable parameter allows you to run the simulation in exactly same way.
w = World(worldType=worldType, gridSize=gridSize, repeatable=repeatableMode)
#Get the number of creatures in the world
numCreatures = w.maxNumCreatures()
#Get the number of creature percepts
numCreaturePercepts = w.numCreaturePercepts()
#Get the number of creature actions
numCreatureActions = w.numCreatureActions()
# Create a list of initial creatures - instantiations of the MyCreature class that you implemented
population = list()
for i in range(numCreatures):
c = MyCreature(numCreaturePercepts, numCreatureActions)
population.append(c)
# Pass the first population to the world simulator
w.setNextGeneration(population)
# Runs the simulation to evaluate the first population
w.evaluate(numTurns)
# Show the visualisation of the initial creature behaviour (you can change the speed of the animation to 'slow',
# 'normal' or 'fast')
w.show_simulation(titleStr='Initial population', speed='normal')
for i in range(numGenerations):
print("\nGeneration %d:" % (i+1))
# Create a new population from the old one
population = newPopulation(population)
# Pass the new population to the world simulator
w.setNextGeneration(population)
# Run the simulation again to evaluate the next population
w.evaluate(numTurns)
# Show the visualisation of the final generation (you can change the speed of the animation to 'slow', 'normal' or
# 'fast')
if i==numGenerations-1:
w.show_simulation(titleStr='Final population', speed='normal')

Python integer programming: looking for constraint on set partitioning

I have a set partitioning problem that requires each available period to be used only once.
def solve (slots, reqs, periods):
# binary variable to id if combination is used
x = pulp.LpVariable.dicts('slot', slots, lowBound=0, upBound=1, cat=pulp.LpInteger)
# linear program problem
sked_model = pulp.LpProblem("NLE Specials Scheduling Model", pulp.LpMaximize)
# objective function
sked_model += sum([x[slot] for slot in slots]), "Slot selection" # all combinations considered equal
# Supply maximum number of slots
sked_model += sum([x[slot] for slot in slots]) == len(reqs), "Number_of_slots to sked"
ERROR OCCURS HERE
# A period can only be used once
for period in periods:
sked_model += sum([x[slot] for slot in slots if period == slot[0:2]]) <= 1, "Period_used_no_more_than_once"
# The problem is solved using PuLP’s choice of Solver
sked_model.solve()
The error received is "pulp.constants.PulpError: overlapping constraint names:" on the # period can only be used once constraint.
Solved this.
The error came from the constraint name, "Period_used_no_more_than_once", being used more than once when the constraints were built. By replacing that name with "", the constraint worked as desired.
New constraint:
for period in periods:
sked_model += sum([x[slot] for slot in slots if period == slot[0:2]]) <= 1, ""

Categories

Resources