Last active
July 5, 2020 20:48
-
-
Save ccritchfield/5fe46dc5d99820434aa421692f4f4adf to your computer and use it in GitHub Desktop.
Python - Coronavirus Model v2 - Dynamic People Lists
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
########################################################## | |
# Viral Model - Global Constants | |
########################################################## | |
# | |
# Copyright Craig Critchfield 2020 | |
# | |
########################################################## | |
""" | |
Shifted a lot of the constants over here, | |
b/c I had to write a lot of comments to explain | |
what they were doing, where I was getting them | |
from (internet links), and / or how they are | |
calculated. Gets annoying scrolling through | |
miles of code, so offloading them here to | |
reduce "main's" file size to make it easier | |
to organize and sift through. | |
----------------------------- | |
ABOUT THE MODEL | |
----------------------------- | |
Using Case Fatality Rate, we can pretty easily estimate | |
how likely someone will die from the virus if they get | |
infected. | |
The problem, though, is trying to determine how likely | |
someone will get infected. It's a constantly moving | |
number as more people become infected, thus increasing | |
chances of others getting infected if exposed. | |
It's also impacted by counter measures, like social | |
distancing, wearing masks, etc. | |
There's probably some grand algorithm that experts | |
use to determine all of this. | |
But, I just wanted to create a simple simulation | |
model to see how things go. The simulation will | |
loop through each person in a population to determine | |
if they come in contact with someone infected, | |
get infected, how long they incubate, chance to | |
infect others, etc. | |
It will do this day-by-day, and at the end of each | |
day it will rack up some KPI (key performance indicators) | |
to see how things are going and to chart out the | |
curves. | |
EG KPI... | |
* population mix of uninfected, infected, recovered & dead. | |
* generic % chance to get infected (this was my main goal with this) | |
* etc | |
I'm considering this a "black box" scenario, beacuse the | |
simulated people in it are just bouncing around w/o | |
knowing what they're doing or with any goal in mind. | |
We just want to see how they interact given some variables. | |
I pulled a lot of metrics from worldometers web site to | |
try to keep consistent in the data source. | |
Caveats to model.... | |
1) assuming people can't get reinfected .. we're seeing a few | |
cases pop up where previously infected & recovered are | |
getting re-infected. But, it seems to be more that the | |
virus in them is flaring up again, not that they're getting | |
the virus again from a different source. Theoretically the | |
virus can mutate, and a new strain is formed that previuosly | |
infected people won't have antibodies to counter. But, for | |
this simple model, we'll just assume people get infected | |
(and recover or pass on) only once. | |
2) people won't be infectious forever .. we're using R0 | |
(transmission rate) to determine how many people an | |
infected carrier can infect. This will be a % chance | |
to infect that many people, because we'll also use | |
counter measures % to reduce the infection chance. | |
So, while there are articles saying how long it takes | |
people to recover, that quarantining for 14 days is | |
prudent until you know if you have the virus or not, | |
all we're doing in this simulation is flagging someone | |
as getting the virus, they go through non-contagious | |
incubation period, then contagious incubation period, | |
and once they're contagious, each day after that they | |
will "try" to infect one other random person in population. | |
If no counter measures, then they will have 100% chance. | |
If counter measures, then they will have < 100% chance. | |
But, each "chance" adds up to the transmission rate. | |
Once they reach that transmission rate, they stop | |
"trying to infect" others. (This isn't coded to have | |
them act like they're purposefully trying to infect | |
others. They're just pebbles bumping around randomly | |
in a jar. They just will bump into someone every day, | |
and have a chance to infect them if infectious. We're | |
assuming no malicious intent to spread infection in | |
the model.) | |
""" | |
BORDER = "-" * 50 # border split to segregate terminal print feedback | |
#--------------------------------- | |
# Transmission Rate (R0) | |
#--------------------------------- | |
""" | |
Also known as Infection Rate, R0 or Reproduction rate. | |
R0 values are how likely something is to let another | |
thing through. In video game shaders, materials use | |
R0 values to determine how likely light is to transmit | |
through an opaque, translucent or transparent surface. | |
EG: rock would have an R0 of 0, since it blocks all light. | |
Clear Water would have an R0 of 1, because it lets all light | |
through. | |
So, R0 is a mathmatical term to determine transmission | |
of something through something else. | |
BBC has a nice article that explains R0 applied to | |
the virus perspective: | |
https://www.bbc.com/news/health-52473523 | |
Basically, it's how many people, on average, a person | |
can infect if they have the virus. One person might | |
infect more. Another person might not infect any. But, | |
it averages out to a value where 1 person infected can | |
transmit it to R0 other people. | |
An R0 of 1 means 1:1, where 1 infected typically transmits | |
to another person. An R0 of 3 (which has been the suggested | |
value for coronavirus) means 1:3, 1 person transmits | |
it to 3 on average. | |
To determine how many people end up infected from a | |
"patient zero", you use the R0 in an exponential formula, | |
like so... | |
Infected = R0 Infection Rate ^ Power to Raise By | |
( ^ symbol in Python is the "Raise to Power of" operator ) | |
Power to Raise by is how many degrees of separation | |
the virus has transmitted. EG: 1 person giving it to | |
3 others is 1 degree of separation, or raised to | |
power of 1. 1 person giving it to 3 others, then | |
those 3 giving it to 3 others (9 total) would | |
be 2 degrees of separation, or raising to power | |
of 2. | |
So, we can re-write the formula as such... | |
Infected = R0 Infection Rate ^ Degrees of Separation | |
In MS Excel you'd write it as... | |
Infected = power(R0, Degrees of Separation) | |
So, the higher the R0, the scarier it gets quickly | |
due to exponential increase. | |
Infected = 3^1 = 3 | |
Infected = 3^2 = 9 | |
Infected = 3^3 = 27 | |
Infected = 3^4 = 81 | |
Infected = 3^5 = 243 | |
Infected = 3^6 = 729 | |
Infected = 3^7 = 2,187 | |
Infected = 3^8 = 6,561 | |
Infected = 3^9 = 19,683 | |
Infected = 3^10 = 59,049 | |
So, 1 person infected could branch it out to 59,000 | |
people if no counter measures (masks, distancing, etc) | |
are implemented. | |
CDC looked at studies from Wuhan China suggesting R0 | |
of 2.2 to 2.7. Then they did their own modelling, and | |
it suggested an R0 of 5.7. | |
https://wwwnc.cdc.gov/eid/article/26/7/20-0282_article | |
I'm just going to go with R0 3 I've heard experts suggest. | |
""" | |
# corona virus | |
TRANSMISSION_RATE = 3 | |
# flu | |
#TRANSMISSION_RATE = 1.35 | |
#--------------------------------- | |
# Counter Measures | |
#--------------------------------- | |
""" | |
Counter measures means we're implementing proactive prevention | |
to try to slow the spread: wearing masks, social distancing, | |
isolation / quarantine, etc. | |
While we know counter measures work, because we can see the curve | |
flatten when implemented, we don't know for sure how effective | |
each one specifically is. IE: there's no definitive research | |
showing % effectiveness of wearing mask (for one person or both), | |
or social distancing. | |
There's a graphic going around saying that wearing masks means | |
the other person is 70% less likely to get the virus, and if | |
both people are wearing masks then 90% less likely. But, | |
Reuters did some fact-checking and found out that graphic isn't | |
valid. It's just some made-up numbers. I think someone felt if | |
they put numbers to the mask wearing, then people could latch | |
on to the idea of wearing masks more due to tangible / concrete | |
effectiveness. But, we simply don't have concrete / tangible metrics | |
for it yet. | |
https://www.reuters.com/article/uk-factcheck-coronavirus-mask-efficacy/partly-false-claim-wear-a-face-mask-covid-19-risk-reduced-by-up-to-98-5-idUSKCN2252T6 | |
Likewise for social distancing, hand-washing, etc. | |
So, we're just going to distill this down into a single | |
"counter measures" metric we can use to skew the chance | |
someone can transmit the virus to others. | |
Counter measures will equal how effective they are, on a | |
scale of 0 to 100% effectiveness. | |
But, it acts as reduction to infection_chance. So, we invert it. | |
Infection Chance % * ( 100% - Counter Measures % ) | |
So, as CM goes up, it lets less infection pass through. | |
Also, if we have a TRANSMISSION_RATE of 3 (1 person can | |
give it to 3 others), we'd need a TRANSMISSION_RATE of 1 | |
to at least flatten the curve. | |
We can easily do the math to determine what kind of | |
counter measure % we'd need to just flatten the curve by | |
setting counter measures to X and solving for it... | |
TRANSMISSION_RATE * ( INFECTION_CHANCE - COUNTER_MEASURES ) = NEW_TRANSMISSION_RATE | |
3 * (100% - x%)= 1 | |
100% - x% = 1/3 # divided both sides by 3 | |
100% - x% = 33% # convert 1/3 to % rounded down | |
- x% = 33% - 100% # subtract 100% from both sides | |
- x% =-67% # reduce right-side equation | |
x% = 67% # divided both sides by -1 | |
So, we'd need at least 67% counter measures to get TRANSMISSION_RATE | |
down to 1 or so. More if we want the new cases to lower via | |
R0 < 1 | |
""" | |
""" | |
!!! DEPRECATED !!! | |
Counter measures are now lists in model_main.py | |
that let us control the days certain CM's start/ | |
stop. This lets us simulate communities locking | |
down and opening up over time. Keeping this here | |
mostly for the notes above, so folks will understand | |
where I'm going with counter measures in the model. | |
""" | |
#COUNTER_MEASURES = 0.00 # no counter measures .. TRANSMISSION_RATE of 3.0 | |
#COUNTER_MEASURES = 0.25 # 25% counter measures .. new TRANSMISSION_RATE of 2.25 | |
#COUNTER_MEASURES = 0.35 # 35% counter measures .. new TRANSMISSION_RATE of 1.95 | |
#COUNTER_MEASURES = 0.50 # 50% counter measures .. new TRANSMISSION_RATE of 1.5 | |
#COUNTER_MEASURES = 0.66 # 66% counter measures .. new TRANSMISSION_RATE about 1.0 | |
#COUNTER_MEASURES = 0.75 # 75% counter measures .. new TRANSMISSION_RATE about 0.75 | |
#--------------------------------- | |
# Vaccine Immunity | |
#--------------------------------- | |
""" | |
A vaccine will stop the virus, but after it's | |
developed it takes time to manufacture, | |
distribute, and get masses of people injected. | |
We can simulate this by kicking off the day it | |
arrives, and then start incrementing immunity | |
as an extra counter measure to simulate more | |
and more people becoming immune over time. | |
""" | |
VACCINE_DAY = 0 # day vaccine is available (0 = no vaccine) | |
VACCINE_IMMUNITY_INCREMENT = 0.01 # counter measure amount vaccine adds every day after vaccine available | |
#--------------------------------- | |
# Case Fatality Rate (CFR) | |
#--------------------------------- | |
""" | |
Helps us determine liklihood of death if someone | |
gets the virus (and is confirmed to have it via | |
testing / diagnosis). As cases show up, they go | |
through the "pipe" until some kind of outcome | |
occurs, either death or recovery. So... | |
Outcomes = deaths + recovered | |
CFR = deaths / outcomes = deaths / (deaths + recovered) | |
EG: | |
worldometer US stats for May 18th, 2020... | |
https://www.worldometers.info/coronavirus/country/us/ | |
Cases = 1,543,842 | |
Deaths = 91,683 | |
Recovered = 351,936 | |
Outcomes = Deaths + Recovered = 91,683 + 351,936 = 443,619 | |
CFR = Deaths / Outcomes = 91,683 / 443,619 = 0.2066705889513299 | |
CFR = 0.21 ... 21% | |
I've monitored CFR for May, and for US it hangs around 20%. | |
So, for this simple model I'm going to just hard-code 20% as CFR | |
to determine death if someone gets the virus. | |
""" | |
CASE_FATALITY_RATE = 0.2 | |
#--------------------------------- | |
# Incubation Period | |
#--------------------------------- | |
""" | |
https://www.acpjournals.org/doi/10.7326/M20-0504 | |
Virus takes 3 to 14 days to incubate, 5 on-average. | |
When a person gets infected, we can use this function | |
to determine how many days they'll silently spread the | |
virus until symptoms show up. | |
Since it's 3 to 14, 5 on-average, it's a right-skewed | |
bell curve, meaning cases stack more towards the 3 to 5 | |
range then the 6 to 14 range. IE: people seem to generally | |
show symptoms sooner rather then later. | |
To represent this, we'll use a pre-canned function | |
that lets us plug in a range (3 to 14) and average (5) | |
and it'll create a distribution curve from it and select | |
a day amount that's statistically representative of that | |
curve. | |
IE: if we took all population and gave them incubation | |
days, if we avg'ed them all together we'd get 5. | |
Python has a "normal" distribution function that we can | |
feed an average and standard deviation to and have it | |
spit out a statistically relevant value. | |
We know the average, it's 5 days. But, we don't know | |
the standard deviation. However, we can ad-hoc it by | |
taking the (max day - min days) / 4, which is the | |
standard deviation estimation formula in statistics. | |
""" | |
# INCUBATION_MIN = 3 | |
# INCUBATION_AVG = 5 | |
# INCUBATION_MAX = 14 | |
# INCUBATION_STD = ( INCUBATION_MAX - INCUBATION_MIN ) / 4 | |
# # debug ... check standard deviation | |
# #print("incubation std_dev = " + INCUBATION_STD) | |
# def IncubationPeriod(): | |
# incubation = triangular( INCUBATION_MIN, INCUBATION_AVG, INCUBATION_MAX ) | |
# # incubation = normal( INCUBATION_AVG, INCUBATION_STD ) | |
# incubation = round( incubation ) # round incubation to nearest integer ( < 0.5 rounds down, 0.5+ rounds up ) | |
# incubation = int( incubation ) # convert to integer and return value | |
# return incubation | |
# # debug ... test incubation function | |
# if 1: | |
# for i in range(1000): | |
# inc = IncubationPeriod() | |
# print(inc) | |
""" | |
!!! | |
normal() is giving numbers < 3 (and even negative incubation numbers). | |
triangular() isn't weighting the center point more towards 5 avg days. | |
so, until I can figure out how to create a more robust incubation curve | |
I'm just going to hard set incubation time to the avg 5 days | |
Also, note that I'm doing 5 days inclusive to incubate, which means | |
day 5 is also considered incubation. So, once we hit 6+, that's when | |
symptoms will show up. | |
!!! | |
""" | |
INCUBATION_DAYS = 5 | |
#--------------------------------- | |
# Contagious Period | |
#--------------------------------- | |
""" | |
We need to know how long someone will be contagious, so we can track | |
how long they have to spread the virus. | |
https://www.health.harvard.edu/diseases-and-conditions/if-youve-been-exposed-to-the-coronavirus | |
'We know that a person with COVID-19 may be contagious 48 to 72 hours | |
before starting to experience symptoms. Emerging research suggests that | |
people may actually be most likely to spread the virus to others during | |
the 48 hours before they start to experience symptoms.' | |
'Most people with coronavirus who have symptoms will no longer be contagious | |
by 10 days after symptoms resolve. People who test positive for the virus | |
but never develop symptoms over the following 10 days after testing are | |
probably no longer contagious, but again there are documented exceptions. | |
So some experts are still recommending 14 days of isolation.' | |
So, what this tells us is that "it's complicated", b/c it depends on if | |
someone shows symptoms or not. | |
They're contagious 2 to 3 days before they show symptoms. With an avg | |
incubation time of 5 days, that means they will become contagious within | |
2 to 3 days after they get the virus (5 - 2 or 3 = 2 or 3). If we just | |
go with 2 days before symptoms, then we can get: | |
Incubation Days - Contagious Lead = 5 - 2 = 3 days to incubate w/o contagiousness | |
total days 1 2 3 4 5 6 7 8 9 10 11 12 13 | |
incubation days 1 2 3 4 5 - - - - - - - - | |
contagious days - - - 1 2 3 - - - - - - - | |
The reason they only have 3 contagious days is because model will | |
have them try to infect only 1 person per-day, and after 3 days | |
they have hit their Transmission Rate (R0) of 3. | |
But, theoretically, in real life a person may be incubating and | |
then contagious for up to 14 days. this is why they suggest people | |
quarantine for 14 days, b/c if you don't have symptoms after 14 | |
days (to err on the side of caution), then you will most likely | |
not be contagious after 14 days. | |
Symtomatic people are contagious during their recovery time + 10 days after | |
recovery. And, it depends on mild vs. severe symptoms... | |
'It depends on how sick you get. Those with mild cases appear to recover | |
within one to two weeks. With severe cases, recovery can take six weeks | |
or more. According to the most recent estimates, about 1% of infected | |
persons will succumb to the disease.' | |
mild = 2 wks * 7 days = 14 days w/symptoms + 10 days after symptoms = 24 days contagious | |
severe = 6 wks * 7 days = 42 days w/symptoms + 10 days after symptoms = 52 days contagious | |
so... | |
contagious days = incubating but contagious overlap days + recovery days + 10 days after recovery | |
If we go with 2 days incubating and contagious, then we get the following breakdown | |
asymptomatic = 2 incubating but contagious + 0 recovery + 10 after recovery = 12 days contagiuos | |
mild = 2 incubating but contagious + 14 recovery + 10 after recovery = 26 days contagious | |
severe = 2 incubating but contagious + 42 recovery + 10 after recovery = 54 days contagious | |
Since we're going to use the number as a countdown, we'll have to | |
kick it off after "incubation days - incubation but not contagious days" | |
In this case 5 - 2 = 3 days.. so 3 days a person will incubate while | |
not being contagious.. and then next day they will become contagious | |
and their contagious days will get added to them and start counting down. | |
During that time they will (obviously) be contagious, and we will | |
then run chances of them getting others infected. | |
""" | |
""" | |
!!! | |
Decided to simplify this down and skip the 'recovery days.' | |
Once a person hits R0, they move to outcome, which eliminates | |
them from processing in order to save cpu cycles. | |
!!! | |
""" | |
INCUBATION_AND_CONTAGIOUS_DAYS = 2 | |
INCUBATION_NOT_CONTAGIOUS_DAYS = INCUBATION_DAYS - INCUBATION_AND_CONTAGIOUS_DAYS | |
RECOVERY_DAYS_ASYMPTOMATIC = 0 | |
RECOVERY_DAYS_MILD = 14 # 2 wks * 7 | |
RECOVERY_DAYS_SEVERE = 42 # 6 wks * 7 | |
RECOVERY_DAYS_POST = 10 # 10 days after recovery needed to no longer be contagious | |
DAYS_CONTAGIOUS_ASYMPTOMATIC = INCUBATION_AND_CONTAGIOUS_DAYS + RECOVERY_DAYS_ASYMPTOMATIC + RECOVERY_DAYS_POST | |
DAYS_CONTAGIOUS_MILD = INCUBATION_AND_CONTAGIOUS_DAYS + RECOVERY_DAYS_MILD + RECOVERY_DAYS_POST | |
DAYS_CONTAGIOUS_SEVERE = INCUBATION_AND_CONTAGIOUS_DAYS + RECOVERY_DAYS_SEVERE + RECOVERY_DAYS_POST | |
# make a list out of it, so we can use | |
# person.symptoms value as index to look up value | |
DAYS_CONTAGIOUS = [ DAYS_CONTAGIOUS_ASYMPTOMATIC, DAYS_CONTAGIOUS_MILD, DAYS_CONTAGIOUS_SEVERE ] | |
# every day that goes by, a person's days_incubated goes down | |
# if it is <= INCUBATION_NOT_CONTAGIOUS_DAYS | |
#--------------------------------- | |
# Person Attributes | |
#--------------------------------- | |
""" | |
We use a person "object" to track people | |
in population bumping around. We use int | |
variables to track certain things, so | |
let's setup some constants to make it | |
easier to refer to them in english instead | |
of having to remember what their numbers are. | |
Originally created an actual person object, | |
but it ran very slow. So, then I switched to | |
tracking each person as a dict, but that was | |
also slower then just tracking them as lists. | |
Also, tried using enums to track person | |
attributes, but that ran 3x slower then just | |
one-off constant variables. | |
The irony of python is if you code properly, | |
eg: create objects/classes, use enums, etc like | |
you should, everything runs a lot slower. That's | |
why so many people find hacks and shortcuts to | |
improve performance. But, it can make code look | |
like a hot mess that's hard to understand. | |
Since I need brute force performance to process | |
millions of people in the model, I'm ignoring | |
"form over function" and instead going with | |
basic, brute force function over elegant form. | |
However, I'm still opting for code that's easy | |
to read. Hence I'm using if/else's and such | |
instead of trying to find hackish ways to | |
get a bit more speed by making the code more | |
obtuse. | |
STATUS | |
0 = uninfected | |
1 = infected (today, skip incubation today) | |
2 = incubating | |
3 = contagious (incubated to point of contagious, but might not be showing symptoms yet) | |
4 = recovered | |
5 = dead | |
INCUBATE | |
how many days they've incubated, up to INCUBATION_DAYS | |
SYMPTOMS | |
0 = asymotpmatic (or never infected if status = 0 uninfected) | |
1 = mild | |
2 = severe (needs hospitalization, possible death on outcome) | |
TRANSMIT | |
TRANSMISSION_RATE people person can transmit to. | |
Represents a percentage, wtih each infection attempt | |
dropping the transmit value by 100% if it's 1+, | |
or using the 0.x leftover decimal if less then 1. | |
EG: Coronavirus has transmission rate of 3. which | |
means a contagious person passes the virus on to | |
3 other people (on average). We can consider that | |
to be 3 people * 100% chance to infect. In our | |
model, we have a contagious person attempt to infect | |
only 1 person per-day. So, we just go 1 person * 100% | |
and then reduce the TRANSMIT down from 3 to 2. | |
If we were dealing with decimal transmission rates, | |
like the 1.3 for the flu, then a contagious | |
person could 100% try to infect 1 person, and then | |
30% try to infect another person. Sum 100% + 30% | |
and you get the 1.3 transmission rate. | |
This gives us a base chance to infect another person, | |
thought. It gets hindered by counter measures, | |
vaccine immunity, and the infected person's own | |
extra counter measures they might take via symptoms. | |
(EG: a severely infected, hospitalized person won't | |
be out and about much, so they have extra counter | |
measures that lower their risk of infecting others.) | |
""" | |
# person attribute constants to easily | |
# refer to the array columns in the code | |
STATUS = 0 | |
INCUBATE = 1 | |
TRANSMIT = 2 | |
# person.status | |
UNINFECTED = 0 # has not been infected yet | |
INFECTED = 1 # infected today, skip incubating until tomorrow | |
INCUBATE_NOT_CONTAGIOUS = 2 # incubating, but not contagious yet | |
INCUBATE_CONTAGIOUS = 3 # incubating, and contagious (silent transmission) | |
SYMPTOMS_CONTAGIOUS = 4 # done incubating, contagious w/ symptoms (symptom counter measures kick in) | |
RECOVERED = 5 # no longer contagious, outcome = recovered | |
DEAD = 6 # no longer contagious, outcome = death | |
# we need to check if someone is incubating | |
# or contagious. So, create some tuples | |
# that will make it easier to "if x in y" | |
STATUS_INCUBATING = ( INCUBATE_NOT_CONTAGIOUS, INCUBATE_CONTAGIOUS ) | |
STATUS_CONTAGIOUS = ( INCUBATE_CONTAGIOUS, SYMPTOMS_CONTAGIOUS ) | |
# person.symptoms | |
# we're blowing out system memory with | |
# very large populations, so we're going to | |
# get rid of the dedicated "SYMPTOMS" attr | |
# on people, and just re-use "INCUBATE" | |
# attr. Initially, "INCUBATE" will track | |
# # of days incubating. After person has | |
# finished incubating, then attr will | |
# shift over to a value for their SYMPTOM. | |
# Incubation days will be positive integers, | |
# and symptoms will be negative. We can just | |
# do an abs() or ( x * -1) to convert it to | |
# positive during processing. We can | |
# determine if person is incubating or | |
# symptomatic via STATUS which reflects | |
# such, so INCUBATE is just a tracking | |
# sub-variable | |
ASYM = 0 # no symptoms | |
MILD = -1 # mild symptoms | |
SEVR = -2 # severe symptoms, needs hospitalization, CASE_FATALITY_RATE can cause death when determing outcome | |
#--------------------------------- | |
# Symptom Ratios | |
#--------------------------------- | |
""" | |
It's still unclear how many people that get the virus can become | |
asymptomatic, and whether they can spread the virus. Because, | |
there seems to be conflicting studies. | |
The issue is viral load in the human body is not an "on/off" switch. | |
A person may have the virus, but not show symptoms (personal immunity | |
and true asymptomatic). Or, they may show such mild symptoms that they | |
don't think they have it. Or, they think it's something else | |
(eg: seasonal allergies, common migraines they usually get, etc). | |
I stared at a few sources, but overall this article sums things up: | |
https://www.sciencealert.com/a-physician-answers-5-questions-about-asymptomatic-covid-19 | |
From the variuos studies it references, we have a range of asymptomatic | |
in sample groups from 10% to 50% high. | |
And Icelandich study suggests 50% of people tested were asymptomatic: | |
https://www.healthline.com/health-news/50-percent-of-people-with-covid19-not-aware-have-virus | |
The Diamond Cruise study suggests 17% asymptomatic: | |
https://med.stanford.edu/content/dam/sm/id/documents/COVID/AsymptCOVID_TransmissionShip.pdf | |
CDC in this article suggests 1 in 4 people in US could be asymptomatic: | |
https://www.livescience.com/undetected-infections-coronavirus-widespread.html | |
The problem we're facing is that most people that get tested are ones | |
that present themselves with symptoms. Folks that don't have symptoms | |
are not likely to go get tested, b/c they think they're fine. | |
So, studies that focus on a proactive testing of a sample, even if some | |
in the sample are not showing symptoms, seem to be the best to go off | |
of. So, the Icelandic study of mass testing of a sample size is | |
a pretty decent one to take into account. | |
I would also take what the CDC says into account, b/c they're | |
aggregating all kinds of study and research into models. | |
So, I would put asymptomatic between the 25% the CDC says, and the | |
50% Icelandic study says. | |
The other caveat to this is that there could be factors in each | |
population that cause more or less transmission, infection, asymptomatics, | |
etc. | |
EG: The Icelandic study may later find out there's some segment of | |
population genetically predisposed to dealing with the virus easier. | |
This may attribute to why they had 50% asymptomatic. There could | |
be a larger concentration of those genetically predisposed asymptomatics | |
in their population. Meanwhile, in the US, we might have more folks | |
genetically predisposed to get symptoms. | |
The other factor is sample bias. If Iceland truly did a simple random | |
sample, then 50% would be a decent number to go with.. for their | |
population. But, their population is not the same mix as, say, | |
the United States. (Again, might be enough of a genetic difference | |
in population to create differences, hence why each country, state, | |
region, etc would need to do mass testing on their own). | |
The problem the CDC may be facing is they could be drawing conclusions | |
from their own biased samples, which would be all the reactive testing | |
going on in the states from people presenting themselves. This would | |
then make sense why the asymptomatic is lower in the US then Iceland, | |
b/c, again, if someone doesn't feel sick, they most likely won't go | |
get a test. So, we could be missing some 25% of asymptomatic people | |
in the US that haven't tested. | |
But, for now, we'll just go with a 25% of infected become asymptomatic | |
per CDC, since we're doing US model. | |
For the other 75% that get infected, this article suggests | |
80% of them end up with mild symptoms, which means 20% end | |
up with severe symptoms: | |
https://www.healthline.com/health-news/50-percent-of-people-with-covid19-not-aware-have-virus | |
Mild = 0.75 * 0.8 = 0.60 = 60% of total infected | |
Severe = 0.75 * 0.2 = 0.15 = 15% of total infected | |
So, we have the following breakdown: | |
-------------------------- | |
Infected Symptoms (United States) | |
-------------------------- | |
25% asymptomatic | |
60% mild | |
15% severe (need hospitalization) | |
-------------------------- | |
The next issue is what is the chance an asymptomatic person can | |
transmit the virus. There seems to be conflicting sources. | |
This article by UC Davis suggests asymptomatic can transmit, | |
but in very limited fashion: | |
https://health.ucdavis.edu/coronavirus/resources/covid-19-faqs-for-health-professionals.html | |
This article has the CDC director saying the 1 in 4 asymptomatic | |
could be leading widespread transmission of the virus: | |
https://www.sciencealert.com/here-s-what-we-know-so-far-about-those-who-can-pass-corona-without-symptoms | |
Plenty of articles are saying that people are most contagious | |
during the 1 to 2 day incubation period before they start | |
showing symptoms. (in our model that would be the INCUBATION_AND_CONTAGIOUS_DAYS). | |
We're not going to get into a bunch of % chances to transmit, | |
and have them decrease over time. | |
Instead, we're going to go with the TRANSMISSION_RATE used | |
up evenly over time until it's gone and person has outcome. | |
TRANSMISSION_RATE is basically an average number of people | |
someone can infect. | |
our model handles this by right-skewing by front-loading | |
the infection chances earlier rather then later. | |
But, doing it this way for now means: | |
1) the model remains simple | |
2) it front-loads infection chance to others by having them try | |
to give it to 3 others over 3 days if possible which goes | |
along with the notion that a person is most infectious | |
during their pre-symptomatic incubation days | |
(IE: INCUBATION_AND_CONTAGIOUS_DAYS) | |
""" | |
ASYM_RATIO = 0.25 | |
MILD_RATIO = 0.60 | |
SEVR_RATIO = 0.15 | |
# these are <= in model, EG: if random() <= ASYM_THRESHOLD | |
ASYM_THRESHOLD = ASYM_RATIO # range 0 to 25% | |
MILD_THRESHOLD = ASYM_RATIO + MILD_RATIO # range 25% to 85% | |
#SEVR_THRESHOLD = 1 - MILD_THRESHOLD # range 85%+ (not used, b/c it's the "else" part of the if statement) | |
#--------------------------------- | |
# Symptom-specific Counter Measures | |
#--------------------------------- | |
""" | |
when people get sick, they tend to convalesce, | |
more so as they get sick more. We don't | |
have any hard metrics for how much people with | |
symptoms convalesce, but we can make some | |
reasonable assumptions. | |
asymptomatic ... wouldn't know they are | |
ill (no symptoms), so they won't change their | |
behavior. So, they won't take additional counter | |
measures. | |
mild (doesn't need hospitalization) will | |
most likely stay home more, possibly trying to | |
self-quarantine just in case. So, they take more | |
counter measures. | |
severe is considered hospitalized in this model | |
(admitted, and either on oxygen or moved to a | |
ventilator). They will obviously be put under | |
heavier counter measures by the hospital to keep | |
others from getting sick. But, they are not | |
completely isolated; there is still a chance | |
someone could get infected from them. (we have | |
nurses and doctors getting infected from working | |
with covid patients). So, their counter measures | |
will be greatly increased, but still pose a | |
chance of infection. | |
So, we're going to have each symptom type | |
apply a multiplier to the infection chance to | |
possibly cut it down more to simulate more | |
counter measures taken by someone that's ill. | |
By using a multiplier, we can ensure that | |
we reduce infection chance, but it will never | |
go to 0% by just convalescing. | |
Also note that since a patient is infectious | |
after 3 days in this model, and then show | |
symptoms after 5 days, they will do silent | |
transmission for 2 days before showing symptoms | |
and having addt'l counter measures kick in. | |
So like this... | |
day 0 .. infected, but not incubating | |
day 1 .. incubating, not contagious | |
day 2 .. incubating, not contagious | |
day 3 .. incubating, not contagious | |
day 4 .. incubating, contagious, silent transmission (most dangerous) | |
day 5 .. incubating, contagious, silent transmission (most dangerous) | |
day 6 .. symptoms, contagious, addt'l counter measures (if any) | |
day 7 .. already hit transmission rate, apply outcome and skip going fwd | |
""" | |
ASYM_COUNTER_MEASURES_MULTIPLIER = 0.0 # asymp don't do anything different | |
MILD_COUNTER_MEASURES_MULTIPLIER = 0.5 # mild quarantine more and limit movement | |
SEVR_COUNTER_MEASURES_MULTIPLIER = 0.9 # severe are hospitalized under greater counter measures | |
# make a tuple out of these, so we can | |
# use the person.symptom int value as an | |
# index to look up the symptom counter | |
# measure value. | |
SYMPTOM_COUNTER_MEASURES = \ | |
( | |
ASYM_COUNTER_MEASURES_MULTIPLIER, | |
MILD_COUNTER_MEASURES_MULTIPLIER, | |
SEVR_COUNTER_MEASURES_MULTIPLIER | |
) | |
# we're tracking counter measures as | |
# "more is better", but using them in | |
# "less blocks more infection", so we | |
# invert them when we use them in | |
# 1 - CM fashion. So, we can pre-calc | |
# that here to save some processing | |
ASYM_COUNTER_MEASURES_MULTIPLIER_INV = 1.0 - ASYM_COUNTER_MEASURES_MULTIPLIER | |
MILD_COUNTER_MEASURES_MULTIPLIER_INV = 1.0 - MILD_COUNTER_MEASURES_MULTIPLIER | |
SEVR_COUNTER_MEASURES_MULTIPLIER_INV = 1.0 - SEVR_COUNTER_MEASURES_MULTIPLIER | |
# tuple for inverted symptom cm's | |
SYMPTOM_COUNTER_MEASURES_INVERSE = \ | |
( | |
ASYM_COUNTER_MEASURES_MULTIPLIER_INV, | |
MILD_COUNTER_MEASURES_MULTIPLIER_INV, | |
SEVR_COUNTER_MEASURES_MULTIPLIER_INV | |
) | |
#--------------------------------- | |
# Community Simulation | |
#--------------------------------- | |
""" | |
The problem with creating a massive population | |
is that we treat everyone like they can instantly | |
bump into anyone else in the model. This isn't | |
true in real life. | |
EG: if we're simulating US population, a person | |
in Florida that's contagious can't instantly | |
infect someone that's in California. | |
To work around this, we create many smaller | |
populations to run them at the same time, but | |
use a TRAVEL_CHANCE to have infectors choose | |
to infect locally vs. remotely. | |
The TRAVEL_CHANCE will act as another counter | |
measure, because it can keep an outbreak | |
contained in a population if there's a low | |
travel chance. | |
This would simulate cities or states deciding | |
to close borders and preven travel in order | |
to isolate and contain the outbreak. | |
The pop sizes we create depend on what we're | |
trying to simualte. | |
EG: Instead of doing a single population for | |
the US, we could do a population per-state, | |
and see how they spread. | |
I live in Dallas / Fort-Worth TX metroplex, | |
so instead of doing DFW as a single population, | |
we can break it out into the many suburbs and | |
small towns that make up the 6M+ people. | |
And, we don't have to be specificially accurate | |
here. | |
For instance, for DFW, we can just use an | |
assumed "community size" to divide the population | |
by. | |
EG: 6M divided by, say, 20 townships... that | |
would give us ~300K per township in the metroplex. | |
That would be good enough to get a ballpark | |
idea of how the virus might spread in the | |
metroplex if we implement various counter | |
measures and travel restrictions. | |
TRAVEL_CHANCE | |
How this will work is when an infector is | |
up for infecting someone, they will first | |
roll a dice to see if they fall within (<=) | |
TRAVEL_CHANCE. If they do, then they went | |
outside their community / town to remotely | |
infect. Otherwise, they stayed local. | |
So, TRAVEL_CHANCE of 0%, it means complete | |
lockdown & no travel. | |
TRAVEL_CHANCE is dependant on the populations | |
we're trying to simulate. For instance, | |
it's far more likely that people within | |
DFW will travel amongst townships, so if | |
we were modelling that we'd use a higher | |
TRAVEL_CHANCE. But, if we were modelling | |
the US States, we'd use a lower travel | |
chance, since many people are not travelling | |
outside their state on a regular basis. | |
If we really wanted to start getting fancy, | |
we'd apply a separate TRAVEL_CHANCE to each | |
population, and go even further to do | |
network analysis by assigning certain | |
populations connecting more then others. | |
EG: Certain cities live along highways | |
that act as key arteries / supply lines | |
in the US. So, they would be much more | |
likely to have people travel back-n-forth | |
then, say, some remote rural area hardly | |
anyone visits. | |
That is going beyond this simulation, | |
though. For now, we just want a basic | |
model we can tweak some easily-understood | |
values on. | |
""" | |
# single-digit travel chance for state-to-state | |
TRAVEL_CHANCE = 0.05 # 5% chance person was remote instead of local when infecting | |
# higher travel chance for city-to-city | |
#TRAVEL_CHANCE = 0.5 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
########################################################## | |
# Viral Model - Functions / Methods | |
########################################################## | |
# | |
# Copyright Craig Critchfield 2020 | |
# | |
########################################################## | |
import model_constants as mc | |
import model_populations as mp | |
from random import random | |
#------------------------------ | |
# Infect | |
#------------------------------ | |
""" | |
Helper functions to kick off | |
infection in people. | |
""" | |
# infect person, and rack up | |
# new infection counter | |
# newly infected will skip incubation today | |
def infect( person, dailystats ): | |
dailystats["new_infected"] += 1 | |
person[mc.STATUS] = mc.INFECTED | |
# find random initial person | |
# and infect them to kick off first wave | |
def infectpatientzero( totalpops, populations ): | |
if totalpops > 1: | |
select = mp.randompopulation( totalpops ) | |
population = populations[ select ] | |
else: | |
population = populations[ 0 ] | |
people = population["people"] | |
random_person = mp.randomperson( population["size"] ) | |
patient_zero = people[ random_person ] | |
infect( patient_zero, population["dailystats"] ) | |
# find someone uninfected and infect them | |
# to kick off new wave of virus | |
# this represents virus coming back | |
# into population from outside source | |
# and will then play out inside pop | |
# once again | |
def infectnextwave( totalpops, populations ): | |
if totalpops > 1: | |
select = mp.randompopulation( totalpops ) | |
population = populations[ select ] | |
else: | |
population = populations[ 0 ] | |
people = population["people"] | |
size = population["size"] | |
while True: | |
select = mp.randomperson( size ) | |
person = people[ select ] | |
if person[mc.STATUS] == mc.UNINFECTED: | |
infect( person, population["dailystats"] ) | |
break | |
#------------------------------ | |
# Determine Symptoms | |
#------------------------------ | |
""" | |
When person past incubation period, | |
determine if Asymp, Mild or Severe | |
symptom breaks down as follows | |
====================== | |
0-25% ... 25% asymptomatic | |
25-85%... 60% mild | |
85%+ ... 15% severe (need hospitalization) | |
====================== | |
100% total | |
we can generate a random 0.0 to 1.0 number, | |
then if/else it to add a symptom based on | |
cumulative category values | |
""" | |
def symptoms( person, dailystats ): | |
category = random() # random number from 0.0 to 1.0 | |
if category <= mc.ASYM_THRESHOLD: # range 0.00 to 0.25 asymp | |
dailystats["new_asym"] += 1 | |
dailystats["asym_contagious"] += 1 | |
person[mc.INCUBATE] = mc.ASYM | |
elif category <= mc.MILD_THRESHOLD: # range 0.25 asymp to (0.25 asymp + 0.60 mild) = range 0.25 to 0.85 | |
dailystats["new_mild"] += 1 | |
dailystats["mild_contagious"] += 1 | |
person[mc.INCUBATE] = mc.MILD | |
else: # range (0.25 asymp + 0.60 mild) to 1.00 = range 0.85 to 1.00 | |
dailystats["new_sevr"] += 1 | |
dailystats["sevr_contagious"] += 1 | |
person[mc.INCUBATE] = mc.SEVR | |
#------------------------------ | |
# Incubation Cycle | |
#------------------------------ | |
""" | |
if someone infected previous day, kick off incubation now. | |
otherwise, incubate people that have been incubating. | |
once they hit incubation days where they're contagious, | |
they'll start possibly infecting others (silent transmission). | |
when they hit max incubation days, they're assigned symptoms. | |
We do an initial incubation pass, so if someone up the chain | |
infects someone down the chain, we don't want that person | |
down the chain incubating the same day they got infected. | |
We want them to spend a day getting infected, then next | |
day+ they're incubating. | |
""" | |
# if incubating, determine when | |
# contagiuos and when symptoms hit | |
def incubate( population ): | |
dailystats = population["dailystats"] | |
people = population["people"] | |
for person in people: | |
# if person was infected yesterday | |
# flag them to start incubating today | |
# and add INCUBATE attr to person list | |
if person[mc.STATUS] == mc.INFECTED: | |
person[mc.STATUS] = mc.INCUBATE_NOT_CONTAGIOUS | |
person.append( 0 ) | |
# if person is in an incubating status | |
# rack up incubation days | |
if person[mc.STATUS] in mc.STATUS_INCUBATING: | |
person[mc.INCUBATE] += 1 | |
# incubated to point of being contagious | |
# flip status, rack up metrics, add TRANSMIT attribute | |
if person[mc.STATUS] == mc.INCUBATE_NOT_CONTAGIOUS \ | |
and person[mc.INCUBATE] > mc.INCUBATION_NOT_CONTAGIOUS_DAYS: | |
person[mc.STATUS] = mc.INCUBATE_CONTAGIOUS | |
dailystats["incu_contagious"] += 1 | |
person.append( mc.TRANSMISSION_RATE ) | |
# incubated to point of showing symptoms | |
# flip status, rack up metrics, flip INCUBATE to track symptom | |
elif person[mc.STATUS] == mc.INCUBATE_CONTAGIOUS \ | |
and person[mc.INCUBATE] > mc.INCUBATION_DAYS: | |
person[mc.STATUS] = mc.SYMPTOMS_CONTAGIOUS | |
dailystats["incu_contagious"] -= 1 | |
symptoms( person, dailystats ) | |
#------------------------------ | |
# Determine Outcomes | |
#------------------------------ | |
""" | |
Once person has possibly infected up to | |
TRANSMISSION_RATE of other people, we'll | |
jump right to an outcome for them to take | |
them out of processing. | |
In real life, mild symptoms would take | |
up to 2 weeks or so to recover while | |
severe cases would take up to 5 weeks. | |
Since we're modelling transmission based | |
on transmission rate, not how long someone | |
is recovering, then after they infect | |
up to TRANSMISSION_RATE, we can just | |
take them out of play. | |
People with severe symptoms (hospitalized) | |
have CASE_FAtALITY_RATE chance to die | |
upon outcome. Otherwise, they'll recover. | |
And, folks that are Asymptomatic or suffering | |
from mild symptoms are considered to automatically | |
recover as well. | |
""" | |
def outcomes( population ): | |
dailystats = population["dailystats"] | |
people = population["people"] | |
for person in people: | |
# if person in a contagious status | |
# and done transmitting to others | |
if person[mc.STATUS] in mc.STATUS_CONTAGIOUS \ | |
and person[mc.TRANSMIT] == 0: | |
# done with TRANSMIT field, so delete it | |
# asymptomatic & mild auto-recover. | |
# severe has chance of death per CFR | |
del person[mc.TRANSMIT] | |
symptom = person[mc.INCUBATE] | |
recovered = True | |
if symptom == mc.ASYM: | |
dailystats["asym_contagious"] -= 1 | |
elif symptom == mc.MILD: | |
dailystats["mild_contagious"] -= 1 | |
elif symptom == mc.SEVR: | |
dailystats["sevr_contagious"] -= 1 | |
if random() <= mc.CASE_FATALITY_RATE: | |
recovered = False | |
if recovered: | |
dailystats["new_recovered"] += 1 # rack up recovered count | |
person[mc.STATUS] = mc.RECOVERED | |
else: | |
dailystats["new_dead"] += 1 # rack up death toll | |
person[mc.STATUS] = mc.DEAD | |
#------------------------------ | |
# Counter Measures & Vaccine Immunity | |
#------------------------------ | |
""" | |
Counter Measures in model might change | |
as days go by, and Vaccine Immunity | |
can kick in on a certain day and need | |
to rack up over time. So, we need | |
to re-calc them on a daily basis. | |
""" | |
# if population is doing vaccine (VACCINE_DAY > 0), | |
# and we've hit day it kicks in | |
def vaccineimmunity( dailystats ): | |
# if mc.VACCINE_DAY > 0 \ | |
# and dailystats["sum_days"] >= mc.VACCINE_DAY: | |
if dailystats["sum_days"] >= mc.VACCINE_DAY > 0: | |
# add more immunity, but cap at 100% | |
dailystats["vaccine_immunity"] += mc.VACCINE_IMMUNITY_INCREMENT | |
dailystats["vaccine_immunity"] = max( 1.0, dailystats["vaccine_immunity"] ) | |
# want to have counter measures | |
# dynamically change over the days | |
# https://covid19.healthdata.org/united-states-of-america/texas | |
# worldometer tracks when counter measures started | |
# impacting TX, and when they drop off | |
# we can use that to determine some counter | |
# measure time lines via days that pass in sim | |
# https://www.worldometers.info/coronavirus/usa/texas/ | |
# texas virus deaths to compare by | |
""" | |
Populations can have different counter | |
measures kick in on different days. | |
So, we need to loop through the CM | |
day ranges to see what CM's apply for | |
the current day to the population. | |
Since we're doing a loop using i, | |
eventually we'll reach a 'IndexError: | |
list index out of range' error by | |
going beyond the range of days we | |
have entries for. When we do, we'll | |
just error trap that and return the | |
last counter measure value from | |
the population as an on-going, | |
final CM value. | |
We track the CM to use by looking | |
at the current day the sim is on | |
and comparing to the beginning and | |
ending date values, with ending | |
day being exclusive... | |
begin day <= current day < end day | |
So, if we had the following... | |
cm beg = 0, 30, 60, 90 | |
cm end = 30, 60, 90 | |
cm val = 0.0, 0.1, 0.2, 0.1 | |
The function would do the following... | |
if 0 <= current day < 30 | |
return 0.0 CM | |
if 30 <= current day < 60 | |
return 0.1 CM | |
if 60 <= current day < 90 | |
return 0.2 | |
if 90 <= current day < (IndexError) | |
go to error trap | |
return 0.1 as on-going final CM | |
""" | |
def countermeasures( population ): | |
dailystats = population["dailystats"] | |
daynow = dailystats["sum_days"] | |
daybeg = population["countermeasurebeg"] | |
dayend = population["countermeasureend"] | |
dayval = population["countermeasureval"] | |
lenbeg = len(daybeg) | |
try: | |
for i in range( lenbeg ): | |
if daybeg[i] <= daynow < dayend[i]: | |
dailystats["counter_measures"] = dayval[i] | |
except IndexError: | |
lenval = len(dayval) | |
dailystats["counter_measures"] = dayval[ lenval - 1 ] | |
""" | |
one-stop function that re-calc's | |
daily counter measures and vaccinne | |
immunity to create an overall CM | |
for the day for the population. | |
""" | |
def immunity( population ): | |
dailystats = population["dailystats"] | |
countermeasures( population ) | |
vaccineimmunity( dailystats ) | |
# counter measures are tracked in | |
# "more is better" % fashion, but | |
# are used to reduce infection %, | |
# so we need to 1-% to invert them | |
# into reduction multipliers. We | |
# can prep these values now to | |
# avoid doing it every time a | |
# contagious person calculates | |
# an infection chance. | |
inv_counter_measures = 1.0 - dailystats["counter_measures"] | |
inv_vaccine_immunity = 1.0 - dailystats["vaccine_immunity"] | |
# since infection chance, counter measures, | |
# vaccine immunity, etc are multiplying | |
# together, we can pre-mul the cm & vi | |
# into a single inverse metric now | |
# for later use. | |
dailystats["inverse_counter_measures"] = inv_counter_measures \ | |
* inv_vaccine_immunity | |
# to save cpu cycles, we can pre-calculate | |
# the cm's each contagious group will use, | |
# because folks with symptoms might get | |
# extra counter measures | |
asym = mc.SYMPTOM_COUNTER_MEASURES_INVERSE[ mc.ASYM ] | |
mild = mc.SYMPTOM_COUNTER_MEASURES_INVERSE[ mc.MILD ] | |
sevr = mc.SYMPTOM_COUNTER_MEASURES_INVERSE[ mc.SEVR ] | |
# incubating contagious don't receive any addtl | |
# counter measures | |
incu = dailystats["inverse_counter_measures"] | |
asym *= dailystats["inverse_counter_measures"] | |
mild *= dailystats["inverse_counter_measures"] | |
sevr *= dailystats["inverse_counter_measures"] | |
# plug them into our daily metrics | |
dailystats["inverse_counter_measures_incu"] = incu | |
dailystats["inverse_counter_measures_asym"] = asym | |
dailystats["inverse_counter_measures_mild"] = mild | |
dailystats["inverse_counter_measures_sevr"] = sevr | |
#------------------------------ | |
# Infection Chance | |
#------------------------------ | |
""" | |
Calculate chance contagious person has of | |
infecting another. This takes into account | |
their "transmit" value, counter measures, | |
vaccine immunity, and symptomatic counter | |
measures. | |
Since vaccine immunity & counter measures | |
are tracked in a "more is better" % value, | |
but are used to decrease the infection | |
chance %, we have to invert them via | |
( 1 - % ) to get the % amount they | |
decrease something. | |
EG: 75% counter measures will let | |
( 1 - 75% ) = 25% of the virus through | |
""" | |
def infectionchancebase( person ): | |
# if infector can infect 1 whole person | |
# use 100% infection chance | |
# and subtract 100% from transmit | |
if person[mc.TRANSMIT] >= 1: | |
basechance = 1.0 | |
person[mc.TRANSMIT] -= 1 | |
# otherwise, use whatever % they have left | |
# and zero out their transmit chances | |
# (they'll stop being contagious and | |
# have an outcome next day) | |
else: | |
basechance = person[mc.TRANSMIT] | |
person[mc.TRANSMIT] = 0 | |
return basechance | |
# apply global counter measures & | |
# vaccine immunity (using inverted CM). | |
# also, determine person's contagious status | |
# which helps us determine any extra | |
# symptom counter measures. Also see how | |
# much of the daily infection chance they | |
# contribute (infectchance * 1 person in pop ratio) | |
def infectionchance( person, dailystats, oneperson ): | |
if person[mc.STATUS] == mc.INCUBATE_CONTAGIOUS: | |
countermeasures = "inverse_counter_measures_incu" | |
onepersoninfect = "new_infect_chance_incu" | |
else: | |
symptom = person[mc.INCUBATE] | |
if symptom == mc.ASYM: | |
countermeasures = "inverse_counter_measures_asym" | |
onepersoninfect = "new_infect_chance_asym" | |
elif symptom == mc.MILD: | |
countermeasures = "inverse_counter_measures_mild" | |
onepersoninfect = "new_infect_chance_mild" | |
else: # if symptom == mc.SEVR: | |
countermeasures = "inverse_counter_measures_sevr" | |
onepersoninfect = "new_infect_chance_sevr" | |
# determine their infection chance | |
# and rack up their daily inf chance contribution | |
infectchance = infectionchancebase( person ) | |
infectchance *= dailystats[ countermeasures ] | |
dailystats[onepersoninfect] += ( infectchance * oneperson ) | |
return infectchance | |
#------------------------------ | |
# Transmission Cycle | |
#------------------------------ | |
""" | |
personA can transmit virus to another if... | |
* infected | |
* incubated to contagious (shedding virus cells) | |
* still have uninfected to transmit to | |
If able to transmit, find random personB, and | |
attempt to infect. It's an attempt, because | |
it might be blocked: | |
* counter measures lower infection chance, so chance to not transmit | |
* personB may already be infected, dead or recovered, so can't get infected again | |
""" | |
def transmit( population, population_index, totalpops, populations ): | |
travelchance = population["travelchance"] | |
dailystats = population["dailystats"] | |
people = population["people"] | |
person_index = 0 | |
for personA in people: | |
# if person is contagious | |
# see if they infect someone | |
if personA[mc.STATUS] in mc.STATUS_CONTAGIOUS: | |
infectchance = infectionchance( personA, dailystats, population["oneperson"] ) | |
# if random 0.0 to 1.0 within threshold | |
# infect someone, otherwise it's been averted | |
if random() <= infectchance: | |
# if doing 2+ pops | |
# and travelling | |
if totalpops > 1 \ | |
and random() <= travelchance: | |
# infect remotely | |
infectotherpopulation( totalpops, population_index, populations ) | |
else: | |
# infect locally | |
infectsamepopulation( population, person_index ) | |
# increment index to next person | |
person_index += 1 | |
# when infecting a different population | |
# exclude infector's population, | |
# but pick anyone in that other population | |
def infectotherpopulation( totalpops, population_index, populations ): | |
select = mp.randomexcludepopulation( totalpops, population_index ) | |
otherpop = populations[ select ] | |
select = mp.randomperson( otherpop["size"] ) | |
personB = otherpop["people"][ select ] | |
if personB[mc.STATUS] == mc.UNINFECTED: | |
infect( personB, otherpop["dailystats"] ) | |
# when infecting same population | |
# exclude infector from random person selection | |
def infectsamepopulation( samepop, person_index ): | |
select = mp.randomexcludeperson( samepop["size"], person_index ) | |
personB = samepop["people"][ select ] | |
# if they're uninfected, infect them | |
if personB[mc.STATUS] == mc.UNINFECTED: | |
infect( personB, samepop["dailystats"] ) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
########################################################## | |
# Viral Model - Debug Logging & Summary Outputs | |
########################################################## | |
# | |
# Copyright Craig Critchfield 2020 | |
# | |
########################################################## | |
import model_constants as mc | |
import model_utility as mu | |
#------------------------------ | |
# Compile Stats | |
#------------------------------ | |
""" | |
Take our daily stats, add the new | |
stuff to the sums, then copy it | |
all to the total stat tracker. | |
""" | |
def compilestats( population ): | |
dailystats = population["dailystats"] | |
# add pop name to the stats | |
dailystats["popname"] = population["name"] | |
# infected | |
dailystats["sum_infected"] += dailystats["new_infected"] | |
dailystats["sum_uninfected"] = population["size"] \ | |
- dailystats["sum_infected"] | |
# outcomes | |
dailystats["sum_recovered"] += dailystats["new_recovered"] | |
dailystats["sum_dead"] += dailystats["new_dead"] | |
dailystats["sum_outcome"] = dailystats["sum_recovered"] \ | |
+ dailystats["sum_dead"] | |
# symptoms | |
dailystats["sum_asym"] += dailystats["new_asym"] | |
dailystats["sum_mild"] += dailystats["new_mild"] | |
dailystats["sum_sevr"] += dailystats["new_sevr"] | |
# contagious | |
dailystats["sum_contagious"] = dailystats["incu_contagious"] \ | |
+ dailystats["asym_contagious"] \ | |
+ dailystats["mild_contagious"] \ | |
+ dailystats["sevr_contagious"] | |
# daily infection chance | |
dailystats["new_infect_chance"] = dailystats["new_infect_chance_incu"] \ | |
+ dailystats["new_infect_chance_asym"] \ | |
+ dailystats["new_infect_chance_mild"] \ | |
+ dailystats["new_infect_chance_sevr"] | |
# get max contagious & daily infection chance | |
# to easily track peak of outbreak | |
# mostly for summary output to terminal | |
dailystats["max_contagious"] = max( dailystats["max_contagious"], dailystats["sum_contagious"] ) | |
dailystats["new_infect_chance_max"] = max( dailystats["new_infect_chance"], dailystats["new_infect_chance_max"] ) | |
# reset certain values in | |
# dailystats for next day | |
# accumulation | |
def restartstats( population): | |
dailystats = population["dailystats"] | |
# reset new counts | |
dailystats["new_infected"] = 0 | |
dailystats["new_recovered"] = 0 | |
dailystats["new_dead"] = 0 | |
dailystats["new_asym"] = 0 | |
dailystats["new_mild"] = 0 | |
dailystats["new_sevr"] = 0 | |
dailystats["new_infect_chance_incu"] = 0 | |
dailystats["new_infect_chance_asym"] = 0 | |
dailystats["new_infect_chance_mild"] = 0 | |
dailystats["new_infect_chance_sevr"] = 0 | |
dailystats["new_infect_chance"] | |
#------------------------------ | |
# Debug Terminal Stats Output | |
#------------------------------ | |
# how long we want the | |
# "value = " part | |
#before showing the actual value | |
PADLEN = 20 | |
PADVAL = " " | |
# create string "name = value" with single var value | |
# padded "name =" to look more uniform in output | |
def modelsummaryvalue( varname, varval ): | |
strvar = varname.ljust( PADLEN, PADVAL ) | |
strval = str( varval ) | |
msgval = strvar + " = " + strval | |
return msgval | |
# create string "name = value" with dict value | |
# padded "name =" to look more uniform in output | |
def modelsummarystats( varname, dailystats ): | |
strvar = varname.ljust( PADLEN, PADVAL ) | |
strval = str( dailystats[varname] ) | |
msgval = strvar + " = " + strval | |
return msgval | |
# create string of "name = value (%)" | |
# padded "name =" to look more uniform in output | |
def modelsummaryratio( varname, dailystats, percent ): | |
strvar = varname.ljust( PADLEN, PADVAL ) | |
strval = str( dailystats[varname] ) | |
strper = str( percent ) | |
msgval = strvar + " = " + strval + " (" + strper + ")" | |
return msgval | |
# debug terminal output | |
def modelsummary( population, decimals ): | |
# from sys import getsizeof | |
# see how badly increasing pop | |
# sizes are blowing out my RAM. | |
# "We're gonna need a bigger boat!" | |
# mem_population = getsizeof( population ) | |
# mem_totalstats = getsizeof( totalstats ) | |
dailystats = population["dailystats"] | |
# total folks with symptoms assigned | |
sum_symptom = dailystats["sum_asym"] \ | |
+ dailystats["sum_mild"] \ | |
+ dailystats["sum_sevr"] | |
# symptom percents | |
percent_asym = mu.formatcalculatepercent( dailystats["sum_asym"], sum_symptom, decimals) | |
percent_mild = mu.formatcalculatepercent( dailystats["sum_mild"], sum_symptom, decimals) | |
percent_sevr = mu.formatcalculatepercent( dailystats["sum_sevr"], sum_symptom, decimals) | |
# severe cases still alive | |
sum_sevr_alive = dailystats["sum_sevr"] \ | |
- dailystats["sum_dead"] | |
# severe case alive / dead ratios | |
percent_sevr_alive = mu.formatcalculatepercent( sum_sevr_alive, dailystats["sum_sevr"], decimals ) | |
percent_sevr_dead = mu.formatcalculatepercent( dailystats["sum_dead"], dailystats["sum_sevr"], decimals ) | |
# population percentages | |
percent_pop_infected = mu.formatcalculatepercent( dailystats["sum_infected"], population["size"], decimals) | |
percent_pop_uninfected = mu.formatcalculatepercent( dailystats["sum_uninfected"], population["size"], decimals) | |
percent_pop_contagious = mu.formatcalculatepercent( dailystats["max_contagious"], population["size"], decimals) | |
# immunity ratios | |
percent_vaccine_immunity = mu.formatpercent( dailystats["vaccine_immunity"], decimals ) | |
percent_counter_measures = mu.formatpercent( dailystats["counter_measures"], decimals ) | |
percent_daily_inf_max = mu.formatpercent( dailystats["new_infect_chance_max"], decimals ) | |
# death ratios | |
percent_mortality_rate = mu.formatcalculatepercent( dailystats["sum_dead"], population["size"], decimals ) | |
from datetime import datetime | |
msg = [] | |
msg.append( mc.BORDER ) | |
msg.append( str(datetime.now()) ) | |
msg.append( modelsummaryvalue( "popname", population["name"] ) ) | |
msg.append( modelsummarystats( "wave", dailystats ) ) | |
msg.append( modelsummarystats( "sum_days", dailystats ) ) | |
msg.append( mc.BORDER ) | |
msg.append( modelsummaryvalue( "total_pop", population["size"] ) ) | |
msg.append( modelsummaryratio( "sum_infected", dailystats, percent_pop_infected ) ) | |
msg.append( modelsummaryratio( "sum_uninfected", dailystats, percent_pop_uninfected ) ) | |
msg.append( modelsummaryratio( "max_contagious", dailystats, percent_pop_contagious ) ) | |
msg.append( mc.BORDER ) | |
msg.append( " Herd Immunity @ " + str(percent_pop_infected) + " population" ) | |
msg.append( " Vaccine Immunity @ " + str(percent_vaccine_immunity) ) | |
msg.append( " Counter Measures @ " + str(percent_counter_measures) ) | |
msg.append( " Max Daily Inf Chance @ " + str(percent_daily_inf_max) ) | |
msg.append( mc.BORDER ) | |
msg.append( modelsummarystats( "sum_outcome", dailystats ) ) | |
msg.append( modelsummarystats( "sum_recovered", dailystats ) ) | |
msg.append( modelsummaryratio( "sum_dead", dailystats, percent_mortality_rate ) + " mortality rate" ) | |
msg.append( mc.BORDER ) | |
msg.append( modelsummaryvalue( "sum_symptom", sum_symptom ) ) | |
msg.append( modelsummaryratio( "sum_asym", dailystats, percent_asym ) ) | |
msg.append( modelsummaryratio( "sum_mild", dailystats, percent_mild ) ) | |
msg.append( modelsummaryratio( "sum_sevr", dailystats, percent_sevr ) ) | |
msg.append( mc.BORDER ) | |
msg.append( "sum_sevr_alive = " + str(sum_sevr_alive) + " (" + percent_sevr_alive + ")" ) | |
msg.append( "sum_sevr_dead = " + str(dailystats["sum_dead"]) + " (" + percent_sevr_dead + ")" ) | |
# print( mc.BORDER ) | |
# print("population RAM = " + mu.bytestring(mem_population) ) | |
# print("totalstats RAM = " + mu.bytestring(mem_totalstats) ) | |
msg.append( mc.BORDER ) | |
return msg | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
########################################################## | |
# COVID-19 Infection Model | |
########################################################## | |
# | |
# Copyright Craig Critchfield 2020 | |
# | |
########################################################## | |
""" | |
Main processing loop | |
""" | |
#------------------------------ | |
# TO DO - Possible Improvements | |
#------------------------------ | |
""" | |
* Resource .. to monitor RAM / CPU use | |
* use triangular() to give better range of | |
incubation days for each person | |
(would need to track incubate days | |
as a countdown instead of count-up | |
then, triggering contagious when it | |
hits < incubate contagious days) | |
* error trap memory error and revert to I/O | |
on disk as fallback instead of crashing | |
(disk I/O adds tons of time to sim run, | |
so not going to implement this.) | |
* travel chance per-population, and needs | |
to be two-way, IE: population won't let | |
folks leave or enter, so have to check | |
both ways. Or, take both pops' travel | |
chances and multiply together to get a | |
single chance for a person A to go | |
to another community and community B | |
to let them in. | |
* population interaction ratios to have | |
certain pops interact more then others | |
(probably way beyond this sim, b/c | |
that gets into advanced social | |
network stuff that other programs | |
are better at, and we'd need a | |
lot more research to see which | |
cities interact more then others.) | |
""" | |
#------------------------------ | |
# Imports | |
#------------------------------ | |
""" | |
chucked a lot of code into separate modules | |
to organize and tidy up, b/c this was getting | |
super-long. | |
""" | |
import model_constants as mc # import model constants | |
import model_functions as mf # import model functions | |
import model_logging as ml # import model logging functions | |
import model_utility as mu # import model utility functions | |
import model_populations as mp # import model population funcs and pre-cans | |
from process_timer import procTimer | |
#from os import getcwd | |
from memory_profiler import profile | |
#---------------------------------- | |
# Constants / Variables | |
#---------------------------------- | |
# get working dir, b/c we'll shove | |
# out logs and csv's to it | |
# nix ... kept using directory where code | |
# was called from. using a .bat file | |
# to run the code from pypy folder would | |
# cause it to dump things to pypy folder. | |
# don't want that, so just hard-coding the | |
# output folder. | |
#FILEPATH = getcwd() + "\\" | |
FILEPATH = "D:\\python\\projects\\viral model\\" | |
#---------------------------------- | |
# Main | |
#---------------------------------- | |
@profile | |
def main( dolog = True, docsv = True ): | |
percent_decimal_places = 1 | |
days_without_contagious = 0 | |
totalstats = [] | |
populations = [] | |
# first US case hit Jan 20, 2020 | |
# it's easier to track the model in 30 day | |
# month-to-month chunks, so we're going to | |
# assume model starts Feb+. We'll create | |
# counter measures for each month for US. | |
# it's important to note that with Transmission | |
# Rate (R0), what matters is if it's > 1, = 1 | |
# or < 1... | |
# > 1 = cases rising, contagious person infecting more folks then resolving | |
# = 1 = cases steady, contagious person infecting 1 other person then resolving | |
# < 1 = cases lowering, contagious person infecting < 1 other person. | |
# Coronavirus has a Transmission Rate of 3, and | |
# we consider that "100% chance to infect 3 people." | |
# to get it to = or < 1, we have to drop that 100% down | |
# per-person we use it on. We can easily figure out | |
# what we need to do to flatten the curve (R0 = 1) | |
# by dividing 100% by our Transmission Rate. | |
# 100% / 3 = 1 / 3 = 33% | |
# so, we need to multiply 100% by 33% to get 33% | |
# infection through... and that would be ... | |
# 3 people * 33% = 0.99 = 99% .. about 100% | |
# which would mean our R0 has flattened. | |
# since we're doing (1 - CM) to invert counter | |
# measure amount, we can do (1 - Infect Chance) | |
# to see what CM's we need to set. | |
# 1 - 33% = 66.6% = 67% counter measures to flatten curve | |
# CM's > 67% will lower R0 curve | |
# with that in mind, we can model out how the | |
# United States responded to the virus each month... | |
#---------------------------------- | |
# days month response | |
#---------------------------------- | |
# Jan .. late January, first cases | |
# 0 - 30 Feb .. few counter measures ("locked down China", no masks or distancing) | |
# 30 - 60 Mar .. some counter measures ("wait n see" stance, isolation, distancing, but no masks) | |
# 60 - 90 Apr .. lockdown (isolation, distancing & masks), curve flattening | |
# 90 - 120 May .. places opening up, people not wearing masks, curve lowering | |
# 120 - 150 Jun .. protests .. lots of congregation w/o masks | |
# 150+ Jul+ we're just going to go with Jun CM's going fwd | |
# we use June's CM going fwd, b/c a lot of states | |
# are now open and acting like the virus is done. | |
# (even though it was transmission of 1 at best in | |
# some places, lowering a bit on some places, and | |
# just a slower-rising curve in others.) Overall, | |
# the US showed lowering curve in March, but June | |
# is seeing rising cases, people congregating at | |
# protests and jobs, people not wearing masks, | |
# people protesting having to wear masks, etc. | |
# so, our model will just assume we're going fwd | |
# with low counter measures.. that might change | |
# when they see another spike hit August or so. | |
# Feb Mar Apr May Jun Jul Aug | |
countermeasurebeg = [ 0, 30, 60, 90, 120, 150, 180 ] | |
countermeasureend = [ 30, 60, 90, 120, 150, 180 ] | |
# no counter measures at all.. to model out worst-case scenario | |
# countermeasureval = [0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00 ] | |
# counter measures based on how US responded each month | |
# since symptoms also provide counter measures, we're not | |
# setting exactly 67% to get flattened curve.. as more folks | |
# go mild or severe, we automatically start isolating certain | |
# parts of population | |
countermeasureval = [0.00, 0.25, 0.60, 0.63, 0.45, 0.45, 0.45 ] | |
# if we had better proactive counter measures early on | |
# countermeasureval = [0.65, 0.65, 0.65, 0.65, 0.65, 0.65, 0.65 ] | |
#------------------------- | |
# multi-population experiment | |
#------------------------- | |
# generate population from pre-canned | |
# population = mp.loadpopulation( mp.test100, countermeasurebeg, countermeasureend, countermeasureval ) | |
# populations.append( population.copy() ) | |
# population = mp.loadpopulation( mp.test1K, countermeasurebeg, countermeasureend, countermeasureval ) | |
# populations.append( population.copy() ) | |
# population = mp.loadpopulation( mp.test10K, countermeasurebeg, countermeasureend, countermeasureval ) | |
# populations.append( population.copy() ) | |
# population = mp.loadpopulation( mp.test1M, countermeasurebeg, countermeasureend, countermeasureval ) | |
# populations.append( population.copy() ) | |
#------------------------- | |
# dallas / fort-worth pop as whole | |
#------------------------- | |
# population = mp.loadpopulation( mp.tx_dfw, countermeasurebeg, countermeasureend, countermeasureval ) | |
# populations.append( population.copy() ) | |
#------------------------- | |
# dallas / fort-worth multi-pop | |
#------------------------- | |
# load up 20 sub-communities (0 to 19) | |
# for Dallas / Fort-Worth, TX. | |
# rename them to keep track of each | |
""" | |
for i in range(20): | |
population = mp.loadpopulation( mp.tx_dfw_sub, countermeasurebeg, countermeasureend, countermeasureval ) | |
population["name"] = population["name"] + "_" + str(i) | |
population["travelchance"] = 0.05 # limited travel during pandemic | |
populations.append( population.copy() ) | |
""" | |
#------------------------- | |
# US multi-pop | |
#------------------------- | |
# US population is too big for my comp's RAM. | |
# stalls out while running with Mem Error. | |
# so need to use HDD I/O to read/write temp files. | |
# already loaded the state pop sizes into dicts | |
# and then loaded those into a list, so we can | |
# just iterate through the list to easily load the pops. | |
# !!!!!!!!!!!!!!!!!!! | |
# started updating code to do this, then once | |
# I got the read/write functions done decided to | |
# experiment with just California population | |
# of 39M. Making people for it, and saving | |
# to csv then reading back in just one time | |
# took 2.5 min... and that was just the file | |
# read / write; didn't include time it took | |
# to generate the population. | |
# for the model run, it'd have to handle | |
# read/writes of pops for each state, and | |
# do so for every "for pop in pops", so that's | |
# a boat-load of read/writes, disk thrashing, | |
# and tons of time. | |
# My computer can't handle this massive of | |
# population analysis, so I'm going to keep | |
# it more local, like TX or DFW, for now. | |
# since I can run that in RAM. | |
# !!!!!!!!!!!!!!!!!!! | |
diskIO = False | |
for state in mp.pop_US: | |
population = mp.loadpopulation( state, countermeasurebeg, countermeasureend, countermeasureval ) | |
if diskIO: | |
population["filepath"] = FILEPATH | |
population["filename"] = "temp_" + population["name"] + ".csv" | |
# dump out the people list to temp file | |
mu.outputcsv( population["people"], population["filename"], population["filepath"] ) | |
# purge people list | |
population["people"] = [] | |
# now we can append it to pops | |
population["travelchance"] = 0.05 # limited travel during pandemic | |
populations.append( population.copy() ) | |
#----------------------------- | |
# Main | |
#----------------------------- | |
print(mc.BORDER) | |
procTimer("run sim") | |
totalpops = len(populations) | |
wavenow = 1 # which wave we're on | |
wavemax = 2 # how many waves to do | |
# infect patient zero. | |
# if 2+ populations, pick a person from one at random | |
# otherwise pick a person from our only population at random | |
mf.infectpatientzero( totalpops, populations ) | |
#----------------------------- | |
# loop until out of contagious people | |
#----------------------------- | |
while True: | |
# model runs in 3 phase... | |
# phase 1: maintenance | |
for population in populations: | |
mf.immunity( population ) # re-calc counter measures for day | |
mf.outcomes( population ) # determine outcomes | |
mf.incubate( population ) # incubate folks a day | |
# phase 2: activity | |
# now that daily statuses are sorted | |
# we can run around and infect people | |
population_index = 0 | |
for population in populations: | |
mf.transmit( population, population_index, totalpops, populations ) | |
population_index += 1 | |
# phase 3: stats | |
# compile the daily numbers | |
# into totalstats tracker for output | |
total_contagious = 0 | |
for population in populations: | |
ml.compilestats( population ) | |
dailystats = population["dailystats"] | |
totalstats.append( dailystats.copy() ) | |
dailystats["sum_days"] += 1 | |
total_contagious += dailystats["sum_contagious"] | |
#------------------------------------- | |
# rack up how many days we've gone | |
# without someone being contagious | |
if total_contagious > 0: | |
days_without_contagious = 0 | |
else: | |
days_without_contagious += 1 | |
# if we've gone INCUBATION_DAYS w/o | |
# more contagious, then virus has | |
# snuffed out ... end sim | |
if days_without_contagious > mc.INCUBATION_DAYS: | |
# if we haven't hit our wavemax yet | |
# try to kick off another wave | |
if wavenow < wavemax: | |
wavenow += 1 | |
for population in populations: | |
population["dailystats"]["wave"] = wavenow | |
# infect next wave patient zero. | |
# if 2+ populations, pick a person from one at random | |
# otherwise pick a person from our only population at random | |
mf.infectnextwave( totalpops, populations ) | |
days_without_contagious = 0 | |
# otherwise, end sim | |
# and dump log file | |
else: | |
# output model summary to terminal | |
if dolog: | |
# dump summary to log file | |
import datetime | |
nowstamp = datetime.datetime.now() | |
nowstamp = nowstamp.strftime( "%Y-%m-%d_%H-%M-%S" ) | |
filename = "model_logging." + nowstamp + ".txt" | |
pathname = FILEPATH + filename | |
with open( pathname, 'a', newline = '' ) as txtfile: | |
for population in populations: | |
msg = ml.modelsummary( population, percent_decimal_places ) | |
# dump summary to debug terminal | |
for i in range( 0, len(msg) ): | |
print( msg[i] ) | |
txtfile.write( msg[i] + "\n" ) | |
# exit loop | |
# which ends sim | |
break | |
# if we're still looping, | |
# reset daily stat trackers | |
for population in populations: | |
ml.restartstats( population ) | |
#----------------------------- | |
procTimer("run sim") | |
# output stats to CSV | |
if docsv: | |
filename = "model_output.csv" | |
mu.outputcsv( totalstats, filename, FILEPATH ) | |
#--------------------------- | |
# Run Main | |
#--------------------------- | |
profile = False | |
if profile: | |
# profiling performance for bottlenecks | |
fileprof = "model_profile.prof" | |
filetext = "model_profile.txt" | |
mu.profilemain( FILEPATH, fileprof, filetext ) | |
else: | |
# not profiling, just run main | |
main() | |
#--------------------------- | |
# End | |
#--------------------------- | |
print(mc.BORDER) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
########################################################## | |
# Viral Model - Preset Populations | |
########################################################## | |
# | |
# Copyright Craig Critchfield 2020 | |
# | |
########################################################## | |
""" | |
This contains variables and functions | |
used to manage people and populations | |
for the model. | |
It also contains pre-canned population | |
data to make it easy to make copies | |
of populations into the model to work | |
with on-the-fly. | |
""" | |
#------------------------------ | |
# Imports | |
#------------------------------ | |
import model_constants as mc | |
from random import randrange | |
""" | |
NOTE - I originally used randint(start, end), | |
but profiling showed it added overhead to the | |
program as an intermediary to just call randrange. | |
So, I switched over to use randrange( start, end ). | |
But, when trying to pick between 2 populations in | |
a populations list of 2 (randrange( 0, 1)), it | |
would only return 0 and get stuck in a loop if | |
the 1st pop was the one to call it. | |
With some experimentation, I realized that | |
randint is end-inclusive while randrange is | |
end-exclusive. | |
EG: randint(0, 2) will spit out 0, 1, but | |
randrange(0, 2) will only spit out 0 & 1. | |
I guess that's because typically if you're | |
calling randrange it's because you're giving | |
it the len() of the range you're dealing with, | |
so an object with 100 elements would have a | |
len of 100. But, a range of 0 to 99. So, if | |
you wanted to pick a random value from the | |
range you'd want 0 to 99... and returning | |
100 would give you an error since you don't | |
have item 100 in your list. | |
Anyways, I retooled the code to use randrange. | |
It's better this way, b/c I don't have to | |
pre-calc an "end" for randint of "len()-1" | |
to keep from blowing past the max elements | |
in population & people lists. | |
But, this is something to note in case I | |
decide to change the code later. | |
""" | |
#------------------------------ | |
# Variables | |
#------------------------------ | |
""" | |
Daily Stats is a template for | |
tracking the day's metrics for | |
a population. When we create | |
a population, we make a copy | |
of it, so each population can | |
track their own metrics. At | |
the end of each day, it will | |
get copied to the pop's totalstats | |
list to create a "spreadsheet" | |
of what's happened. | |
""" | |
dailystats = { | |
"popname": "", | |
"wave": 1, | |
"sum_days": 0, | |
"sum_uninfected": 0, # total pop - sum_infected | |
"sum_infected": 0, | |
"new_infected": 0, | |
"sum_recovered": 0, | |
"new_recovered": 0, | |
"sum_dead": 0, | |
"new_dead": 0, | |
"sum_asym": 0, | |
"new_asym": 0, | |
"sum_mild": 0, | |
"new_mild": 0, | |
"sum_sevr": 0, | |
"new_sevr": 0, | |
"sum_outcome": 0, # sum_dead + sum_recovered | |
"counter_measures": 0, # "more is better" value | |
"vaccine_immunity": 0, # "more is better" value | |
"inverse_counter_measures": 0, # 1 - counter_measures, so we can reduce infection chance | |
"inverse_counter_measures_incu": 0, # pre-calc'ed for incu contagious | |
"inverse_counter_measures_asym": 0, # pre-calc'ed for asym symptoms | |
"inverse_counter_measures_mild": 0, # pre-calc'ed for mild symptoms | |
"inverse_counter_measures_sevr": 0, # pre-calc'ed for sevr symptoms | |
"inverse_vaccine_immunity": 0, # 1 - vaccine_immunity, so we can reduce infection chance | |
"new_infect_chance_incu": 0, | |
"new_infect_chance_asym": 0, | |
"new_infect_chance_mild": 0, | |
"new_infect_chance_sevr": 0, | |
"new_infect_chance": 0, # sum of incu + asym + mild + sevr infect chances | |
"new_infect_chance_max": 0, # max chance at peak of outbreak | |
"incu_contagious": 0, | |
"asym_contagious": 0, | |
"mild_contagious": 0, | |
"sevr_contagious": 0, | |
"sum_contagious": 0, # incu + asym + mild + sevr contagious | |
"max_contagious": 0 # peak contagious people during outbreak | |
} | |
#------------------------------ | |
# Load People into Population | |
#------------------------------ | |
""" | |
load uninfected people into population. | |
give them TRANSMISSION_RATE to individually | |
work down through as they possibly | |
infect others. | |
""" | |
# make a person list object out of person attributes. | |
# since our attr vals are 0 to 255, we can run | |
# it as a bytearray instead of a list which speeds | |
# up processing some | |
# !!! | |
# don't use bytearray... can't do floats, | |
# and if we use model for something with a | |
# transmission rate that isn't an integer | |
# eg: flu with R0 of 1.3 to 1.4, then bytearray | |
# blows up with error. So, just keep it a normal | |
# list | |
# !!! | |
# !!! | |
# just kick off uninfected folks with STATUS | |
# attribute.. we'll append more attributes to | |
# person as they get infected, but we want to | |
# min / max memory usage. | |
# !!! | |
def makeperson(): | |
status = mc.UNINFECTED | |
person = [ status ] | |
return person | |
# make a person, and use them to | |
# create the people list in population | |
def loadpeople( size ): | |
person = makeperson() | |
people = [] | |
for i in range( size ): | |
people.append( person.copy() ) | |
return people | |
#------------------------------ | |
# Generate Population | |
#------------------------------ | |
""" | |
We store precanned population names | |
and sizes, because we know what those | |
values are now. But, we load them into | |
a population dict when we run the model, | |
that way we can adjust transmission rate, | |
counter measures for the pop, etc. | |
"oneperson" | |
we rack up each person's infection chance | |
(chance when possibly infecting someone) | |
to see what the daily infection chance is. | |
(IE: to see "what the weather's like"). | |
to do that, we have to multiply the chance | |
they calculate when possibly infecting by | |
1 / POP_TOTAL, then we add it to their | |
contagious group's infection sum to see | |
how much infection chance each group is | |
giving, which we can then sum to a total | |
infection chance. So, we need to pre-calc | |
the % ratio of 1 person in the population. | |
"travelchance" | |
how likely someone in a population is to | |
go to a remote population to infect instead | |
of infecting locally. A population will | |
use "travelchance" to head out, but a | |
population will "defend" itself from | |
remote visitors via counter measures. | |
It stands to reason that a population | |
taking a pandemic seriuosly will both | |
limit travel from it's occupants and | |
increase counter measures to protect | |
them. But, we can model out odd pops | |
that decide to have high counter measures | |
to protect themselves, but also have | |
high travel chance letting them head | |
out and potentially spread. This would | |
be a place where they have a hard time | |
spreading in their local population, | |
but people are easily spreading it to | |
other populations (if those populations | |
don't have good counter measures | |
themselves). | |
""" | |
def loadpopulation( poptoload, countermeasurebeg, countermeasureend, countermeasureval ): | |
name = poptoload["name"] | |
size = poptoload["size"] | |
population = { | |
"name": name, | |
"size": size, | |
"filename": "", # if doing HDD I/O, this will be temp file location | |
"filepath": "", | |
"oneperson": 1.0 / size, # % of 1 person in pop for daily infection chance sums | |
"travelchance": 0, # chance folks in pop will go to other pops to infect | |
"countermeasure": 0, # day's amount infect chance is reduced | |
"countermeasurebeg": countermeasurebeg.copy(), | |
"countermeasureend": countermeasureend.copy(), | |
"countermeasureval": countermeasureval.copy(), | |
"dailystats": dailystats.copy(), | |
"people": loadpeople( size ) | |
} | |
return population | |
#------------------------------ | |
# Pick Random | |
#------------------------------ | |
""" | |
need to pick simple random samples | |
of various things. randint calls | |
randrange, but creates overhead, | |
so got rid of it and just went | |
with randrange. Lists use zero- | |
based counting, | |
""" | |
# pick a random population to infect first | |
# if dealing with 2 populations, randrange(0, 1) | |
# will only return 0, not 0 or 1. | |
def randompopulation( totalpops ): | |
return randrange( 0, totalpops ) | |
# pick random person to infect first | |
def randomperson( popsize ): | |
return randrange( 0, popsize ) | |
""" | |
When someone is infecting others, we | |
need to exclude infector when picking | |
random, and exclude their population | |
when they travel to infect another pop. | |
Python doesn't have a "random excluding" | |
pre-canned func, so we make our own. | |
""" | |
def randomexclude( end, exclusion ): | |
while True: | |
randomvalue = randrange( 0, end ) | |
if randomvalue != exclusion: | |
break | |
return randomvalue | |
def randomexcludeperson( popsize, this_person ): | |
return randomexclude( popsize, this_person ) | |
def randomexcludepopulation( totalpops, this_pop ): | |
return randomexclude( totalpops, this_pop ) | |
#------------------------------ | |
# Pre-Canned Populations | |
#------------------------------ | |
""" | |
US population 2020 | |
https://www.worldometers.info/world-population/us-population/ | |
Experts think herd immunity starts kicking in when 70% of the | |
population is infected. | |
https://www.jhsph.edu/covid-19/articles/achieving-herd-immunity-with-covid19.html | |
Our model just keeps running until it | |
hits INCUBATION_DAYS w/o having any more | |
contagious people. This lets us see how | |
much of the pop creates herd immunity | |
"naturally" in the simulation instead | |
of forcing a pre-conveived end-limit on | |
it. Because the whole point of this model | |
is to let all the marbles bump around and | |
see how they interact instead of just | |
using the preconveived end-result assumptions. | |
Setting up pre-canned populations to | |
import and work with. | |
""" | |
test100 = { | |
"name": "test100", | |
"size": 100 | |
} | |
test1K = { | |
"name": "test1K", | |
"size": 1000 | |
} | |
test10K = { | |
"name": "test10K", | |
"size": 10000 | |
} | |
test100K = { | |
"name": "test100K", | |
"size": 100000 | |
} | |
test1M = { | |
"name": "test1M", | |
"size": 1000000 | |
} | |
test10M = { | |
"name": "test10M", | |
"size": 10000000 | |
} | |
# Dallas / Fort-Worth, TX population | |
tx_dfw = { | |
"name": "tx_dfw", | |
"size": 6301000 | |
} | |
# some places are better modelled as a lot of | |
# sub-pops or communities, EG: DFW is like 20+ | |
# smaller towns all converged into a metroplex. | |
# to model it out better, we can take the | |
# population, and divide it by a general | |
# sub-community count, then iterate and copy | |
# that sub-community pre-canned pop to generate | |
# sub-pops totalling the main pop. | |
# EG: for DFW we can take 6,301,000 metro pop | |
# divided by 20 "major" communities to get | |
# 315,050 in 20 sub-pops.. we can load those | |
# 20 sub-pops and then use travel restrictions | |
# to see how individual communities restricting | |
# travel might help curtail viral spread. | |
# Dallas / Fort-Worth, TX generic sub-community | |
tx_dfw_sub = { | |
"name": "tx_dfw_sub", | |
"size": 315050 | |
} | |
#--------------------------- | |
# US state populations | |
#--------------------------- | |
""" | |
Want to model the US, but it's | |
better to do each state as a population | |
to let them interact a bit organically | |
instead of just using US pop as a whole. | |
!!! trying to do US pop blows out my 16gb of ram !!! | |
""" | |
pop_AK = {"name": "pop_AK", "size": 734002} | |
pop_AL = {"name": "pop_AL", "size": 4908621} | |
pop_AR = {"name": "pop_AR", "size": 3038999} | |
pop_AZ = {"name": "pop_AZ", "size": 7378494} | |
pop_CA = {"name": "pop_CA", "size": 39937489} | |
pop_CO = {"name": "pop_CO", "size": 5845526} | |
pop_CT = {"name": "pop_CT", "size": 3563077} | |
pop_DC = {"name": "pop_DC", "size": 720687} | |
pop_DE = {"name": "pop_DE", "size": 982895} | |
pop_FL = {"name": "pop_FL", "size": 21992985} | |
pop_GA = {"name": "pop_GA", "size": 10736059} | |
pop_HI = {"name": "pop_HI", "size": 1412687} | |
pop_IA = {"name": "pop_IA", "size": 3179849} | |
pop_ID = {"name": "pop_ID", "size": 1826156} | |
pop_IL = {"name": "pop_IL", "size": 12659682} | |
pop_IN = {"name": "pop_IN", "size": 6745354} | |
pop_KS = {"name": "pop_KS", "size": 2910357} | |
pop_KY = {"name": "pop_KY", "size": 4499692} | |
pop_LA = {"name": "pop_LA", "size": 4645184} | |
pop_MA = {"name": "pop_MA", "size": 6976597} | |
pop_MD = {"name": "pop_MD", "size": 6083116} | |
pop_ME = {"name": "pop_ME", "size": 1345790} | |
pop_MI = {"name": "pop_MI", "size": 10045029} | |
pop_MN = {"name": "pop_MN", "size": 5700671} | |
pop_MO = {"name": "pop_MO", "size": 6169270} | |
pop_MS = {"name": "pop_MS", "size": 2989260} | |
pop_MT = {"name": "pop_MT", "size": 1086759} | |
pop_NC = {"name": "pop_NC", "size": 10611862} | |
pop_ND = {"name": "pop_ND", "size": 761723} | |
pop_NE = {"name": "pop_NE", "size": 1952570} | |
pop_NH = {"name": "pop_NH", "size": 8936574} | |
pop_NH = {"name": "pop_NH", "size": 1371246} | |
pop_NM = {"name": "pop_NM", "size": 2096640} | |
pop_NV = {"name": "pop_NV", "size": 3139658} | |
pop_NY = {"name": "pop_NY", "size": 19440469} | |
pop_OH = {"name": "pop_OH", "size": 11747694} | |
pop_OK = {"name": "pop_OK", "size": 3954821} | |
pop_OR = {"name": "pop_OR", "size": 4301089} | |
pop_PA = {"name": "pop_PA", "size": 12820878} | |
pop_PR = {"name": "pop_PR", "size": 3032165} | |
pop_RI = {"name": "pop_RI", "size": 1056161} | |
pop_SC = {"name": "pop_SC", "size": 5210095} | |
pop_SD = {"name": "pop_SD", "size": 903027} | |
pop_TN = {"name": "pop_TN", "size": 6897576} | |
pop_TX = {"name": "pop_TX", "size": 29472295} | |
pop_UT = {"name": "pop_UT", "size": 3282115} | |
pop_VA = {"name": "pop_VA", "size": 8626207} | |
pop_VT = {"name": "pop_VT", "size": 628061} | |
pop_WA = {"name": "pop_WA", "size": 7797095} | |
pop_WI = {"name": "pop_WI", "size": 5851754} | |
pop_WV = {"name": "pop_WV", "size": 1778070} | |
pop_WY = {"name": "pop_WY", "size": 567025} | |
# load all US state pops into a list | |
# to make it easier to iterate through | |
# and build population objects from | |
pop_US = [ | |
pop_AK, | |
pop_AL, | |
pop_AR, | |
pop_AZ, | |
pop_CA, | |
pop_CO, | |
pop_CT, | |
pop_DC, | |
pop_DE, | |
pop_FL, | |
pop_GA, | |
pop_HI, | |
pop_IA, | |
pop_ID, | |
pop_IL, | |
pop_IN, | |
pop_KS, | |
pop_KY, | |
pop_LA, | |
pop_MA, | |
pop_MD, | |
pop_ME, | |
pop_MI, | |
pop_MN, | |
pop_MO, | |
pop_MS, | |
pop_MT, | |
pop_NC, | |
pop_ND, | |
pop_NE, | |
pop_NH, | |
pop_NH, | |
pop_NM, | |
pop_NV, | |
pop_NY, | |
pop_OH, | |
pop_OK, | |
pop_OR, | |
pop_PA, | |
pop_PR, | |
pop_RI, | |
pop_SC, | |
pop_SD, | |
pop_TN, | |
pop_TX, | |
pop_UT, | |
pop_VA, | |
pop_VT, | |
pop_WA, | |
pop_WI, | |
pop_WV, | |
pop_WY | |
] | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
########################################################## | |
# Viral Model - General Utility Functions | |
########################################################## | |
# | |
# Copyright Craig Critchfield 2020 | |
# | |
########################################################## | |
""" | |
These are util functions I created | |
to help with certain things, but | |
they're more general purpose then | |
model-specific. So, chucking them | |
into a util module to help trim | |
down the size of the model_functions | |
module to make it easier to scroll | |
and work on things there. | |
""" | |
#------------------------------ | |
# Calculate & Format Percents | |
#------------------------------ | |
# catch "divide by 0" errors and return 0 | |
def calculatepercent( numerator, denominator ): | |
try: | |
return numerator / denominator | |
except ZeroDivisionError: | |
return 0 | |
# format % as string to decimal places | |
def formatpercent( percentage, decimals ): | |
percent = "{:." + str(decimals) + "%}" | |
return percent.format( percentage ) | |
# both above as one function call | |
def formatcalculatepercent( numerator, denominator, decimals ): | |
result = calculatepercent( numerator, denominator ) | |
return formatpercent( result, decimals ) | |
#------------------------------ | |
# Format Bytes to String | |
#------------------------------ | |
""" | |
sys.getsizeof() returns bytes. I want to | |
reduce huge byte sizes down to more | |
reasonable numbers with proper size | |
suffix on end. | |
EG: 100000 bytes / 1000 = 100 KB | |
To get ballpark sizes, we can just | |
divide by 1000 until we get a num len < 4 | |
""" | |
""" | |
!!! | |
nixed this, b/c some folks feel getsizeof() | |
isn't a very accurate profile of resource. | |
So, think I'll try implementing resource | |
module instead later on. | |
!!! | |
""" | |
""" | |
def bytestring( bytesize ): | |
suffix = ( "Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ) | |
result = bytesize | |
append = 0 | |
# if we have even 1000, | |
# bump it to the next | |
# unit size as 1.0 | |
while result > 999: | |
result /= 1000 | |
append += 1 | |
if 0: # ... debug | |
print("result = " + str(result)) | |
print("append = " + str(append)) | |
# round the result to 1 decimal place | |
result = round( result, 1 ) | |
return str( result ) + " " + suffix[ append ] | |
""" | |
#------------------------------ | |
# Output CSV | |
#------------------------------ | |
""" | |
output list of lists or | |
list of dicts to csv file. | |
started with stat tracker | |
being a list of lists, then | |
switched to being a list of | |
dicts. Decided to keep the | |
code to do either or in | |
the future. | |
""" | |
# pass a list of lists of list of dicts | |
# will determine which by testing 1st | |
# object in list to see which csv func | |
# to call below for parsing data to csv | |
def outputcsv( output, filename, filepath ): | |
pathname = filepath + filename | |
if isinstance( output[0], list ): | |
listcsv( output, pathname ) | |
elif isinstance( output[0], dict ): | |
dictcsv( output, pathname ) | |
# in csv outputs below... | |
# 'w' ... opens file to overwrite | |
# newline = '' ... keeps from inserting blank rows between items | |
# list of lists to csv | |
def listcsv( output, pathname ): | |
from csv import writer#, QUOTE_ALL | |
with open( pathname, 'w', newline='' ) as newfile: | |
wr = writer( newfile )#, quoting = QUOTE_ALL ) | |
wr.writerows( output ) | |
# list of dicts to csv | |
def dictcsv( output, pathname ): | |
from csv import DictWriter | |
keys = output[0].keys() | |
with open( pathname, 'w', newline='' ) as newfile: | |
dictwriter = DictWriter( newfile, keys ) | |
dictwriter.writeheader() | |
dictwriter.writerows( output ) | |
def importcsvlist( filename, filepath ): | |
from csv import reader | |
pathname = filepath + filename | |
intlist = [] | |
with open( pathname, 'r' ) as csvfile: | |
r = reader ( csvfile, quotechar = ' ' ) | |
rowlist = list( r ) | |
for row in rowlist: | |
rowlist = [] | |
for value in row: | |
rowlist.append( int(value) ) | |
intlist.append( rowlist ) | |
return intlist | |
""" | |
!!!!!!!!!!!!!!!! | |
pickle serializes IO faster then just | |
dumping out csv's. So, might expand on this | |
later to do large file IO of populations | |
to from disk to keep from blowing out RAM. | |
Might find another way to do this. Main | |
issue is speed at this point. What would | |
be the fastest way to do disk IO w/o | |
thrashing HDD in the process? | |
!!!!!!!!!!!!!!!! | |
filepath = "D:\\python\\projects\\viral model\\" | |
filename = "model_test_pickle" | |
pathname = filepath + filename | |
# wb = write binary | |
pickle.dump( pop, open(pathname, "wb") ) | |
# rb = read binary | |
pop = pickle.load( open(pathname, "rb") ) | |
""" | |
#------------------------------ | |
# Profile Code for Bottlenecks | |
#------------------------------ | |
""" | |
cProfile dumps out a binary file | |
that has to get loaded into pstats | |
to then dump out as a txt file. | |
Really convoluted to have to write | |
so many lines of code to just dump | |
out profile stats to txt file. | |
This just profiles time constraints | |
in code (what's using up time), not | |
RAM or CPU or other resources. | |
""" | |
def profilemain( filepath, fileprof, filetext ): | |
# lazy import | |
from cProfile import run | |
from pstats import Stats | |
# generate path + name locations | |
outputprof = filepath + fileprof | |
outputtext = filepath + filetext | |
# profile function call | |
run( "main()", outputprof ) | |
# open binary file, | |
# sort by cumulative times | |
# then dump to txt file | |
with open( outputtext, "w") as f: | |
ps = Stats( outputprof, stream = f) | |
ps.sort_stats('cumulative') | |
ps.print_stats() | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
######################################## | |
""" | |
Process Timer | |
As you do more complex programming, you might | |
find yourself in need of a process timer... | |
something that lets you time your overall | |
code run, or snippets of it. | |
This is where an ad-hoc process timer comes in. | |
--------------------------------------- | |
Main Process Timer functions are: | |
procTimer(name: string) -> (returns nothing, but prints/logs results if ending a timer) | |
Send it a timer name as a string. | |
If it can't find the timer name in the timer dictionary, it creates | |
the timer and snapshots the current time it added it. | |
If it can find the timer name, then it removes the timer from | |
the dictionary and returns the elapsed time (stop time - start time) | |
procLog(activate: bool, filename: string) -> (returns nothing) | |
Use this function to activate text file logging of timers. | |
The text file dumps out to the same folder that the | |
"process_timer.py" code file is in (eg: if it's in | |
c:\temp\ then your log fille will end up in c:\temp\ too. | |
activate = boolean True/False ... | |
True = activate and log timers to log file | |
False = stop logging timers | |
filename = filename to use for appending timer string messages to. | |
default = "timer_log.txt", so if you dont' provide a value, | |
it will use "timer_log.txt". | |
procInfo() -> (returns nothing, but prints summary of proc timer stuff) | |
Call it to get a summary of whether logging is on/off, | |
what the log file name currently is, and a list of timers | |
still active in the process timer dictionary. | |
This is mainly useful if you're running code from the | |
console one line at a time, b/c the code and variables | |
there remain running after each line of input. So, | |
you can run procInfo() to get a quick check of timers | |
still active. | |
--------------------------------------- | |
If you want to import process_timer.py stuff from any | |
code you're doing in the IDE without having to worry | |
about copy/paste'ing process_timer.py file to the | |
same folder as your code file, then you can toss | |
process_timer.py into ... | |
C:\\Users\\(username)\\Anaconda3\\Lib | |
(have to use double \\, otherwise Python thinks | |
the single slash is an escape character, even in comment ) | |
That's the Windows directory that houses all of the | |
extra boiler-plate Python code libraries, eg: random.py. | |
(Not sure what directory to use for Linux or Mac). | |
The problem with doing this is that if you turn on | |
timer logging, it will spit out a log file to that | |
lib directory still. (I could have imported os.py | |
and used that to come up with directory creation | |
and such, but logging is an extracurricular feature | |
and I didn't want to open up the os.py can of worms. | |
This code file is already hitting scope-creep levels | |
of bloat as it is.) | |
I may expand on this later if I think of more | |
stuff to add to it, or optimize it or whatever. | |
But, for now, I feel it's good enough. | |
- Craig Critchfield | |
""" | |
######################################## | |
# CODE LIBRARY IMPORTS | |
######################################## | |
# datetime.datetime.now() function | |
# tracks time in milliseconds | |
from datetime import datetime | |
######################################## | |
# GLOBAL VARIABLES | |
######################################## | |
proc_timers = {} # dictionary stores timer names & start times | |
log_timers = False # flag if we're logging timers to txt log or not (default = no) | |
log_file = "timer_log.txt" # default text file name to log to | |
######################################## | |
# FUNCTIONS - TIMER | |
######################################## | |
# single function to call to start/stop timers | |
def procTimer( name ): | |
if name in proc_timers: | |
_endTimer(name) | |
else: | |
_addTimer(name) | |
# add new timer to dict | |
# with snapshot of current time | |
def _addTimer( name ): | |
proc_timers[name] = datetime.now() | |
# pop timer from dict, | |
# printing results | |
def _endTimer( name ): | |
stop = datetime.now() | |
start = proc_timers.pop(name) | |
procTime = stop - start | |
msg = '{:^20}'.format(name) # center name within 20 char padding | |
logtime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
msg = logtime + " ... " + "timer ... " + msg + " ... " + str(procTime) + " (h:mm:ss.ms)" | |
print(msg) | |
if log_timers: | |
logTimer( msg ) | |
######################################## | |
# FUNCTIONS - LOG FILE | |
######################################## | |
# allows user to activate / deactivate log file | |
# and change the filename that's used | |
# Note that we have these vars setup as globals | |
# so we use the "global" keyword to tell the | |
# function to refer to the global vars, and not | |
# simply kick off local variables with the same names. | |
def procLog( activate = log_timers, filename = log_file): | |
global log_timers | |
global log_file | |
log_timers = activate | |
log_file = filename | |
# append timer to output log file | |
def logTimer( msg ): | |
file = open(log_file, "a") # create / open file to append | |
file.write(msg + "\n") # add line break after input | |
file.close # close file to free object | |
######################################## | |
# FUNCTIONS - TIMER INFO | |
######################################## | |
def procInfo(): | |
border = "-" * 50 | |
print(border) | |
print( "logging timers = " + str(log_timers)) | |
print( "log file name = " + log_file) | |
print(border) | |
print( "active timers...") | |
for name in proc_timers: | |
print("\t* " + name) | |
print(border) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment