An Algorithm For Scheduling Your Time
There is a strong body of research in cognitive science which has large implications for how best to plan your day to get the most out of it. However, it isn't practical for everyone to understand it, keep on up to date on recent findings, weigh up the relative importance of different principles, then apply them to optimally plan their day. What if there was an algorithm which could do it for you?
This is my first attempt at the first peice of the puzze: how to go about algorithmically scheduling a day? The literature on scheduling mainly addresses the following variables: activity duration, activity order, what time to perform an activity relative to your circadium rhymthms. I present an attempt to integrate these types of infomation into a cost function, represent schedules mathematically, then optimise schedules according to the cost function.
(At the moment, the GA uses a contrived cost-function designed to produce a clear optimal solution against which the algorithm's outputs can be compared, no real world data is incorperated yet.)
I'm looking for big picture advice on the approach I've taken, but I'd also be very grateful for information about errors I've made in the execution of my approach. This is my first ever python project, so I'm sure there are plenty of mistakes. With that in mind, it would be extremely useful to me if anyone could identify systematic errors in my thinking.
Dependencies and global variables
from collections import Counter
import statistics as stat
import random as random
import numpy as np
import operator
import pandas as pd
import matplotlib.pyplot as plt
import pdb
numActivities = 8
popSize = 500
eliteSize = 4
mutationRate = 0.08
generations = 200
numDurationGenes = 8 # This should be equal to number of ST genes
Thus far, these have been the values with which have most reliably produced good schedules. To test it using more than eight activities, first delete all line featuring the word "custom" then run as normal. Those modules which aren't called directly are included because they are very useful for debugging or analysing populations of candidate solutions.
Representing Activities, Time, and How They Interact
quantaDuration = 5 # each block of time is equal to 5 mins long
hoursPerDayToSchedule = 12
dailyQuanta = int(60 * 24 * (hoursPerDayToSchedule / 24) / quantaDuration)
The algorithm divides a time period into 5 minute quanta, so named because they are the fundamental unit of time. However, unlike the vast majority of scheduling algorithms I've seen, this algorithm's computerational complexity scales nicely with the number of quanta so one can efficiently achieve a high granularity of time.
activitiesInteractionList = []
for i in range(0, numActivities**2):
activitiesInteractionList.append(random.randint(1, 10))
activitiesInteractionMatrix = np.array(activitiesInteractionList).reshape(
[numActivities, numActivities])
activitiesInteractionMatrix
array([[ 1, 4, 5, 7, 7, 5, 2, 8],
[ 9, 8, 5, 5, 2, 7, 10, 5],
[ 4, 4, 4, 9, 4, 2, 3, 6],
[ 5, 8, 1, 8, 8, 8, 7, 10],
[10, 5, 1, 8, 8, 8, 8, 2],
[ 2, 8, 7, 2, 8, 5, 3, 7],
[ 6, 2, 5, 10, 4, 2, 8, 7],
[ 5, 9, 2, 9, 1, 7, 5, 5]])
This generates a numpy array consisting of random integers such that index x,y represents the cost associated with activity x preceeding activity y. This exists purely as a placeholder which can be replaced with something more complex representing real world data
timesOfDay = ["morning", "mid-day", "afternoon", "night"]
activityTimeInteractionList = []
for i in range(0, numActivities * len(timesOfDay)):
activityTimeInteractionList.append(random.randint(1, 10) / 10)
activityTimeInteractionMatrix = np.array(
activityTimeInteractionList).reshape(numActivities, len(timesOfDay))
activityTimeInteractionMatrix
array([[0.4, 0.3, 0.2, 0.3],
[0.9, 0.1, 0.8, 0.7],
[0.3, 1. , 0.2, 0.1],
[0.1, 0.9, 0.5, 0.6],
[1. , 0.7, 0.7, 0.3],
[0.8, 0.2, 0.7, 0.9],
[0.1, 0.2, 0.7, 0.8],
[0.9, 0.4, 0.7, 0.4]])
For now, four time periods are considered, each of equal aproximately length. An array is created such that index [x,y] corresponds to the cost associated with scheduling a quanta of activity x during time period y
quantaClassificationsPrecursor = []
quantaClassifications = []
for i in range(len(timesOfDay)):
quantaClassificationsPrecursor.append(
(int(dailyQuanta // len(timesOfDay)) * [i])) # this creates a compound list
for i in range(len(timesOfDay)): # this flattens the aforementioned list
quantaClassifications = quantaClassifications + \
quantaClassificationsPrecursor[i]
# fixes imperfect divisions by extending last time period to cover remainder
while len(quantaClassifications) < dailyQuanta:
quantaClassifications = quantaClassifications + [len(timesOfDay) - 1]
# using this algo, last time period can only exceed others by
# len(timesOfDay) -1 quanta
To capture the fact that it is worse to mischedule long tasks, cost is attributed on a per quanta basis. quantaClassifications is a list created such that index [x] will return the period of time (i.e. morning, mid-day, afternoon, night) that the quanta is classified as belonging to.
customActivitiesInteractionMatrix=np.zeros([8,8])
customActivitiesInteractionMatrix[0,]=np.array([0,0,5,10,15,20,25,30])
customActivitiesInteractionMatrix[1,]=np.array([0,0,0,5,10,15,20,25])
customActivitiesInteractionMatrix[2,]=np.array([5,0,0,0,5,10,15,20])
customActivitiesInteractionMatrix[3,]=np.array([10,5,0,0,0,5,10,15])
customActivitiesInteractionMatrix[4,]=np.array([15,10,5,0,0,0,5,10])
customActivitiesInteractionMatrix[5,]=np.array([20,15,10,5,0,0,0,5])
customActivitiesInteractionMatrix[6,]=np.array([25,20,15,10,5,0,0,0])
customActivitiesInteractionMatrix[7,]=np.array([30,25,15,10,5,0,0,0])
activitiesInteractionMatrix=customActivitiesInteractionMatrix
activitiesInteractionMatrix
array([[ 0., 0., 5., 10., 15., 20., 25., 30.],
[ 0., 0., 0., 5., 10., 15., 20., 25.],
[ 5., 0., 0., 0., 5., 10., 15., 20.],
[10., 5., 0., 0., 0., 5., 10., 15.],
[15., 10., 5., 0., 0., 0., 5., 10.],
[20., 15., 10., 5., 0., 0., 0., 5.],
[25., 20., 15., 10., 5., 0., 0., 0.],
[30., 25., 15., 10., 5., 0., 0., 0.]])
This matrix is designed to produce an ideal session order of 0,1,2,3,4,5,6,7. I choose this order as it is easy to verfiy visually when inspecting scheudules. The same is true of the below matrix which covers the interaction between time of day and an activity. It overwrites the randomly generated version for demonstration puposes. To replace with your own custom version, alter the matrix. To use random values, delete this and every line featuring the word "custom".
customActivityTimeInteractionMatrix= np.zeros([8,len(timesOfDay)])
customActivityTimeInteractionMatrix[0,] = np.array([0,3,6,9])
customActivityTimeInteractionMatrix[1,] = np.array([0,3,6,9])
customActivityTimeInteractionMatrix[2,] = np.array([3,0,3,6])
customActivityTimeInteractionMatrix[3,] = np.array([3,0,3,6])
customActivityTimeInteractionMatrix[4,] = np.array([6,3,0,3])
customActivityTimeInteractionMatrix[5,] = np.array([6,3,0,3])
customActivityTimeInteractionMatrix[6,] = np.array([9,6,3,0])
customActivityTimeInteractionMatrix[7,] = np.array([9,6,3,0])
customActivityTimeInteractionMatrix=customActivityTimeInteractionMatrix/10
activityTimeInteractionMatrix= customActivityTimeInteractionMatrix
activityTimeInteractionMatrix
array([[0. , 0.3, 0.6, 0.9],
[0. , 0.3, 0.6, 0.9],
[0.3, 0. , 0.3, 0.6],
[0.3, 0. , 0.3, 0.6],
[0.6, 0.3, 0. , 0.3],
[0.6, 0.3, 0. , 0.3],
[0.9, 0.6, 0.3, 0. ],
[0.9, 0.6, 0.3, 0. ]])
The index a, b gives the cost of scheduling a quanta of activity a during time period b.
Class and Method Overview
class Schedule:
def __init__(self, durationGenes, startTimeGenes):
self.durationGenes = durationGenes
self.startTimeGenes = startTimeGenes
self.list = ScheduleListGenerator(durationGenes=self.durationGenes,
startTimeGenes=self.startTimeGenes)
For the purpose of minimising the number of variables to be optimised, I chose to represent a block of scheduled activity as a start time, a duration, and an index. The index indicates which activity is being scheduled. e.g. activity 1 will begin at Schedule.startTimeGenes[1] and last for a total of Schedule.durationGenes[1] quanta.
def ExtractSessionInfomation(self):
sessionOrder = []
for i in self.list:
if i == "-":
pass
else:
sessionOrder.append(i)
break
sessionChangeQuanta = []
sameCounter = 0
changeCounter = 0
sessionLengthsChronological = []
for i in range(0, len(self.list)):
if self.list[i] == "-":
continue
elif self.list[i - 1] == self.list[i]:
sameCounter = sameCounter + 1
else:
changeCounter = changeCounter + 1
sessionLengthsChronological.append(sameCounter + 1)
sameCounter = 0
sessionOrder.append(self.list[i])
sessionChangeQuanta.append(i)
sessionLengthsChronological.append(sameCounter + 1)
numSessions = changeCounter + 1
sessionLengthsChronological.pop(0)
sessionOrder.pop(0)
self.sessionOrder = sessionOrder
self.sessionLengthsChronological = sessionLengthsChronological
self.sessionChangeQuanta = sessionChangeQuanta
Many crucial methods belonging to this class depend upon knowing the order in which activities are scheduled and how long each activity is scheduled for. This method extracts and stores that infomation as attributes. One session is defined as a succession of quanta dedicated exclusively to one activity. They appear on schedules as many of the same number in an uninterupted chain.
def CalculateOrderCosts(self):
sessionOrderCosts = []
for i in range(0, len(self.sessionOrder) - 1):
sessionOrderCosts.append(
activitiesInteractionMatrix[self.sessionOrder[i], self.sessionOrder[i + 1]])
self.sessionOrderCosts = sessionOrderCosts
self.SessionOrderCostTotal = sum(sessionOrderCosts)
def CaclulateTimeOfDayCosts(self):
timeOfDayCostsByActivity = []
for i in range(0, len(self.list)):
if self.list[i] == "-":
timeOfDayCostsByActivity.append(0)
else:
timeOfDayCostsByActivity.append(
activityTimeInteractionMatrix[self.list[i], quantaClassifications[i]])
self.timeOfDayCostsByActivity = timeOfDayCostsByActivity
self.timeOfDayCostByActivityTotal = sum(timeOfDayCostsByActivity)
These methods exploit the properties of the matrices created above to calculate the cost associated with the timing of activities
def CalculateSessionLengthCosts(self):
sessionLengthsCosts = []
for i in self.sessionLengthsChronological:
if i < 18:
sessionLengthsCosts.append((18 - i) * 5)
else:
sessionLengthsCosts.append(0)
self.sessionLengthCosts = sessionLengthsCosts
self.sessionLengthCostsTotal = sum(sessionLengthsCosts)
For ease of visualising the ideal solution when viewing candidates solutions proposed by the algorithm, the cost function demands that all each of the eight activities take up precisely one eighth of the schedule. This could later be adapted to consider the relative importance of tasks, and to deal with different ideal task lengths.
def CalculateTotalCosts(self):
self.ExtractSessionInfomation()
self.CaclulateTimeOfDayCosts()
self.CalculateOrderCosts()
self.CalculateSessionLengthCosts()
# self.CalculateOpportunityCost()
self.CalculateExclusionCosts()
self.suboptimality = (
self.SessionOrderCostTotal +
self.timeOfDayCostByActivityTotal +
self.sessionLengthCostsTotal +
# self.opportunityCost +
self.exclusionCostsTotal)
def FillerSoThatAboveFunctionIsntAtEndOfClassDefinition(self):
pass # exists to prevent silent misindentation errors
The nature of the rest of the cost calculating methods shown so far causes the algorithm to schedule as few activities per day. CalculateExclusionCosts is designed to counter that.
CalculateTotalCosts acts as a way of calling all cost related methods in a single line of code.
There is an error in the above class definition which causes the last-defined method to break. I wasn't able to find it, so I included a filler method to protect CalculateTotalCosts()
The Core of the Algorithm
def GenerateRandomStartTimeGenes():
randomStartTimeGenes = np.zeros([numActivities], dtype=int)
for i in range(numActivities):
randomStartTimeGenes[i] = round(random.uniform(0, dailyQuanta), 0)
return (randomStartTimeGenes)
def GenerateRandomDurationGenes():
randomDurationGenes = np.zeros([numActivities], dtype=int)
for i in range(numActivities):
randomDurationGenes[i] = round(random.uniform(5, 40), 0)
return randomDurationGenes
These generate random genes. They are called when creating the initial population, and every time a mutation is made.
It is this representation of time which allows for computationally efficient optimisation despite high time granularity, because it maximally reduces the number of variables to be optimised per item to be scheduled.
The arguments inside random.uniform() are chosen to reduce the number of very bad genes within the pool.
def ScheduleListGenerator(durationGenes, startTimeGenes):
schedule = ["-"] * dailyQuanta
for g in range(startTimeGenes.size):
indiciesToChange = range(
startTimeGenes[g],
startTimeGenes[g] +
durationGenes[g])
for i in indiciesToChange:
if i > dailyQuanta -1:
break
else:
schedule[i] = g
return(schedule)
This function generates schedules in the form of lists. It locates the first quanta of a session, then changes it to a number which corresponds to the number representing the activity to be scheduled. It then changes the next element downstream to the same number, and repeats this until the duration of the session matches the value given by it's corresponding duration gene.
def GenerateInitialPopulation(popSize):
initialPopulation = []
for i in range(popSize):
initialPopulation.append(
Schedule(
durationGenes=GenerateRandomDurationGenes(),
startTimeGenes=GenerateRandomStartTimeGenes()))
initialPopulation[i].index = i
return(initialPopulation)
This produces randomised genes, uses those to create a schedule object, and repeats until a population of the desired size is achieved.
def CalculatePopulationSuboptimalities(population):
for i in population:
i.CalculateTotalCosts()
def SortPopulation(pop):
pop.sort(key=lambda x: x.suboptimality,
reverse=False) # sort population by suboptimality
return(pop)
Once an initial population is generated, the list is sorted by it's suboptimality (the lesser the index, the lesser the suboptimality). This comes in handy for the tournament selection function shown below and makes it simple to extract the best schedule within each generation for comparison purposes (using population[0].list).
def Selection(sortedPopulation): # uses tournament selection
selectionResults = []
for i in range(eliteSize):
selectionResults.append(sortedPopulation[i])
for i in range(eliteSize, popSize):
# returns a list with 2 random integers
a = random.sample(range(popSize), 2)
if a[0] < a[1]:
selectionResults.append(sortedPopulation[a[0]])
else:
selectionResults.append(sortedPopulation[a[1]])
return(selectionResults)
As the population is already sorted by suboptimality, it's possible to implement tournament selection (randomly paired schedules "fight", only the winner, i.e. the one with the lowest suboptimalitiy, passes on their genes) by simply comparing the indices of combatants.
def Breed(selectedPopulation):
matingPool = selectedPopulation[eliteSize:popSize]
children = []
breedingPartnersInOrder = random.sample(matingPool, len(matingPool))
for i in range(
len(matingPool)): # for every in the mating pool, choose another at randomu
parent1DurationGenes = matingPool[i].durationGenes
parent1StartTimeGenes = matingPool[i].startTimeGenes
parent2DurationGenes = breedingPartnersInOrder[i].durationGenes
parent2StartTimeGenes = breedingPartnersInOrder[i].startTimeGenes
childDurationGenes = np.zeros(numDurationGenes, dtype=int)
childStartTimeGenes = np.zeros(numDurationGenes, dtype=int)
for d in range(
numDurationGenes): # for every gene pair, decide at random which parents genes
p = random.sample(range(1, 3), 1) # to use in the next generation
if p == 1:
childDurationGenes[d] = parent1DurationGenes[d]
childStartTimeGenes[d] = parent1StartTimeGenes[d]
else:
childDurationGenes[d] = parent2DurationGenes[d]
childStartTimeGenes[d] = parent2StartTimeGenes[d]
# create a new entity per set of child genes
children.append(Schedule(childDurationGenes, childStartTimeGenes))
return (children)
The breeding step, sometimes referred to as "crossing over", is the use of the selected parents genes to create the next generation. Every schedule within the mating pool produces one offspring by "mating" with another member of the mating pool at random. I use a random "sample" of size equal to that of the mating pool, as this generates a new list in random order representing the same population. This allows for mating to be performed randomly, which I believe is better than mating the best with the best (i.e. selective breeding) as it leads more diverse resultant populations which are better able to escape local minima.
def Mutate(population, mutationRate=mutationRate):
numberOfGenesToMutatePerType = int(
round(mutationRate * numDurationGenes * popSize))
for d in range(numberOfGenesToMutatePerType):
indexOfAnimalToMutate = random.sample(range(eliteSize, len(population)), 1)
geneMutationIndex = random.sample(range(numDurationGenes), 1)
population[indexOfAnimalToMutate[0]].durationGenes[geneMutationIndex[0]] = GenerateRandomDurationGenes()[
geneMutationIndex[0]]
for ST in range(numberOfGenesToMutatePerType):
indexOfAnimalToMutate = random.sample(
range(eliteSize, len(population)), 1)
geneMutationIndex = random.sample(range(numDurationGenes), 1)
population[indexOfAnimalToMutate[0]].startTimeGenes[geneMutationIndex[0]] = GenerateRandomStartTimeGenes()[
geneMutationIndex[0]]
return(population)
The mutation step exists to increase the diversity of the resultant generation. I chose to equally distribute mutations between start-time and duration genes because I couldn't see any reason not to. In hindsight, if the startTime or duration genes are already very close to perfect, but the other type were weak, it would be more useful to have an uneven distribution of mutations between gene types as that way some of the mutants would only see changes to the weak genes (leaving the near perfect genes intact). This will be verified then fixed in my next draft.
def IndexAdder(population):
for i in range(len(population)):
population[i].index = i
return (population)
The index of populations are absent in children. This exists to add them. While these index attributes aren't required for the algorithm to function, it is useful to have if you want to see the before and after of a schedule while testing a function which involves rearranging the population as it allows you to query a resultant schedule for the index of its precursor.
def GeneticAlgorithm(
initialPopulation = initialPopulation,
popSize = popSize,
eliteSize = eliteSize,
mutationRate = mutationRate,
generations = generations):
global progress
population = initialPopulation
for i in range(generations):
for p in population:
p.CalculateTotalCosts()
population = SortPopulation(population)
progress.append(population[0].suboptimality)
selectedPopulation = Selection(population)
newGenerationPreMutation = Breed(
selectedPopulation) + selectedPopulation[0:eliteSize]
population = Mutate(newGenerationPreMutation)
return(population)
This calls every step in order and iterates for the desired number of generations. With every generation it appends a list with the suboptimality of the best schedule within a population, and this list can be used to visualise how schedule quality improved with each generation.
initialPopulation = GenerateInitialPopulation(popSize)
finalGeneration = GeneticAlgorithm(initialPopulation)
for i in finalGeneration:
i.CalculateTotalCosts()
plt.plot(progress)
plt.show()
progress = []
sortedFinalGeneration=SortPopulation(finalGeneration)
sortedFinalGeneration[0].list
This runs the genetic algorithm, and opens a window featuring a graph of schedule quality vs time, then shows you the best solution found.
from collections import Counter
import statistics as stat
import random as random
import numpy as np
import operator
import pandas as pd
import matplotlib.pyplot as plt
import pdb
numActivities = 8
popSize = 100
eliteSize = 4
mutationRate = 0.04
generations = 100
numDurationGenes = 8 # This should be equal to number of ST genes
quantaDuration = 5 # each block of time is equal to 5 mins long
hoursPerDayToSchedule = 12
dailyQuanta = int(60 * 24 * (hoursPerDayToSchedule / 24) / quantaDuration)
activitiesInteractionList = []
for i in range(0, numActivities**2):
activitiesInteractionList.append(random.randint(1, 10))
activitiesInteractionMatrix = np.array(activitiesInteractionList).reshape(
[numActivities, numActivities])
timesOfDay = ["morning", "mid-day", "afternoon", "night"]
activityTimeInteractionList = []
for i in range(0, numActivities * len(timesOfDay)):
activityTimeInteractionList.append(random.randint(1, 10) / 10)
activityTimeInteractionMatrix = np.array(
activityTimeInteractionList).reshape(numActivities, len(timesOfDay))
quantaClassificationsPrecursor = []
quantaClassifications = []
for i in range(len(timesOfDay)):
quantaClassificationsPrecursor.append(
(int(dailyQuanta // len(timesOfDay)) * [i])) # this creates a compound list
for i in range(len(timesOfDay)): # this flattens the aforementioned list
quantaClassifications = quantaClassifications + \
quantaClassificationsPrecursor[i]
# fixes imperfect divisions by extending last time period to cover remainder
while len(quantaClassifications) < dailyQuanta:
quantaClassifications = quantaClassifications + [len(timesOfDay) - 1]
# using this algo, last time period can only exceed others by
# len(timesOfDay) -1 quanta
customActivitiesInteractionMatrix = np.zeros([8, 8])
customActivitiesInteractionMatrix[0, ] = np.array(
[0, 0, 5, 10, 15, 20, 25, 30])
customActivitiesInteractionMatrix[1, ] = np.array([0, 0, 0, 5, 10, 15, 20, 25])
customActivitiesInteractionMatrix[2, ] = np.array([5, 0, 0, 0, 5, 10, 15, 20])
customActivitiesInteractionMatrix[3, ] = np.array([10, 5, 0, 0, 0, 5, 10, 15])
customActivitiesInteractionMatrix[4, ] = np.array([15, 10, 5, 0, 0, 0, 5, 10])
customActivitiesInteractionMatrix[5, ] = np.array([20, 15, 10, 5, 0, 0, 0, 5])
customActivitiesInteractionMatrix[6, ] = np.array([25, 20, 15, 10, 5, 0, 0, 0])
customActivitiesInteractionMatrix[7, ] = np.array([30, 25, 15, 10, 5, 0, 0, 0])
activitiesInteractionMatrix = customActivitiesInteractionMatrix
customActivityTimeInteractionMatrix = np.zeros([8, len(timesOfDay)])
customActivityTimeInteractionMatrix[0, ] = np.array([0, 3, 6, 9])
customActivityTimeInteractionMatrix[1, ] = np.array([0, 3, 6, 9])
customActivityTimeInteractionMatrix[2, ] = np.array([3, 0, 3, 6])
customActivityTimeInteractionMatrix[3, ] = np.array([3, 0, 3, 6])
customActivityTimeInteractionMatrix[4, ] = np.array([6, 3, 0, 3])
customActivityTimeInteractionMatrix[5, ] = np.array([6, 3, 0, 3])
customActivityTimeInteractionMatrix[6, ] = np.array([9, 6, 3, 0])
customActivityTimeInteractionMatrix[7, ] = np.array([9, 6, 3, 0])
customActivityTimeInteractionMatrix = customActivityTimeInteractionMatrix / 10
activityTimeInteractionMatrix = customActivityTimeInteractionMatrix
class Schedule:
def __init__(self, durationGenes, startTimeGenes):
self.durationGenes = durationGenes
self.startTimeGenes = startTimeGenes
self.list = ScheduleListGenerator(durationGenes=self.durationGenes,
startTimeGenes=self.startTimeGenes)
def ExtractSessionInfomation(self):
sessionOrder = []
for i in self.list:
if i == "-":
pass
else:
sessionOrder.append(i)
break
sessionChangeQuanta = []
sameCounter = 0
changeCounter = 0
sessionLengthsChronological = []
for i in range(0, len(self.list)):
if self.list[i] == "-":
continue
elif self.list[i - 1] == self.list[i]:
sameCounter = sameCounter + 1
else:
changeCounter = changeCounter + 1
sessionLengthsChronological.append(sameCounter + 1)
sameCounter = 0
sessionOrder.append(self.list[i])
sessionChangeQuanta.append(i)
sessionLengthsChronological.append(sameCounter + 1)
numSessions = changeCounter + 1
sessionLengthsChronological.pop(0)
sessionOrder.pop(0)
self.sessionOrder = sessionOrder
self.sessionLengthsChronological = sessionLengthsChronological
self.sessionChangeQuanta = sessionChangeQuanta
def CalculateOrderCosts(self):
sessionOrderCosts = []
for i in range(0, len(self.sessionOrder) - 1):
sessionOrderCosts.append(
activitiesInteractionMatrix[self.sessionOrder[i],
self.sessionOrder[i + 1]])
self.sessionOrderCosts = sessionOrderCosts
self.SessionOrderCostTotal = sum(sessionOrderCosts)
def CaclulateTimeOfDayCosts(self):
timeOfDayCostsByActivity = []
for i in range(0, len(self.list)):
if self.list[i] == "-":
timeOfDayCostsByActivity.append(0)
else:
timeOfDayCostsByActivity.append(
activityTimeInteractionMatrix[self.list[i], quantaClassifications[i]])
self.timeOfDayCostsByActivity = timeOfDayCostsByActivity
self.timeOfDayCostByActivityTotal = sum(timeOfDayCostsByActivity)
def CalculateSessionLengthCosts(self):
sessionLengthsCosts = []
for i in self.sessionLengthsChronological:
if i < 18:
sessionLengthsCosts.append((18 - i) * 5)
else:
sessionLengthsCosts.append(0)
self.sessionLengthCosts = sessionLengthsCosts
self.sessionLengthCostsTotal = sum(sessionLengthsCosts)
# def CalculateOpportunityCost(self):
# self.opportunityCost = Counter(self.list)["-"] * 10
def CalculateExclusionCosts(self):
exclusionCosts = []
for i in range(numActivities):
if Counter(self.list)[i] < 1:
exclusionCosts.append(100)
else:
continue
self.exclusionCosts = exclusionCosts
self.exclusionCostsTotal = sum(exclusionCosts)
def CalculateTotalCosts(self):
self.ExtractSessionInfomation()
self.CaclulateTimeOfDayCosts()
self.CalculateOrderCosts()
self.CalculateSessionLengthCosts()
# self.CalculateOpportunityCost()
self.CalculateExclusionCosts()
self.suboptimality = (
self.SessionOrderCostTotal +
self.timeOfDayCostByActivityTotal +
self.sessionLengthCostsTotal +
# self.opportunityCost +
self.exclusionCostsTotal)
def FillerSoThatAboveFunctionIsntAtEndOfClassDefinition(self):
pass # exists to prevent silent misindentation errors
def GenerateRandomStartTimeGenes():
randomStartTimeGenes = np.zeros([numActivities], dtype=int)
for i in range(numActivities):
randomStartTimeGenes[i] = round(random.uniform(0, dailyQuanta - 4), 0)
return (randomStartTimeGenes)
def GenerateRandomDurationGenes():
randomDurationGenes = np.zeros([numActivities], dtype=int)
for i in range(numActivities):
randomDurationGenes[i] = round(random.uniform(4, 40), 0)
return randomDurationGenes
def ScheduleListGenerator(durationGenes, startTimeGenes):
schedule = ["-"] * dailyQuanta
for g in range(startTimeGenes.size):
indiciesToChange = range(
startTimeGenes[g],
startTimeGenes[g] +
durationGenes[g])
for i in indiciesToChange:
if i > dailyQuanta - 1:
break
else:
schedule[i] = g
return(schedule)
def GenerateInitialPopulation(popSize):
initialPopulation = []
for i in range(popSize):
initialPopulation.append(
Schedule(
durationGenes=GenerateRandomDurationGenes(),
startTimeGenes=GenerateRandomStartTimeGenes()))
# initialPopulation[i].index = i
return(initialPopulation)
def CalculatePopulationSuboptimalities(population):
for i in population:
i.CalculateTotalCosts()
def SortPopulation(pop):
pop.sort(key=lambda x: x.suboptimality,
reverse=False) # sort population by suboptimality
return(pop)
def Selection(sortedPopulation): # uses tournament selection
selectionResults = []
for i in range(eliteSize):
selectionResults.append(sortedPopulation[i])
for i in range(eliteSize, popSize):
# returns a list with 2 random integers
a = random.sample(range(popSize), 2)
if a[0] < a[1]:
selectionResults.append(sortedPopulation[a[0]])
else:
selectionResults.append(sortedPopulation[a[1]])
return(selectionResults)
def Breed(selectedPopulation):
matingPool = selectedPopulation[eliteSize:popSize]
children = []
breedingPartnersInOrder = random.sample(matingPool, len(matingPool))
for i in range(
len(matingPool)): # for every in the mating pool, choose another at randomu
parent1DurationGenes = matingPool[i].durationGenes
parent1StartTimeGenes = matingPool[i].startTimeGenes
parent2DurationGenes = breedingPartnersInOrder[i].durationGenes
parent2StartTimeGenes = breedingPartnersInOrder[i].startTimeGenes
childDurationGenes = np.zeros(numDurationGenes, dtype=int)
childStartTimeGenes = np.zeros(numDurationGenes, dtype=int)
for d in range(
numDurationGenes): # for every gene pair, decide at random which parents genes
p = random.sample(range(1, 3), 1) # to use in the next generation
if p == 1:
childDurationGenes[d] = parent1DurationGenes[d]
childStartTimeGenes[d] = parent1StartTimeGenes[d]
else:
childDurationGenes[d] = parent2DurationGenes[d]
childStartTimeGenes[d] = parent2StartTimeGenes[d]
# create a new entity per set of child genes
children.append(Schedule(childDurationGenes, childStartTimeGenes))
return (children)
def Mutate(population, mutationRate=mutationRate):
numberOfGenesToMutatePerType = int(
round(mutationRate * numDurationGenes * popSize))
for d in range(numberOfGenesToMutatePerType):
indexOfAnimalToMutate = random.sample(
range(eliteSize, len(population)), 1)
geneMutationIndex = random.sample(range(numDurationGenes), 1)
population[indexOfAnimalToMutate[0]].durationGenes[geneMutationIndex[0]
] = GenerateRandomDurationGenes()[geneMutationIndex[0]]
for ST in range(numberOfGenesToMutatePerType):
indexOfAnimalToMutate = random.sample(
range(eliteSize, len(population)), 1)
geneMutationIndex = random.sample(range(numDurationGenes), 1)
population[indexOfAnimalToMutate[0]].startTimeGenes[geneMutationIndex[0]
] = GenerateRandomStartTimeGenes()[geneMutationIndex[0]]
return(population)
#def IndexAdder(population):
# for i in range(len(population)):
# population[i].index = i
# return (population)
initialPopulation = GenerateInitialPopulation(popSize)
progress = []
def GeneticAlgorithm(
initialPopulation = initialPopulation,
popSize = popSize,
eliteSize = eliteSize,
mutationRate = mutationRate,
generations = generations):
global progress
population = initialPopulation
for i in range(generations):
for p in population:
p.CalculateTotalCosts()
population = SortPopulation(population)
progress.append(population[0].suboptimality)
selectedPopulation = Selection(population)
newGenerationPreMutation = Breed(
selectedPopulation) + selectedPopulation[0:eliteSize]
population = Mutate(newGenerationPreMutation)
return(population)
initialPopulation = GenerateInitialPopulation(popSize)
finalGeneration = GeneticAlgorithm(initialPopulation)
for i in finalGeneration:
i.CalculateTotalCosts()
plt.plot(progress)
plt.show()
progress = []
sortedFinalGeneration=SortPopulation(finalGeneration)
sortedFinalGeneration[0].list
There is an error in the above class definition which causes the last-defined method to break
, if you expect community input to help fix this, CR isn't the site you need; you'd be better off posting on StackOverflow. If you can live with that bug for now, and you need a review of your code, you should instead describe what it is you're looking for in a code review - are your concerns mainly about performance, or code style, etc.? \$\endgroup\$