library(knitr) # load knitr to enable options
library(respR) # load respR

opts_chunk$set(collapse = TRUE, 
               comment = "#>", 
               cache = FALSE, 
               tidy = FALSE, 
               highlight = TRUE, 
               fig.width = 10, 
               fig.height = 5,
               fig.align = "center",
               R.options = list(scipen = 999, 
                                digits = 3))

Introduction {#ftintro}

Flowthrough respirometry differs from other methods in that rates of oxygen change are measured from a controlled, continuous flow of water through a respirometer rather than a known volume, typically after the system has achieved equilibrium. In this method, two oxygen concentrations are needed: 'downstream' and 'upstream' of the experimental chamber, or the outflow and inflow concentrations. These are used to calculate an oxygen delta (outflow minus inflow values), and this is used with the flow rate to calculate oxygen consumption or production rates.

An outflow recording or value, or already determined delta oxygen values are required to calculate the rate. The inflow concentration is typically a simultaneous recording, but an alternative is to use a known value from a source of constant oxygen concentration such as fully air-saturated water, or water from a header tank of controlled oxygen concentration, and so often is not continuously monitored. respR will accept any combination of available data to allow rates to be calculated.

respR contains a workflow to process flowthrough respirometry data comprising five functions:

Not all of these are necessary depending on the analysis. See examples below.

Exploring flowthrough respirometry data

Typically the first step in data analysis is inspecting and visualising the data.

The inspect.ft function can be used to quickly examine and visualise outflow and inflow data from a single chamber, but also a large multi-column dataset. Using it with no inputs will inspect and plot every column, assuming time is in column 1 and all others are delta oxygen data (delta.oxy). Even if your data is not delta oxygen values, you can use this without saving the result for a quick look at the data. Note, if you do not tell it otherwise, the function assumes data are delta oxygen and plots them on a reverse y-axis. However, we can use the rate.rev input to override this

inspect.ft(flowthrough_mult.rd, rate.rev = FALSE)

The result does not have to be saved. This functionality can be used simply to get a quick overview of a dataset, check there are no common issues, and make sure the columns contain the data we expect. Column 1 contains the time data. Here, we can see columns 2 to 4 contain declining oxygen, and so as expected are the outflow oxygen data. There is also outflow oxygen from a control experiment which does not appear to decline very much (column 5). We can see there are no anomalies in the inflow oxygen data (columns 6 to 9) that might affect calculated rates. We can quickly see the existing delta oxygen columns (10,11,12) have, when the experiment has reached equilibrium, consistent values (around -8), and that the background delta (column 13) is also consistent. There is also a column of oxygen recorded in a shared header tank (column 14), and one of temperature (column 15).

Note, if multiple columns are inspected like this, and the object saved, subsequent functions such as calc_rate.ft will use only the first column of delta oxygen data for analysis. Best practice is to inspect each experimental dataset or group of columns individually. See later examples.

Case Studies

respR contains several example flowthrough datasets: flowthrough.rd, flowthrough_mult.rd, flowthrough_sim.rd.

We will use these here to run through some specific use cases and example analyses of flowthrough respirometry that should cover most user needs. If there are any not covered please get in touch and we will attempt to accommodate them.

Case 1: Outflow oxygen only {#case1}

"We only have an outflow oxygen recording, but we know the concentration of the inflowing water does not vary much, because it was from an aerated, constant temperature and salinity header tank that we tested to be 100% air saturated frequently. We also know from pilot trials background is negligible, so we don't need to perform an adjustment."

Here, we will inspect the data, calculate a rate from a stable, consistent region, and convert it to units

Example data

The data flowthrough.rd contains measurements of oxygen consumption in a species of chiton, (Mopalia lignosa). Detailed information about the data, can be obtained with the command ?flowthrough.rd.

head(flowthrough.rd, n = 4)

We can see this dataset contains time values (secs), both outflow and inflow oxygen concentrations (mg/L), and an oxygen delta, which is simply the difference between outflow and inflow. We will ignore these last two columns here.

Determine inflow oxygen {#case1oxy}

Knowing the temperature and salinity of the header tank allows us to calculate the saturated concentration (strictly speaking atmospheric pressure is also required, but we will use the default value). Obviously, we want the same oxygen units as the outflow recording.

convert_DO(100, from = "%Air", to = "mg/L",
           t = 12, S = 30)

Therefore we know the theoretical concentration of the header tank in these conditions and this can be used as an inflow oxygen value.

Inspect data

The concentration of the header tank can be entered as the in.oxy.value in inspect.ft, which will allow it to calculate an oxygen delta. We use time and out.oxy to specify the respective columns.

# inspect
insp1 <- inspect.ft(flowthrough.rd, time = 1, out.oxy = 2, in.oxy.value = 8.92)
# inspect
insp1 <- (suppressMessages(inspect.ft(flowthrough.rd, time = 1, out.oxy = 2, in.oxy.value = 8.92, plot = F)))
plot(insp1, quiet = TRUE)

The data checks show us there are no structural issues with the data, such as missing values or gaps. The top plot shows outflow and the entered, constant inflow oxygen against both time (bottom axis) and row index (top axis), which are the same here because data are recorded every second. The bottom plot shows the calculated delta oxygen values, which is a proxy for the rate. In these plots, consistent oxygen uptake or production rates will be represented by level or flat regions. Here there is some fluctuation towards the end of the data, but the first 400 rows look to have stable rates.

To help with choosing regions of the data from which to calculate rates, you can pass the width input in the main function call or when plotting the output object. This should be a value between 0 and 1 representing a proportion of the total data length, and smooths the delta oxygen values by performing a rolling mean.

# inspect
plot(insp1, width = 0.05)
# inspect
plot(insp1, width = 0.05, quiet = TRUE)

This is a visual aid and only affects plotted values. It does not alter the delta oxygen values in the output.

Calculate rate {#case1plot}

Calculating a rate requires the flowrate of water through the respirometer in a volume (ul, ml, or L) per unit time (sec, min, hr, or day). Here, only the value is required; the actual units will be entered in a later conversion function. For this dataset experimental data are in ?flowthrough.rd, and in this example the flowrate is 2.34 mL/min. (Yes, this flowrate is extremely low! It was controlled with a peristaltic pump, and gives you an idea of what it takes to get a viable rate via flowthrough respirometry from a small coldwater mollusc!).

# calculate rate
rate1 <- calc_rate.ft(insp1, from = 1, to = 400, by = "row", flowrate = 2.34)
# calculate rate
rate1 <- suppressMessages(calc_rate.ft(insp1, from = 1, to = 400, by = "row", flowrate = 2.34, plot = FALSE))
print(rate1)
plot(rate1, quiet = TRUE)

calc_rate.ft calculates the rate by averaging all delta oxygen values within the chosen data region, and multiplying this by the flowrate. The bottom plot shows this region in close-up. Note, the dashed line is only there to illustrate the trend.

Convert rate {#case1rate}

New we take the unitless rate from calc_rate.ft and convert it to output units. This can be an absolute rate, that is of the whole animal or chamber, or a mass- or area-specific rate if either of these are entered. Here we will calculate both the absolute and mass-specific rate of this chiton.

Here we need to tell the function the units of oxygen of the original data and of the flowrate, as well as the mass of the specimen in kg. Lastly, we specify the output units.

# absolute rate
rate1_abs <- convert_rate.ft(rate1,  
                             oxy.unit = "mg/L",  
                             flowrate.unit = "ml/min",  
                             output.unit = "mg/h")  
# mass-specific rate  
rate1_ms <- convert_rate.ft(rate1,  
                            oxy.unit = "mg/L",  
                            flowrate.unit = "ml/min",  
                            mass = 0.000070,  # mass must always be in kg
                            output.unit = "mg/h/g")  
# absolute rate
rate1_abs <- suppressMessages(convert_rate.ft(rate1, 
                                              oxy.unit = "mg/L",
                                              flowrate.unit = "ml/min",
                                              output.unit = "mg/h"))
# mass-specific rate
rate1_ms <- suppressMessages(convert_rate.ft(rate1, 
                                             oxy.unit = "mg/L",
                                             flowrate.unit = "ml/min",
                                             mass = 0.000070,
                                             output.unit = "mg/h/g"))
cat("########### Absolute rate ##########")
print(rate1_abs)

cat("\n######## Mass-specific rate ########")
print(rate1_ms)

Rates can easily be extracted from the output using $rate.output.

rate1_final <- rate1_ms$rate.output
print(rate1_final)

Or you can use summary and export = TRUE to save the summary table as a new data frame which contains all rate regression parameters and data locations, adjustments (if applied), units, and more. This is a great way of exporting all the relevant data for your final results.

rate1_final <- summary(rate1_ms, export = TRUE)
rate1_final

Case 2: Outflow and inflow oxygen

"We have both outflow and inflow oxygen recordings. We also know from pilot trials background is negligible, so we don't need to perform an adjustment."

This is a very similar analysis to Case 1 above, but we will use the inflow oxygen recording as the in.oxy input. The process is otherwise the same: inspect.ft, calc_rate.ft, and convert_rate.ft.

Inspect data

# inspect
insp2 <- inspect.ft(flowthrough.rd, time = 1, out.oxy = 2, in.oxy = 3)
# inspect
insp2 <- (suppressMessages(inspect.ft(flowthrough.rd, time = 1, out.oxy = 2, in.oxy = 3, plot = F)))
plot(insp2, quiet = TRUE)

Compared to the inspect.ft plot in Case 1 the delta oxygen values are less variable, which suggests that at least some of the variation observed in the previous example is explained by variation in inflowing oxygen levels. This means that in this example delta oxygen are more consistent, so we will use the entire dataset to calculate the rate.

Calculate rate

Since we are happy the rate is consistent across the whole datatset, running calc_rate.ft using the default values will calculate rate as the average of every delta oxygen value multiplied by the flowrate.

# calculate rate
rate2 <- calc_rate.ft(insp2, flowrate = 2.34)
# calculate rate
rate2 <- suppressMessages(calc_rate.ft(insp2, flowrate = 2.34, plot = FALSE))
print(rate2)
plot(rate2, quiet = TRUE)

The slightly lower rate value we get here than in Case 1 (1.65 vs 1.74) suggests the header tank supply is not quite at the theoretical maximum air saturation that we calculated in Case 1, and demonstrates that it is always a good idea to have a recording of the inflow water or shared water source if it is practical to do so.

Convert rate

We will convert to only the mass-specific rate this time.

# mass-specific rate  
rate2_ms <- convert_rate.ft(rate2,  
                            oxy.unit = "mg/L",  
                            flowrate.unit = "ml/min",  
                            mass = 0.000070,  
                            output.unit = "mg/h/g")  
# mass-specific rate
rate2_ms <- suppressMessages(convert_rate.ft(rate2, 
                                             oxy.unit = "mg/L",
                                             flowrate.unit = "ml/min",
                                             mass = 0.000070,
                                             output.unit = "mg/h/g"))
print(rate2_ms)

Case 3: Outflow and inflow oxygen plus control {#case3}

"We have both outflow and inflow oxygen recordings. We have also have a concurrent recording from a blank control chamber that we want to use to adjust the specimen rates."

The flowthrough_mult.rd dataset contains several columns of data from a flowthrough experiment using three specimen chambers and one control chamber: paired columns of outflow (cols 2,3,4,5) and inflow (cols 6,7,8,9) oxygen recordings, delta oxygen columns (cols 10,11,12,13) calculated from these paired columns, and a recording from a shared header tank (col 14). Other experimental parameters can be seen with ?flowthrough_mult.rd. The units of time are minutes, and of oxygen percent air saturation (%Air). We will convert to actual units of oxygen concentration when we convert the rate.

Inspect data

We will use inspect.ft to examine the first specimen column pair of outflow and inflow oxygen.

# inspect
insp3 <- inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 2, in.oxy = 6)
# inspect
insp3 <- inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 2, in.oxy = 6, plot = F)
plot(insp3, quiet = TRUE)

Here we see a warning about not evenly-spaced time values. This comes from this dataset using decimalised minutes as the time values and can be safely ignored (the full print() command provides more details).

This data is typical of flowthrough experiments, in that it takes some time before the experiment stabilises, that is the specimen's respiration rate reaches equilibrium with the supply of oxygen. Here this occurs after around 25 minutes, so we don't want to use any data before this. Rates after this are very stable however.

Calculate rate

No inputs for to and by means the function will calculate the rate from the from input to the end of the dataset in the default units of "time".

# calculate rate
rate3 <- calc_rate.ft(insp3, from = 30, flowrate = 0.1)
# calculate rate
rate3 <- suppressMessages(calc_rate.ft(insp3, from = 30, flowrate = 0.1, plot = FALSE))
print(rate3)
plot(rate3, quiet = TRUE)

Calculate background rate {#case3bg}

The fourth column pair contains outflow and inflow oxygen from a blank control chamber that we will use to determine the contribution of microbial respiration to the oxygen used, with the assumption that this is the same in the specimen chambers. Note, it is important that background experiments are conducted with the same equipment, under the same conditions, and using the same flowrate as specimen experiments, and data are in the same units. They do not necessarily have to be run concurrently; respR allows you to save a background rate to adjust multiple different experiments.

# inspect
bg <- inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 5, in.oxy = 9)
# inspect
bg <- suppressWarnings(suppressMessages(inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 5, in.oxy = 9)))
plot(bg, quiet = TRUE)

Here we see there is a slight but noticeable difference in outflow and inflow oxygen due to microbial action. Moreover, it appears to be constant, so we can use the entire dataset to calculate a background rate.

We use the same calc_rate.ft function to calculate and save background rates.

# calculate rate
bgrate <- calc_rate.ft(bg, flowrate = 0.1)
# calculate rate
bgrate <- suppressMessages(bgrate <- calc_rate.ft(bg, flowrate = 0.1, plot = FALSE))
print(bgrate)
plot(bgrate, quiet = TRUE)

Adjust rate

Now we use the saved background rate object to adjust the specimen rate we determined earlier.

# adjust rate
rate3adj <- adjust_rate.ft(rate3, by = bgrate)
# calculate rate
rate3adj <- suppressMessages(adjust_rate.ft(rate3, by = bgrate))
print(rate3adj)

We can see this performs a small but significant adjustment to the specimen rate.

Note how adjust_rate.ft can accept calc_rate.ft objects for both inputs. This same background rate object can similarly be applied to the other specimen experiments in this dataset, or indeed other datasets if they are collected under the same conditions. However, it also accepts numeric values. This will give the exact same result.

# adjust rate
rate3adj <- adjust_rate.ft(rate3, by = -0.03174)
# adjust rate
rate3adj <- suppressMessages(adjust_rate.ft(rate3, by = -0.03174))
print(rate3adj)

This means you can quantify a value for a background rate and apply it to many experiments as you see fit, for example the mean rate of several background experiments. In fact, if you enter multiple background rates this is the default behaviour.

# adjust rate
adjust_rate.ft(rate3, by = c(-0.030, -0.032, -0.038, -0.040))
# adjust rate
tmp <- suppressMessages(adjust_rate.ft(rate3, c(-0.030, -0.032, -0.038, -0.040)))
print(tmp)

Note: be careful when entering rates manually like this. In respR oxygen uptake rates are negative since they represent a negative slope of oxygen against time. Background rates are typically similarly negative (though not always). If you are examining oxygen production, rates are positive, and background rates may be either sign. To summarise: oxygen removal or use is always negative, oxygen input or production is always positive.

Convert adjusted rate

Now we convert the adjusted rate. We will convert to an absolute rate this time, and a different output oxygen unit.

Because the original oxygen data were in units of percent air saturation, we need to enter the temperature, salinity and atmospheric pressure to convert these to actual concentration units of oxygen. These can be found the data help file: ?flowthrough_mult.rd.

# absolute rate  
rate3_abs <- convert_rate.ft(rate3adj,  
                             oxy.unit = "%Air",  
                             flowrate.unit = "L/min",  
                             output.unit = "ml/h",
                             t = 18, S = 0, P = 1.013)  
# absolute rate  
rate3_abs <- suppressMessages(convert_rate.ft(rate3adj,  
                                              oxy.unit = "%Air",  
                                              flowrate.unit = "L/min",  
                                              output.unit = "ml/h",
                                              t = 18, S = 0, P = 1.013))
print(rate3_abs)

Case 4: Delta oxygen values

"We have already calculated a delta oxygen between outflow and inflow oxygen concentrations."

For cases where delta oxygen between outflow and inflow concentrations has already been calculated, these data can be inspected using the delta.oxy input and used to calculate rates. flowthrough_mult.rd contains these in columns 10 to 13. We will use the second column of these.

Inspect data

# inspect
insp4 <- inspect.ft(flowthrough_mult.rd, time = 1, delta.oxy = 11)
# inspect
insp4 <- suppressWarnings(suppressMessages(inspect.ft(flowthrough_mult.rd, time = 1, delta.oxy = 11)))
plot(insp4, quiet = TRUE)

When delta oxygen data are inspected, only one plot is produced of the entered delta oxygen values. Note how delta oxygen are plotted on a reverse y-axis, so that higher uptake rates are plotted higher even though they are lower numerically (more negative). This can be changed by passing rate.rev = FALSE in the main call or when plotting the output.

Again we see the experiment has not reached equilibrium until after around 20 minutes, so we will not use any data before this.

Calculate rate

This time we will calculate rates using subsetting by "row" which can be see in the top red x-axis.

# calculate rate
rate4 <- calc_rate.ft(insp4, from = 2000, to = 3000, by = "row", flowrate = 0.1)
# calculate rate
rate4 <- suppressMessages(calc_rate.ft(insp4, from = 2000, to = 3000, by = "row", flowrate = 0.1, plot = F))
print(rate4)
plot(rate4, quiet = TRUE)

Adjust rate

We already determined background rate in Case 3, so we can use the same object here.

# adjust rate
rate4adj <- adjust_rate.ft(rate4, by = bgrate)
# calculate rate
rate4adj <- suppressMessages(adjust_rate.ft(rate4, by = bgrate))
print(rate4adj)

Convert adjusted rate

Now we convert the adjusted rate.

# mass-specific rate  
rate4_abs <- convert_rate.ft(rate4adj,  
                             oxy.unit = "%Air",  
                             flowrate.unit = "L/min",  
                             output.unit = "ml/h",
                             t = 18, S = 0, P = 1.013)  
# mass-specific rate
rate4_abs <- suppressMessages(convert_rate.ft(rate4adj,  
                                              oxy.unit = "%Air",  
                                              flowrate.unit = "L/min",  
                                              output.unit = "ml/h",
                                              t = 18, S = 0, P = 1.013))
print(rate4_abs)

Case 5: Active rates

"We imposed a treatment at a specific time to increase the specimen's activity and want to determine an active metabolic rate from this region of the data."

Inspect data

Inspecting the data from the third chamber, we can see a prominent area where the specimen's oxygen consumption rate increased, before slowly recovering to routine levels.

# inspect 
insp5 <- inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 4, in.oxy = 8)
# inspect
insp5 <- suppressWarnings(suppressMessages(inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 4, in.oxy = 8, plot = FALSE)))
plot(insp5, quiet = TRUE)

Note how delta oxygen are plotted on a reverse y-axis, so that higher uptake rates are plotted higher even though they are lower numerically (more negative). If you are examining oxygen production rates, you can pass the rate.rev = FALSE input in the main call or when plotting the output to see higher production rates plotted higher.

Since we are only interested in this active region, we can use the subset_data() function and pipe (%>%) the result to inspect.ft for a closer look.

# subset and inspect
insp5 <- subset_data(flowthrough_mult.rd, from = 30, to = 50, by = "time") %>%
  inspect.ft(time = 1, out.oxy = 4, in.oxy = 8)
# inspect
insp5 <- suppressWarnings(suppressMessages(subset_data(flowthrough_mult.rd, from = 30, to = 50, by = "time") %>%
                                             inspect.ft(time = 1, out.oxy = 4, in.oxy = 8, plot = FALSE)))
plot(insp5, quiet = TRUE)

Note, the row index x-axis now refers to the subset not the original data. We can see the highest active rate occurs between around 38 and 42 minutes, so we will use this region to calculate the active rate.

Calculate rate

# calculate rate
rate5 <- calc_rate.ft(insp5, from = 38, to = 42, by = "time", flowrate = 0.1)
# calculate rate
rate5 <- suppressMessages(calc_rate.ft(insp5, from = 38, to = 42, by = "time", flowrate = 0.1, plot = FALSE))
plot(rate5, quiet = TRUE)

Adjust rate

We can also adjust this rate by the background rate we saved earlier.

# adjust rate
rate5adj <- adjust_rate.ft(rate5, by = bgrate)
# calculate rate
rate5adj <- suppressMessages(adjust_rate.ft(rate5, by = bgrate))
print(rate5adj)

Convert adjusted rate

Lastly we convert the adjusted rate, this time to a mass-specific rate, and try another different oxygen amount metric in the output units.

# mass-specific rate  
rate5_ms <- convert_rate.ft(rate5adj,  
                            oxy.unit = "%Air",  
                            flowrate.unit = "L/min",  
                            output.unit = "umol/h/g",
                            mass = 0.020,
                            t = 18, S = 0, P = 1.013)  
# mass-specific rate
rate5_ms <- suppressMessages(convert_rate.ft(rate5adj,  
                                             oxy.unit = "%Air",  
                                             flowrate.unit = "L/min",  
                                             output.unit = "umol/h/g",
                                             mass = 0.020,
                                             t = 18, S = 0, P = 1.013))
print(rate5_ms)

Case 6: Non-constant background rates

"We want to correct for both non-constant background activity and any possible fluctuation in inflow oxygen concentrations using a concurrent empty control or \"blank\" chamber."

In some experiments, as well as possible fluctuations in the oxygen content of the inflowing water, the background rate may change over the course of the experiment. While the former will be accounted for by having an inflow oxygen recording, they can both be accounted for simultaneously by using the outflow recording from a blank control chamber to adjust a specimen chamber. This can be done by using it as the in.oxy input in inspect.ft.

The flowthrough_sim.rd dataset contains data from an experiment where the background rate increases as the experiment progresses. There are four columns; time, outflow oxygen from the specimen and blank chambers, and the inflow oxygen in a header tank supplying both.

Inspect data

We will inspect the specimen chamber as we normally would, using the header tank as the inflow recording.

# inspect 
insp6 <- inspect.ft(flowthrough_sim.rd, time = 1, out.oxy = 2, in.oxy = 4)
# inspect
insp6 <- suppressMessages(inspect.ft(flowthrough_sim.rd, time = 1, out.oxy = 2, in.oxy = 4, plot = FALSE))
plot(insp6, quiet = TRUE)

Here we see, after the initial period before the experiment has reached equilibrium, the specimen rate apparently continues to increase. This suggests that the background rate may not be constant.

Inspect background

If we examine the background data in the same way:

# inspect 
bg <- inspect.ft(flowthrough_sim.rd, time = 1, out.oxy = 3, in.oxy = 4)
# inspect
bg <- suppressMessages(inspect.ft(flowthrough_sim.rd, time = 1, out.oxy = 3, in.oxy = 4, plot = FALSE))
plot(bg, quiet = TRUE)

We can see the background rate increases as the experiment progresses, in what looks like a constant manner. With the presumption that this is also occurring in the specimen chamber, we only need to use the outflow recording from the control chamber as the in.oxy input to account for this.

Account for background

# inspect 
insp6 <- inspect.ft(flowthrough_sim.rd, time = 1, out.oxy = 2, in.oxy = 3)
# inspect
insp6 <- suppressMessages(inspect.ft(flowthrough_sim.rd, time = 1, out.oxy = 2, in.oxy = 3, plot = FALSE))
plot(insp6, quiet = TRUE)

Now we see that when the background is accounted for, the specimen rates are consistent, and we can go ahead and calculate a rate.

Calculate rate

# calculate rate
rate6 <- calc_rate.ft(insp6, from = 2000, to = 3000, by = "row", flowrate = 0.1)
# calculate rate
rate6 <- suppressMessages(calc_rate.ft(insp6, from = 2000, to = 3000, by = "row", flowrate = 0.1, plot = FALSE))
plot(rate6, quiet = TRUE)

The rate can now be converted as in the above examples.

Note, that in this case the background rate increased in a constant, linear manner. Under other conditions, the background may vary in other ways, such as increase exponentially, or decrease due to lower light levels at night. Inflowing oxygen may also vary due to various reasons. It's important to note that the above method of accounting for background and other fluctuations in oxygen will work for all these cases, under the presumption that the same thing is occurring in specimen chambers.

Case 7: Single values only

"We do not have continuous recordings. We took spot readings of inflow and outflow recordings, after they had stabilised (i.e. the experiment had reached equilibrium), and we want to use these to calculate the rate."

Flowthrough respirometry has advantages over other respirometry methods, in that because there is a constant supply of water (and therefore oxygen) experiments can be run for much longer, allowing specimens to acclimate better to the experimental conditions without concerns about effects of hypoxia or waste build up. They can also be more practical and a more efficient use of equipment, in that continuous recordings of oxygen (while ideal practice) are not always necessary as long as experiments are monitored to ensure they have reached equilibrium. In these cases, a single oxygen probe could be used to sample the outflow and inflow oxygen of any number of separate chambers, and so in cases of limited equipment availability help with increasing the amount of data collected.

All the functions (where possible) in respR accept numeric inputs, as well as other R objects such as data frames and data tables. As long as care is taken to keep units and inputs consistent, these can be used to calculate rates. In this example, after monitoring to ensure equilibrium has been reached, spot readings can be used to calculate the routine respiration rate of specimens.

Create dataframe

In this example, we have a spot sample of outflow and inflow oxygen. We just need to put these into a data frame with any time value to be able to process them in inspect.ft.

## Single spot checks of outflow and inflow
## Create dataframe
df <- data.frame(time = 1,
                 outflow = 7.32,
                 inflow = 8.04)

Inspect, calculate rate, convert

Now all we need to do is the usual workflow: inspect.ft > calc_rate.ft > convert_rate.ft.

insp7 <- inspect.ft(df, time = 1, out.oxy = 2, in.oxy = 3)
rate7 <- calc_rate.ft(insp7, flowrate = 0.25)
rate7_abs <- convert_rate.ft(rate7,
                             oxy.unit = "mg/l",  
                             flowrate.unit = "L/min",  
                             output.unit = "mg/h")
insp7 <- inspect.ft(df, time = 1, out.oxy = 2, in.oxy = 3, plot = F)
rate7 <- calc_rate.ft(insp7, flowrate = 0.25, plot = F)
rate7_abs <- convert_rate.ft(rate7,
                             oxy.unit = "mg/l",  
                             flowrate.unit = "L/min",  
                             output.unit = "mg/h")

print(rate7)
print(rate7_abs)

Delta oxygen values

In fact, things are even simpler if spot readings are converted to delta oxygen values, as calc_rate.ft can convert these directly. We can also adjust the rate in adjust_rate.ft using a rate calculated from similar spot readings from a control chamber.

## specimen delta oxygen = outflow minus inflow
del7 <- 7.32 - 8.04
## control delta oxygen
delbg <- 7.97 - 8.04

rate7 <- calc_rate.ft(del7, flowrate = 0.25)
ratebg <- calc_rate.ft(delbg, flowrate = 0.25)

rate7_adj <- adjust_rate.ft(rate7, by = ratebg)

rate7_abs <- convert_rate.ft(rate7_adj,
                             oxy.unit = "mg/l",  
                             flowrate.unit = "L/min",  
                             output.unit = "mg/h")
del7 <- 7.32 - 8.04
print("# del7")
del7
delbg <- 7.97 - 8.04
print("# bg7")
delbg

rate7 <- calc_rate.ft(del7, flowrate = 0.25, plot = FALSE)
ratebg <- calc_rate.ft(delbg, flowrate = 0.25, plot = FALSE)

rate7_adj <- adjust_rate.ft(rate7, by = ratebg)

rate7_abs <- convert_rate.ft(rate7_adj,
                             oxy.unit = "mg/l",  
                             flowrate.unit = "L/min",  
                             output.unit = "mg/h")
print(rate7)
print(rate7_adj)
print(rate7_abs)

Multiple delta oxygen values

You can even use vectorised operations to convert multiple rates.

## specimen delta oxygen = outflow minus inflow
outflows <- c(7.32, 7.45, 7.19, 7.27)
## control delta oxygen
inflows <- 8.04

deltas <- outflows - inflows
rates <- calc_rate.ft(deltas, flowrate = 0.25)
rates_abs <- convert_rate.ft(rates,
                             oxy.unit = "mg/l",  
                             flowrate.unit = "L/min",  
                             output.unit = "mg/h")
## specimen delta oxygen = outflow minus inflow
outflows <- c(7.32, 7.45, 7.19, 7.27)
## control delta oxygen
inflows <- 8.04

deltas <- outflows - inflows

rates <- calc_rate.ft(deltas, flowrate = 0.25, plot = FALSE)
rates_abs <- convert_rate.ft(rates,
                             oxy.unit = "mg/l",  
                             flowrate.unit = "L/min",  
                             output.unit = "mg/h")
print(rates_abs)
summary(rates_abs)

By default the first output is printed, but you can print others by using the pos input, e.g. print(rates_abs, pos = 2) or by using summary() to see all results.

Case 8: Rolling rate {#rollrate}

"We want to calculate every rate of a fixed width across the entire dataset and then filter the results manually. We are interested in the routine metabolic rate and the maximum metabolic rate from particular regions of the data."

calc_rate.ft can calculate a rolling rate of fixed width in rows across the dataset. In this case there will be multiple rates in output$rate and the same number of rows in output$summary. The select_rate.ft() function can be used to filter the results according to various criteria. See vignette("select_rate") for examples with non-flowthrough data, but which are essentially the same.

Note the calc_rate.ft summary table contains linear regression coefficients alongside other metadata. These should not be confused with those returned by other functions. In outputs of functions such as calc_rate, slopes represent rates and coefficients such as a high r-squared are important. With flowthrough data the linear model is fit through the delta oxygen values averaged to provide a final rate. Therefore slope generally represents the stability of the data region, in that the closer it is to zero, the less the delta oxygen values in that region vary, which is an indication of a region of stable rates. They are included to enable possible future functionality where stable regions may be automatically identified, and should generally be ignored. However, advanced users can use these and select_rate.ft() to explore and subset the results if they wish.

In this example, we want to extract a routine metabolic rate (RMR) which we will define as the most consistent rate sustained across a five minute window, and also a maximum metabolic rate (MMR) or the highest rate sustained across a five minute window.

Inspect data

## inspect
insp8 <- inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 4, in.oxy = 7)
## inspect
insp8 <- insp8 <- inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 4, in.oxy = 7, plot = F)
plot(insp8)

