Functionality Guide

This step-by-step functionality guide explores the functions in the package structure flowchart. All functions below labelled with an * are required steps of the standard data reading flow. Everything else is optional. Rarely used additional features that are mentioned here but not part of the standard flowchart are labeled as bonus.

# libraries
library(isoreader2) # load isoreader2 R package
library(dplyr) # for select syntax and mutating data frames

Reading isotope files

The first step is finding and reading your isotope data files. isoreader2 supports a range of continuous flow, dual inlet, and scan file formats:

# supported file types
ir_get_supported_file_types()
# A tibble: 9 × 3
  file_type min_isoextract_version vendor_software
  <chr>     <chr>                  <chr>          
1 dxf       0.3.0                  Isodat         
2 cf        0.3.0                  Isodat         
3 iarc      0.3.0                  IonOS          
4 larc      0.3.0                  LyticOS        
5 bch       0.3.0                  Callisto       
6 imexp     0.3.0                  Qtegra         
7 did       0.3.0                  Isodat         
8 caf       0.3.0                  Isodat         
9 scn       0.3.0                  Isodat         

ir_find_isofiles() *

Point isoreader2 at a folder to discover the data files inside it. The generic ir_find_isofiles() finds all supported types, while ir_find_continuous_flow(), ir_find_dual_inlet(), and ir_find_scans() narrow the search to specific measurement types. The bundled example files are available via ir_examples_folder().

# path to your data folder (here a local copy of the example files)
data_folder <- file.path("tmp_examples")

# find continuous flow files (.dxf/.cf) in the folder
file_paths <- data_folder |> ir_find_continuous_flow()

# show what was found
file_paths
[1] "tmp_examples/continuous_flow_ea_example.dxf"
[2] "tmp_examples/continuous_flow_gc_example.cf" 

ir_read_isofiles() *

Read the discovered files. This returns an ir_isofiles object - a tibble with one row per file and the extracted datasets stored in nested columns.

# read the files
isofiles <- file_paths |> ir_read_isofiles()
 [1ms] ir_extract_isofiles() is finished, 0 files/archives required
(re-)extraction
 [219ms] ir_read_isofiles() finished reading 2 isotope data files/archives
# show what was read
isofiles
─────── 2 isofiles with 2 analyses - combine with ir_aggregate_isofiles() ──────
1. continuous_flow_ea_example.dxf: with 1.1k time points for N2 (masses 28, 29,
and 30); 1.34k time points for CO2 (masses 44, 45, and 46); 20 metadata columns
2. continuous_flow_gc_example.cf:   with 8.6k time points for HD (masses 2 and
3); 19 metadata columns

bonus combine collections with c()

Multiple ir_isofiles collections can be combined into one with a simple c(), which row-binds them while preserving the object type.

# combine two collections (here just the same files twice for illustration)
c(isofiles, isofiles)
─────── 4 isofiles with 4 analyses - combine with ir_aggregate_isofiles() ──────
1. continuous_flow_ea_example.dxf: with 1.1k time points for N2 (masses 28, 29,
and 30); 1.34k time points for CO2 (masses 44, 45, and 46); 20 metadata columns
2. continuous_flow_gc_example.cf:   with 8.6k time points for HD (masses 2 and
3); 19 metadata columns
3. continuous_flow_ea_example.dxf: with 1.1k time points for N2 (masses 28, 29,
and 30); 1.34k time points for CO2 (masses 44, 45, and 46); 20 metadata columns
4. continuous_flow_gc_example.cf:   with 8.6k time points for HD (masses 2 and
3); 19 metadata columns

bonus ir_save_isofiles() / ir_load_isofiles()

You can store an entire ir_isofiles collection to disk (as an RDS file) and read it back exactly as it was, without re-reading the original data files.

# save and reload the read isofiles
isofiles |> ir_save_isofiles(file.path("tmp_examples", "my_isofiles"))
 [29ms] ir_save_isofiles() saved 2 isofiles to tmp_examples/my_isofiles.rds
reloaded <- ir_load_isofiles(file.path("tmp_examples", "my_isofiles"))
 [5ms] ir_load_isofiles() loaded 2 isofiles from tmp_examples/my_isofiles.rds

Aggregating data

The nested ir_isofiles structure is convenient for reading but not for analysis. Aggregation pulls the data together into a tidy set of data frames (metadata, traces, cycles, scans, resistors) that are easy to work with.

ir_aggregate_isofiles() *

