Definitions

This model is based on the model/simulation found found here.

We will try to incorporate opinion dynamics, network modeling, and a bit of the game theory to understand epidemic spread.

The agent

We will use a simplified task network model to represent the biological progression of the disease. First, let’s suppose that an agent has two states: its psychological state and its biological state. psychological state might be ‘practicing distancing’, ‘believes conspiracy theory’, ‘in quarantine’, and we can explore these later. Let’s just consider everyone is in a generic ‘informed’ state [1].

The biological state has a few specific cases:

  1. Unexposed
  2. Asymptomatic but infected/contagious
  3. Symptomatic and contagious
  4. Symptomatic and not contagious
  5. Post-COVID Immune
  6. Naturally immune (will not contract)
  7. Death

We could identify several others, but this might be fine. So, we can define the agent according to just two numbers. We might assume that initially, most people are in bio-state 1, but some would be in state 6 already, which is essentially the same as state 5.

We will just define these as a named list.

## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
## Loading required package: statnet.common
## 
## Attaching package: 'statnet.common'
## The following object is masked from 'package:base':
## 
##     order
## Loading required package: network
## network: Classes for Relational Data
## Version 1.16.0 created on 2019-11-30.
## copyright (c) 2005, Carter T. Butts, University of California-Irvine
##                     Mark S. Handcock, University of California -- Los Angeles
##                     David R. Hunter, Penn State University
##                     Martina Morris, University of Washington
##                     Skye Bender-deMoll, University of Washington
##  For citation information, type citation("network").
##  Type help("network-package") to get started.
## sna: Tools for Social Network Analysis
## Version 2.5 created on 2019-12-09.
## copyright (c) 2005, Carter T. Butts, University of California-Irvine
##  For citation information, type citation("sna").
##  Type help(package="sna") to get started.
## 
## Attaching package: 'igraph'
## The following objects are masked from 'package:sna':
## 
##     betweenness, bonpow, closeness, components, degree, dyad.census,
##     evcent, hierarchy, is.connected, neighborhood, triad.census
## The following objects are masked from 'package:network':
## 
##     %c%, %s%, add.edges, add.vertices, delete.edges, delete.vertices,
##     get.edge.attribute, get.edges, get.vertex.attribute, is.bipartite,
##     is.directed, list.edge.attributes, list.vertex.attributes,
##     set.edge.attribute, set.vertex.attribute
## The following objects are masked from 'package:dplyr':
## 
##     as_data_frame, groups, union
## The following objects are masked from 'package:stats':
## 
##     decompose, spectrum
## The following object is masked from 'package:base':
## 
##     union
## $psychstate
## [1] 1
## 
## $biostate
## [1] 2

Timecourse and Biological model

We need a way to transition the biological state in a reasonable way, much like a task network model that keeps track of time of sub-events. This should consider ONLY the natural progression of the disease. We will model timecourse on a timecourse of 1-day units.

An easy way to do this is with ballistic events. That is, we can keep track of the next transition point for any state, if it is programmed at the beginning. This is similar to how I implemented this in the task network model, but a bit simpler because we only have to worry about changes each day.

Create transition matrix.

If on each timestep we could move between states probabilistically, this would be a markov process. We could represent this by a simple transition network. If we represented the possible transitions in a network, that would simplify the logic above. But we need more like a task network because the time taken to transition matters. To keep it simple, Let’s make all timing distributions uniform with a min and max parameter for time in each state. We can program two pathways through the stages, with a couple branch points (death vs recovery; the possibility of recovering after acquiring with no symptoms). The progression of the disease is completely specified by the data, and a generic update function will automatically progress the agent each day.

# * 1. Unexposed
# * 2. Asymptomatic but infected/contagious
# * 3. Symptomatic and contagious
# * 4. Symptomatic and not contagious
# * 5. Post-COVID Immune
# * 6. Naturally immune (will not contract)
# * 7. Death


bioTransition <- matrix(0,STATES,STATES)
bioMin <- matrix(1,STATES)      #state time minimum
bioMax <- matrix(1,STATES)      #state time maximum



bioMin[2] <- 3            #infected but asymptomatic for 3 to 10 days
bioMax[2] <- 15          
bioTransition[2,3] <- .15  #transition to infected with symptoms
bioTransition[2,5] <- .85  #transition to no longer contagious/cured


bioMin[3] <-    3             #symptoms + contagion
bioMax[3] <- 8                #symptoms + contagion max
bioTransition[3,4] <-  .95    #transitioon to no longer contagious
bioTransition[3,7] <-  .05    #transitioon to death state 


bioMin[4] <- 1          #symptoms bot no longer contagiious
bioMax[4] <- 7
bioTransition[4,5] <- 1  #Transition to 'immune' cured state.




setAgentState<- function(agent, biostate)
{
  agent$biostate <- biostate
  if(sum(bioTransition[biostate,])>0) # this state transitions to something else.
  {
    ##which state do we go to?
     agent$biostatecountdown <- sample(x=seq(bioMin[biostate],bioMax[biostate]),1) #how long will we state in this state?
     agent$nextbiostate <- sample(1:STATES, prob=bioTransition[agent$biostate,],size=1)
     
  } else{
   agent$biostatecountdown <- NA
   agent$nextbiostate <- NA   ##just so we can tell if the agent is finished.
  }
  return(agent) 
}

transitionAgent<- function(agent)
{
   return(setAgentState(agent,agent$nextbiostate))
}

updateAgent<- function(agent)
{
  if(!is.na(agent$biostatecountdown))
  {
   agent$biostatecountdown <- agent$biostatecountdown -1
    if(agent$biostatecountdown <=0)  ##new state
    {
       agent <- transitionAgent(agent)

    }
  }
   return(agent)
}

If we start an agent in an unexposed state, nothing happens. Agent’s do not just naturally develop the disease. We can just update the agent until there is no next-state programmed.

Social model

This is not quite right, because we’d really expect interactions to be clumpy–you interact with the same people every day, but this model is like the opinion dynamics model in that anyone can interact with anyone. Let’s make a network using preferential attachment to represent that social world. We can make a couple of them and add them together to represent how we each have different types of relationships and interactions–you may be the president of your drama club and so are a central member, but you are also in a pottery class where you don’t talk to very many people (although your teacher does).

makeNetwork<- function(numAgents,numsets=3,steps=1,power=1)
{
  ord <- sample(numAgents)
  tmp<-as_adjacency_matrix(sample_pa(numAgents,power=power),sparse=F)
  tmp <- tmp + t(tmp)
  tmp <- tmp[ord,ord]
  if(numsets>1)
  {
    for(i in 2:numsets)
  {
    ord <- sample(numAgents)
    sn2 <-as_adjacency_matrix(sample_pa(numAgents,power=power),sparse=F)[ord,ord]
    tmp <- tmp + sn2 + t(sn2)
    
   }
  }
  if(steps>1)
  {
   for(i in 2:steps)
   {
      tmp <- tmp + tmp %*% tmp  ## iterate to neighbors
   }
  }
  (tmp>0)+0
}

## This tries to make a network with families, schools, workplaces..
## 
makeDemographicNetwork <- function(numAgents=1000, 
                                   householdsize=2.5,
                                   numSchools=4,
                                   numWorkplaces=25,
                                   agePyramid = c(20,8,10,12,14,13,12,11))
  
{
  
  numChildren <- floor(sum(agePyramid[1:2])/sum(agePyramid) * numAgents)
  numAdults <- numAgents-numChildren

  ##Adult ages age:
  
  adultAge<- sample(c(3:8)*10-5,size=numAdults,replace=T,prob=agePyramid[-(1:2)])
  childrenAge <- sample(c(5,15),size=numChildren,replace=T,prob=agePyramid[1:2])
  ages <- c(adultAge,childrenAge)
  ##Each household
  numHouseholds <-floor( numAgents/householdsize)
  householdXY <- matrix(runif(numHouseholds*2)*10,ncol=2)
  
  #Now, let's assign people to households.  Adults can be assigned to anywhere;
  ##children need to be assigned to a household that already exists.
  
  ##assign 'head-of-household' ##first the adults, then the children
  myHousehold <- rep(NA,numAgents)
  myHousehold[1:numHouseholds] <- 1:numHouseholds
  myHousehold[(numHouseholds+1):numAgents] <-  sample(numHouseholds,replace=T,
                                                     size=numAgents-numHouseholds)
  
  
  ##now, all households are assigned.  Let's assign workplaces and schools.  
  ##for simplicity, every adult has a workplace, and every child has a school, even though children
  ## or adults may work from home/too young for school, or be retired,
  ##wokplaces 1 is hosptial
  ## workplace 1+1;numschools are considered schools
   
  ##Workplace size is roughly zipfs-law
  workPlaces <-sample(1:numWorkplaces,size=numAdults,prob=1/(1+1:numWorkplaces),replace=T)
  schoolPlaces <- sample(1+(1:numSchools),size=numChildren,replace=T)
  work <- c(workPlaces,rep(0,numChildren))
  school <- c(rep(0,numAdults),schoolPlaces)
  pool <- list()
  for(i in 1:numAgents)
   {
   tmpAgent <- makeAgent(psychstate=1,
                          biostate=1,
                          age=ages[i])

    tmpAgent$household = myHousehold[i]
    tmpAgent$work = work[i]  ##work or school
    tmpAgent$school=school[i]
    tmpAgent$head <- (i<numHouseholds)
    tmpAgent$xy <- householdXY[myHousehold[i] ]
    pool[[i]] <-tmpAgent
   }
 
  
  ##now, let's lay out the social network
  schoolmat <- (outer(school,school,"==") * outer(school,school,"*"))>0
  workmat <- (outer(work,work,"==") * outer(work,work,"*"))>0
  housemat <- ( outer(myHousehold,myHousehold,"=="))>0
  agentXY <- householdXY[myHousehold,] +  matrix(.15* rnorm(length(myHousehold)*2),ncol=2)
  neighborhood <- as.matrix(dist(agentXY)) < runif(nrow(agentXY)^2)*2
#  neighborhood <- outer(agentXY,agentXY, function(a,b){(a-b)^2 < runif(1)*.5})
  network <- schoolmat+ workmat + housemat + neighborhood
   return (list(xy=agentXY, pool=pool,network=network,
                neighborhood=neighborhood,
                worknet=workmat,
                schoolnet=schoolmat,
                housenet=housemat))
}




mygplot <- function(coord, network,states,main="",edgecol="grey40",add=F)
{
  if(is.null(coord))
  {
    coord  <- gplot.layout.fruchtermanreingold(network,layout.par=list(niter=500))
  }
  
  newmin <- mean(coord[,2]) - (-min(coord[,2]) + mean(coord[,2])) * 1.4
   palette=c("white","yellow","red","green","darkgreen","blue","black")
   if(add==F)
     plot(coord,col="black",bty="n",pch=16,cex=2.7,xaxt="n",yaxt="n",main=main,
        xlab="",ylab="",axes=F,
        ylim=c(newmin,max(coord[,2])),type="n")
   
  for(i in 1:nrow(network))
  {
    if(sum(network[i,])>0)
    {
     segments(coord[i,1],
            coord[i,2],
            coord[network[i,]>0,1,drop=F],
            coord[network[i,]>0,2,drop=F],col=edgecol)
    }
   }
       points(coord,pch=16,cex=2.3,col= palette[states])
              
       points(coord,pch=1,cex=2.3,col="black")
      legend(mean(coord[,1]),min(coord[,2]),bty='n',y.intersp=.7,cex=.8,
              STATENAMES, pch=16,col=palette)
              
      return (coord)
}

net <- makeDemographicNetwork(500, numSchools=4,
                                   numWorkplaces=50)

cc <-  mygplot(coord=net$xy,net$housenet,rep(1,nrow(net$network)),main="Households",edgecol="blue")