Here, we see it takes around 10 minutes for the the experiment to reach equilibrium, so we want to exclude rates before this time. We then see a region of consistent rates from which we will extract the RMR, then a region of higher activity and therefore higher rates from which we will get the MMR.

Calculate rate across rolling window

In these data (with oxygen recorded once per second, but the time values in decimalised minutes) a five minute window would be 300 rows. We will calculate a rolling rate across this window, adjust and convert the results, and in the following sections filter them to get a final RMR and MMR.

# calculate rolling 5-minute rate
roll_rate <- calc_rate.ft(insp8, width = 300, by = "row", flowrate = 0.1)
# adjust rates
roll_rate_adj <- adjust_rate.ft(roll_rate, by = bgrate)
# convert rates to mass-specific
roll_rate_ms <- convert_rate.ft(roll_rate_adj,  
                                oxy.unit = "%Air",  
                                flowrate.unit = "L/min",  
                                output.unit = "umol/h/g",
                                mass = 0.020,
                                t = 18, S = 0, P = 1.013)
# calculate rolling 5-minute rate
roll_rate <-  suppressMessages(calc_rate.ft(insp8, width = 300, by = "row", flowrate = 0.1))
# adjust rates
roll_rate_adj <- suppressMessages(adjust_rate.ft(roll_rate, by = bgrate))
# convert rates to mass-specific
roll_rate_ms <- suppressMessages(convert_rate.ft(roll_rate_adj,  
                                                 oxy.unit = "%Air",  
                                                 flowrate.unit = "L/min",  
                                                 output.unit = "umol/h/g",
                                                 mass = 0.020,
                                                 t = 18, S = 0, P = 1.013))