# aggregate the data
dataset <- isofiles |> ir_aggregate_isofiles()
 [75ms] ir_aggregate_isofiles() aggregated metadata (2) and traces (24.5k,
intensity in mV) from 2 files using the standard aggregator
# show all that was recovered
# (as well as what was ignored / not aggregated)
dataset
aggregated data from 2 isofiles with 2 analyses - retrieve with ir_get_data(
metadata (2): uidx, file_path, file_name, analysis, timestamp, type,
h3_factor (1 NA), Row (1 NA), Peak Center (1 NA), Check Ref. Dilution (1 NA),
H3 Stability (1 NA), H3 Factor (1 NA), Amount (1 NA), Type (1 NA), EA Method (1
NA), Identifier 1, Identifier 2 (1 NA), Analysis, Comment (1 NA), Preparation
(1 NA), Method, Line (1 NA), GC Method (1 NA), AS Sample (1 NA), AS Method (1
NA), Pre Script (all NA), Post Script (all NA)
traces (24.5k): uidx, analysis, species, mass, tp, time.s, intensity.mV; (not
aggregated: channel)
problems: has no issues

bonus intensity units

By default the raw signals are reported in millivolts (mV). Pass a different intensity_units to report voltage (mV, V), current (fA, pA, nA, µA, mA, A), or counts per second (cps) instead. Conversions that cross the voltage/current/cps boundary use the recorded resistor values automatically (the same machinery is exposed as ir_convert_intensity()).

# report intensities in nanoamperes instead of the default millivolts
isofiles |>
  ir_aggregate_isofiles(intensity_units = "nA") |>
  ir_get_data(traces = c("species", "mass", "time.s", "intensity.nA"))
 [76ms] ir_aggregate_isofiles() aggregated metadata (2) and traces (24.5k,
intensity in nA) from 2 files using the standard aggregator
 [3ms] ir_get_data() retrieved 24.5k records from the combination of metadata
(2) and traces (24.5k) via uidx and analysis
# A tibble: 24,515 × 7
    uidx analysis file_name                  species mass  time.s intensity.nA
   <int>    <int> <chr>                      <chr>   <chr>  <dbl>        <dbl>
 1     1        1 continuous_flow_ea_example N2      28     0.209       0.0705
 2     1        1 continuous_flow_ea_example N2      28     0.418       0.0704
 3     1        1 continuous_flow_ea_example N2      28     0.627       0.0704
 4     1        1 continuous_flow_ea_example N2      28     0.836       0.0704
 5     1        1 continuous_flow_ea_example N2      28     1.04        0.0703
 6     1        1 continuous_flow_ea_example N2      28     1.25        0.0703
 7     1        1 continuous_flow_ea_example N2      28     1.46        0.0703
 8     1        1 continuous_flow_ea_example N2      28     1.67        0.0702
 9     1        1 continuous_flow_ea_example N2      28     1.88        0.0702
10     1        1 continuous_flow_ea_example N2      28     2.09        0.0701
# ℹ 24,505 more rows

bonus ir_get_aggregator()

You can optionally use a different aggregator. Besides the default standard aggregator, the package ships (from smallest to largest) the metadata aggregator (metadata only), the minimal aggregator (metadata plus the core data series), and the extended aggregator, which is more elaborate and provides access to additional columns (such as the resistor/cup configuration and the vendor data table) from the data files.

# minimal vs. extended aggregator
ir_get_aggregator("minimal")
────────────────────────────── Aggregator minimal ──────────────────────────────
Dataset metadata:
 → file_name = as.character(sub(file_name, pattern = "\\.[^.]+$", replacement =
""))analysis = as.integer(analysis)timestamp = as.POSIXct(timestamp)
Dataset traces:
 → analysis = as.integer(analysis)species = as.character(species)mass = as.character(mass)tp = as.integer(tp)time.s = as.numeric(time.s)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))
Dataset cycles:
 → analysis = as.integer(analysis)species = as.character(species)cycle = as.integer(cycle)type = as.character(type)mass = as.character(mass)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))
Dataset scans:
 → analysis = as.integer(analysis)species = as.character(species)mass = as.character(mass)x = as.numeric(x)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))
ir_get_aggregator("extended")
────────────────────────────── Aggregator extended ─────────────────────────────
Dataset metadata:
 → file_name = as.character(sub(file_name, pattern = "\\.[^.]+$", replacement =
""))analysis = as.integer(analysis)timestamp = as.POSIXct(timestamp)(.*) = as.character(all_matches("(.*)"))
Dataset traces:
 → analysis = as.integer(analysis)species = as.character(species)mass = as.character(mass)tp = as.integer(tp)time.s = as.numeric(time.s)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))channel = as.integer(channel)config = as.integer(config)
Dataset cycles:
 → analysis = as.integer(analysis)species = as.character(species)cycle = as.integer(cycle)type = as.character(type)mass = as.character(mass)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))channel = as.integer(channel)
Dataset scans:
 → analysis = as.integer(analysis)species = as.character(species)mass = as.character(mass)x = as.numeric(x)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))channel = as.integer(channel)
Dataset resistors:
 → species = as.character(species)config = as.integer(config)channel = as.integer(channel)mass = as.character(mass)cup = as.integer(cup)resistance.Ohm = as.numeric(resistance.Ohm)nominal.Ohm = as.numeric(nominal.Ohm)
Dataset vendor_data_table:
 → (.*) = keep_as_is(all_matches("(.*)"))
