Code
Godot Game Engine
- Open-Source Game Engine
- GDScript, Python-like scripting language
- Nodes for various uses
- Flexible Scene-System, Nodetrees
- Visual Editor for Scenes
- Easy deployment to many platforms
- Comes with a multi-platform editor
- Many more features
Structure
The software consists of two scenes, five base classes and a global class with constants and static functions.The classes extending the scenes contain the main thread for the correspoding scene. They also function as an adapter between inputs and outputs.
The game-management class handles all the input from the Dashboard-scene and generates the output to the Dashboard-scene with data from the entity classes 'Country' and 'State'.
Both entity classes model the real-life, simulting and storing the data. Inside each state runs the main simulation function.
Main-Thread inside Dash Control Class
This is the function of the main thread. The _process function gets called every frame by Godot. As you can see the simulation runs in a different thread than the main thread to ensure that the interface doesn't freeze and stays usable.func _process(_delta): # called every frame, _delta is the elapsed time between two framesif !paused:if remainingDays > 0 and !running:Constants.currentProgress = 0statOutput[CONSTANTS.PROGRESSPANEL].visible = true# For DEBUGGING# self.running = true# game_manager.simulate()# self.remainingDays -= 1# self.running = false# For RUNNINGgame_manager._simThread.wait_to_finish()game_manager._simThread.start(self, "runSimulation", null)updateProgress()
Inside Game-Management Class
This is the moment the game-management class passes on the order to simulate to the country and updates the current day. Additionally here it gets checked if the pandemic is over. If there are no new infections inside a certain time interval, the pandmic has ended.func simulate():entities[CONSTANTS.DEU].simulateALL()if self.days.size() > 10:var checkNewInfections = []for i in range(self.days.max() - int(CONSTANTS.MONTH * CONSTANTS.ENDEMICTIMEFACTOR), self.days.max()):if i < 2:continueelse:checkNewInfections.append(entities[CONSTANTS.DEU].getDailyInfections(i, true))if checkNewInfections.max() < 1:setMode(CONSTANTS.ENDMODE)if !ended:endDay = currentDayprint("Day ", self.currentDay, ": PANDEMIC OVER")updateDay()
Inside the Country Class
The country now produces the vaccine doses and distributes them. Also the commuters get distributed to the neighboring state here. To fasten this process, the distribution of commuters is run in threads. Just like this the simulation is also running in 16 Threads (like the 16 federal states of Germany).func simulateALL():produceVax()distributeVax()distributeCommuters()for state in states.values():# state.simulate() # for easier debuggingstate._thread.start(self, "simulateState", state.getName())for state in states.values():state._thread.wait_to_finish()homeCommuters()for state in states.values():state.collectNumbers()getNumbers()recalculateStatePopulation()
Distributing Commuters
- Commuter-Rates based on data from the 'Agentur für Arbeit'
- Commuters get evenly distributed to neighboring states in round-robin style
- Randomly selected one by one from a category
- Positive tested, hospitalised and individuals inside the waiting interval don't commute
Inside the State Class
This is where the Gillespie-Algorithm is realised. Also here the individuals in the waiting interval for the second vaccination get handled. This has similarities to a conveyor belt, as each day the individuals get shafted one step ahead. Visible as well here is the change of infection-rates. With the infection-rates events can be altered in case of e.g. a lockdown.func simulate():var startTime = OS.get_ticks_msec()events = 0infectRate = [getInfectRate(), getInfectRate()*infectTestFactor, getInfectRate()*infectFactorHosp, getInfectRate()*infectFactorV1, getInfectRate()*infectFactorV2] # untested, tested, hospitalised, 1x-vaxed, 2x-vaxedvar t = timeDifferencewhile t<1:t = gillespieIteration(t)events += 1if(t>1):timeDifference = fmod(t,1)continuewaitDay += 1waitDay = waitDay % Constants.VACDELAYV1eligible[0] += V1[0][waitDay]V1[0][waitDay] = 0V1eligible[1] += V1[1][waitDay]V1[1][waitDay] = 0# and so on
Gillespie-Iteration
This is where one Gillespie-Event takes place. As you can see the time gets forwarded with -log(r1)/reactTotal, so that the number of events fit one day.func gillespieIteration(t):var r1 = rnd.randf()var reactionRates = updateReactionRates()var reactTotal = CONSTANTS.sum(reactionRates)if reactTotal == 0:return 1var waitTime = -log(r1)/reactTotalt = t + waitTimevar r2 = rnd.randf()var reactionRatesCumSum = CONSTANTS.cumulative_sum(reactionRates)for i in range(reactionRatesCumSum.size()):reactionRatesCumSum[i] = reactionRatesCumSum[i] / reactTotalvar rulefor i in range(reactionRatesCumSum.size()):if(r2 <= reactionRatesCumSum[i]):rule = ibreakupdatePersonNumbers(rule)return t
Reaction-Rates
There are in total 149 reaction rates. All of these rates are derived from the basic rates seen on the modeling page.func updateReactionRates():var rates = []# 0 1 2 3 Infection of untested, non-vaxed individualsrates.append((infectRate[0]/population)*S[0]*I[0]) # by untested infected (non-vaxed)rates.append((infectRate[1]/population)*S[0]*I[1]) # by tested infected (non-vaxed)rates.append((infectRate[0]/population)*S[0]*I[2]) # by unknowingly infected (non-vaxed)rates.append((infectRate[2]/population)*S[0]*I[3]) # by non-vaxed hospitalised# many more rules, 149 in total
Updating the individual numbers
Some rates are mapped onto the same rules, because they trigger the same events. If an event happens to an individual inside the vaccination waiting period or a commuter, the rule gets carried out on a random member of the corresponding group.func updatePersonNumbers(rule):match rule:0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12: # Infection untested non-vaxedS[0] -= 1I[0] += 113, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25: # Infection tested non-vaxedS[1] -= 1I[2] += 126, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38: # Infection 1x-vaxed, that are in the waiting intervalvar randomDay = rnd.randi() % Constants.VACDELAY # assign infection randomly to a waiting-blockwhile true:if V1[0][randomDay] > 0:breakelse:randomDay += 1randomDay = randomDay % Constants.VACDELAYV1[0][randomDay] -= 1V1[1][randomDay] += 1# 149 rates have to be matched to rules
Saving the data
GDScript allows arrays to have multiple data types. So at the beginning the name of the individual group gets saved and after that the data with the correspoding day as an index. This makes it easy to use the data to create output, because you always have the name available and can easily access the data via day.var sus0 = [CONSTANTS.NTESTED + CONSTANTS.SUSCEPTIBLE] # untested susceptiblevar sus1 = [CONSTANTS.TESTED + CONSTANTS.SUSCEPTIBLE] # tested susceptiblevar inf0 = [CONSTANTS.NTESTED + CONSTANTS.INFECTED] # untested infectedvar inf1 = [CONSTANTS.TESTED + CONSTANTS.INFECTED] # tested infectedvar inf2 = [CONSTANTS.UNAWARE + CONSTANTS.INFECTED] # unaware infectedvar hosp = [CONSTANTS.HOSPITALISED] # non-vaxed hosptialisedvar rec0 = [CONSTANTS.NTESTED + CONSTANTS.RECOVERED] # untested recoveredvar rec1 = [CONSTANTS.TESTED + CONSTANTS.RECOVERED] # tested recoveredvar rec2 = [CONSTANTS.UNAWARE + CONSTANTS.RECOVERED] # unaware recoveredvar dead0 = [CONSTANTS.NTESTED + CONSTANTS.DEAD] # untested deadvar dead1 = [CONSTANTS.TESTED + CONSTANTS.DEAD] # tested deadvar dead2 = [CONSTANTS.UNAWARE + CONSTANTS.DEAD] # unaware deadvar vax1sus = [CONSTANTS.VAX1 + CONSTANTS.SUSCEPTIBLE] # 1x-vaxed susceptiblevar vax1inf = [CONSTANTS.VAX1 + CONSTANTS.INFECTED] # 1x-vaxed infectedvar vax1hosp = [CONSTANTS.VAX1 + CONSTANTS.HOSPITALISED] # 1x-vaxed hosptialisedvar vax1rec = [CONSTANTS.VAX1 + CONSTANTS.RECOVERED] # 1x-vaxed recoveredvar vax1dead = [CONSTANTS.VAX1 + CONSTANTS.DEAD] # 1x-vaxed deadvar vax2sus = [CONSTANTS.VAX2 + CONSTANTS.SUSCEPTIBLE] # 2x-vaxed susceptiblevar vax2inf = [CONSTANTS.VAX2 + CONSTANTS.INFECTED] # 2x-vaxed infectedvar vax2hosp = [CONSTANTS.VAX2 + CONSTANTS.HOSPITALISED] # 2x-vaxed hospitalisedvar vax2rec = [CONSTANTS.VAX2 + CONSTANTS.RECOVERED] # 2x-vaxed recovered