summary(roll_rate_ms)

We can see there are 3441 results, which is obviously a lot of information. As of respR v2.2 convert_rate.ft objects can be plotted to help explore the results in three different ways. See help("convert_rate.ft") for full details. Here we will use the type = "rate" option which plots output rates in a way that you can see how they vary across the dataset.

# plot rates
plot(roll_rate_ms, type = "rate")

At a fixed width the rolling rate is obviously closely related to the delta oxygen values. We can use pos to more closely examine output rates from particular regions.

# plot rates
plot(roll_rate_ms, type = "rate", pos = 500:1700)

This tells us our extracted RMR should be around 7.30 to 7.40 umol/hr/g.

Select results for RMR

The select_rate.ft function allows you to apply multiple selection criteria by either saving the result and processing it through the function multiple times or by using pipes (|> or %>%). Here, to get RMR we will only use rates from 10 to 30 minutes, then select the lowest tenth percentile of these rates, and then take the mean of these. Please note, this is not a recommendation that this is how you should extract an RMR from your own data; it is simply an example of one approach and how the respR functions are flexible and adaptable. An alternative approach might be to select only the single lowest rate, for instance.

rmr <- 
  roll_rate_ms |>
  select_rate.ft(method = "time", n = c(10,30)) |>
  select_rate.ft(method = "lowest_percentile", n = 0.1) |>
  summary() |>
  mean()
rmr <- 
  roll_rate_ms |>
  select_rate.ft(method = "time", n = c(10,30)) |>
  select_rate.ft(method = "lowest_percentile", n = 0.1) |>
  summary() |>
  mean()

Note how the slope_b1 values are very close to zero, that is the linear regression fit through the delta mean values averaged to get each rate is close to a flat line. This is an indication that the region has stable delta oxygen values and therefore stable rates. select_rate.ft has a slope method so these could be used to select results based on stable values.

Select results for MMR

To get MMR we will simply extract the single highest 5-minute rate from the region of elevated rates at around 40 minutes.

mmr <- 
  roll_rate_ms |>
  select_rate.ft(method = "time", n = c(30,50)) |>
  select_rate.ft(method = "highest", n = 1) |>
  summary(export = TRUE)
mmr <- 
  roll_rate_ms |>
  select_rate.ft(method = "time", n = c(30,50)) |>
  select_rate.ft(method = "highest", n = 1) |>
  summary(export = TRUE)

Here we have used the export option in summary which saves the summary table as a data frame. This is a great way of saving results for archiving or further analysis.

The rank column tells us which result this is and can be used to plot this result using pos for a closer look using the type = "full" option.

plot(roll_rate_ms, pos = 2270, type = "full")

Case 9: Multiple rates from the same dataset

"We want to extract rates from several different time periods during an experiment."

As can be seen in the above example, where we used a rolling window, multiple rates can be extracted from different regions of the same dataset, even overlapping ones. As well as using the width input, this can be done by entering vectors of paired start and end values as the from and to inputs in the appropriate by units.

Inspect and calculate rates

# inspect 
insp9 <- inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 3, in.oxy = 7)
# calculate rates
rate9 <- calc_rate.ft(insp9, 
                      from = c(25, 35, 45), 
                      to = c(30, 40, 50), 
                      by = "time", 
                      flowrate = 0.1)
insp9 <- suppressWarnings(suppressMessages(inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 3, in.oxy = 7, plot = FALSE)))

rate9 <- suppressMessages(calc_rate.ft(insp9, 
                                       from = c(25, 35, 45), 
                                       to = c(30, 40, 50), 
                                       by = "time", 
                                       flowrate = 0.1, plot = F))

Examine output

The different rates can be plotted using the pos input.

plot(rate9, pos = 2)

And they can all be viewed using summary.

summary(rate9)

Convert results

The results can be adjusted and converted as in the above examples. Both adjust_rate.ft and convert_rate.ft accept objects containing multiple rates and can be plotted and explored in the same way. See above for how multiple rate results can be filtered using select_rate.ft.

Case 10: Concise, reportable analysis using piping

"We want to report our analyses in a reproducible way as concisely as possible."

The overall aim of respR is to facilitate reporting of reproducible data analyses. This example is simply to show how a complete analysis of a flowthrough respirometry experiment can be reported concisely.

We will repeat the analysis from Case 3, but use piping to feed the results of one function into the next. We will use %>% or dplyr pipes here, but the new native |> pipes introduced in R v4.1 work just as well.

## calc background rate 
## (might only need to be done once and used for multiple experiments)
bgrate <- 
  inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 5, in.oxy = 9) %>%
  calc_rate.ft(flowrate = 0.1)

# inspect > calc rate > adjust > convert
rate3_abs <- 
  inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 2, in.oxy = 6) %>%
  calc_rate.ft(from = 30, flowrate = 0.1) %>%
  adjust_rate.ft(by = bgrate) %>%
  convert_rate.ft(oxy.unit = "%Air",  
                  flowrate.unit = "L/min",  
                  output.unit = "ml/h",
                  t = 18, S = 0, P = 1.013)

print(rate3_abs)
print(rate3_abs)

Notes

## Plot column 15 (temperature) alongside oxygen timeseries
inspect.ft(flowthrough_mult.rd, time = 1, out.oxy = 2, in.oxy = 5, 
           add.data = 15)


januarharianto/respR documentation built on March 29, 2024, 6:51 p.m.