# using the extended aggregator instead of the default (standard)
isofiles |> ir_aggregate_isofiles(aggregator = "extended")
 [165ms] ir_aggregate_isofiles() aggregated metadata (2), traces (24.5k,
intensity in mV), resistors (8), and vendor_data_table (25) from 2 files using
the extended aggregator
aggregated data from 2 isofiles with 2 analyses - retrieve with ir_get_data(
metadata (2): uidx, file_path, file_name, analysis, timestamp, type,
h3_factor (1 NA), Row (1 NA), Peak Center (1 NA), Check Ref. Dilution (1 NA),
H3 Stability (1 NA), H3 Factor (1 NA), Amount (1 NA), Type (1 NA), EA Method (1
NA), Identifier 1, Identifier 2 (1 NA), Analysis, Comment (1 NA), Preparation
(1 NA), Method, Line (1 NA), GC Method (1 NA), AS Sample (1 NA), AS Method (1
NA), Pre Script (all NA), Post Script (all NA)
traces (24.5k): uidx, analysis, species, mass, tp, time.s, intensity.mV,
channel, config (all NA)
resistors (8): uidx, species, config (all NA), channel, mass, cup,
resistance.Ohm, nominal.Ohm (all NA)
vendor_data_table (25): uidx, analysis, species, Start [s], Rt [s], End [s],
Ampl 28 [mV] (22 NA), Ampl 29 [mV] (22 NA), Ampl 30 [mV] (22 NA), BGD 28 [mV]
(22 NA), BGD 29 [mV] (22 NA), BGD 30 [mV] (22 NA), Nr., rIntensity 28 [mVs] (22
NA), rIntensity 29 [mVs] (22 NA), rIntensity 30 [mVs] (22 NA), rIntensity All
[mVs], Intensity 28 [Vs] (22 NA), Intensity 29 [Vs] (22 NA), Intensity 30 [Vs]
(22 NA), Intensity All [Vs], Sample Dilution [%] (19 NA), List First Peak (22
NA), rR 29N2/28N2 (22 NA), Is Ref.?, R 29N2/28N2 (22 NA), Ref. Name, rd
29N2/28N2 [per mil] (22 NA), d 29N2/28N2 [per mil] (22 NA), R 15N/14N (22 NA),
δ 15N/14N [per mil] (22 NA), AT% 15N/14N [%] (22 NA), Rps 29N2/28N2 (24 NA),
Ampl 44 [mV] (22 NA), Ampl 45 [mV] (22 NA), Ampl 46 [mV] (22 NA), BGD 44 [mV]
(22 NA), BGD 45 [mV] (22 NA), BGD 46 [mV] (22 NA), rIntensity 44 [mVs] (22 NA),
rIntensity 45 [mVs] (22 NA), rIntensity 46 [mVs] (22 NA), Intensity 44 [Vs] (22
NA), Intensity 45 [Vs] (22 NA), Intensity 46 [Vs] (22 NA), rR 45CO2/44CO2 (22
NA), rR 46CO2/44CO2 (22 NA), R 45CO2/44CO2 (22 NA), rd 45CO2/44CO2 [per mil]
(22 NA), d 45CO2/44CO2 [per mil] (22 NA), R 46CO2/44CO2 (22 NA), rd 46CO2/44CO2
[per mil] (22 NA), d 46CO2/44CO2 [per mil] (22 NA), R 13C/12C (22 NA), δ
13C/12C [per mil] (22 NA), AT% 13C/12C [%] (22 NA), R 18O/16O (22 NA), δ
18O/16O [per mil] (22 NA), AT% 18O/16O [%] (22 NA), R 17O/16O (22 NA), d
17O/16O (22 NA), Rps 45CO2/44CO2 (24 NA), Rps 46CO2/44CO2 (24 NA), Ampl 2 [mV]
(6 NA), Ampl 3 [mV] (6 NA), BGD 2 [mV] (6 NA), BGD 3 [mV] (6 NA), rIntensity 2
[mVs] (6 NA), rIntensity 3 [mVs] (6 NA), Intensity 2 [Vs] (6 NA), Intensity 3
[Vs] (6 NA), rR 3H2/2H2 (6 NA), R 3H2/2H2 (6 NA), rd 3H2/2H2 [per mil] (6 NA),
d 3H2/2H2 [per mil] (6 NA), R 2H/1H (6 NA), δ 2H/1H [per mil] (6 NA), AT% 2H/1H
[%] (6 NA), Rps 3H2/2H2 (20 NA)
problems: has no issues

bonus ir_register_aggregator()

Or build your own aggregator with ir_start_aggregator() and/or expand an existing one with ir_add_to_aggregator(), then register it via ir_register_aggregator(). This functionality is rarely needed and thus not part of the package structure flowchart.

my_agg <-
  ir_get_aggregator("minimal") |>
  # pull the "Identifier 1" metadata field out under a friendlier name
  ir_add_to_aggregator("metadata", "sample_id", source = "Identifier 1") |>
  ir_register_aggregator(name = "my_aggregator")

# show my aggregator summary
my_agg
────────────────────────────── Aggregator minimal ──────────────────────────────
Dataset metadata:
 → file_name = as.character(sub(file_name, pattern = "\\.[^.]+$", replacement =
""))analysis = as.integer(analysis)timestamp = as.POSIXct(timestamp)sample_id = as.character(`Identifier 1`)
Dataset traces:
 → analysis = as.integer(analysis)species = as.character(species)mass = as.character(mass)tp = as.integer(tp)time.s = as.numeric(time.s)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))
Dataset cycles:
 → analysis = as.integer(analysis)species = as.character(species)cycle = as.integer(cycle)type = as.character(type)mass = as.character(mass)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))
Dataset scans:
 → analysis = as.integer(analysis)species = as.character(species)mass = as.character(mass)x = as.numeric(x)(intensity\\..*) = as.numeric(all_matches("(intensity\\..*)"))
# use it
isofiles |> ir_aggregate_isofiles(aggregator = "my_aggregator")
 [46ms] ir_aggregate_isofiles() aggregated metadata (2) and traces (24.5k,
intensity in mV) from 2 files using the my_aggregator aggregator
aggregated data from 2 isofiles with 2 analyses - retrieve with ir_get_data(
metadata (2): uidx, file_path, file_name, analysis, timestamp, sample_id;
(not aggregated: file_path, type, h3_factor, Row, Peak Center, Check Ref.
Dilution, H3 Stability, H3 Factor, Amount, Type, EA Method, Identifier 2,
Analysis, Comment, Preparation, Method, Line, GC Method, AS Sample, AS Method,
Pre Script, Post Script)
traces (24.5k): uidx, analysis, species, mass, tp, time.s, intensity.mV; (not
aggregated: channel)
problems: has no issues

bonus ir_get_problems() / ir_show_problems()

Reading and aggregation are designed to be fail-safe: instead of stopping at the first error, problems are collected and reported. There were no problems with these example files so the result is empty, but this can be very helpful for figuring out what went wrong.

ir_get_problems() returns the problems as a data frame for further inspection:

isofiles |> ir_get_problems()
# A tibble: 0 × 6
# ℹ 6 variables: uidx <int>, file <chr>, type <chr>, call <chr>, message <chr>,
#   condition <list>
dataset |> ir_get_problems()
# A tibble: 0 × 6
# ℹ 6 variables: uidx <int>, file <chr>, type <chr>, call <chr>, message <chr>,
#   condition <list>

ir_show_problems() instead just prints out all the problems directly:

isofiles |> ir_show_problems()
dataset |> ir_show_problems()

Accessing the data

ir_get_data()

At any point you can pull the data of interest out of the aggregated dataset with ir_get_data(), selecting columns from each dataset using dplyr::select() syntax. Columns selected from more than one dataset are automatically combined with a join.

# direct access to the individual datasets
dataset$metadata
# A tibble: 2 × 27
   uidx file_path   file_name analysis timestamp           type  h3_factor Row  
  <int> <chr>       <chr>        <int> <dttm>              <chr> <chr>     <chr>
1     1 tmp_exampl… continuo…        1 2017-02-09 19:55:40 cf    NA        5    
2     2 tmp_exampl… continuo…        1 2013-07-13 23:42:40 cf    2.794310… NA   
# ℹ 19 more variables: `Peak Center` <chr>, `Check Ref. Dilution` <chr>,
#   `H3 Stability` <chr>, `H3 Factor` <chr>, Amount <chr>, Type <chr>,
#   `EA Method` <chr>, `Identifier 1` <chr>, `Identifier 2` <chr>,
#   Analysis <chr>, Comment <chr>, Preparation <chr>, Method <chr>, Line <chr>,
#   `GC Method` <chr>, `AS Sample` <chr>, `AS Method` <chr>,
#   `Pre Script` <chr>, `Post Script` <chr>
dataset$traces
# A tibble: 24,515 × 7
    uidx analysis species mass     tp time.s intensity.mV
   <int>    <int> <chr>   <chr> <int>  <dbl>        <dbl>
 1     1        1 N2      28        1  0.209         21.2
 2     1        1 N2      28        2  0.418         21.1
 3     1        1 N2      28        3  0.627         21.1
 4     1        1 N2      28        4  0.836         21.1
 5     1        1 N2      28        5  1.04          21.1
 6     1        1 N2      28        6  1.25          21.1
 7     1        1 N2      28        7  1.46          21.1
 8     1        1 N2      28        8  1.67          21.1
 9     1        1 N2      28        9  1.88          21.1
10     1        1 N2      28       10  2.09          21.0
# ℹ 24,505 more rows
# retrieve + combine data with dplyr select syntax
dataset |>
  ir_get_data(
    metadata = c("file_name", "analysis", sample = "Identifier 1"),
    traces = c("species", "mass", "time.s", "intensity.mV")
  )
 [6ms] ir_get_data() retrieved 24.5k records from the combination of metadata
(2) and traces (24.5k) via uidx and analysis
# A tibble: 24,515 × 8
    uidx analysis file_name             sample species mass  time.s intensity.mV
   <int>    <int> <chr>                 <chr>  <chr>   <chr>  <dbl>        <dbl>
 1     1        1 continuous_flow_ea_e… aceta… N2      28     0.209         21.2
 2     1        1 continuous_flow_ea_e… aceta… N2      28     0.418         21.1
 3     1        1 continuous_flow_ea_e… aceta… N2      28     0.627         21.1
 4     1        1 continuous_flow_ea_e… aceta… N2      28     0.836         21.1
 5     1        1 continuous_flow_ea_e… aceta… N2      28     1.04          21.1
 6     1        1 continuous_flow_ea_e… aceta… N2      28     1.25          21.1
 7     1        1 continuous_flow_ea_e… aceta… N2      28     1.46          21.1
 8     1        1 continuous_flow_ea_e… aceta… N2      28     1.67          21.1
 9     1        1 continuous_flow_ea_e… aceta… N2      28     1.88          21.1
10     1        1 continuous_flow_ea_e… aceta… N2      28     2.09          21.0
# ℹ 24,505 more rows

shortcuts ir_get_metadata() / ir_get_traces() / …

For the common case of grabbing a whole dataset, the shortcut functions ir_get_metadata(), ir_get_traces(), ir_get_cycles(), ir_get_scans(), and ir_get_resistors() retrieve all columns of the respective dataset.

# all metadata columns
dataset |> ir_get_metadata()
 [2ms] ir_get_data() retrieved 2 records from metadata
# A tibble: 2 × 27
   uidx analysis file_path   file_name timestamp           type  h3_factor Row  
  <int>    <int> <chr>       <chr>     <dttm>              <chr> <chr>     <chr>
1     1        1 tmp_exampl… continuo… 2017-02-09 19:55:40 cf    NA        5    
2     2        1 tmp_exampl… continuo… 2013-07-13 23:42:40 cf    2.794310… NA   
# ℹ 19 more variables: `Peak Center` <chr>, `Check Ref. Dilution` <chr>,
#   `H3 Stability` <chr>, `H3 Factor` <chr>, Amount <chr>, Type <chr>,
#   `EA Method` <chr>, `Identifier 1` <chr>, `Identifier 2` <chr>,
#   Analysis <chr>, Comment <chr>, Preparation <chr>, Method <chr>, Line <chr>,
#   `GC Method` <chr>, `AS Sample` <chr>, `AS Method` <chr>,
#   `Pre Script` <chr>, `Post Script` <chr>
# all traces (joined with the file metadata)
dataset |> ir_get_traces()
 [4ms] ir_get_data() retrieved 24.5k records from the combination of metadata
(2) and traces (24.5k) via uidx and analysis
# A tibble: 24,515 × 8
    uidx analysis file_name              species mass     tp time.s intensity.mV
   <int>    <int> <chr>                  <chr>   <chr> <int>  <dbl>        <dbl>
 1     1        1 continuous_flow_ea_ex… N2      28        1  0.209         21.2
 2     1        1 continuous_flow_ea_ex… N2      28        2  0.418         21.1
 3     1        1 continuous_flow_ea_ex… N2      28        3  0.627         21.1
 4     1        1 continuous_flow_ea_ex… N2      28        4  0.836         21.1
 5     1        1 continuous_flow_ea_ex… N2      28        5  1.04          21.1
 6     1        1 continuous_flow_ea_ex… N2      28        6  1.25          21.1
 7     1        1 continuous_flow_ea_ex… N2      28        7  1.46          21.1
 8     1        1 continuous_flow_ea_ex… N2      28        8  1.67          21.1
 9     1        1 continuous_flow_ea_ex… N2      28        9  1.88          21.1
10     1        1 continuous_flow_ea_ex… N2      28       10  2.09          21.0
# ℹ 24,505 more rows

bonus ir_get_vendor_data_table()

Some file formats also store the vendor’s own computed results table (the table Isodat shows in its results view: per-peak or per-cycle intensities, retention times, ratios, deltas, etc.). This vendor_data_table is only available from a few formats — the isodat .dxf, .cf, .did, and .caf files — and it is only aggregated by the "extended" aggregator (it is dropped by the default "standard" aggregator as well as the "minimal" and "metadata" aggregators). Its columns are kept under their original names (with units, e.g. "Rt [s]"), numeric by default with the rare string/integer columns (e.g. "Ref. Name", "Nr.") kept as-is.

# aggregate with the extended aggregator so the vendor data table is included
dataset_ext <- isofiles |> ir_aggregate_isofiles(aggregator = "extended")
 [180ms] ir_aggregate_isofiles() aggregated metadata (2), traces (24.5k,
intensity in mV), resistors (8), and vendor_data_table (25) from 2 files using
the extended aggregator
# retrieve the vendor data table (joined with the file metadata)
dataset_ext |> ir_get_vendor_data_table()
 [3ms] ir_get_data() retrieved 25 records from the combination of metadata (2)
and vendor_data_table (25) via uidx and analysis
# A tibble: 25 × 80
    uidx analysis file_name               species `Start [s]` `Rt [s]` `End [s]`
   <int>    <int> <chr>                   <chr>         <dbl>    <dbl>     <dbl>
 1     1        1 continuous_flow_ea_exa… N2             40.1     60.0      63.3
 2     1        1 continuous_flow_ea_exa… N2             90.9    111.      114. 
 3     1        1 continuous_flow_ea_exa… N2            126.     155.      217. 
 4     1        1 continuous_flow_ea_exa… CO2           274.     300.      366. 
 5     1        1 continuous_flow_ea_exa… CO2           417.     436.      446. 
 6     1        1 continuous_flow_ea_exa… CO2           467.     487.      497. 
 7     2        1 continuous_flow_gc_exa… H2            283.     286.      293. 
 8     2        1 continuous_flow_gc_exa… H2            318.     321.      328. 
 9     2        1 continuous_flow_gc_exa… H2            606.     612.      635. 
10     2        1 continuous_flow_gc_exa… H2            666.     672.      699. 
# ℹ 15 more rows
# ℹ 73 more variables: `Ampl 28 [mV]` <dbl>, `Ampl 29 [mV]` <dbl>,
#   `Ampl 30 [mV]` <dbl>, `BGD 28 [mV]` <dbl>, `BGD 29 [mV]` <dbl>,
#   `BGD 30 [mV]` <dbl>, Nr. <int>, `rIntensity 28 [mVs]` <dbl>,
#   `rIntensity 29 [mVs]` <dbl>, `rIntensity 30 [mVs]` <dbl>,
#   `rIntensity All [mVs]` <dbl>, `Intensity 28 [Vs]` <dbl>,
#   `Intensity 29 [Vs]` <dbl>, `Intensity 30 [Vs]` <dbl>, …

ir_filter_metadata() / ir_mutate_metadata() / ir_join_metadata()

You can filter, add to, or join into the metadata while keeping the rest of the datasets consistent. ir_filter_metadata() cascades the filter to all other datasets so they always stay in sync.

# keep only continuous flow files and add a derived label column
dataset |>
  ir_filter_metadata(type == "cf") |>
  ir_mutate_metadata(label = paste(file_name, analysis, sep = " / ")) |>
  ir_get_metadata(metadata = c("file_name", "analysis", "label"))
 [2ms] ir_get_data() retrieved 2 records from metadata
# A tibble: 2 × 4
   uidx analysis file_name                  label                         
  <int>    <int> <chr>                      <chr>                         
1     1        1 continuous_flow_ea_example continuous_flow_ea_example / 1
2     2        1 continuous_flow_gc_example continuous_flow_gc_example / 1

These functions also work directly on an unaggregated ir_isofiles object (applied individually to each file), though that is significantly slower than working on an aggregated dataset.

bonus ir_filter_for_continuous_flow() / ir_filter_for_dual_inlet() / ir_filter_for_scans()

If a dataset mixes measurement types, these convenience filters keep only the files of one type (filtering on the metadata type field) and cascade to all datasets, just like ir_filter_metadata().

# keep only the continuous flow files
dataset |>
  ir_filter_for_continuous_flow() |>
  ir_get_metadata(metadata = c("file_name", "type"))
 [2ms] ir_get_data() retrieved 2 records from metadata
# A tibble: 2 × 4
   uidx analysis file_name                  type 
  <int>    <int> <chr>                      <chr>
1     1        1 continuous_flow_ea_example cf   
2     2        1 continuous_flow_gc_example cf   

bonus ir_save_aggregated_data() / ir_load_aggregated_data()

Aggregated data can be stored to (and loaded from) a parquet file for fast, language-independent access. This requires the suggested arrow package.

# save and reload the aggregated data
dataset |> ir_save_aggregated_data(file.path("tmp_examples", "my_dataset"))
reloaded <- ir_load_aggregated_data(file.path("tmp_examples", "my_dataset"))

Calculating ratios

ir_calculate_ratios()

Isotope work is all about ratios. ir_calculate_ratios() adds two columns - ratio_name (e.g. "45/44") and ratio - to the traces, cycles, and/or scans of an aggregated dataset. By default each mass is divided by the numerically lowest (“base”) mass of the same species at each time/cycle/scan position.

dataset_ratios <- dataset |> ir_calculate_ratios()
 [10ms] ir_calculate_ratios() calculated 13.5k ratios with additive offsets
num_add.V = 100 and denom_add.V = 100 and added ratio_name and ratio columns to
the traces
dataset_ratios |>
  ir_get_data(
    metadata = "file_name",
    traces = c("species", "mass", "time.s", "ratio_name", "ratio")
  )
 [3ms] ir_get_data() retrieved 24.5k records from the combination of metadata
(2) and traces (24.5k) via uidx and analysis
# A tibble: 24,515 × 8
    uidx analysis file_name                species mass  time.s ratio_name ratio
   <int>    <int> <chr>                    <chr>   <chr>  <dbl> <chr>      <dbl>
 1     1        1 continuous_flow_ea_exam… N2      28     0.209 NA            NA
 2     1        1 continuous_flow_ea_exam… N2      28     0.418 NA            NA
 3     1        1 continuous_flow_ea_exam… N2      28     0.627 NA            NA
 4     1        1 continuous_flow_ea_exam… N2      28     0.836 NA            NA
 5     1        1 continuous_flow_ea_exam… N2      28     1.04  NA            NA
 6     1        1 continuous_flow_ea_exam… N2      28     1.25  NA            NA
 7     1        1 continuous_flow_ea_exam… N2      28     1.46  NA            NA
 8     1        1 continuous_flow_ea_exam… N2      28     1.67  NA            NA
 9     1        1 continuous_flow_ea_exam… N2      28     1.88  NA            NA
10     1        1 continuous_flow_ea_exam… N2      28     2.09  NA            NA
# ℹ 24,505 more rows

bonus base mass, additive offsets, and normalization

The base mass can be overridden per species by name (e.g. N2 = 28, CO2 = 44). For continuous flow (traces) and scans data a small additive offset is added to numerator and denominator before dividing, which stabilizes the ratio where the signal is near the baseline; it is configurable per unit family (num_add.V/denom_add.V for voltage, num_add.nA/denom_add.nA for current, num_add.cps/denom_add.cps for counts) and is always 0 for dual inlet cycles. Finally, normalize_ratios can divide each ratio by a per-file summary function such as mean, median, min, or max.

dataset |>
  ir_calculate_ratios(N2 = 28, normalize_ratios = median) |>
  ir_get_data(
    metadata = "file_name",
    traces = c("species", "mass", "ratio_name", "ratio")
  )
 [10ms] ir_calculate_ratios() calculated 13.5k ratios with additive offsets
num_add.V = 100 and denom_add.V = 100 normalized by `median()` and added
ratio_name and ratio columns to the traces
 [3ms] ir_get_data() retrieved 24.5k records from the combination of metadata
(2) and traces (24.5k) via uidx and analysis
# A tibble: 24,515 × 7
    uidx analysis file_name                  species mass  ratio_name ratio
   <int>    <int> <chr>                      <chr>   <chr> <chr>      <dbl>
 1     1        1 continuous_flow_ea_example N2      28    NA            NA
 2     1        1 continuous_flow_ea_example N2      28    NA            NA
 3     1        1 continuous_flow_ea_example N2      28    NA            NA
 4     1        1 continuous_flow_ea_example N2      28    NA            NA
 5     1        1 continuous_flow_ea_example N2      28    NA            NA
 6     1        1 continuous_flow_ea_example N2      28    NA            NA
 7     1        1 continuous_flow_ea_example N2      28    NA            NA
 8     1        1 continuous_flow_ea_example N2      28    NA            NA
 9     1        1 continuous_flow_ea_example N2      28    NA            NA
10     1        1 continuous_flow_ea_example N2      28    NA            NA
# ℹ 24,505 more rows

Visualizing data

isoreader2 provides quick-look plotting functions for each measurement type. They operate on aggregated data and return regular ggplot objects that you can further customize. To add your own ggplot2 layers (e.g. + labs(...) or + theme(...)), attach ggplot2 with library(ggplot2) first.

ir_plot_continuous_flow()

dataset |> ir_plot_continuous_flow(facet = file_name)

Narrow the plot down with species/mass and zoom in with a time window given either in seconds (time_window.s) or minutes (time_window.min):

dataset |>
  ir_plot_continuous_flow(
    species = "CO2",
    facet = file_name,
    time_window.min = c(4.5, 8.5)
  )

Add calculated ratios to the plot. When more than one data type is present (intensities and ratios), they are automatically split into facet rows (data_type_as_facet = auto()):

dataset |>
  ir_calculate_ratios(normalize_ratios = median) |>
  ir_plot_continuous_flow(
    species = "CO2",
    ratio = "45/44",
    facet = file_name,
    time_window.min = c(4.5, 8.5)
  )
 [11ms] ir_calculate_ratios() calculated 13.5k ratios with additive offsets
num_add.V = 100 and denom_add.V = 100 normalized by `median()` and added
ratio_name and ratio columns to the traces

The plots are ordinary ggplot objects (the package theme ir_default_theme() is always applied), so you can keep customizing with ggplot2 layers - and tune the aesthetics with arguments like color, scientific, linetype, or color_values:

library(ggplot2)
dataset |>
  ir_plot_continuous_flow(
    species = "N2",
    facet = file_name,
    scientific = TRUE
  ) +
  labs(title = "N2 traces") +
  theme(legend.position = "bottom")

bonus ir_generate_traces_tibble() / ir_generate_cycles_tibble() / ir_generate_scans_tibble()

If you want the exact tibble that a plot would draw (e.g. to build your own plot or do further analysis), the ir_generate_*_tibble() helpers return it without plotting. They take the same species/mass/ratio arguments and add the trace, data_type, and value columns the plotting functions use.

dataset |> ir_generate_traces_tibble(species = "CO2")
# A tibble: 4,008 × 35
    uidx analysis species mass     tp time.s intensity.mV file_path    file_name
   <int>    <int> <chr>   <chr> <int>  <dbl>        <dbl> <chr>        <chr>    
 1     1        1 CO2     44        1   232.      26.6    tmp_example… continuo…
 2     1        1 CO2     44        2   232.      26.4    tmp_example… continuo…
 3     1        1 CO2     44        3   232.      26.4    tmp_example… continuo…
 4     1        1 CO2     44        4   232.      22.2    tmp_example… continuo…
 5     1        1 CO2     44        5   232.       3.76   tmp_example… continuo…
 6     1        1 CO2     44        6   233.       0.408  tmp_example… continuo…
 7     1        1 CO2     44        7   233.       0.0550 tmp_example… continuo…
 8     1        1 CO2     44        8   233.      -0.0193 tmp_example… continuo…
 9     1        1 CO2     44        9   233.      -0.0174 tmp_example… continuo…
10     1        1 CO2     44       10   233.      -0.0231 tmp_example… continuo…
# ℹ 3,998 more rows
# ℹ 26 more variables: timestamp <dttm>, type <chr>, h3_factor <chr>,
#   Row <chr>, `Peak Center` <chr>, `Check Ref. Dilution` <chr>,
#   `H3 Stability` <chr>, `H3 Factor` <chr>, Amount <chr>, Type <chr>,
#   `EA Method` <chr>, `Identifier 1` <chr>, `Identifier 2` <chr>,
#   Analysis <chr>, Comment <chr>, Preparation <chr>, Method <chr>, Line <chr>,
#   `GC Method` <chr>, `AS Sample` <chr>, `AS Method` <chr>, …

ir_plot_dual_inlet()

di_dataset <-
  data_folder |>
  ir_find_dual_inlet() |>
  ir_read_isofiles() |>
  ir_aggregate_isofiles("V") |>
  ir_calculate_ratios()
 [0ms] ir_extract_isofiles() is finished, 0 files/archives required
(re-)extraction
 [197ms] ir_read_isofiles() finished reading 1 isotope data file/archive
 [39ms] ir_aggregate_isofiles() aggregated metadata (1) and cycles (102,
intensity in V) from 1 file using the standard aggregator
 [3ms] ir_calculate_ratios() calculated 85 ratios and added ratio_name and
ratio columns to the cycles
di_dataset |> ir_plot_dual_inlet(facet = file_name)

The same species/mass/ratio filters apply, and cycle_window zooms to a range of cycles:

di_dataset |> ir_plot_dual_inlet(mass = c(44, 45, 46), facet = file_name)

ir_plot_scans()

For scan files with more than one scan type, specify which one to plot (x_window zooms the x axis the same way time_window.s does for continuous flow):

data_folder |>
  ir_find_scans() |>
  ir_read_isofiles() |>
  ir_aggregate_isofiles("V") |>
  ir_plot_scans(scan_type = "high voltage")
 [0ms] ir_extract_isofiles() is finished, 0 files/archives required
(re-)extraction
 [61ms] ir_read_isofiles() finished reading 4 isotope data files/archives
 [99ms] ir_aggregate_isofiles() aggregated metadata (4) and scans (17.8k,
intensity in V) from 4 files using the standard aggregator

Exporting data

ir_export_to_excel()

Finally, you can export individual data frames (typically retrieved with the ir_get_*() functions) to an Excel file, one sheet per data frame. Named arguments set the sheet names; unnamed ones use "Sheet{position}". This requires the suggested openxlsx package.

ir_export_to_excel(
  metadata = dataset |> ir_get_metadata(),
  traces = dataset |> ir_get_traces(),
  file = "tmp_examples/my_dataset.xlsx"
)
 [2ms] ir_get_data() retrieved 2 records from metadata
 [3ms] ir_get_data() retrieved 24.5k records from the combination of metadata
(2) and traces (24.5k) via uidx and analysis
 [534ms] ir_export_to_excel() exported 2 rows of metadata and 24.5k rows of
traces to tmp_examples/my_dataset.xlsx

Package options

bonus ir_options() / ir_get_option()

A handful of package-wide options control isoreader2’s behavior. ir_get_options() lists them, ir_get_option() reads one, and ir_options() sets them. For example, debug = TRUE disables the fail-safe error catching so problems surface with a full traceback, which is useful when troubleshooting a reader.

# list available options and read one
names(ir_get_options())
[1] "aggregators"   "debug"         "auto_use_ansi"
ir_get_option("debug")
[1] FALSE
# set an option (here enable debug mode), then restore the default
invisible(ir_options(debug = TRUE))
ir_get_option("debug")
[1] TRUE