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)
Related
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.
Given below is the stock data set which shows stock and its predicted price over the next 7 working days/sessions an end-user I want to know what should be my buy price and what should be my sell price for each share so that I can earn maximum profit.
E.g. For RIL, if I buy it in the first session and sell it in the 4th session I will get 200 profit.
Also, what would you do if the maximum price is on the 1st day itself?
Test Case:
Input:
StockId,PredictedPricr
RIL,[1000,1005,1090,1200,1000,900,890]
HDFC,[890,940,810,730,735,960,980]
INFY,[1001,902,1000,990,1230,1100,1200]
Output:
StockId|BuyPrice|SellPrice|Profit
RIL|1000|1200|200
HDFC|730|980|250
INFY|902|1230|328
You can use any language of your choice, I preferably would use python dictionaries for this.
So, I remembered that there is a leetcode question that asks almost exactly your problem. For different solutions and explanations, you can look at the discussion section of the question. The difference is that there you only have to solve the problem for one stock. Here is the solution I used back when I solved it:
def max_profit(prices):
p1 = 0
max_profit = -1
current_buy = -1
current_sell = -1
for p2 in range(1, len(prices)):
if prices[p2] < prices[p1]:
p1=p2
elif prices[p2] - prices[p1] > max_profit:
current_buy = prices[p1]
current_sell = prices[p2]
max_profit = current_sell - current_buy
return (current_buy, current_sell, max_profit)
The input is simply the array of prices that you have for one of the stocks. What you have to do is loop over your stocks and call the method for each of them.
Now, how does the code work? We iterate over the array and try to find the sequence where we have the highest profit. We simply have a start index (p1), where our sequence begins and increase the end pointer (p2). The endpointer is increased to find the highest profit with our start index. If we find a lower price than our start index (via the end pointer) we move the start index to this lower price and simply continue to increase the end pointer. So, the start pointer always points to the lowest prices that we currently know.
Edit: How to apply it on all stocks? As I said we simply have to iterate over the stocks and use the function on each one. How it is handled exactly depends on your input/output format. I assumed you use a dictionary:
input_values = {
'RIL':[1000,1005,1090,1200,1000,900,890],
'HDFC':[890,940,810,730,735,960,980],
'INFY':[1001,902,1000,990,1230,1100,1200],
}
result = []
for k, v in input_values.items():
buy, sell, profit = max_profit(v)
result.append([k, buy, sell, profit])
print(result)
Output:
[
['RIL', 1000, 1200, 200],
['HDFC', 730, 980, 250],
['INFY', 902, 1230, 328]
]
For example lets say I have something like this:
solver().Add(this.solver.ActiveVar(start) == this.solver.ActiveVar(end));
for a specific route. this means that start index must end on end index.
What if I want to limit the number of visits that can happen in between this?
Example if the limit is 2 then only solutions that have something like so will be valid
start-> n1 -> n2 -> end
start -> n1 -> end
start -> end
Normally I would try something involving vehicle constraints, but in this case one vehicle can have multiple starts and ends
Few things:
1.
solver().Add(this.solver.ActiveVar(start) == this.solver.ActiveVar(end));
just mean that both locations must be active (i.e. visited) or unvisited (aka 0) (i.e. are part of a disjunction).
What about creating a counter dimension then restrict the difference between both nodes ?
In Python should be more or less:
routing.AddConstantDimension(
1, # increase by one at each visit
42, # max count
True, # Force Start at zero
'Counter')
counter_dim = routing.GetDimensionOrDie('Counter')
start = manager.NodeToIndex(start_node)
end = manager.NodeToIndex(end_node)
solver = routing.solver()
# start must be visited at most two nodes before end node
solver.Add(count_dim.CumulVar(start) + 3 >= count_dim.CumulVar(end))
# start must be visited before end
solver.Add(count_dim.CumulVar(start) <= count_dim.CumulVar(end))
Don't get your "vehicle multiple start", each vehicle has only one start. node....
I have a list of 12 cities connected to each other without exception. The only thing of concern is travel time. The name of each city is here. The distance matrix (representing travel time in minutes) between city pairs is here.
How can I find out how many cities I can visited given a certain travel budget (say 800 minutes) from a city of origin (it can be any of the 12).
You can't visit the same city twice during the trip and you don't need to worry about returning to your origin. I can't go above my travel budget.
import numpy as np
from scipy.spatial import distance_matrix
from sklearn.cluster import AgglomerativeClustering
def find_cities(dist, budget): # dist: a 12x12 matrix of travel time in minutes between city pairs; budget: max travel time allowed for the trip (in mins)
assert len(dist) == 12 # there are 12 cities to visit and each one has a pairwise cost with all other 11 citis
clusters = [] # list of cluster labels from 1..n where n is no of cities to be visited
dists = [0] + [row[1:] for row in dist] # start-to-start costs have been excluded from the distances array which only contains intercity distances
linkage = 'complete' # complete linkage used here because we want an optimal solution i.e., finding minimum number of clusters required
ac = AgglomerativeClustering(affinity='precomputed', linkage=linkage, compute_full_tree=True) # affinity must be precomputed or function otherwise it will use euclidean distance by default !!!
# compute full tree ensures that I get all possible clustesr even if they don't make up entire population! This is needed so that I can determine how many clusters need to be created given my budget constraints below
Z = ac.fit_predict(dists).tolist() # AgglomerativeClustering.fit_predict returns list of cluster labels for each city
while budget >= min(dists): # while my budget is greater than the minimum intercity travel cost, i.e., I can still visit another city
if len(set(Z)) > 1: # at least 2 clusters are needed to form a valid tour so continue only when there are more than one cluster left in Z
c1 = np.argmax([sum([i==j for j in Z]) for i in set(Z)]) # find which clustes has max no of cities associated with it and that will be the next destination (cities within this cluster have same label as their parent cluster!) # numpy argmax returns index of maximum value along an axis; here we want to know which group has most elements!
c2 = [j for j,val in enumerate(Z) if val == Z[c1]][0] # retrieve first element from the group whose parent is 'cluster' returned by previous line
clusters += [c2 + 1] ## add new destination found into our trip plan/list "clusters" after converting its city id back into integer starting from 1 instead of 0 like array indices do!!
dists += [dist[c1][c2]] ## update total distance travelled so far based on newly added destination ... note: distances between two adjacent cities always equals zero because they fall under same cluster
budget -= dists[-1] ## update travel budget by subtracting the cost of newly added destination from our total budget
else: break # when there is only one city left in Z, then stop! it's either a single city or two cities are connected which means cost between them will always be zero!
return clusters # this is a list of integers where each integer represents the id of city that needs to be visited next!
def main():
with open('uk12_dist.txt','r') as f: ## read travel time matrix between cities from file ## note: 'with' keyword ensures file will be closed automatically after reading or writing operation done within its block!!!
dist = [[int(num) for num in line.split()] for line in f] ## convert text data into array/list of lists using list comprehension; also ensure all data are converted into int before use!
with open('uk12_name.txt','r') as f: ## read names of 12 cities from file ## note: 'with' keyword ensures file will be closed automatically after reading or writing operation done within its block!!!
name = [line[:-1].lower().replace(" ","") for line in f] ## remove newline character and any leading/trailing spaces, then convert all characters to lowercase; also make sure there's no space between first and last name (which results in empty string!) otherwise won't match later when searching distances!!
budget = 800 # max travel budget allowed (in mins) i.e., 8 hrs travelling at 60 mins per km which means I can cover about 800 kms on a full tank!
print(find_cities(dist,budget), "\n") ## print(out list of city ids to visit next!
print("Total distance travelled: ", sum(dist[i][j] for i, j in enumerate([0]+find_cities(dist,budget))), "\n" ) # calculate total cost/distance travelled so far by adding up all distances between cities visited so far - note index '0' has been added at start because 0-2 is same as 2-0 and it's not included in find_cities() output above !
while True:
try: ## this ensures that correct input from user will be obtained only when required!!
budget = int(raw_input("\nEnter your travel budget (in minutes): ")) # get new travel budget from user and convert into integer before use!!!
if budget <= 800: break ## stop asking for valid input only when the value entered by user isn't greater than 800 mins or 8 hrs !!
except ValueError: ## catch exception raised due to invalid data type; continue asking until a valid number is given by user!!
pass
print(name[find_cities(dist,budget)[1]],"->",name[find_cities(dist,budget)[2]],"-> ...",name[find_cities(dist,budget)[-1]] )## print out the city names of cities to visit next!
return None
if __name__ == '__main__': main()
I am trying to limit the minimum locations visit per vehicle, I have implemented the maximum location constraint successfully but having issues in figuring out minimum locations. My code for maximum location:
def counter_callback(from_index):
"""Returns 1 for any locations except depot."""
# Convert from routing variable Index to user NodeIndex.
from_node = manager.IndexToNode(from_index)
return 1 if (from_node != 0) else 0;
counter_callback_index = routing.RegisterUnaryTransitCallback(counter_callback)
routing.AddDimensionWithVehicleCapacity(
counter_callback_index,
0, # null slack
[16,16,16], # maximum locations per vehicle
True, # start cumul to zero
'Counter')
You should not put a hard limit on the number of nodes as it easily makes the model unfeasible.
The recommended way is to create a new dimension which just counts the number of visits (the evaluator always returns 1), then push a soft lower bound on the cumulvar of this dimension at the end of each vehicle.