diff --git a/README b/README index d865bff..bc30a52 100644 --- a/README +++ b/README @@ -159,6 +159,11 @@ agentDepressionPercentage: float Note: The starting agent population will have this same percentage of depressed agents. Default: 0.0 +agentDiseaseProtectionChance: [float, float] + Sets the chance an agent can get infected by another agent. + Note: a value of 0.0 mean no protection, while a value of 1.0 means complete protection + Default: [0.0, 1.0] + agentFemaleInfertilityAge: [int, int] Set the timestep age at which female agents become infertile. Default: [0, 0] @@ -301,6 +306,10 @@ diseaseFertilityPenalty: [float, float] Note: Negative values constitute a fertility decrease. Default: [0, 0] +diseaseIncubationPeriod: [int, int] + Set the number of timesteps a disease can remain hidden while still infecting other agents. + Default: [0, 0] + diseaseMovementPenalty: [int, int] Set the impact a disease will have on an agent's movement distance. Note: Negative values constitute a decrease in movement range. @@ -311,6 +320,10 @@ diseaseSpiceMetabolismPenalty: [float, float] Note: Negative values constitute a decrease in agent spice metabolism. Default: [0, 0] +diseaseStartTimeframe: [int, int] + Set the timestep a disease can be initialized. + Default: [0, 0] + diseaseSugarMetabolismPenalty: [float, float] Set the impact a disease will have on an agent's sugar metabolism rate. Note: Negative values constitute a decrease in agent sugar metabolism. @@ -321,6 +334,10 @@ diseaseTagStringLength: [int, int] The longer the length, the longer an agent will have the disease. Default: [0, 0] +diseaseTransmissionChance: [float, float] + Set the chance a disease can transmit between agents. + Default: [0.0, 1.0] + diseaseVisionPenalty: [int, int] Set the impact a disease will have on an agent's vision. Note: Negative values constitute a decrease in agent vision. diff --git a/agent.py b/agent.py index fdd69bf..6b6d8ad 100644 --- a/agent.py +++ b/agent.py @@ -18,6 +18,7 @@ def __init__(self, agentID, birthday, cell, configuration): self.decisionModelLookaheadFactor = configuration["decisionModelLookaheadFactor"] self.decisionModelTribalFactor = configuration["decisionModelTribalFactor"] self.depressionFactor = configuration["depressionFactor"] + self.diseaseProtectionChance = configuration["diseaseProtectionChance"] self.fertilityAge = configuration["fertilityAge"] self.fertilityFactor = configuration["fertilityFactor"] self.immuneSystem = configuration["immuneSystem"] @@ -58,7 +59,8 @@ def __init__(self, agentID, birthday, cell, configuration): self.childEndowmentHashes = None self.conflictHappiness = 0 self.depressed = False - self.diseases = [] + self.diseaseDeath = False + self.depressed = False self.familyHappiness = 0 self.fertile = False self.fertilityFactorModifier = 0 @@ -81,6 +83,7 @@ def __init__(self, agentID, birthday, cell, configuration): self.spiceMeanIncome = 1 self.spiceMetabolismModifier = 0 self.spicePrice = 0 + self.startingDiseases = 0 self.sugarMeanIncome = 1 self.sugarMetabolismModifier = 0 self.sugarPrice = 0 @@ -91,6 +94,12 @@ def __init__(self, agentID, birthday, cell, configuration): self.visionModifier = 0 self.wealthHappiness = 0 + self.immuneDiseases = [] # disease only + self.susceptibleDiseases = [] # disease only + self.incubatingDiseases = [] # disease record + self.symptomaticDiseases = [] # disease record + self.recoveredDiseases = [] # different disease record + # Change metrics for depressed agents if self.depressionFactor == 1: self.depressed = True @@ -106,6 +115,22 @@ def __init__(self, agentID, birthday, cell, configuration): # Depressed agents have a smaller friend network due to social withdrawal self.maxFriends = math.ceil(self.maxFriends - (self.maxFriends * 0.3667)) + def addLoanFromAgent(self, agent, timestep, sugarLoan, spiceLoan, duration): + agentID = agent.ID + if agentID not in self.socialNetwork: + self.addAgentToSocialNetwork(agent) + self.socialNetwork[agentID]["timesLoaned"] += 1 + loan = {"creditor": agentID, "debtor": self.ID, "sugarLoan": sugarLoan, "spiceLoan": spiceLoan, "loanDuration": duration, + "loanOrigin": timestep} + self.socialNetwork["creditors"].append(loan) + + def addAgentToSocialNetwork(self, agent): + agentID = agent.ID + if agentID in self.socialNetwork: + return + self.socialNetwork[agentID] = {"agent": agent, "lastSeen": self.lastMoved, "timesVisited": 1, "timesReproduced": 0, + "timesTraded": 0, "timesLoaned": 0, "marginalRateOfSubstitution": 0} + def addChildToCell(self, mate, cell, childConfiguration): sugarscape = self.cell.environment.sugarscape childID = sugarscape.generateAgentID() @@ -126,22 +151,6 @@ def addChildToCell(self, mate, cell, childConfiguration): child.setMother(mate) return child - def addAgentToSocialNetwork(self, agent): - agentID = agent.ID - if agentID in self.socialNetwork: - return - self.socialNetwork[agentID] = {"agent": agent, "lastSeen": self.lastMoved, "timesVisited": 1, "timesReproduced": 0, - "timesTraded": 0, "timesLoaned": 0, "marginalRateOfSubstitution": 0} - - def addLoanFromAgent(self, agent, timestep, sugarLoan, spiceLoan, duration): - agentID = agent.ID - if agentID not in self.socialNetwork: - self.addAgentToSocialNetwork(agent) - self.socialNetwork[agentID]["timesLoaned"] += 1 - loan = {"creditor": agentID, "debtor": self.ID, "sugarLoan": sugarLoan, "spiceLoan": spiceLoan, "loanDuration": duration, - "loanOrigin": timestep} - self.socialNetwork["creditors"].append(loan) - def addLoanToAgent(self, agent, timestep, sugarPrincipal, sugarLoan, spicePrincipal, spiceLoan, duration): agentID = agent.ID if agentID not in self.socialNetwork: @@ -156,6 +165,33 @@ def addLoanToAgent(self, agent, timestep, sugarPrincipal, sugarLoan, spicePrinci agent.sugar = agent.sugar + sugarPrincipal agent.spice = agent.spice + spicePrincipal + def canCatchDisease(self, disease, infector=None): + if self.diseaseProtectionChance == 1: + return False + if self.checkDiseaseImmunity(disease) == True: + if disease in self.susceptibleDiseases: + # susceptible to immune + self.updateDiseaseGroups(disease, disease, self.susceptibleDiseases, self.immuneDiseases) + return False + else: + # check if disease is in immune[] - this is to cover the case that the neighbor hasn't had their turn to doTimestep() + if disease in self.immuneDiseases: + # immune to susceptible + self.updateDiseaseGroups(disease, disease, self.immuneDiseases, self.susceptibleDiseases) + diseaseID = disease.ID + combinedDiseases = self.incubatingDiseases + self.symptomaticDiseases + for currDisease in combinedDiseases: + currDiseaseID = currDisease["disease"].ID + if diseaseID == currDiseaseID: + return False + if self.diseaseProtectionChance == 0 or infector == None: + return True + randomTransmission = random.random() + randomProtection = random.random() + if randomTransmission > disease.transmissionChance and randomProtection <= self.diseaseProtectionChance: + return False + return True + def canReachCell(self, cell): if cell == self.cell or cell in self.cellsInRange: return True @@ -171,25 +207,29 @@ def canTradeWithNeighbor(self, neighbor): return False def catchDisease(self, disease, infector=None): - diseaseID = disease.ID - for currDisease in self.diseases: - currDiseaseID = currDisease["disease"].ID - # If currently sick with this disease, do not contract it again - if diseaseID == currDiseaseID: - return - diseaseInImmuneSystem = self.findNearestHammingDistanceInDisease(disease) - hammingDistance = diseaseInImmuneSystem["distance"] - # If immune to disease, do not contract it + if self.canCatchDisease(disease, infector) == True: + diseaseInImmuneSystem = self.findNearestHammingDistanceInDisease(disease) + startIndex = diseaseInImmuneSystem["start"] + endIndex = diseaseInImmuneSystem["end"] + caughtDisease = {"disease": disease, "startIndex": startIndex, "endIndex": endIndex, "startIncubation": self.timestep, "endIncubation": self.timestep + disease.incubationPeriod} + if infector != None: + caughtDisease["infector"] = infector + disease.infectors.add(infector.ID) + else: + self.startingDiseases += 1 + disease.startingInfectedAgents += 1 + disease.newInfections += 1 + # susceptible to infected + self.updateDiseaseGroups(disease, caughtDisease, self.susceptibleDiseases, self.incubatingDiseases) + self.showSymptoms() + self.findCellsInRange() + + def checkDiseaseImmunity(self, disease): + hammingDistance = self.findNearestHammingDistanceInDisease(disease)["distance"] if hammingDistance == 0: - return - startIndex = diseaseInImmuneSystem["start"] - endIndex = diseaseInImmuneSystem["end"] - caughtDisease = {"disease": disease, "startIndex": startIndex, "endIndex": endIndex} - if infector != None: - caughtDisease["infector"] = infector - self.diseases.append(caughtDisease) - self.updateDiseaseEffects(disease) - self.findCellsInRange() + return True + else: + return False def collectResourcesAtCell(self): sugarCollected = self.cell.sugar @@ -235,6 +275,8 @@ def doCombat(self, cell): def doDeath(self, causeOfDeath): self.alive = False self.causeOfDeath = causeOfDeath + if self.isSick(): + self.diseaseDeath = True self.resetCell() self.doInheritance() @@ -242,11 +284,20 @@ def doDeath(self, causeOfDeath): self.socialNetwork = {"debtors": self.socialNetwork["debtors"], "children": self.socialNetwork["children"]} self.neighbors = [] self.neighborhood = [] - self.diseases = [] + self.symptomaticDiseases = [] def doDisease(self): - random.shuffle(self.diseases) - for diseaseRecord in self.diseases: + for immuneDisease in self.immuneDiseases: + if self.checkDiseaseImmunity(immuneDisease) == False: + # immune to susceptible + self.updateDiseaseGroups(immuneDisease, immuneDisease, self.immuneDiseases, self.susceptibleDiseases) + self.showSymptoms() + if self.age >= self.infertilityAge: + self.diseaseProtectionChance = round(self.diseaseProtectionChance - 0.01, 2) + if self.diseaseProtectionChance < 0: + self.diseaseProtectionChance = 0 + random.shuffle(self.symptomaticDiseases) + for diseaseRecord in self.symptomaticDiseases: diseaseTags = diseaseRecord["disease"].tags immuneResponseStart = diseaseRecord["startIndex"] immuneResponseEnd = min(diseaseRecord["endIndex"] + 1, len(self.immuneSystem)) @@ -256,10 +307,11 @@ def doDisease(self): self.immuneSystem[immuneResponseStart + i] = diseaseTags[i] break if diseaseTags == immuneResponse: - self.diseases.remove(diseaseRecord) - self.updateDiseaseEffects(diseaseRecord["disease"]) - - diseaseCount = len(self.diseases) + self.recoverFromDisease(diseaseRecord) + for recoveredDisease in self.recoveredDiseases: + if self.checkDiseaseImmunity(recoveredDisease["disease"]) == False and recoveredDisease["disease"] not in self.susceptibleDiseases: + self.susceptibleDiseases.append(recoveredDisease["disease"]) + diseaseCount = len(self.symptomaticDiseases) if diseaseCount == 0: return neighborCells = self.cell.neighbors.values() @@ -270,7 +322,8 @@ def doDisease(self): neighbors.append(neighbor) random.shuffle(neighbors) for neighbor in neighbors: - neighbor.catchDisease(self.diseases[random.randrange(diseaseCount)]["disease"], self) + diseases = self.incubatingDiseases + self.symptomaticDiseases + neighbor.catchDisease(diseases[random.randrange(diseaseCount)]["disease"], self) def doInheritance(self): if self.inheritancePolicy == "none": @@ -389,7 +442,7 @@ def doMetabolism(self): elif (self.sugar <= 0 and sugarMetabolism > 0) or (self.spice <= 0 and spiceMetabolism > 0): self.doDeath("starvation") - def doReproduction(self): + def doReproduction(self, diseasesList): # Agent marked for removal or not interested in reproduction should not reproduce if self.isAlive() == False or self.isFertile() == False: return @@ -425,6 +478,14 @@ def doReproduction(self): self.updateTimesReproducedWithAgent(neighbor, self.lastMoved) self.lastReproduced += 1 + # traced sugarscape.diseases just for this line + for potentialDisease in diseasesList: + if child.checkDiseaseImmunity(potentialDisease) == True: + child.immuneDiseases.append(potentialDisease) + + else: + child.susceptibleDiseases.append(potentialDisease) + sugarCost = self.startingSugar / (self.fertilityFactor * 2) spiceCost = self.startingSpice / (self.fertilityFactor * 2) mateSugarCost = neighbor.startingSugar / (neighbor.fertilityFactor * 2) @@ -449,7 +510,7 @@ def doTagging(self): neighbor.flipTag(position, self.tags[position]) neighbor.tribe = neighbor.findTribe() - def doTimestep(self, timestep): + def doTimestep(self, timestep, diseasesList): self.timestep = timestep # Prevent dead or already moved agent from moving if self.isAlive() == True and self.lastMoved != self.timestep: @@ -466,7 +527,7 @@ def doTimestep(self, timestep): return self.doTagging() self.doTrading() - self.doReproduction() + self.doReproduction(diseasesList) self.doLending() self.doDisease() self.doAging() @@ -606,6 +667,7 @@ def findBestCell(self): prey = cell.agent if cell.isOccupied() and self.isNeighborValidPrey(prey) == False: continue + preyTribe = prey.tribe if prey != None else "empty" preySugar = prey.sugar if prey != None else 0 preySpice = prey.spice if prey != None else 0 @@ -705,7 +767,7 @@ def findChildEndowment(self, mate): parentEndowments = { "aggressionFactor": [self.aggressionFactor, mate.aggressionFactor], "baseInterestRate": [self.baseInterestRate, mate.baseInterestRate], - "depressionFactor": [self.depressionFactor, mate.depressionFactor], + "diseaseProtectionChance": [self.diseaseProtectionChance, mate.diseaseProtectionChance], "fertilityAge": [self.fertilityAge, mate.fertilityAge], "fertilityFactor": [self.fertilityFactor, mate.fertilityFactor], "infertilityAge": [self.infertilityAge, mate.infertilityAge], @@ -1150,7 +1212,7 @@ def isNeighborValidPrey(self, neighbor): return False def isSick(self): - if len(self.diseases) > 0: + if len(self.symptomaticDiseases) > 0: return True return False @@ -1230,6 +1292,13 @@ def printEthicalCellScores(self, cells): print(f"Ethical cell {i + 1}/{len(cells)}: {cellString}") i += 1 + def recoverFromDisease(self, diseaseRecord): + recoveredDisease = diseaseRecord["disease"] + recoveredDiseaseRecord = {"disease": recoveredDisease, "timestep": self.timestep} + # symptomatic to recovered + self.updateDiseaseGroups(diseaseRecord, recoveredDiseaseRecord, self.symptomaticDiseases, self.recoveredDiseases) + self.updateDiseaseEffects(recoveredDisease) + def removeDebt(self, loan): for debtor in self.socialNetwork["debtors"]: if debtor == loan: @@ -1252,6 +1321,13 @@ def setMother(self, mother): self.addAgentToSocialNetwork(mother) self.socialNetwork["mother"] = mother + def showSymptoms(self): + for disease in self.incubatingDiseases: + if self.timestep >= disease["endIncubation"]: + # incubating to symptomatic + self.updateDiseaseGroups(disease, disease, self.incubatingDiseases, self.symptomaticDiseases) + self.updateDiseaseEffects(disease["disease"]) + def sortCellsByWealth(self, cells): # Insertion sort of cells by wealth in descending order with range as a tiebreaker i = 0 @@ -1271,24 +1347,28 @@ def spawnChild(self, childID, birthday, cell, configuration): def updateDiseaseEffects(self, disease): # If disease not in list of diseases, agent has recovered and undo its effects recoveryCheck = -1 - for diseaseRecord in self.diseases: + for diseaseRecord in self.symptomaticDiseases: if disease == diseaseRecord["disease"]: recoveryCheck = 1 break - - sugarMetabolismPenalty = disease.sugarMetabolismPenalty * recoveryCheck + aggressionPenalty = disease.aggressionPenalty * recoveryCheck + fertilityPenalty = disease.fertilityPenalty * recoveryCheck + movementPenalty = disease.movementPenalty * recoveryCheck spiceMetabolismPenalty = disease.spiceMetabolismPenalty * recoveryCheck + sugarMetabolismPenalty = disease.sugarMetabolismPenalty * recoveryCheck visionPenalty = disease.visionPenalty * recoveryCheck - movementPenalty = disease.movementPenalty * recoveryCheck - fertilityPenalty = disease.fertilityPenalty * recoveryCheck - aggressionPenalty = disease.aggressionPenalty * recoveryCheck - self.sugarMetabolismModifier += sugarMetabolismPenalty + self.aggressionFactorModifier += aggressionPenalty + self.fertilityFactorModifier += fertilityPenalty + self.movementModifier += movementPenalty self.spiceMetabolismModifier += spiceMetabolismPenalty + self.sugarMetabolismModifier += sugarMetabolismPenalty self.visionModifier += visionPenalty - self.movementModifier += movementPenalty - self.fertilityFactorModifier += fertilityPenalty - self.aggressionFactorModifier += aggressionPenalty + + def updateDiseaseGroups(self, oldDisease, newDisease, oldGroup, newGroup): + # old disease and new disease for the case of disease records (incubating, symptomatatic, recovered) + oldGroup.remove(oldDisease) + newGroup.append(newDisease) def updateFriends(self, neighbor): neighborID = neighbor.ID diff --git a/cell.py b/cell.py index c961649..2cd8978 100644 --- a/cell.py +++ b/cell.py @@ -1,7 +1,7 @@ import math class Cell: - def __init__(self, x, y, environment, maxSugar=0, maxSpice=0, growbackRate=0): + def __init__(self, x, y, environment, maxSugar=0, maxSpice=0): self.x = x self.y = y self.environment = environment diff --git a/config.json b/config.json index ef0f151..b753d27 100644 --- a/config.json +++ b/config.json @@ -19,6 +19,7 @@ "agentDecisionModelLookaheadFactor": 0, "agentDecisionModelTribalFactor": [-1, -1], "agentDepressionPercentage": 0, + "agentDiseaseProtectionChance": [0.0, 1.0], "agentFemaleInfertilityAge": [40, 50], "agentFemaleFertilityAge": [12, 15], "agentFertilityFactor": [1, 1], @@ -51,10 +52,13 @@ "debugMode": ["none"], "diseaseAggressionPenalty": [-1, 1], "diseaseFertilityPenalty": [-1, 1], + "diseaseIncubationPeriod": [0,10], "diseaseMovementPenalty": [0, 0], "diseaseSpiceMetabolismPenalty": [1, 3], "diseaseSugarMetabolismPenalty": [1, 3], + "diseaseStartTimeframe": [0, 0], "diseaseTagStringLength": [11, 21], + "diseaseTransmissionChance": [0.0, 1.0], "diseaseVisionPenalty": [-1, 1], "environmentEquator": -1, "environmentHeight": 50, @@ -87,8 +91,8 @@ "interfaceHeight": 1000, "interfaceWidth": 900, "keepAlivePostExtinction": false, - "logfile": "log.json", - "logfileFormat": "json", + "logfile": "log.csv", + "logfileFormat": "csv", "neighborhoodMode": "vonNeumann", "profileMode": false, "screenshots": false, diff --git a/disease.py b/disease.py index a0c10be..b41cf1a 100644 --- a/disease.py +++ b/disease.py @@ -7,11 +7,21 @@ def __init__(self, diseaseID, configuration): self.configuration = configuration self.aggressionPenalty = configuration["aggressionPenalty"] self.fertilityPenalty = configuration["fertilityPenalty"] + self.incubationPeriod = configuration["incubationPeriod"] self.movementPenalty = configuration["movementPenalty"] self.spiceMetabolismPenalty = configuration["spiceMetabolismPenalty"] self.sugarMetabolismPenalty = configuration["sugarMetabolismPenalty"] self.tags = configuration["tags"] + self.transmissionChance = configuration["transmissionChance"] self.visionPenalty = configuration["visionPenalty"] + self.newInfections = 0 + self.infectors = set() + self.startingInfectedAgents = 0 + + def resetRStats(self): + self.newInfections = 0 + self.infectors = set() + def __str__(self): return f"{self.ID}" diff --git a/gui.py b/gui.py index be31f8f..9fb91ee 100644 --- a/gui.py +++ b/gui.py @@ -124,6 +124,7 @@ def configureButtons(self, window): statsLabel = tkinter.Label(window, text=self.defaultSimulationString, font="Roboto 10", justify=tkinter.CENTER) statsLabel.grid(row=1, column=0, columnspan=self.menuTrayColumns, sticky="nsew") + # need to add diseases to default agent string cellLabel = tkinter.Label(window, text=f"{self.defaultCellString}\n{self.defaultAgentString}", font="Roboto 10", justify=tkinter.CENTER) cellLabel.grid(row=2, column=0, columnspan=self.menuTrayColumns, sticky="nsew") @@ -476,7 +477,7 @@ def drawLines(self): elif self.activeNetwork.get() == "Disease": for agent in self.sugarscape.agents: if agent.isSick() == True: - for diseaseRecord in agent.diseases: + for diseaseRecord in agent.symptomaticDiseases: # Starting diseases without an infector are not considered if "infector" not in diseaseRecord: continue @@ -596,7 +597,7 @@ def lookupFillColor(self, cell): elif self.activeColorOptions["agent"] == "Depression": return self.colors["sick"] if agent.depressed == True else self.colors["healthy"] elif self.activeColorOptions["agent"] == "Disease": - return self.colors["sick"] if len(agent.diseases) > 0 else self.colors["healthy"] + return self.colors["sick"] if agent.isSick() else self.colors["healthy"] elif self.activeColorOptions["agent"] == "Metabolism": return self.colors["metabolism"][agent.sugarMetabolism + agent.spiceMetabolism] elif self.activeColorOptions["agent"] == "Movement": @@ -679,6 +680,7 @@ def updateHighlightedCellStats(self): agentStats = f"Agent: {str(agent)} | Age: {agent.age} | Vision: {round(agent.findVision(), 2)} | Movement: {round(agent.findMovement(), 2)} | " agentStats += f"Sugar: {round(agent.sugar, 2)} | Spice: {round(agent.spice, 2)} | " agentStats += f"Metabolism: {round(((agent.findSugarMetabolism() + agent.findSpiceMetabolism()) / 2), 2)} | Decision Model: {agent.decisionModel} | Tribe: {agent.tribe}" + # add disease to end else: agentStats = self.defaultAgentString cellStats += f"\n{agentStats}" diff --git a/sugarscape.py b/sugarscape.py index 672ddcc..b72f13b 100644 --- a/sugarscape.py +++ b/sugarscape.py @@ -54,6 +54,7 @@ def __init__(self, configuration): self.bornAgents = [] self.deadAgents = [] self.diseases = [] + self.diseasesCount = [[] for i in range(configuration["diseaseStartTimeframe"][1] + 1)] self.activeQuadrants = self.findActiveQuadrants() self.configureAgents(configuration["startingAgents"]) self.configureDiseases(configuration["startingDiseases"]) @@ -69,6 +70,14 @@ def __init__(self, configuration): "environmentWealthTotal": 0, "agentWealthCollected": 0, "agentWealthBurnRate": 0, "agentMeanTimeToLive": 0, "agentTotalMetabolism": 0, "agentCombatDeaths": 0, "agentAgingDeaths": 0, "agentDeaths": 0, "largestTribe": 0, "largestTribeSize": 0, "remainingTribes": self.configuration["environmentMaxTribes"], "sickAgents": 0, "carryingCapacity": 0} + diseaseStats = {} + for disease in self.diseases: + diseaseStats[f"disease{disease.ID}Immune"] = 0 + diseaseStats[f"disease{disease.ID}Susceptible"] = 0 + diseaseStats[f"disease{disease.ID}Infected"] = 0 + diseaseStats[f"disease{disease.ID}Recovered"] = 0 + diseaseStats[f"disease{disease.ID}RValue"] = 0.0 + self.runtimeStats.update(diseaseStats) self.graphStats = {"ageBins": [], "sugarBins": [], "spiceBins": [], "lorenzCurvePoints": [], "meanTribeTags": [], "maxSugar": 0, "maxSpice": 0, "maxWealth": 0} self.log = open(configuration["logfile"], 'a') if configuration["logfile"] != None else None @@ -184,37 +193,20 @@ def configureDiseases(self, numDiseases): numDiseases = numAgents diseaseEndowments = self.randomizeDiseaseEndowments(numDiseases) - random.shuffle(self.agents) - diseases = [] for i in range(numDiseases): diseaseID = self.generateDiseaseID() diseaseConfiguration = diseaseEndowments[i] newDisease = disease.Disease(diseaseID, diseaseConfiguration) - diseases.append(newDisease) - - startingDiseases = self.configuration["startingDiseasesPerAgent"] - minStartingDiseases = startingDiseases[0] - maxStartingDiseases = startingDiseases[1] - currStartingDiseases = minStartingDiseases - for agent in self.agents: - random.shuffle(diseases) - for newDisease in diseases: - if len(agent.diseases) >= currStartingDiseases and startingDiseases != [0, 0]: - currStartingDiseases += 1 - break - hammingDistance = agent.findNearestHammingDistanceInDisease(newDisease)["distance"] - if hammingDistance == 0: - continue - agent.catchDisease(newDisease) - self.diseases.append(newDisease) - if startingDiseases == [0, 0]: - diseases.remove(newDisease) - break - if currStartingDiseases > maxStartingDiseases: - currStartingDiseases = minStartingDiseases - - if startingDiseases == [0, 0] and len(diseases) > 0 and ("all" in self.debug or "sugarscape" in self.debug): - print(f"Could not place {len(diseases)} diseases.") + timestep = diseaseConfiguration["startTimestep"] + self.diseases.append(newDisease) + self.diseasesCount[timestep].append(newDisease) + # agents start either immune or susceptible - this part should be fine + for agent in self.agents: + if agent.checkDiseaseImmunity(newDisease) == True: + agent.immuneDiseases.append(newDisease) + else: + agent.susceptibleDiseases.append(newDisease) + self.infectAgents() def configureEnvironment(self, maxSugar, maxSpice, sugarPeaks, spicePeaks): height = self.environment.height @@ -240,6 +232,8 @@ def doTimestep(self): if self.timestep >= self.maxTimestep: self.toggleEnd() return + for disease in self.diseases: + disease.resetRStats() if "all" in self.debug or "sugarscape" in self.debug: print(f"Timestep: {self.timestep}\nLiving Agents: {len(self.agents)}") self.timestep += 1 @@ -247,9 +241,10 @@ def doTimestep(self): self.toggleEnd() else: self.environment.doTimestep(self.timestep) + self.infectAgents() random.shuffle(self.agents) for agent in self.agents: - agent.doTimestep(self.timestep) + agent.doTimestep(self.timestep, self.diseases) self.removeDeadAgents() self.replaceDeadAgents() self.updateRuntimeStats() @@ -353,6 +348,38 @@ def generateTribeTags(self, tribe): random.shuffle(tags) return tags + def infectAgents(self): + timestep = self.timestep + if timestep > self.configuration["diseaseStartTimeframe"][1]: + return + diseases = self.diseasesCount[timestep] + if len(diseases) == 0: + return + startingDiseases = self.configuration["startingDiseasesPerAgent"] + minStartingDiseases = startingDiseases[0] + maxStartingDiseases = startingDiseases[1] + currStartingDiseases = minStartingDiseases + random.shuffle(self.agents) + random.shuffle(diseases) + for agent in self.agents: + for newDisease in diseases: + if newDisease.startingInfectedAgents == 1 and startingDiseases == [0, 0]: + continue + if agent.startingDiseases >= currStartingDiseases and startingDiseases != [0, 0]: + currStartingDiseases += 1 + break + if agent.canCatchDisease(newDisease) == False: + continue + agent.catchDisease(newDisease) + if startingDiseases == [0, 0]: + diseases.remove(newDisease) + break + if currStartingDiseases > maxStartingDiseases: + currStartingDiseases = minStartingDiseases + if startingDiseases == [0, 0] and self.timestep == self.configuration["diseaseStartTimeframe"][1] and len(diseases) > 0: + if "all" in self.debug or "sugarscape" in self.debug: + print(f"Could not place {len(diseases)} diseases.") + def pauseSimulation(self): while self.run == False: if self.gui != None and self.end == False: @@ -364,83 +391,114 @@ def randomizeDiseaseEndowments(self, numDiseases): configs = self.configuration aggressionPenalty = configs["diseaseAggressionPenalty"] fertilityPenalty = configs["diseaseFertilityPenalty"] + incubationPeriod = configs["diseaseIncubationPeriod"] movementPenalty = configs["diseaseMovementPenalty"] spiceMetabolismPenalty = configs["diseaseSpiceMetabolismPenalty"] + startTimeframe = configs["diseaseStartTimeframe"] sugarMetabolismPenalty = configs["diseaseSugarMetabolismPenalty"] tagLengths = configs["diseaseTagStringLength"] + transmissionChance = configs["diseaseTransmissionChance"] visionPenalty = configs["diseaseVisionPenalty"] minAggressionPenalty = aggressionPenalty[0] minFertilityPenalty = fertilityPenalty[0] + minIncubationPeriod = incubationPeriod[0] minMovementPenalty = movementPenalty[0] minSpiceMetabolismPenalty = spiceMetabolismPenalty[0] + minStartTimeframe = startTimeframe[0] minSugarMetabolismPenalty = sugarMetabolismPenalty[0] minTagLength = tagLengths[0] + minTransmissionChance = transmissionChance[0] minVisionPenalty = visionPenalty[0] maxAggressionPenalty = aggressionPenalty[1] maxFertilityPenalty = fertilityPenalty[1] + maxIncubationPeriod = incubationPeriod[1] maxMovementPenalty = movementPenalty[1] maxSpiceMetabolismPenalty = spiceMetabolismPenalty[1] + maxStartTimeframe = startTimeframe[1] maxSugarMetabolismPenalty = sugarMetabolismPenalty[1] maxTagLength = tagLengths[1] + maxTransmissionChance = transmissionChance[1] maxVisionPenalty = visionPenalty[1] aggressionPenalties = [] diseaseTags = [] endowments = [] fertilityPenalties = [] + incubationPeriods = [] movementPenalties = [] spiceMetabolismPenalties = [] + startTimesteps = [] sugarMetabolismPenalties = [] + transmissionChances = [] visionPenalties = [] currAggressionPenalty = minAggressionPenalty currFertilityPenalty = minFertilityPenalty + currIncubationPeriod = minIncubationPeriod currMovementPenalty = minMovementPenalty - currSugarMetabolismPenalty = minSugarMetabolismPenalty currSpiceMetabolismPenalty = minSpiceMetabolismPenalty + currStartTimestep = minStartTimeframe + currSugarMetabolismPenalty = minSugarMetabolismPenalty currTagLength = minTagLength + currTransmissionChance = minTransmissionChance currVisionPenalty = minVisionPenalty for i in range(numDiseases): aggressionPenalties.append(currAggressionPenalty) diseaseTags.append([random.randrange(2) for i in range(currTagLength)]) fertilityPenalties.append(currFertilityPenalty) + incubationPeriods.append(currIncubationPeriod) movementPenalties.append(currMovementPenalty) spiceMetabolismPenalties.append(currSpiceMetabolismPenalty) + startTimesteps.append(currStartTimestep) sugarMetabolismPenalties.append(currSugarMetabolismPenalty) + transmissionChances.append(currTransmissionChance) visionPenalties.append(currVisionPenalty) currAggressionPenalty += 1 currFertilityPenalty += 1 + currIncubationPeriod += 1 currMovementPenalty += 1 currSpiceMetabolismPenalty += 1 + currStartTimestep += 1 currSugarMetabolismPenalty += 1 currTagLength += 1 + # To make sure the disease's transmissionChance is a properly-formatted decimal + currTransmissionChance = round(currTransmissionChance + (10 ** -1), 1) currVisionPenalty += 1 if currAggressionPenalty > maxAggressionPenalty: currAggressionPenalty = minAggressionPenalty if currFertilityPenalty > maxFertilityPenalty: currFertilityPenalty = minFertilityPenalty + if currIncubationPeriod > maxIncubationPeriod: + currIncubationPeriod = minIncubationPeriod if currMovementPenalty > maxMovementPenalty: currMovementPenalty = minMovementPenalty if currSpiceMetabolismPenalty > maxSpiceMetabolismPenalty: currSpiceMetabolismPenalty = minSpiceMetabolismPenalty + if currStartTimestep > maxStartTimeframe: + currStartTimestep = minStartTimeframe if currSugarMetabolismPenalty > maxSugarMetabolismPenalty: currSugarMetabolismPenalty = minSugarMetabolismPenalty if currTagLength > maxTagLength: currTagLength = minTagLength + if currTransmissionChance > maxTransmissionChance: + currTransmissionChance = minTransmissionChance if currVisionPenalty > maxVisionPenalty: currVisionPenalty = minVisionPenalty randomDiseaseEndowment = {"aggressionPenalties": aggressionPenalties, "diseaseTags": diseaseTags, "fertilityPenalties": fertilityPenalties, + "incubationPeriods": incubationPeriods, "movementPenalties": movementPenalties, "spiceMetabolismPenalties": spiceMetabolismPenalties, + "startTimesteps": startTimesteps, "sugarMetabolismPenalties": sugarMetabolismPenalties, + "transmissionChances": transmissionChances, "visionPenalties": visionPenalties} # Map configuration to a random number via hash to make random number generation independent of iteration order @@ -460,10 +518,13 @@ def randomizeDiseaseEndowments(self, numDiseases): for i in range(numDiseases): diseaseEndowment = {"aggressionPenalty": aggressionPenalties.pop(), "fertilityPenalty": fertilityPenalties.pop(), + "incubationPeriod": incubationPeriods.pop(), "movementPenalty": movementPenalties.pop(), "spiceMetabolismPenalty": spiceMetabolismPenalties.pop(), + "startTimestep": startTimesteps.pop(), "sugarMetabolismPenalty": sugarMetabolismPenalties.pop(), "tags": diseaseTags.pop(), + "transmissionChance": transmissionChances.pop(), "visionPenalty": visionPenalties.pop()} endowments.append(diseaseEndowment) return endowments @@ -476,6 +537,7 @@ def randomizeAgentEndowments(self, numAgents): decisionModelLookaheadDiscount = configs["agentDecisionModelLookaheadDiscount"] decisionModelLookaheadFactor = configs["agentDecisionModelLookaheadFactor"] decisionModelTribalFactor = configs["agentDecisionModelTribalFactor"] + diseaseProtectionChance = configs["agentDiseaseProtectionChance"] femaleFertilityAge = configs["agentFemaleFertilityAge"] femaleInfertilityAge = configs["agentFemaleInfertilityAge"] fertilityFactor = configs["agentFertilityFactor"] @@ -515,6 +577,7 @@ def randomizeAgentEndowments(self, numAgents): "decisionModelFactor": {"endowments": [], "curr": decisionModelFactor[0], "min": decisionModelFactor[0], "max": decisionModelFactor[1]}, "decisionModelLookaheadDiscount": {"endowments": [], "curr": decisionModelLookaheadDiscount[0], "min": decisionModelLookaheadDiscount[0], "max": decisionModelLookaheadDiscount[1]}, "decisionModelTribalFactor": {"endowments": [], "curr": decisionModelTribalFactor[0], "min": decisionModelTribalFactor[0], "max": decisionModelTribalFactor[1]}, + "diseaseProtectionChance": {"endowments": [], "curr": diseaseProtectionChance[0], "min": diseaseProtectionChance[0], "max": diseaseProtectionChance[1]}, "femaleFertilityAge": {"endowments": [], "curr": femaleFertilityAge[0], "min": femaleFertilityAge[0], "max": femaleFertilityAge[1]}, "femaleInfertilityAge": {"endowments": [], "curr": femaleInfertilityAge[0], "min": femaleInfertilityAge[0], "max": femaleInfertilityAge[1]}, "fertilityFactor": {"endowments": [], "curr": fertilityFactor[0], "min": fertilityFactor[0], "max": fertilityFactor[1]}, @@ -637,114 +700,6 @@ def randomizeAgentEndowments(self, numAgents): endowments.append(agentEndowment) return endowments - def randomizeDiseaseEndowments(self, numDiseases): - configs = self.configuration - sugarMetabolismPenalty = configs["diseaseSugarMetabolismPenalty"] - spiceMetabolismPenalty = configs["diseaseSpiceMetabolismPenalty"] - movementPenalty = configs["diseaseMovementPenalty"] - visionPenalty = configs["diseaseVisionPenalty"] - fertilityPenalty = configs["diseaseFertilityPenalty"] - aggressionPenalty = configs["diseaseAggressionPenalty"] - tagLengths = configs["diseaseTagStringLength"] - - minSugarMetabolismPenalty = sugarMetabolismPenalty[0] - minSpiceMetabolismPenalty = spiceMetabolismPenalty[0] - minMovementPenalty = movementPenalty[0] - minVisionPenalty = visionPenalty[0] - minFertilityPenalty = fertilityPenalty[0] - minAggressionPenalty = aggressionPenalty[0] - minTagLength = tagLengths[0] - - maxSugarMetabolismPenalty = sugarMetabolismPenalty[1] - maxSpiceMetabolismPenalty = spiceMetabolismPenalty[1] - maxMovementPenalty = movementPenalty[1] - maxVisionPenalty = visionPenalty[1] - maxFertilityPenalty = fertilityPenalty[1] - maxAggressionPenalty = aggressionPenalty[1] - maxTagLength = tagLengths[1] - - endowments = [] - sugarMetabolismPenalties = [] - spiceMetabolismPenalties = [] - movementPenalties = [] - visionPenalties = [] - fertilityPenalties = [] - aggressionPenalties = [] - diseaseTags = [] - - currSugarMetabolismPenalty = minSugarMetabolismPenalty - currSpiceMetabolismPenalty = minSpiceMetabolismPenalty - currMovementPenalty = minMovementPenalty - currVisionPenalty = minVisionPenalty - currFertilityPenalty = minFertilityPenalty - currAggressionPenalty = minAggressionPenalty - currTagLength = minTagLength - - for i in range(numDiseases): - sugarMetabolismPenalties.append(currSugarMetabolismPenalty) - spiceMetabolismPenalties.append(currSpiceMetabolismPenalty) - movementPenalties.append(currMovementPenalty) - visionPenalties.append(currVisionPenalty) - fertilityPenalties.append(currFertilityPenalty) - aggressionPenalties.append(currAggressionPenalty) - diseaseTags.append([random.randrange(2) for i in range(currTagLength)]) - - currSugarMetabolismPenalty += 1 - currSpiceMetabolismPenalty += 1 - currMovementPenalty += 1 - currVisionPenalty += 1 - currFertilityPenalty += 1 - currAggressionPenalty += 1 - currTagLength += 1 - - if currSugarMetabolismPenalty > maxSugarMetabolismPenalty: - currSugarMetabolismPenalty = minSugarMetabolismPenalty - if currSpiceMetabolismPenalty > maxSpiceMetabolismPenalty: - currSpiceMetabolismPenalty = minSpiceMetabolismPenalty - if currMovementPenalty > maxMovementPenalty: - currMovementPenalty = minMovementPenalty - if currVisionPenalty > maxVisionPenalty: - currVisionPenalty = minVisionPenalty - if currFertilityPenalty > maxFertilityPenalty: - currFertilityPenalty = minFertilityPenalty - if currAggressionPenalty > maxAggressionPenalty: - currAggressionPenalty = minAggressionPenalty - if currTagLength > maxTagLength: - currTagLength = minTagLength - - randomDiseaseEndowment = {"sugarMetabolismPenalties": sugarMetabolismPenalties, - "spiceMetabolismPenalties": spiceMetabolismPenalties, - "movementPenalties": movementPenalties, - "visionPenalties": visionPenalties, - "fertilityPenalties": fertilityPenalties, - "aggressionPenalties": aggressionPenalties, - "diseaseTags": diseaseTags} - - # Map configuration to a random number via hash to make random number generation independent of iteration order - if (self.diseaseConfigHashes == None): - self.diseaseConfigHashes = {} - for penalty in randomDiseaseEndowment: - hashed = hashlib.md5(penalty.encode()) - self.diseaseConfigHashes[penalty] = int(hashed.hexdigest(), 16) - - # Keep state of random numbers to allow extending agent endowments without altering original random object state - randomNumberReset = random.getstate() - for endowment in randomDiseaseEndowment.keys(): - random.seed(self.diseaseConfigHashes[endowment] + self.timestep) - random.shuffle(randomDiseaseEndowment[endowment]) - random.setstate(randomNumberReset) - - for i in range(numDiseases): - diseaseEndowment = {"aggressionPenalty": aggressionPenalties.pop(), - "fertilityPenalty": fertilityPenalties.pop(), - "movementPenalty": movementPenalties.pop(), - "sugarMetabolismPenalty": sugarMetabolismPenalties.pop(), - "spiceMetabolismPenalty": spiceMetabolismPenalties.pop(), - "tags": diseaseTags.pop(), - "visionPenalty": visionPenalties.pop()} - endowments.append(diseaseEndowment) - return endowments - def removeDeadAgents(self): deadAgents = [] for agent in self.agents: @@ -949,6 +904,11 @@ def updateRuntimeStatsPerGroup(self, group=None, notInGroup=False): agentsReplaced = 0 tribes = {} + immuneDiseaseStats = [0 for i in range(len(self.diseases))] + susceptibleDiseaseStats = [0 for i in range(len(self.diseases))] + infectedDiseaseStats = [0 for i in range(len(self.diseases))] + recoveredDiseaseStats = [0 for i in range(len(self.diseases))] + for agent in self.agents: if group != None and agent.isInGroup(group, notInGroup) == False: continue @@ -988,6 +948,17 @@ def updateRuntimeStatsPerGroup(self, group=None, notInGroup=False): tribes[agent.tribe] += 1 numAgents += 1 + for immuneDisease in agent.immuneDiseases: + immuneDiseaseStats[immuneDisease.ID] += 1 + for susceptibleDisease in agent.susceptibleDiseases: + susceptibleDiseaseStats[susceptibleDisease.ID] += 1 + for incubatingDisease in agent.incubatingDiseases: + infectedDiseaseStats[incubatingDisease["disease"].ID] += 1 + for symptomaticDisease in agent.symptomaticDiseases: + infectedDiseaseStats[symptomaticDisease["disease"].ID] += 1 + for recoveredDisease in agent.recoveredDiseases: + recoveredDiseaseStats[recoveredDisease["disease"].ID] += 1 + if numAgents > 0: agentMeanTimeToLive = round(agentMeanTimeToLive / numAgents, 2) agentWealthBurnRate = round(agentWealthBurnRate / numAgents, 2) @@ -1043,7 +1014,7 @@ def updateRuntimeStatsPerGroup(self, group=None, notInGroup=False): agentWealthCollected += agentWealth - (agent.lastSugar + agent.lastSpice) agentAgingDeaths += 1 if agent.causeOfDeath == "aging" else 0 agentCombatDeaths += 1 if agent.causeOfDeath == "combat" else 0 - agentDiseaseDeaths += 1 if agent.causeOfDeath == "disease" else 0 + agentDiseaseDeaths += 1 if agent.diseaseDeath == True else 0 agentStarvationDeaths += 1 if agent.causeOfDeath == "starvation" else 0 numDeadAgents += 1 meanAgeAtDeath = round(meanAgeAtDeath / numDeadAgents, 2) if numDeadAgents > 0 else 0 @@ -1091,6 +1062,20 @@ def updateRuntimeStatsPerGroup(self, group=None, notInGroup=False): for key in runtimeStats.keys(): self.runtimeStats[key] = runtimeStats[key] + for disease in self.diseases: + r = 0.0 + if numAgents > 0: + infectors = len(disease.infectors) + incidence = disease.newInfections + r = 0.0 + if infectors > 0: + r = round(float(incidence / infectors), 2) + self.runtimeStats[f"disease{disease.ID}RValue"] = r + self.runtimeStats[f"disease{disease.ID}Immune"] = immuneDiseaseStats[disease.ID] + self.runtimeStats[f"disease{disease.ID}Susceptible"] = susceptibleDiseaseStats[disease.ID] + self.runtimeStats[f"disease{disease.ID}Infected"] = infectedDiseaseStats[disease.ID] + self.runtimeStats[f"disease{disease.ID}Recovered"] = recoveredDiseaseStats[disease.ID] + def writeToLog(self): if self.log == None: return @@ -1235,6 +1220,15 @@ def verifyConfiguration(configuration): print(f"Cannot have agent maximum selfishness factor of {configuration['agentSelfishnessFactor'][1]}. Setting agent maximum selfishness factor to 1.0.") configuration["agentSelfishnessFactor"][1] = 1 + if configuration["agentDiseaseProtectionChance"][0] < 0: + if "all" in configuration["debugMode"] or "agent" in configuration["debugMode"]: + print(f"Cannot have agent minimum disease protection chance of {configuration['agentDiseaseProtectionChance'][0]}. Setting agent minimum disease protection chance to 0.0.") + configuration["agentDiseaseProtectionChance"][0] = 0.0 + if configuration["agentDiseaseProtectionChance"][1] > 1: + if "all" in configuration["debugMode"] or "agent" in configuration["debugMode"]: + print(f"Cannot have agent maximum disease protection chance of {configuration['agentDiseaseProtectionChance'][1]}. Setting agent maximum disease protection chance to 1.0.") + configuration["agentDiseaseProtectionChance"][1] = 1.0 + if configuration["agentTagStringLength"] < 0: if "all" in configuration["debugMode"] or "agent" in configuration["debugMode"]: print(f"Cannot have a negative agent tag string length. Setting agent tag string length to 0.") @@ -1251,6 +1245,15 @@ def verifyConfiguration(configuration): print(f"Cannot have a longer agent tag string length than maximum number of tribes. Setting the number of tribes to {configuration['agentTagStringLength']}.") configuration["environmentMaxTribes"] = configuration["agentTagStringLength"] + if configuration["diseaseTransmissionChance"][0] < 0.0: + if "all" in configuration["debugMode"] or "disease" in configuration["debugMode"]: + print(f"Cannot have a minimum disease transmission chance of {configuration['diseaseTransmissionChance'][0]}. Setting disease transmission chance to 0.0.") + configuration["diseaseTransmissionChance"][0] = 0.0 + if configuration["diseaseTransmissionChance"][1] > 1.0: + if "all" in configuration["debugMode"] or "disease" in configuration["debugMode"]: + print(f"Cannot have a maximum disease transmission chance of {configuration['diseaseTransmissionChance'][1]}. Setting disease transmission chance to 1.0.") + configuration["diseaseTransmissionChance"][1] = 1.0 + # Ensure at most number of tribes and decision models are equal to the number of colors in the GUI maxColors = 25 uniqueAgentDecisionModels = set(configuration["agentDecisionModels"]) @@ -1370,6 +1373,7 @@ def verifyConfiguration(configuration): "agentDecisionModelLookaheadFactor": [0], "agentDecisionModelTribalFactor": [-1, -1], "agentDepressionPercentage": 0, + "agentDiseaseProtectionChance": [0.0, 1.0], "agentFemaleInfertilityAge": [0, 0], "agentFemaleFertilityAge": [0, 0], "agentFertilityFactor": [0, 0], @@ -1402,10 +1406,13 @@ def verifyConfiguration(configuration): "debugMode": ["none"], "diseaseAggressionPenalty": [0, 0], "diseaseFertilityPenalty": [0, 0], + "diseaseIncubationPeriod": [0, 0], "diseaseMovementPenalty": [0, 0], "diseaseSpiceMetabolismPenalty": [0, 0], + "diseaseStartTimeframe": [0, 0], "diseaseSugarMetabolismPenalty": [0, 0], "diseaseTagStringLength": [0, 0], + "diseaseTransmissionChance": [0.0, 1.0], "diseaseVisionPenalty": [0, 0], "environmentEquator": -1, "environmentHeight": 50,