# Introduction to Seahtrue

# Seahtrue overview

The seahtrue package offers a set of functions to be able to perform reproducible data analysis of extracellular flux analysis. The main function `revive_xfplate()` reads, preprocess and validates the input data and outputs experimental details and outcome variables in an organized (tidy) way. The output of the `revive_xfplate()` is a nested tibble.

# Extracellular flux analysis scientific primer

With instruments such as the Seahorse XF analyzer from Agilent, but also the O2K from Oroboros, other oyxgraphs (from Hansatech instruments for example) and the ReSipher from Lucid Scientific, scientists are able to analyze oxygen consumption of living biological samples.

Oxygen consumption of cells or small model organism can provide insights into the function of the mitochondria, since mitochondria are most of the time the main O2 consumers of cells. Apart form oxygen consumption the Seahorse XF analyzer is able to analyze in parallel the extracellular acidification of the culture medium in which the sample is emerged. This can be a proxy for glycolytic activity of samples.

Seahorse extracellular flux instruments performs analysis of O2 and pH in either 96 wells, 24 wells or 8 wells, and typically O2 and pH are monitored over a period of around 1 hour, in discrete measurements of typically 3 minutes each. Furthermore, perturbations of cellular functional states can be induced by adding compounds while performing the assay. The most common perturbations that are performed are injections of oligomycin, fccp and anitmycin/rotenone, known as a mitostress test.

# Resources

Divakaruni, Ajit S., and Martin Jastroch. “A Practical Guide for the Analysis, Standardization and Interpretation of Oxygen Consumption Measurements.” Nature Metabolism 4, no. 8 (August 15, 2022): 978–94. https://doi.org/10.1038/s42255-022-00619-4.

Gerencser, A. A., A. Neilson, S. W. Choi, U. Edman, N. Yadava, R. J. Oh, D. A. Ferrick, D. G. Nicholls, and M. D. Brand. “Quantitative Microplate-Based Respirometry with Correction for Oxygen Diffusion.” Anal Chem 81, no. 16 (August 15, 2009): 6868–78. https://doi.org/10.1021/ac900881z.

Janssen, J. J. E., B. Lagerwaard, A. Bunschoten, H. F. J. Savelkoul, R. J. J. van Neerven, J. Keijer, and V. C. J. de Boer. “Novel Standardized Method for Extracellular Flux Analysis of Oxidative and Glycolytic Metabolism in Peripheral Blood Mononuclear Cells.” Sci Rep 11, no. 1 (January 18, 2021): 1662. https://doi.org/10.1038/s41598-021-81217-4.

Zhang, Xiang, Taolin Yuan, Jaap Keijer, and Vincent C. J. de Boer. “OCRbayes: A Bayesian Hierarchical Modeling Framework for Seahorse Extracellular Flux Oxygen Consumption Rate Data Analysis.” PLOS ONE 16, no. 7 (July 15, 2021): e0253926. https://doi.org/10.1371/journal.pone.0253926.

# Seahtrue

## Inspect the seahtrue data

    # data_file_path <-
    #   system.file("data",
    #               "revive_output_donor_A.rda",
    #               package = "seahtrue")
    #
    # load(data_file_path)

    #library(seahtrue)
    #library(tidyverse)
    revive_output_donor_A <-
      system.file("extdata",
                  "20191219_SciRep_PBMCs_donor_A.xlsx",
                  package = "seahtrue") %>%
      seahtrue::revive_xfplate()
    #> → Start function to read seahorse plate data from Excel file:
    #> 20191219_SciRep_PBMCs_donor_A.xlsx
    #> ℹ Finished collecting assay information.
    #> → plateid is identified as:V0174416419V
    #> → Rate was exported WITH background correction
    #> ℹ Finished preprocessing of the input data
    #> ℹ Finished validating the input data

Take a glimpse at the generated data from the `revive_xfplate()` function:

    revive_output_donor_A %>%
      dplyr::glimpse()
    #> Rows: 1
    #> Columns: 9
    #> $ plate_id          <chr> "V0174416419V"
    #> $ filepath_seahorse <list> [<tbl_df[1 x 3]>]
    #> $ date_run          <chr> "19-12-2019 17:25"
    #> $ date_processed    <dttm> 2026-04-30 06:26:51
    #> $ assay_info        <list> [<tbl_df[1 x 24]>]
    #> $ injection_info    <list> [<tbl_df[12 x 3]>]
    #> $ raw_data          <list> [<tbl_df[13824 x 21]>]
    #> $ rate_data         <list> [<tbl_df[1140 x 12]>]
    #> $ validation_output <list> [TRUE, TRUE, [<tbl_df[12 x 10]>], [<tbl_df[13824 x 4…

