Files
2015-GCAT/R/GCAT/R/slope.analysis.R
T
2015-04-10 15:12:45 -05:00

420 lines
19 KiB
R

#Copyright 2012 The Board of Regents of the University of Wisconsin System.
#Contributors: Jason Shao, James McCurdy, Enhai Xie, Adam G.W. Halstead,
#Michael H. Whitney, Nathan DiPiazza, Trey K. Sato and Yury V. Bukhman
#
#This file is part of GCAT.
#
#GCAT is free software: you can redistribute it and/or modify
#it under the terms of the GNU Lesser General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.
#
#GCAT is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU Lesser General Public License for more details.
#
#You should have received a copy of the GNU Lesser General Public License
#along with GCAT. If not, see <http://www.gnu.org/licenses/>.
########################################################################
# #
# Estimate the growth curve slope at each timepoint of a well #
# #
########################################################################
#
# uses the functions <data.from> and <well.name> (see well.class.R)
# adds estimated slopes as a new column to the "screen.data" slot
calculate.slopes = function(input.well, silent = T){
# Get the growth curve data (excluding removed points, but not excluding points marked as tanking)
growth.data = data.from(input.well, remove = T, remove.tanking = F)
x = growth.data[,1]
y = growth.data[,2]
# Get a list of timepoint indices
indices = as.numeric(rownames(growth.data))
# Default slope is NA, values will be filled in as they are calculated
slopes = rep(NA, length(input.well))
for (i in 1:length(x)){
if (i == 1)
slopes[indices[i]] = NA
# Calculate the slope of the line drawn from each point to the point preceding it.
else
slopes[indices[i]] = (y[i] - y[i-1])/(x[i] - x[i-1])
}
# Add a Slope column to the "screen.data" slot
input.well@screen.data$Slope = slopes
if (!silent)
cat("slopes filled for", input.well@position[1], well.name(input.well), "\n")
return(input.well)
}
########################################################################
# #
# Use slope estimates to check growth curves for tanking and OD jumps #
# #
########################################################################
#
# uses the functions <data.from> and <well.name> (see well.class.R)
# Arguments:
# ----- stringency parameters ----
# remove.jumps - should the program remove OD jumps? default F (just report them) -
# should be set to T if data contains distinct jumps in OD that need to be eliminated
# otherwise, this might be too stringent and will result in loss of data.
# check.start - which timepoint should checking for jumps and tanking start at? this is included because early timepoints can be unstable.
# fall.cutoff - what downward slope should constitute a fall in OD?
# jump.cutoffs - multipliers to determine whether a curve jumps up or down (see methods 1 and 2, below)
# tank.limit - how many timepoints in a row can have falling slopes until the curve is marked as tanking?
# tank.cutoff - what proportion of the maximum OD can the curve go below until it is considered tanking?
# ---- input/output ----
# silent - output to R console?
# draw - plot growth curve with curve checking details?
#
# Fills the "curve.par" slot in the well with the starting index of tanking (NA if none is found)
#
#check.slopes = function(input.well, check.start = 8, fall.cutoff = -.0025, remove.jumps = F,
# jump.multipliers = -c(15, 500, 10), tank.cutoff = 1.0, tank.limit = 3, silent = T, draw = T){
#changed default values to parameters to account for settling
check.slopes = function(input.well, check.start = 22, fall.cutoff = -.0025, remove.jumps = F,
jump.multipliers = -c(15, 500, 10), tank.cutoff = 1.0, tank.limit = 6, silent = T, draw = T){
if (!silent)
cat("\nNow checking slopes for", plate.name(input.well), well.name(input.well))
# Get estimated slopes and untransformed points from the well
slopes = input.well@screen.data$Slope
x = data.from(input.well, remove=F, remove.tanking=F)[,1]
y = data.from(input.well, remove=F, remove.tanking=F)[,2]
# Get a list of indices with valid slope estimates
indices = (1:length(input.well))[!is.na(slopes)]
# Do not report tanking or check for jumps if there are fewer valid points than the number needed to detect it
if (length(indices) < tank.limit){
input.well@curve.par$tanking.start = NA
if (!silent)
cat("...does not have enough points to check.\n")
return(input.well)
}
#######################################################################################
# Create indicator variables and recalculate cutoffs based on timepoint density. #
#######################################################################################
#
# Create <slope.indicator>, a vector of indicator variables for each timepoint with a valid slope estimate.
slope.indicator = rep(NA, length(slopes))
# Calculate the mean time difference between two timepoints (this typically doesn't vary too much)
time.diff = mean(diff(x[-1]))
# Use the mean time difference to recalculate what should constitute a fall in OD using <fall.cutoff> (which should be a proportion)
# Honestly I don't remember why the fifth root thing is in here...this is probably going to be revised later.
fall.cutoff = fall.cutoff * time.diff ^ (1/5) / 0.9506785
# Recalculate stringency parameters for jump detection based on spread of timepoints
jump.cutoffs = jump.multipliers* fall.cutoff
# Recalculate tanking limit based on spread of timepoints
tank.limit = round(tank.limit / time.diff ^ (1/5) * 0.9506785)
# Cycle through the indices of input.wells with valid slope estimate
counter = 0
for(j in 1:length(indices)){
#######################################################################################
# Method #1 for finding OD jumps: compare the slope estimate of each point to the #
# ones for the closest surrounding points. #
#######################################################################################
# Get indices of the two closest surrounding timepoints with valid slope estimates.
if (j == 1)
prev.i = indices[2]
else
prev.i = indices[j-1]
if (j == length(indices))
next.i = indices[length(indices) - 1]
else
next.i = indices[j+1]
i = indices[j]
# How the program determines a jump up:
# If slope estimate of current timepoint is larger than <jump.cutoffs[2]> times the highest surrounding slope estimate plus <jump.cutoffs[1]>
# Add a "2" to the indicator variable
# How the program determines a fall:
# If slope estimate of current timepoint is more negative than <fall.cutoff>
# Add a "5" to the indicator variable
# How the program determines a jump down:
# If slope estimate is lower than <fall.cutoff> AND is smaller than <jump.cutoffs[2]> times the lowest surrounding slope estimate minus <jump.cutoffs[1]>
# Add a "6" to the slope indicator variable
#
# If none of these are true, add a "0" to the indicator variable
if (slopes[i] > jump.cutoffs[2] * max(c(slopes[next.i],slopes[prev.i]),0) + jump.cutoffs[1])
slope.indicator[i] = 2
else if (slopes[i] < fall.cutoff){
slope.indicator[i] = 5
if (slopes[i] < jump.cutoffs[2] * min(c(slopes[next.i],slopes[prev.i], 0)) - jump.cutoffs[1])
slope.indicator[i] = 6
}
else
slope.indicator[i] = 0
#######################################################################################
# Method #2 for finding OD jumps: see if each point lies close to a line drawn #
# between the closest surrounding points. #
#######################################################################################
#
# Use <counter> variable to track the location of each point. If two subsequent points lie farther
# away than the cutoff from their respectively drawn lines AND are on different sides, then count that as a jump.
if (j > 1 & j < length(indices)){
# Make equation (y=mx+b) for line drawn between two surrounding points
m = (y[next.i] - y[prev.i])/(x[next.i] - x[prev.i])
b = y[prev.i] - m * x[prev.i]
# Estimate y from that line. Points will be judged by how much their true y value deviate from this estimate.
# calculate b for perpendicular line from observed point to drawn line (slope is -1/m)
b2 = y[i] + x[i]/m
# solve equation for intersection to determine the shortest Euclidean distance between the point and line.
# assign a sign to the distance based on the vertical distance.
est.x = (b2 - b) / (m + 1/m)
est.y = est.x * m + b
#est.y = m * x[i] + b
#est.x = x[i]
if(m != 0)
point.distance = sqrt((y[i]-est.y)^2 + (x[i]-est.x)^2) * sign(y[i]-est.y)
else # horizontal case
point.distance = y[i] - b
#print(paste(i, point.distance, slopes[i], jump.cutoffs[2] * max(c(slopes[next.i],slopes[prev.i]),0) + jump.cutoffs[1], point.distance > jump.cutoffs[3]))
color = "gray30"
# If the true point exceeds that estimate by more than <jump.cutoffs[3]>, update <counter> to positive.
# if the counter weas previously negative, mark this as a jump up.
if (point.distance > jump.cutoffs[3]){
if (counter == -1){
slope.indicator[i] = 2
color = "red"
}
counter = 1
}
# If the true point is under that estimate by more than <jump.cutoffs[3]>, update <counter> to negative.
# if the counter was previously positive, mark this as a jump down.
else if (point.distance < -jump.cutoffs[3]){
if (counter == 1){
slope.indicator[i] = 6
color = "red"
}
counter = -1
}
# If the true point lies within <jump.cutoffs[3]> of that estimate, update <counter> to zero.
else
counter = 0
if(draw)
# Graphic representation: draw each line used in Method #2 as a dotted line,
# and highlight in red if a jump was detected
lines(x[c(prev.i, next.i)], y[c(prev.i, next.i)], lty = 2, col = color)
}
}
#######################################################################################
# Check for tanking by looking for unbroken series of points with falling slopes. #
#######################################################################################
#
# Cycle through <slope.indicator>, adding to <tank> until the end of the curve or until <tank> reaches the <tank.limit>
tank = 0
i = 1
while(i < length(slope.indicator) & tank < tank.limit){
# If a fall was not detected, reset <tank> to 0.
if (is.na(slope.indicator[i]))
tank = 0
# If a fall was detected at a point index greater than <check.start>, add 1 to <tank> .
else if (slope.indicator[i] >= 5 & i > check.start)
tank = tank + 1
else
tank = 0
i = i + 1
}
# If the above loop was terminated because <tank> reached <tank.limit>, update the "curve.par"
# slot to denote the first point at which tanking started (should be the last index checked minus <tank.limit>)
# also truncate <slope.indicator> so that it does not include the timepoints after tanking.
if (tank == tank.limit){
input.well@curve.par$tanking.start = i - tank.limit
slope.indicator = slope.indicator[1:i]
if (!silent)
cat("...tanks at timepoint", i - tank.limit, ".\n")
}
else{
input.well@curve.par$tanking.start = NA
if (!silent)
cat("...does not tank.\n")
}
#######################################################################################
# Method #2 of checking for tanking: see if OD falls below cutoff #
# (as a proportion of max OD) without recovery (according to <tank.limit>) #
#######################################################################################
#
i = check.start
tanking.start = NA
while(i < length(y) & is.na(tanking.start)){
# If the <tank.limit> next ODs beyond i do not reach the cutoff, then mark i as tanking.
if (all(y[i+(1:tank.limit)] < max(y,na.rm=T)*tank.cutoff, na.rm=T))
tanking.start = i
i = i+1
}
# Graphic representation: draw the indicators used in Method #1 using the pch symbols in <slope.indicator>
# slope index = 2: an upward-pointing traingle for an upward jump
# slope index = 5: a diamond for a fall
# slope index = 6: a downward-pointing triangle for downward jump
if (draw){
points(data.from(input.well, remove = F, remove.tanking = F)[which(slope.indicator != 0),],
col = 2, bg = 2, cex = 1.3, pch = slope.indicator[which(slope.indicator != 0)])
}
#######################################################################################
# Decide what to do about any remaining jumps in OD #
#######################################################################################
jump.up = which(slope.indicator == 2)
jump.down = which(slope.indicator == 6)
# <jump.all> is a variable which keeps track of all the jumps, whether up or down.
jump.all = sort(c(match(jump.down, indices), match(jump.up, indices)))
# commented out; jump not working
# if (length(jump.all) > 0)
# add.info = paste("Jump(s) detected at timepoint(s)",paste(indices[jump.all],collapse=" "))
# else
add.info = ""
# If <remove.jumps> is true, use the following automated process to try and remove OD jumps by selectively removing points from analysis.
# if not, just return the well with the above slot filled.
if (!remove.jumps)
input.well@add.info = add.info
else{
# Cycle through first few jumps (before <check.start>). <remove.initial.jump> is a logical that controls this loop.
remove.initial.jump = T
while (length(jump.all) > 0 & jump.all[1] < check.start & remove.initial.jump){
# If any other jumps are also before <check.start>...
if (any(jump.all[-1] < check.start)){
# ...and the next jump is in a different direction, stop the loop and don't remove the first one.
if(slope.indicator[indices[min(jump.all[-1])]] != slope.indicator[indices[jump.all[1]]])
remove.initial.jump = F
# ...or if the next two jumps are different, stop the loop and don't remove the first one.
else if(length(jump.all[-1]) > 1 &
slope.indicator[indices[jump.all[2]]] != slope.indicator[indices[jump.all[3]]])
remove.initial.jump = F
# ...otherwise, remove the jump and keep looping.
else
remove.initial.jump = T
}
# If no jumps other than the first one are before <check.start>, remove it and keep looping.
else
remove.initial.jump = T
# If the initial jump is to be removed, remove all points before the jump from analysis.
# also delete the initial jump from <jump.all>
if (remove.initial.jump){
input.well = remove.points(input.well, 1:(indices[jump.all[1]] - 1))
input.well@add.info = paste(add.info, "and removed.")
jump.all = jump.all[-1]
}
}
# If greater than 3 jumps remain, discard the curve as uninterpretable
if (length(jump.all) >= 4){
input.well = remove.points(input.well, 1:length(input.well))
input.well@add.info = paste(add.info, " - data was discarded.")
}
else{
# If there are 3 jumps, remove all points after the last one from analysis and delete the last jump from <jump.all>
if(length(jump.all) == 3){
input.well = remove.points(input.well, indices[jump.all[3]]:length(input.well))
input.well@add.info = paste(add.info, "and removed.")
jump.all = jump.all[-3]
}
# If there are now 2 jumps...
if(length(jump.all) == 2){
# ...and they are different (one up, one down), remove the points in between them from analysis.
if (diff(slope.indicator[indices[jump.all]]) != 0 ){
input.well = remove.points(input.well, indices[jump.all[1]:(jump.all[2] - 1)])
input.well@add.info = paste(add.info, "and removed.")
}
# ...and they are in the same direction, remove all the points after the first one from analysis.
else{
input.well = remove.points(input.well, indices[jump.all[1]]:length(input.well))
input.well@add.info = paste(add.info, "and removed.")
}
}
# If there is only one jump, remove all points after it from analysis.
else if (length(jump.all == 1)){
input.well = remove.points(input.well, indices[jump.all[1]]:length(input.well))
input.well@add.info = paste(add.info, "and removed.")
jump.all = jump.all[-1]
}
}
}
if(!silent)
cat("\t", input.well@add.info)
return(input.well)
}
########################################################################
# #
# Check wells for growth, remove from analysis if OD is too low #
# #
########################################################################
#
# The well will be tagged with no.growth = T in the slot "curve.par" if raw OD values (except for <points.to.remove>)
# do not increase beyond <growth.cutoff> above the specified time of inoculation for that well (<start.index>)
check.growth = function(input.well, growth.cutoff, start.index = 2){
# Get raw ODs (not including <points.to.remove>) and slope estimates from the well
# as well as OD at inoculation timepoint <start.index>
raw.ODs = raw.data(input.well)[,2]
start.OD = raw.ODs[start.index]
raw.ODs[input.well@screen.data$Remove] = NA
slope.estimates = slopes(input.well, remove.tanking = T, na.rm = T)
# If fewer than 3 points remain in the analysis with valid slope estimates, discard the well.
if (length(slope.estimates) < 3 | all(is.na(slope.estimates)))
input.well@curve.par$no.growth= T
else{
# If there are no points at all in the raw ODs
if(all(is.na(raw.ODs)))
input.well@curve.par$no.growth = T
else if(max(raw.ODs, na.rm=T) - start.OD < growth.cutoff) # See if OD increases by at least <growth.cutoff> above
input.well@curve.par$no.growth = T
else
input.well@curve.par$no.growth = F
}
if(all(raw.data(input.well)[,2] - raw.data(input.well)[start.index,2] < growth.cutoff))
input.well@add.info = "" # This is simply to reduce the amount of unnecessary info in the output.
# If the well is below growth cutoff anyway, don't bother reporting other errors.
return(input.well)
}