
# example of discrete-event simulation, 

# here we use using the "event oriented" approach:

#   have an object f which stores all future events
#   while (simulated time < max time) {
#       extract from f the event of earliest time, set to e
#       process e; typically this will create 1 or more new events, add to f
#    }

#   e.g. queuing model: jobs arrive at random times, served by a server;
#   e = arrival of new job; processing e will generate 2 items to add to 
#   f--the future event of the completion of job e, and the future event 
#   of the next job arriving
#   
#   f can be implemented efficiently with a priority queue (heap); here
#   we just do argmin each time
#   
#   R environment sim.env is formed to store global variables, allowing
#   much streamlining and clarity in the code

# from a software engineering point of view, the "process-oriented"
# approach is better, basically a threads-based system; see the 'simmer'
# package in R, the SimPy library in Python, and various commercial
# products

# advice nurses sim, ECS 256 Term Project, Fall 2022

#  If upon completion of a phone call, there are 0 patients waiting in the
#  queue, the number of nurses will be reduced by 1 (with a minimum of 1).
  
#  If a patient arrival occurs at the time the queue is full, either 1 new
#  nurse will be added (with a maximum of n), with probability p, or the
#  call will be terminated

#  Here we find prop. of refused calls, and mean waiting time (among
#  calls not refused).  Of course, many other quantities could be
#  computed, and the exponetial distributions here could be replaced by
#  anything.

# params

#  m: max waiting room size
#  n: max number of nurses
#  mu: mean call duration
#  nu: mean time between new calls
#  p: probability a new nurse added
#  simTimeLim: maximum simulated time

# globals (in sim.env)

# general:

#  evntList: set of pending events, NOT ordered in time here, thus not
#     efficient; implemented here as a data frame, consisting of
#     event types (col. 1), event times (col. 2) and arrival times (col.
#     3); the first 2 cols. are necessary for the general operation,
#     while the third is used compute our desired output quantities
#  currTime: current simulated time
#  simTimeLim: maximum simTimeLim time; we run until this sim time

# specific to this application (also in sim.env)

# functions with names of the form 'DES*' are general, for use in all
# applications

# the "main()" function
sim <- function(m,n,mu,nu,p,simTimeLim) 
{
   sim.env <<- new.env()

   sim.env$currTime <- 0
   sim.env$simTimeLim <- simTimeLim
   # sim.env$evntList -- see blow

   sim.env$m <- m
   sim.env$n <- n
   sim.env$p <- p
   sim.env$interCallRate <- 1/mu
   sim.env$callDurRate <- 1/nu
   sim.env$nWaiting <- 0  # number in waiting room
   sim.env$nNurses <- 1  # number of nurses
   sim.env$nNursesBusy <- 0  # number of busy nurses
   sim.env$nCallsServed <- 0
   sim.env$totWait <- 0
   sim.env$nArrvs <- 0
   sim.env$nRefuse <- 0

   # initialize the event list, scheduling the first arrival (start at
   # simulated time 0)
   time1stArrival <- rexp(1,sim.env$interCallRate)
   sim.env$evntList <- data.frame(
      evntTime=time1stArrival,evntType='arrival',arrvTime=time1stArrival)

   # start the simulation!; loop through the event list through the end 
   # of simulated time!
   while (sim.env$currTime < sim.env$simTimeLim) {
      evnt <- DESgetEarliest()  # extract next event from event list
      DESAdvanceTime(evnt)  # currTime advanced here
      switch(evnt$evntType,  # process newly-occurring event, according to type
         arrival=processArrival(evnt),
         callDone=processCallDone(evnt))
   }

   # return mean wait, prop. of refused calls
   c(sim.env$totWait / sim.env$currTime, 
      sim.env$nRefuse / sim.env$nArrvs)
}

# event that just occurred (via removal from event list) gives us the
# new simulated time
DESAdvanceTime <- function(evnt)  sim.env$currTime <- evnt$evntTime

# remove the earliest event; as noted above, this implementation is inefficient
DESgetEarliest <- function() 
{
   # find earliest event, remove it from the event list, and return it
   el <- sim.env$evntList
   earliestRowNum <- which(el$evntTime==min(el$evntTime))
   evnt <- el[earliestRowNum,]
   sim.env$evntList <- sim.env$evntList[-earliestRowNum,]
   evnt
}

# schedule a future event, via adding to the event list
DESaddToEvntList <- function(evntType,evntTime,arrvTime) 
{
   el <- sim.env$evntList
   newRow <- 
      data.frame(evntType=evntType,evntTime=evntTime,arrvTime=arrvTime)
   if (nrow(el) == 0) sim.env$evntList <- newRow
   else sim.env$evntList <- rbind(sim.env$evntList,newRow)
}

# application-specific ftn, process an arrival event
processArrival <- function(evnt) 
{
   # possible scenarios:
   # 
   # queue full, random chance fails, call refused
   # queue full, random chance succeeds, call accepted
   # call accepted, no available nurses, call joins queue
   # call accepted, >=1 available nurse, call joins queue but leaves 
   #   and is served

   sim.env$nArrvs <- sim.env$nArrvs + 1 

   K <- sim.env$nWaiting
   N <- sim.env$nNurses
   NB <- sim.env$nNursesBusy

   sim.env$callRefused <- FALSE  # tentative
   if (K == sim.env$m) {  # full queue
      if (runif(1) < sim.env$p) {  # add nurse
         K <- K + 1
         N <- N + 1
         sim.env$nWaiting <- K
         sim.env$nNurses <- N
      } else  {
           sim.env$callRefused <- TRUE
           sim.env$nRefuse <- sim.env$nRefuse + 1
           startCall <- FALSE
      }
   } else sim.env$nWaiting <-  sim.env$nWaiting + 1

   if (!sim.env$callRefused) {
      # no nurses available?
      if (NB == N) startCall <- FALSE
      else startCall <- TRUE
    }

   if (startCall) {
      sim.env$nWaiting <-  sim.env$nWaiting - 1
      # start call, schedule end time
      sim.env$nNursesBusy <- NB + 1
      callDoneTime <- sim.env$currTime + rexp(1,sim.env$callDurRate)
      DESaddToEvntList('callDone',callDoneTime,evnt$arrvTime)
   }

   # schedule next arrival
   nextArrivalTime <- sim.env$currTime + rexp(1,sim.env$interCallRate)
   DESaddToEvntList('arrival',nextArrivalTime,nextArrivalTime)
}

processCallDone <- function(evnt) 
{
   sim.env$nCallsServed <- sim.env$nCallsServed + 1
   sim.env$totWait <- sim.env$totWait + sim.env$currTime - evnt$arrvTime
   sim.env$nNursesBusy <- sim.env$nNursesBusy - 1
   if (sim.env$nWaiting > 0) {  # this nurse is now free, can serve another
      sim.env$nNursesBusy <- sim.env$nNursesBusy + 1
      sim.env$nWaiting <- sim.env$nWaiting - 1
      # start call, schedule end time
      callDoneTime <- sim.env$currTime + rexp(1,sim.env$callDurRate)
      DESaddToEvntList('callDone',callDoneTime,evnt$arrvTime)
   } else sim.env$nNurses <- max(1,sim.env$nNurses-1)
}