Our data starts with 4 columns of identifiers. `plate_id`, `filepath_seahorse`, `date_run` and `date_processed`, this keeps the data easily traceable with ids on the top level. After that, 5 columns of nested tibbles follow. The `assay_info` column contains a tibble/dataframe with information that was stored in the experimental file. This is either information that the user put into the software before running the experiment, after running the software when processing the data, or was generated by the software. The next colum is the `injection_info` containing measurement, interval and injection. Then the two data columns for `raw_data` and `rate_data` are listed as tibble/dataframe. The final column is the `validation_output` that has the output of the validations as well as its rules. In the next sections, we will explore each data output separately.

## Rate data

The `rate_data` is what scientist typically use for their interpretation of their XF experiments. It contains the calculated OCR (oxygen consumption rate) and ECAR (extracellular acidification rate) values, together with the PER (proton efflux rate). The PER is calculated from ECAR when the buffer capacity is known and set in the experiment. In our `rate_data` we only have the OCR and ECAR data, since PER can be easily calculated.

    revive_output_donor_A %>%
      purrr::pluck("rate_data", 1)
    #> # A tibble: 1,140 × 12
    #>    measurement well  group time_wave OCR_wave OCR_wave_bc ECAR_wave ECAR_wave_bc
    #>          <dbl> <chr> <chr>     <dbl>    <dbl>       <dbl>     <dbl>        <dbl>
    #>  1           1 A01   Back…      1.31        0        0            0         0
    #>  2           1 A02   50.0…      1.31        0        6.22         0         2.90
    #>  3           1 A03   100.…      1.31        0       26.6          0         5.87
    #>  4           1 A04   100.…      1.31        0       21.4          0         4.40
    #>  5           1 A05   150.…      1.31        0        3.08         0        12.4
    #>  6           1 A06   200.…      1.31        0       41.1          0         8.98
    #>  7           1 A07   150.…      1.31        0       39.5          0         9.27
    #>  8           1 A08   200.…      1.31        0       40.4          0         4.75
    #>  9           1 A09   250.…      1.31        0       58.8          0         7.39
    #> 10           1 A10   250.…      1.31        0       59.4          0         6.88
    #> # ℹ 1,130 more rows
    #> # ℹ 4 more variables: cell_n <dbl>, interval <dbl>, injection <chr>,
    #> #   flagged_well <lgl>

The `rate_data` has `well`,`measurement`, `group` identifiers for each row followed by the `time_wave` column which provides the time of the measurement in minutes, and the OCR and ECAR data columns. Also the `cell_n` and `flagged_well` status is joined in this dataframe. This provides all information for exploring and plotting the data. Since the OCR and ECAR data can be exported with either background on or off, the read functions in the seahtrue package determine whether the OCR and ECAR are background corrected or not, based on whether the Background wells have an OCR of zero. If the data was not corrected for background the the OCR is corrected while reading the .xlsx file. The background corrected data is given in `OCR_wave_bc` and `ECAR_wave_bc`. If the data was exported without background correction the `OCR_wave` and `ECAR_wave` data columns would contain the non corrected OCR and ECAR.

## Raw data

The `raw_data` tibble contains the raw data that is collected in an XF experiment. This is essential data that can give detailed insights on the quality of the assay. Apart from the data that is presented in the raw data sheet of the .xlsx, some preprocessing output is given. Such as the timescale in seconds (`timescale`) and minutes (`minutes`), as well as an `interval` and `injection` id. Also, the background corrected raw data values for pH, O2 and its emissions are given. Again, just like in the `rate_data` tibble, the `cell_n` and `flagged_well` status is given.

    revive_output_donor_A %>%
      purrr::pluck("raw_data", 1)
    #> # A tibble: 13,824 × 21
    #>    well  measurement  tick timescale minutes group interval injection O2_em_corr
    #>    <chr>       <dbl> <dbl>     <dbl>   <dbl> <chr>    <dbl> <chr>          <dbl>
    #>  1 A01             1     0         0       0 Back…        1 Baseline      12422.
    #>  2 A02             1     0         0       0 50.0…        1 Baseline      12323.
    #>  3 A03             1     0         0       0 100.…        1 Baseline      12483.
    #>  4 A04             1     0         0       0 100.…        1 Baseline      12362.
    #>  5 A05             1     0         0       0 150.…        1 Baseline      12103.
    #>  6 A06             1     0         0       0 200.…        1 Baseline      12274.
    #>  7 A07             1     0         0       0 150.…        1 Baseline      12354.
    #>  8 A08             1     0         0       0 200.…        1 Baseline      12325.
    #>  9 A09             1     0         0       0 250.…        1 Baseline      12347.
    #> 10 A10             1     0         0       0 250.…        1 Baseline      12209.
    #> # ℹ 13,814 more rows
    #> # ℹ 12 more variables: pH_em_corr <dbl>, O2_mmHg <dbl>, pH <dbl>,
    #> #   pH_em_corr_corr <dbl>, O2_em_corr_bkg <dbl>, pH_em_corr_bkg <dbl>,
    #> #   O2_mmHg_bkg <dbl>, pH_bkgd <dbl>, pH_em_corr_corr_bkg <dbl>,
    #> #   bufferfactor <dbl>, cell_n <dbl>, flagged_well <lgl>

## Assay info

The `assay_info` has information from user or software provided meta data that is associated with the experiment and plate. For example, the barcode of the cartridge that was used:

    revive_output_donor_A %>%
      purrr::pluck("assay_info", 1) %>%
      pull(cartridge_barcode)
    #> [1] "W0013917519B**+405-6+101-2300F+240-2***000A+219-4**+450-1125&"

The XF analyzer reads for each cartridge a barcode that is then associated with the assay. There is some information in the barcode that the software uses for OCR calculation. The emission of the fluorescent O2 sensors at zero oxygen `F0` (see Gerencser et al. (2009) Quantitative microplate-based respirometry with correction for oxygen diffusion. Anal Chem 81:6868, for details) is derived from the Stern-Volmer constant `KSV`. Where the emission at ambient oxygen is typically set at 12500 AU and ambient oxygen levels in wells in culture medium is set to 151.6900241 mmHg.

    # KSV in barcode
    revive_output_donor_A %>%
      purrr::pluck("assay_info", 1) %>%
      pull(cartridge_barcode) %>%
      stringr::str_sub(-18, -13)
    #> [1] "+219-4"

    # KSV in assay info sheet
    revive_output_donor_A %>%
      purrr::pluck("assay_info", 1) %>%
      pull(KSV)
    #> [1] 0.0219

    # F0 can be calculated using the stern-volmer equation
    # and the info
    # emission target at ambient O2 = 12500
    # O2 level at ambient in sample medium in well = 151.69
    #
    # F0/F = 1 + KSV*O2
    # F0 = (1+KSV*O2)*F
    # F0 = (1+ KSV*151.69)*12500

    # F0 from assay info sheet
    revive_output_donor_A %>%
      purrr::pluck("assay_info", 1) %>%
      pull(F0)
    #> [1] 54025.14

Apart from user and software generated meta info, the functions in the seahtrue package also put some relevant info into this tibble. Such as the time to start the actual measurements (`minutes_to_start_measurement_one`), that shows how long the user took to insert the cell plate and start running the measurements. The timer starts at t = 0 minutes when the cartridge is calibrated by the user.

    revive_output_donor_A %>%
      purrr::pluck("assay_info", 1) %>%
      pull(minutes_to_start_measurement_one)
    #> [1] 37.23333

Apart from the `assay_info` there can be some more meta info associated with the data tibbles in the form of `attributes`. These can also be viewed as shown in the following examples:

    revive_output_donor_A %>%
      purrr::pluck("rate_data", 1) %>% str()
    #> tibble [1,140 × 12] (S3: tbl_df/tbl/data.frame)
    #>  $ measurement : num [1:1140] 1 1 1 1 1 1 1 1 1 1 ...
    #>  $ well        : chr [1:1140] "A01" "A02" "A03" "A04" ...
    #>  $ group       : chr [1:1140] "Background" "50.000" "100.000" "100.000" ...
    #>  $ time_wave   : num [1:1140] 1.31 1.31 1.31 1.31 1.31 ...
    #>  $ OCR_wave    : num [1:1140] 0 0 0 0 0 0 0 0 0 0 ...
    #>  $ OCR_wave_bc : num [1:1140] 0 6.22 26.64 21.42 3.08 ...
    #>  $ ECAR_wave   : num [1:1140] 0 0 0 0 0 0 0 0 0 0 ...
    #>  $ ECAR_wave_bc: num [1:1140] 0 2.9 5.87 4.4 12.36 ...
    #>  $ cell_n      : num [1:1140] 0 32472 114732 83567 153510 ...
    #>  $ interval    : num [1:1140] 1 1 1 1 1 1 1 1 1 1 ...
    #>  $ injection   : chr [1:1140] "Baseline" "Baseline" "Baseline" "Baseline" ...
    #>  $ flagged_well: logi [1:1140] FALSE FALSE FALSE FALSE FALSE FALSE ...
    #>  - attr(*, "was_background_corrected")= logi TRUE

      revive_output_donor_A %>%
        purrr::pluck("rate_data", 1) %>%
        attributes() %>%
        purrr::pluck("was_background_corrected")
    #> [1] TRUE

## Injection info

Since every XF experiment uses pertubations with chemicals or nutrients the `injection_info` is important for interpretation of the experiment. The injection info can be plucked from the data tibble.

      revive_output_donor_A %>%
        purrr::pluck("injection_info", 1)
    #> # A tibble: 12 × 3
    #>    measurement interval injection
    #>          <int>    <dbl> <chr>
    #>  1           1        1 Baseline
    #>  2           2        1 Baseline
    #>  3           3        1 Baseline
    #>  4           4        2 FCCP
    #>  5           5        2 FCCP
    #>  6           6        2 FCCP
    #>  7           7        3 AM/ROT
    #>  8           8        3 AM/ROT
    #>  9           9        3 AM/ROT
    #> 10          10        4 Monensin/Hoechst
    #> 11          11        4 Monensin/Hoechst
    #> 12          12        4 Monensin/Hoechst

# Session info

    sessionInfo()
    #> R version 4.6.0 (2026-04-24)
    #> Platform: x86_64-pc-linux-gnu
    #> Running under: Ubuntu 24.04.4 LTS
    #>
    #> Matrix products: default
    #> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
    #> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
    #>
    #> locale:
    #>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C
    #>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8
    #>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8
    #>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C
    #>  [9] LC_ADDRESS=C               LC_TELEPHONE=C
    #> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
    #>
    #> time zone: Etc/UTC
    #> tzcode source: system (glibc)
    #>
    #> attached base packages:
    #> [1] stats     graphics  grDevices utils     datasets  methods   base
    #>
    #> other attached packages:
    #> [1] seahtrue_1.6.0   dplyr_1.2.1      BiocStyle_2.40.0
    #>
    #> loaded via a namespace (and not attached):
    #>  [1] utf8_1.2.6          tidyr_1.3.2         sass_0.4.10
    #>  [4] generics_0.1.4      stringi_1.8.7       rematch_2.0.0
    #>  [7] digest_0.6.39       magrittr_2.0.5      evaluate_1.0.5
    #> [10] grid_4.6.0          timechange_0.4.0    RColorBrewer_1.1-3
    #> [13] fastmap_1.2.0       cellranger_1.1.0    jsonlite_2.0.0
    #> [16] BiocManager_1.30.27 purrr_1.2.2         scales_1.4.0
    #> [19] jquerylib_0.1.4     cli_3.6.6           rlang_1.2.0
    #> [22] withr_3.0.2         cachem_1.1.0        yaml_2.3.12
    #> [25] tidyxl_1.0.10       tools_4.6.0         ggplot2_4.0.3
    #> [28] buildtools_1.0.0    vctrs_0.7.3         logger_0.4.1
    #> [31] R6_2.6.1            settings_0.2.7      lifecycle_1.0.5
    #> [34] lubridate_1.9.5     snakecase_0.11.1    stringr_1.6.0
    #> [37] janitor_2.2.1       pkgconfig_2.0.3     validate_1.1.7
    #> [40] pillar_1.11.1       bslib_0.10.0        gtable_0.3.6
    #> [43] glue_1.8.1          Rcpp_1.1.1-1.1      xfun_0.57
    #> [46] tibble_3.3.1        tidyselect_1.2.1    sys_3.4.3
    #> [49] knitr_1.51          farver_2.1.2        htmltools_0.5.9
    #> [52] rmarkdown_2.31      maketools_1.3.2     compiler_4.6.0
    #> [55] S7_0.2.2            readxl_1.4.5