R/growthSimulation.R

Defines functions add_organism universe_polygon_preset init_simulation is.growthSimulation

Documented in add_organism init_simulation

#' Structure of the S4 class "growthSimulation"
#'
#' @description Structure of the S4 class \link{growthSimulation} as framework
#' for the growth environment and container for agent-based flux balance
#' analysis.
#'
#' @aliases growthSimulation
#'
#' @exportClass growthSimulation
#'
#' @slot n_rounds integer for the number of simulation rounds that were already
#' performed for this \link{growthSimulation} object.
#' @slot deltaTime double for length of each time step for the simulation in
#' hours.
#' @slot rMotion double. Maximum x- and y distance a cell can travel by means of
#' random movement per simulation round. Unit: \eqn{\mu}m per minute.
#' @slot models List for \link{Organism} objects to represent the different
#' strains in the simulation.
#' @slot history list with recordings of simulation status information at each
#' simulation round.
#' @slot cellDT Data table with individual cell information (e.g. size,
#' position, velocity, type)
#' @slot universePolygon Matrix specifying the corners of the polygon that
#' defines the growth environment boundaries. 2-dimensional: x and y.
#' @slot environ Object of S4-class \link{growthEnvironment}, that specifies the
#' environment mesh layout, compounds, and their concentrations.
#' @slot recordDir Directory name, in which intermediate compound concentrations
#' are recorded if turned on in \link{run_simulation}. Files in this directory
#' are meant as internal resource and not for direct analysis outside of this
#' package.
#' @slot rcdt data.table that stores the last reduced cost values of each cell's
#' exchange reactions. This information could indicate growth limiting
#' compounds.
setClass("growthSimulation",

         slots = c(

           n_rounds = "numeric",

           # simulation slots
           deltaTime      = "numeric",
           rMotion        = "numeric",
           models         = "list",
           history        = "list",

           # ind. cell info
           cellDT = "data.table",
           rcdt   = "data.table",

           # Environment slots
           universePolygon = "matrix",
           environ         = "growthEnvironment",

           # file for compound recordings
           recordDir = "character"
         )
)

# small helper for sanity checks
is.growthSimulation <- function(x) inherits(x, "growthSimulation")

# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
#  Initialize Simulation Object #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #

#' Initialize a growth simulation.
#'
#' Method to initialize a \link{growthSimulation} object
#'
#' @param universePolygon A two column matrix specifying the x and y
#' coordinates of the polygon corners, that describe the growth environment
#' boundaries. Alternatively, a character indicating one of the polygon presets
#' can be provided (see details).
#' @param gridFieldSize double. Distance between neighboring environments 3D
#' mesh field elements (rhombic dodecahedrons) in \eqn{\mu}m.
#' @param gridFieldLayers integer. z-dimension (height) as the number of layers
#' of field elements.
#' @param deltaTime double specifying the length of each time step for the
#' simulation in hours.
#' @param rMotion double. Maximum distance a cell can travel by means of
#' Brownian motion in \eqn{\mu} per minute. Default: 0.1 \eqn{\mu}m
#'
#' @return Object of class \link{growthSimulation}.
#'
#' @details
#' Available universe polygon presets:
#' \itemize{
#'   \item{"Petri_<R>"}{ is a Petri dish-like object (actually a 99-corner polygon),
#'   where `<R>` should be replaced with an integer, indicating the radius of the
#'   dish in \eqn{\mu}m.}
#'   \item{"Rectangle_<X>_<Y>"}{ is a, *surprise*, rectangle. `<X>` and `<Y>` should be
#'   integers specifying the width and height in \eqn{\mu}m, respectively.}
#'   \item{"Kiel_<L>"}{ let microbes thrive within Kiel's city limits. Use `<L>`
#'   to specify the latitude dimension in \eqn{\mu}m (integer). The longitude is automatically
#'   scaled accordingly.}
#' }
#'
#' @examples
#' # Construction a square environment of dimensions 100\eqn{\mu}m x 120\eqn{\mu}m x 3\eqn{\mu}m
#' sim <- init_simulation(cbind(c(-50, -50, 50, 50),
#'                              c(-60, 60, 60, -60)),
#'                        gridFieldSize = 1, gridFieldLayers = 3)
#' sim <- init_simulation("rectangle_100_120", gridFieldSize = 1,
#'                        gridFieldLayers = 3)
#'
#' # Construct a Petri dish-like simulation environment (radius: 75 \eqn{\mu}m)
#' sim <- init_simulation("Petri_75", gridFieldSize = 1,
#'                        gridFieldLayers = 3)
#'
#' @import data.table
#' @importFrom methods new
#'
#' @export
init_simulation <- function(universePolygon,
                            gridFieldSize   = 1,
                            gridFieldLayers = 3,
                            deltaTime       = 1/6,
                            rMotion         = 0.1) {

  # generate poygon matrix if preset is chosen
  if(is.character(universePolygon))
    universePolygon <- universe_polygon_preset(universePolygon)

  # last polygon coordinate must be identical to first one
  if(any(universePolygon[1,] != universePolygon[nrow(universePolygon),])) {
    universePolygon <- rbind(universePolygon,
                             universePolygon[1,])
  }


  # init growth environment
  environ <- new("growthEnvironment",
                 polygon.coords  = universePolygon,
                 field.size      = gridFieldSize,
                 field.layers    = gridFieldLayers)

  # construct empty cell info table
  cellDT <- data.table(cell = numeric(),
                       type = character(),
                       x = numeric(), y = numeric(),
                       x.vel = numeric(), y.vel = numeric(),
                       mass = numeric(),
                       size = numeric(),
                       parent = numeric())
  rcdt   <- data.table()

  # init actual growth simulation object
  simobj <- new("growthSimulation",
                n_rounds = 0,
                deltaTime = deltaTime,
                rMotion = rMotion,
                models = list(),
                history = list(),
                cellDT = cellDT,
                rcdt = rcdt,
                universePolygon = universePolygon,
                environ = environ,
                recordDir = NA_character_
  )

  return(simobj)
}

universe_polygon_preset <- function(universePolygon) {

  universePolygon <- universePolygon[1]

  # Petri dish (99 corner polygon)
  if(grepl("^[P|p]etri_[0-9]+$",universePolygon)) {
    p_r <- as.numeric(sub("^[P|p]etri_(\\S+)$", "\\1", universePolygon))

    if(p_r < 2.5)
      stop("Petri dish radius too small. (min 2.5 \u03BCm)")

    petri <- matrix(0, ncol = 2, nrow = 100)

    petri[,1] <- sin(seq(0, 2*pi, length.out = 100))
    petri[,2] <- cos(seq(0, 2*pi, length.out = 100))
    petri[100,] <- petri[1,]

    petri <- petri * p_r

    return(petri)
  }

  # Square dish (99 corner polygon)
  if(grepl("^[R|r]ectangle_[0-9]+_[0-9]+$",universePolygon)) {
    x <- as.numeric(sub("^[R|r]ectangle_(\\S+)_[0-9]+$", "\\1", universePolygon)) / 2
    y <- as.numeric(sub("^[R|r]ectangle_[0-9]+_(\\S+)$", "\\1", universePolygon)) / 2

    if(x < 5 | y < 5)
      stop("Rectangle dimesions too small. (x,y >= 5 \u03BCm)")

    upg <- cbind(c(x,x,-x,-x),
                 c(y,-y,-y,y))

    return(upg)
  }

  # Kiel city limits (proof of principle)
  if(grepl("^[K|k]iel_[0-9]+$",universePolygon)) {
    Kiel <- as.matrix(fread(system.file("extdata","kiel.csv", package = "Eutropia")))
    Kiel_dim <- max(Kiel[,1])
    Kiel <- Kiel / Kiel_dim

    x <- as.numeric(sub("^[K|k]iel_(\\S+)$", "\\1", universePolygon)) / 2
    Kiel <- Kiel * x/2

    if(x < 10)
      stop("Kiel latitude dimension too small (min 10 \u03BCm).")

    return(Kiel)
  }

  stop(paste0("The polygon preset \"",universePolygon,
              "\" does not exist / is not yet supported."))
}


# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
#  Add organism cells to simulation object  #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
#' Add a model/organism to simulation
#'
#' Adds an organism an its genome-scale metabolic network model to the growth simulation object.
#'
#' @param object S4-object of type \link{growthSimulation}.
#' @param model The organisms metabolic model of S4-type \link[sybil]{modelorg}
#' @param name Character for the name of the model, that will also be used for
#' plotting.
#' @param ncells integer. Number of initial cells to be added to the growth
#' simulation.
#' @param coords (optional) A two column numerical matrix specifying the
#' coordinates (1st column x, 2nd column y) of the initial cells. If provided,
#' the number of rows should be equal to \code{ncells}. Default: NULL
#' @param distribution.method If `coords` is `NULL`, this parameter specifies
#' the distribution method for initial cells. Default: "random_centroid"
#' @param distribution.center Numeric vector of length 2, which specifies the
#' coordinates of the centre for the `distribution.method`.
#' @param distribution.radius double. Spcifies the radius (in \eqn{\mu}m) in which
#' initial cells are distributed.
#' @param cellDiameter double. Diameter in \eqn{\mu}m of initial cells.
#' @param cellMassInit double. Mass in pg of initial cells. Default is 0.28 pg
#' @param cellMassAtDivision double. Cell mass at which a cell divides into two
#' daughter cells. Default: 0.56 pg
#' @param cellShape character. Shape of cells. Currently only "coccus" is
#' supported.
#' @param vmax double. Maximum velocity of a cell in \eqn{\mu}m per second.
#' @param scavengeDist double. Distance in \eqn{\mu}m a cell can scavenge nutrients from
#' its surrounding/microoenvironment.
#' @param rm.deadends If TRUE, dead-end metabolites and reactions are removed
#' from the `model`, which reduces the computation time for FBA, but has
#' otherwise no effect on the flux distribution solutions.
#' @param chemotaxisCompound Character vector of compound IDs, that are signals
#' for directed movement of the organism.
#' @param chemotaxisStrength Numeric vector that indicates the strength of
#' chemotaxis. Positive value for attraction; Negative for repelling effect. A
#' value of 1 indicates that in case of a maximum gradient (concentration-weighted
#' center in cell's scavenge area is at the edge of the area) the cell moves
#' with its maximum speed (vmax) in the direction of the gradient. Default: 0.01
#' @param chemotaxisHillKA Numeric vector for K_A value (unit: mM) in Hill
#' equation in chemotactic metabolite sensing. Default: 0.1 mM
#' @param chemotaxisHillCoef Numeric vector for the Hill coefficient (unitless)
#' in metabolite sensing. Default: 1.2
#' @param open.bounds Numeric value that is used to reset lower bounds of
#' exchange reactions, which have a current lower bound of 0. See Details.
#' @param color Color of organism in visualizations.
#'
#' @details
#' Genome-scale metabolic models usually come pre-constraint, which means that
#' lower bounds for exchange reactions (= max. uptake rates) are set to represent, both,
#' (a) a specific growth environment and (b) the physiological limit of nutrient uptake.
#' Yet, lower bounds that have a value of 0 might also be utilizable by the
#' organism if the compound is present in the environment.
#' If the option `open.bounds` is used, those 0-lower bounds are replaced with a
#' new lower bound to enable the potential uptake in the agent-based simulation.
#' Please note that the value should by convention be negative; however
#' this package changes the value to it's negative counterpart if a positive value
#' is provided.
#'
#' The default cell diameter (\eqn{(3 * 1 / (4 * pi))^(1/3) * 2}) is that of a
#' sphere with 1 \eqn{\mu}m^3 volume.
#'
#' 'chemotaxisHillKA' and 'chemotaxisHillCoef' are metabolite sensing sensitivity
#' parameters, which is modeled as a Hill equation. Default values correspond to
#' numbers estimated by Sourjik and Berg (2001, PNAS) for \emph{Escherichia coli}.
#'
#' @return Object of class \link{growthSimulation}.
#'
#' @references
#'  \url{https://bionumbers.hms.harvard.edu/bionumber.aspx?id=100008} \cr
#'  \url{http://book.bionumbers.org/how-big-is-an-e-coli-cell-and-what-is-its-mass/} \cr
#'  \url{https://bionumbers.hms.harvard.edu/bionumber.aspx?id=115616&ver=0&trm=speed+e.+coli&org=} \cr
#'  Victor Sourjik and Howard C. Berg. (2001). Receptor sensitivity in bacterial
#'  chemotaxis. \emph{PNAS} \strong{99}, 123-127.
#'
#' @examples
#' # add two bacterial models (Eubacterium rectale, Bifidobacterium longum)
#' # to the environment; each with 15 initial cells
#' models <- list()
#' models[['eure']] <- readRDS(system.file("extdata", "eure.RDS",
#'                             package="Eutropia"))
#' models[['bilo']] <- readRDS(system.file("extdata", "bilo.RDS",
#'                             package="Eutropia"))
#'
#' sim <- init_simulation(cbind(c(-100, -100, 100, 100),
#'                              c(-100, 100, 100, -100)),
#'                        gridFieldSize = 1.75, gridFieldLayers = 3)
#'
#' sim <- add_organism(sim, model = models[["eure"]], name = "E. rectale",
#'                     ncells = 15, distribution.radius = 30)
#' sim <- add_organism(sim, model = models[["bilo"]], name = "B. longum",
#'                     ncells = 15, distribution.radius = 30)
#'
#' plot_cells(sim, xlim = c(-50,50), ylim= c(-50,50))
#'
#' @import particles
#' @import tidygraph
#' @importFrom methods new
#' @import sf
#'
#' @export
add_organism <- function(object,
                         model,
                         name,
                         ncells,
                         coords = NULL,
                         distribution.method = "random_centroid",
                         distribution.center = NULL,
                         distribution.radius = NULL,
                         cellDiameter = (3 * 1 / (4 * pi))^(1/3) * 2, # diameter of sphere with 1 micro-m^3 volume
                         cellMassInit = 0.28,
                         cellMassAtDivision = 0.56,
                         cellShape = "coccus",
                         vmax = 11,
                         scavengeDist = cellDiameter*2.5,
                         rm.deadends = T,
                         chemotaxisCompound = NULL,
                         chemotaxisStrength = 0.01,
                         chemotaxisHillKA   = 0.1,
                         chemotaxisHillCoef = 1.2,
                         open.bounds = NULL,
                         color = NULL) {
  # sanity checks
  if(!is.growthSimulation(object))
    stop("'Object' not of class 'growthSimulation'.")
  if(!(inherits(model, "modelorg")))
    stop("'model' not of class 'modelorg'.")
  if(ncells <= 0 | (ncells %% 1) != 0)
    stop("'ncells' should be a positive non-zero integer.")


  if(is.null(chemotaxisCompound)) {
    chemotaxisCompound <- character(0)
    chemotaxisStrength <- double(0)
    chemotaxisHillKA   <- double(0)
    chemotaxisHillCoef <- double(0)
  } else {
    if(!(length(chemotaxisStrength) %in% c(1,length(chemotaxisCompound))))
      stop("'chemotaxisStrength' should be the same length as chemotaxisCompound or 1.")
    if(!(length(chemotaxisHillKA) %in% c(1,length(chemotaxisCompound))))
      stop("'chemotaxisHillKA' should be the same length as chemotaxisCompound or 1.")
    if(!(length(chemotaxisHillCoef) %in% c(1,length(chemotaxisCompound))))
      stop("'chemotaxisHillCoef' should be the same length as chemotaxisCompound or 1.")

    if(any(chemotaxisHillKA <= 0) | any(chemotaxisHillCoef <= 0))
      stop("'chemotaxisHillKA' and 'chemotaxisHillCoef' must be non-zero positive numbers.")

    if(length(chemotaxisStrength) == 1)
      chemotaxisStrength <- rep(chemotaxisStrength, length(chemotaxisCompound))
    if(length(chemotaxisHillKA) == 1)
      chemotaxisHillKA <- rep(chemotaxisHillKA, length(chemotaxisCompound))
    if(length(chemotaxisHillCoef) == 1)
      chemotaxisHillCoef <- rep(chemotaxisHillCoef, length(chemotaxisCompound))
  }

  # set color automatically if NULL
  if(is.null(color)) {
    default.colors <- c("#D55E00","#56B4E9","#F0E442","#009E73","#0072B2",
                        "#E69F00","#CC79A7","#DEDEDE")
    colors.in.use <- unlist(lapply(object@models, function(x) x@color))
    color <- default.colors[!(default.colors %in% colors.in.use)]
    if(length(color) == 0) {
      color <- "#333333"
      warnings("Eutropia ran out of distinct and colorblind-friendly colors. Assigning gray instead.")
    } else {
      color <- color[1]
    }
  }

  # init new organism object
  object@models[[name]] <- new("Organism",
                               cellDiameter = cellDiameter,
                               cellMassInit = cellMassInit,
                               cellMassAtDivision = cellMassAtDivision,
                               vmax = vmax,
                               scavengeDist = scavengeDist,
                               mod = model,
                               rm.deadends = rm.deadends,
                               chemotaxisCompound = chemotaxisCompound,
                               chemotaxisStrength = chemotaxisStrength,
                               chemotaxisHillKA = chemotaxisHillKA,
                               chemotaxisHillCoef = chemotaxisHillCoef,

                               open.bounds = open.bounds,
                               color = color)

  #if not provided -> assign cell positions:
  if(is.null(coords)) {
    if(!(distribution.method %in% c("random","random_centroid")))
      stop("Distribution method not supported. Choose one of \"random\",\"random_centroid\".")

    universePG <- st_polygon(list(object@universePolygon))

    if(distribution.method == "random") {
      #coords <- spsample(universePG, ncells, type = "random")@coords
      coords <- st_sample(universePG, ncells, type = "random")
    }

    if(distribution.method == "random_centroid") {

      PGcent <- distribution.center
      if(is.null(PGcent)) {
        PGcent <- st_centroid(universePG)
      } else {
        PGcent <- st_point(PGcent)
      }

      maxRadius <- distribution.radius

      if(is.null(maxRadius)) {
        maxRadius <- st_distance(PGcent, st_cast(universePG,
                                                     "LINESTRING"))[1,1]
      }
      distr_area <- st_buffer(PGcent, dist = maxRadius)
      distr_area <- st_intersection(universePG, distr_area)

      coords <- st_sample(distr_area, ncells, type = "random")
    }

    coords <- lapply(coords, as.matrix)
    coords <- do.call(rbind, coords)
  }

  newcells <- data.table(cell = (1:ncells)+nrow(object@cellDT),
                         type = name,
                         x = coords[,1], y = coords[,2],
                         x.vel = 0, y.vel = 0,
                         mass = cellMassInit,
                         size = cellDiameter,
                         parent = NA_integer_)

  # making sure cells are placed within the universe polygon (trapping cells)
  # oder: "Schäfchen zurück ins Gehege holen"
  sim_addorg <- tbl_graph(nodes = newcells, directed = F, node_key = "cell") %>%
    simulate(setup = predefined_genesis(x = newcells$x,
                                        y = newcells$y,
                                        x_vel = newcells$x.vel,
                                        y_vel = newcells$y.vel)) %>%
    # wield(collision_force, radius = newcells$size/2, n_iter = 50, strength = 0.7) %>%
    wield(trap_force, polygon = object@universePolygon, strength = 0.7,
          min_dist = 5, distance_falloff = 2) %>%
    impose(velocity_constraint,
           vmax = rep(1, nrow(newcells))) %>%
    impose(polygon_constraint,
           polygon = object@universePolygon)

  keepJittering <- T
  tmp_currentpos <- c(newcells$x, newcells$y)
  while(keepJittering) {
    sim_addorg <- evolve(sim_addorg, steps = 10)
    if(all(tmp_currentpos == c(sim_addorg$position[,1],sim_addorg$position[,2]))) {
      keepJittering <- F
    } else {
      # handbreak... (setting velocity to 0)
      sim_addorg$velocity[,1] <- sim_addorg$velocity[,1]*0
      sim_addorg$velocity[,2] <- sim_addorg$velocity[,2]*0
      tmp_currentpos <- c(sim_addorg$position[,1],sim_addorg$position[,2])
    }
  }

  newcells$x <- sim_addorg$position[,1]
  newcells$y <- sim_addorg$position[,2]

  # add cells to cell info table
  object@cellDT <- rbind(object@cellDT, newcells)

  # add compounds, for which there are exchange reactions in at least one model
  # and which are not yet part of the environment
  mod <- object@models[[name]]@mod

  dt_exr <- data.table(id = mod@react_id,
                       lb = mod@lowbnd,
                       name = mod@react_name)
  dt_exr <- dt_exr[grepl("^EX_", id)]
  dt_exr[, id := gsub("^EX_","",id)]
  dt_exr <- dt_exr[id != "cpd11416_c0"]
  dt_exr[, name := gsub("-e0-e0 Exchange$","",name)]
  dt_exr[, name := gsub("-e0 Exchange$","",name)]
  dt_exr[, name := gsub(" Exchange$","",name)]
  dt_exr[, name := gsub(" exchange$","",name)]

  dt_exr <- dt_exr[!(id %in% object@environ@compounds)] # only new compounds should be added

  if(nrow(dt_exr) > 0) {
    object <- add_compounds(object,
                            compounds = dt_exr$id,
                            concentrations = rep(0, nrow(dt_exr)),
                            compound.names = dt_exr$name,
                            is.constant = rep(FALSE, nrow(dt_exr)))
  }

  return(object)
}

# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# show method for small summary #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #

#' @title Print a short summary of a growth simulation
#'
#' @description Displays a few numbers to describe the current status of a growth simulation.
#'
#' @param object S4-object of type \link{growthSimulation}.
#'
#' @import data.table
#'
#' @export
setMethod(f          = "show",
          signature  = signature(object = "growthSimulation"),
          definition = function(object) {

            cat("Cell community growth simulation\n")
            cat("Passed simulation time:\t",object@n_rounds*object@deltaTime," hours (",
                object@n_rounds," rounds)\n", sep= '')
            cat("\n")

            cat("Cell counts:\n")
            print(object@cellDT[, .(`cells` = .N, `mass in pg` = sum(`mass`)), by = "type"])
            cat("\n")

            # Environment
            cat("Cell growth environment\n")
            cat("    Universe volume (\u03BCm^3):\t\t", round(object@environ@fieldVol * object@environ@nfields, digits = 2) , "\n")
            cat("    Number of rhombic dodecahedrons:\t",object@environ@nfields,"\n")
            cat("    Number of compounds:\t\t",length(object@environ@compounds),
                "(", sum(!object@environ@conc.isConstant),"variable )\n")

          }
)



# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# Add compounds to environment  #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
#' @title Add compounds to the growth environment
#'
#' @description The function can be used to add substances to the growth
#' environment by providing concentrations in mM.
#'
#' @param object S4-object of type \link{growthSimulation}.
#' @param compounds Character vector with the compound IDs of substances to add
#' to the environment. Compound IDs should correspond to the models' exchange
#' reactions IDs ("EX_[cpdid]"), without the "EX_" prefix.
#' @param concentrations Numeric vector with the concentrations of the compounds
#' from `compounds` in the same order. Values in mM.
#' @param compound.names Character vector with the compound names.
#' @param is.constant Logical vector that indicates if the compound should
#' remain constant over time despite of potential uptake or production by cells.
#' @param compound.D Numeric vector with the compounds' diffusion coefficients
#' in \eqn{\mu}m^2/s. Default: 75
#'
#' @details Compound concentration are equally distributed across the whole
#' growth environment.
#' If the compound is already present, old and new concentrations are added.
#' More options are planned.
#'
#' If no compound names are provided, the current names are kept (if compound
#' is already present) or the compound ID is also used as name (in case the
#' compound is new).
#'
#' You can also define "Inf" for the compound diffusion rates in 'compound.D'.
#' This has the effect, that the compound will be evenly distributed across the
#' whole growth environment again at every iteration during the simulation.
#'
#' @return Return a S4-object of type \link{growthSimulation}.
#'
#' @examples
#' sim <- init_simulation(cbind(c(-100, -100, 100, 100), c(-100, 100, 100, -100)),
#'                        gridFieldSize = 2, gridFieldLayers = 3)
#'
#' sim <- add_compounds(sim,
#'                      compounds = c("cpd00027_e0","cpd00029_e0","cpd00047_e0",
#'                                    "cpd00159_e0","cpd00211_e0"),
#'                      concentrations = c(50,0,0,0,0),
#'                      compound.names = c("D-Glucose","Acetate","Formate",
#'                                         "L-Lactate","Butyrate"),
#'                      is.constant = rep(FALSE, 5))
#'
#' @export
add_compounds <- function(object, compounds, concentrations,
                          compound.names = NULL,
                          is.constant = NULL,
                          compound.D  = NULL) {
  # Sanity checks
  if(!is.growthSimulation(object))
    stop("'Object' not of class 'growthSimulation'.")

  default_D <- 75

  if(length(compounds) != length(concentrations))
    stop("Lengths of 'compounds' and 'concentrations' differ.")

  # make compounds non-constant if nothing else is specified
  if(is.null(is.constant)) {
    is.constant <- rep(F, length(compounds))
  }

  # assign each compound the default diffusion coefficient if nothing
  # else is specified
  if(is.null(compound.D)) {
    compound.D <- rep(default_D, length(compounds))
  } else {
    compound.D <- ifelse(is.na(compound.D), default_D, compound.D)
  }

  # if only one diff. coeff. is specified -> use it for all metabolites
  if(length(compound.D) == 1) {
    compound.D <- rep(compound.D, length(compounds))
  }

  # is.constant should be NULL (see above) or of length 1 or n (nr. of compounds)
  if(length(is.constant) != 1 & length(is.constant) != length(compounds)) {
    stop("Length of 'is.constant' is not 1 or the same length as 'compounds'")
  }

  # compound.D should be NULL (see above) or of length 1 or n (nr. of compounds)
  if(length(compound.D) != 1 & length(compound.D) != length(compounds)) {
    stop("Length of 'compound.D' is not 1 or the same length as 'compounds'")
  }

  # extent is.constant vector if only one value
  if(length(is.constant) == 1) {
    is.constant <- rep(is.constant, length(compounds))
  }

  # if no names are supplied -> NA
  if(is.null(compound.names)) {
    compound.names <- rep(NA_character_, length(compounds))
  }

  # if length of names is unequal the length of ids -> something's odd
  if(length(compound.names) != length(compounds)) {
    stop("Length of 'compound.names' is not the same length als 'compounds'")
  }

  for(i in 1:length(compounds)) {
    # Metabolite is already present in environment
    if(compounds[i] %in% object@environ@compounds) {
      c_ind <- which(object@environ@compounds == compounds[i])
      object@environ@concentrations[, c_ind] <- object@environ@concentrations[, c_ind] + concentrations[i]
      object@environ@conc.isConstant[c_ind]  <- is.constant[i]
      object@environ@compound.D[c_ind]       <- compound.D[i]
      if(!is.na(compound.names[i]))
        object@environ@compound.names[c_ind] <- compound.names[i]

      # Metabolite is new
    } else {
      object@environ@compounds       <- c(object@environ@compounds, compounds[i])
      object@environ@concentrations  <- cbind(object@environ@concentrations,
                                              matrix(concentrations[i], ncol = 1, nrow = object@environ@nfields))
      object@environ@conc.isConstant <- c(object@environ@conc.isConstant, is.constant[i])
      object@environ@compound.D      <- c(object@environ@compound.D, compound.D[i])
      if(!is.na(compound.names[i])) {
        if(compound.names[i] %in% object@environ@compound.names) {
          warning(paste0("Compound with name '",compound.names[i],"' already exists. Replacing with '",compounds[i],"'."))
          compound.names[i] <- compounds[i]
        }
        object@environ@compound.names <- c(object@environ@compound.names, compound.names[i])
      } else {
        object@environ@compound.names <- c(object@environ@compound.names, compounds[i])
      }
    }
  }

  return(object)
}

# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# Add compound gradient         #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
#' @title Add compounds to the growth environment in a gradient
#'
#' @description The function can be used to add substances to the growth
#' environment where the compound is distribution in a concentration gradient.
#'
#' @param object S4-object of type \link{growthSimulation}.
#' @param compound Character with the compound ID of substance to add
#' to the environment in a gradient. Compound ID should correspond to the
#' models' exchange reaction ID ("EX_[cpdid]"), without the "EX_" prefix.
#' @param p1 Numeric vector of length 2 or 3 defining the xy(z)-coordinates of
#' the first gradient reference point. See details.
#' @param p2 Numeric vector of length 2 or 3 defining the xy(z)-coordinates of
#' the second gradient reference point. See details.
#' @param c1 Numeric value with the concentration of the compound at 'p1'.
#' Values in mM.
#' @param c2 Numeric value with the concentration of the compound at 'p2'.
#' Values in mM.
#' @param gradient.dir Either "radial", "linear", or "linear_mirrored". See
#' details.
#' @param compound.name Character with the compound name.
#' @param is.constant Logical defining if the compound should remain constant
#' over time despite of potential uptake or production by cells.
#' @param compound.D Numeric value with the compound's diffusion coefficient
#' in \eqn{\mu}m^2/s. Default: 75
#'
#' @details If only xy-coordinated are provided the z-coordinate is assumed to
#' be 0.
#'
#' If ''gradient.dir' is set to "linear", any point that is in the
#' opposite direction of the gradient will get the concentration c1. If
#' "linear_mirrored", the concentration gradient is mirrored at the plane that
#' is defined by p1 and the p1-p2 as normal vector.
#'
#' If the compound is already present, old and new concentrations are added.
#'
#' If no compound names are provided, the current names are kept (if compound
#' is already present) or the the compound ID is also used as name (in case the
#' compound is new).
#'
#'
#' @return Return a S4-object of type \link{growthSimulation}.
#'
#' @examples
#' sim <- init_simulation(cbind(c(-70, -70, 70, 70), c(-45, 45, 45, -45)),
#' gridFieldSize = 2, gridFieldLayers = 3)
#' sim <- add_compound_gradient(sim,
#'                              compound = "cpd00027_e0",
#'                              p1 = c(-60,33), p2 = c(60,-40),
#'                              c1 = 25, c2 = 0,
#'                              compound.name = "D-Glucose")
#' sim <- add_compound_gradient(sim,
#'                              compound = "cpd00036_e0",
#'                              p1 = c(60,-20), p2 = c(50,60),
#'                              c1 = 27, c2 = 0,
#'                              gradient.dir = "linear",
#'                              compound.name = "Succinate")
#'
#' plot_environment(sim, c("cpd00027_e0","cpd00036_e0"), incl.timestamp = FALSE)
#' @import sf
#' @export
add_compound_gradient <- function(object, compound, p1, p2, c1, c2,
                                  gradient.dir = "radial",
                                  compound.name = NULL,
                                  is.constant = FALSE,
                                  compound.D  = 75) {
  # Sanity checks
  if(!is.growthSimulation(object))
    stop("'Object' not of class 'growthSimulation'.")

  # grad dir
  if(!(gradient.dir %in% c("radial","linear","linear_mirrored")))
    stop("gradient.dir should be \"radial\", \"linear\", or \"linear_mirrored\".")

  # Both coordinates should have numeric length of 2 or 3.
  if(!(length(p1) %in% c(2,3) & length(p2) %in% c(2,3))) {
    stop("Coordinates p1 and p2 should each be vectors of length 2 (xy) or 3 (xyz).")
  }
  if(length(p1) == 2)
    p1 <- c(p1,0)
  if(length(p2) == 2)
    p2 <- c(p2,0)

  # p1 and p2 same?
  if(all(p1 == p2))
    stop("'p1' and 'p2' cannot be identical for defining a gradient.")

  # Calculate concentration gradient
  p1sf <- st_point(p1)
  p2sf <- st_point(p2)
  d <- st_distance(p1sf, p2sf)[1,1]
  if(gradient.dir == "radial") {
    envp <- st_cast(st_sfc(object@environ@field.pts), "POINT")
    env_dist <- st_distance(envp, p1sf)
  }
  if(grepl("^linear",gradient.dir)) {
    envp <- as.matrix(object@environ@field.pts)

    norm_vec <- p2-p1

    env_dist <- norm_vec[1]*(envp[,1] - p1[1]) + norm_vec[2]*(envp[,2] - p1[2]) + norm_vec[3]*(envp[,3] - p1[3])
    #env_dist <- norm_vec[1]*envp[,1] + norm_vec[2]*envp[,2] + norm_vec[3]*envp[,3]
    #env_dist <- env_dist + norm_vec[1]*p1[1] + norm_vec[2]*p1[2] + norm_vec[3]*p1[3]
    env_dist <- env_dist / sqrt(sum(norm_vec^2))

    if(grepl("mirrored$", gradient.dir)) {
      env_dist <- abs(env_dist)
    } else {
      env_dist <- ifelse(env_dist < 0, 0, env_dist)
    }
  }

  # translate distances to concentrations
  grad_func <- function(x) {
    m <- (c2 - c1) / d
    y <- x * m + c1
    y <- ifelse(y < min(c(c1,c2)), min(c(c1,c2)), y)
    y <- ifelse(y > max(c(c1,c2)), max(c(c1,c2)), y)
  }
  concadd <- grad_func(env_dist)

  # Add new concentration to growth environment
  if(compound %in% object@environ@compounds) {
    # Metabolite is already present in environment
    c_ind <- which(object@environ@compounds == compound)
    object@environ@concentrations[, c_ind] <- object@environ@concentrations[, c_ind] + concadd
    object@environ@conc.isConstant[c_ind]  <- is.constant
    object@environ@compound.D[c_ind]       <- compound.D
    if(!is.null(compound.name) && !is.na(compound.name))
      object@environ@compound.names[c_ind] <- compound.name

  } else {
    # Metabolite is new
    object@environ@compounds       <- c(object@environ@compounds, compound)
    object@environ@concentrations  <- cbind(object@environ@concentrations,
                                            matrix(concadd, ncol = 1, nrow = object@environ@nfields))
    object@environ@conc.isConstant <- c(object@environ@conc.isConstant, is.constant)
    object@environ@compound.D      <- c(object@environ@compound.D, compound.D)
    if(!is.na(compound.name)) {
      if(compound.name %in% object@environ@compound.names) {
        warning(paste0("Compound with name '",compound.name,"' already exists. Replacing with '",compound,"'."))
        compound.name <- compound
      }
      object@environ@compound.names <- c(object@environ@compound.names, compound.name)
    } else {
      object@environ@compound.names <- c(object@environ@compound.names, compound)
    }
  }

  return(object)
}


# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# Dilute compounds            #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
#' @title Dilute compounds
#'
#' @description Dilutes all or selected compounds with a given dilution factor.
#'
#' @param object A \link{growthSimulation} object
#' @param dilution.factor Numeric within the range of [0,1], by which the
#' compound concentrations are diluted. `1` completely dilutes concentrations to
#' 0 mM, while `0` does not change anything.
#' @param compounds Character of compound IDs that should be diluted. If `NULL`,
#' all compounds are diluted.
#' @param incl.constant Logical specifying whether also constant compounds
#' should be diluted. Default: FALSE
#'
#' @export
dilute_compounds <- function(object, dilution.factor,
                             compounds = NULL, incl.constant = FALSE) {

  # sanity checks
  if(!is.growthSimulation(object))
    stop("'Object' not of class 'growthSimulation'.")
  if(dilution.factor > 1 | dilution.factor < 0) {
    stop("Dilution factor should be between 0 and 1.")
  }

  if(is.null(compounds))
    compounds <- object@environ@compounds

  available_compounds <- object@environ@compounds
  if(!incl.constant)
    available_compounds <- object@environ@compounds[!object@environ@conc.isConstant]

  compounds <- compounds[compounds %in% available_compounds]

  if(length(compounds) == 0) {
    warning("No valid compounds selected. Returning original simulation object.")
    return(object)
  }

  ind_relcpds <- match(compounds, object@environ@compounds)

  object@environ@concentrations[, ind_relcpds] <- object@environ@concentrations[, ind_relcpds] * (1-dilution.factor)

  return(object)
}


# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
# Summary exchanges           #
# ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ #
#' @title Summary of uptake / production by organism type
#'
#' @description Uptake/Production rates in fmol summarized by organism type
#'
#' @param object S4-object of type \link{growthSimulation}.
#' @param iter Positive integer number of the simulation step/iteration to plot
#' the rates.
#'
#' @return A data.table
#'
#' @export
summary_exchanges <- function(object, iter = NULL) {

  # sanity checks
  if(!is.growthSimulation(object))
    stop("'Object' not of class 'growthSimulation'.")

  # Sanity checks
  if(object@n_rounds < 1)
    stop("Simulation did not yet run for at least 1 iteration. No exchange rates availabe (yet).")

  if(!is.null(iter)) {
    if(iter > object@n_rounds) {
      warning(paste0("Simulation did not run ",iter," iterations yet. Displaying results for last iteration (",object@n_rounds,")"))
      iter <- object@n_rounds
    }
  } else {
    iter <- object@n_rounds
  }

  # data.table X R CMD check workaround
  type <- compound <- cell <- exflux <- compound.name <- fmol <- NULL

  dt_exch <- merge(object@history[[iter]]$cell.exchanges,
                   object@history[[iter]]$cells[,.(cell, type)],
                   by = "cell")
  dt_exch <- dt_exch[, .(fmol = sum(exflux)), by = .(type, compound)]
  dt_exch[, compound := gsub("^EX_","",compound)]
  dt_exch <- dt_exch[compound != "cpd11416_c0"] # Biomass...
  dt_exch$compound.name <- object@environ@compound.names[match(dt_exch$compound,
                                                               object@environ@compounds)]

  return(dt_exch[,.(type, compound, compound.name, fmol)])

}
Waschina/Eutropia documentation built on Jan. 4, 2023, 12:42 p.m.