Getting Started with tdtr

tdtr is an R interface to Tucker-Davis Technologies (TDT) tank/block data. It uses the Python tdt package through reticulate, then gives R users a compact set of functions for the common workflow:

  1. check the Python/TDT configuration;
  2. read a raw TDT block;
  3. inspect streams and events;
  4. collect the selected data into ordinary R objects.

Check The Backend

Loading tdtr declares its Python requirement through reticulate with reticulate::py_require("tdt>=0.7.3"). Python is not initialized just because the package is attached. For ordinary R use, reticulate can create and manage a Python environment that contains tdt when Python is first needed.

Before starting an analysis, confirm that Python and tdt resolve in the current R session.

cfg <- tdt_config(initialize = TRUE)

list(
  reticulate = cfg$reticulate,
  tdt_available = cfg$tdt_available,
  python_version = as.character(cfg$python$version)
)
#> $reticulate
#> [1] TRUE
#> 
#> $tdt_available
#> [1] TRUE
#> 
#> $python_version
#> [1] "3.12"

Reticulate chooses and initializes Python once per R session. If you want to use a specific Python environment, configure that before calling tdt_config(initialize = TRUE) or read_block(). For example:

reticulate::use_python("/path/to/python", required = TRUE)
library(tdtr)
tdt_config(initialize = TRUE)

When you force a specific Python environment with reticulate, Conda, Pixi, or environment variables, that environment must already provide tdt>=0.7.3.

Read The Example Block

The package includes one small raw TDT block from the official TDT example data. It is stored as TDT files, not as pre-collected R data.

example_path <- tdtr_example_block_path()

list(
  block = basename(example_path),
  files = list.files(example_path, pattern = "\\.(tsq|tev|Tbk|Tdx|tin|tnt)$")
)
#> $block
#> [1] "Subject1-211115-094936"
#> 
#> $files
#> [1] "VidOverlay-211115-093859_Subject1-211115-094936.Tbk"
#> [2] "VidOverlay-211115-093859_Subject1-211115-094936.Tdx"
#> [3] "VidOverlay-211115-093859_Subject1-211115-094936.tev"
#> [4] "VidOverlay-211115-093859_Subject1-211115-094936.tin"
#> [5] "VidOverlay-211115-093859_Subject1-211115-094936.tnt"
#> [6] "VidOverlay-211115-093859_Subject1-211115-094936.tsq"

Read the first five seconds. read_block() returns a Python-backed object, so large stream arrays are not copied into R just because the block was opened.

block <- read_block(
  example_path,
  evtype = c("epocs", "streams"),
  t1 = 0,
  t2 = 5,
  verbose = 0
)

class(block)
#> [1] "tdt_block_py" "tdt_py"

Inspect the block without printing the local library path.

info <- block_info(block)

list(
  blockname = info$blockname,
  duration = as.character(info$duration),
  streams = stream_names(block),
  epocs = epoc_names(block)
)
#> $blockname
#> [1] "Subject1-211115-094936"
#> 
#> $duration
#> [1] "0:00:37.103101"
#> 
#> $streams
#> [1] "_405A" "_465A" "Fi1r" 
#> 
#> $epocs
#> [1] "Cam1" "Tick"

summary() gives a compact stream overview.

summary(block)$streams
#> # A tibble: 3 × 4
#>   stream    fs shape nbytes
#>   <chr>  <dbl> <chr>  <dbl>
#> 1 _405A  1017. 5087   20348
#> 2 _465A  1017. 5087   20348
#> 3 Fi1r   6104. 30518 122072

The example contains three streams:

  • _405A and _465A, lower-rate single-channel fiber photometry streams;
  • Fi1r, a higher-rate raw/reference stream.

It also contains two event stores:

  • Cam1, camera frame events;
  • Tick, one-second tick events.

Work With Events

Events collect into a tibble with one row per event interval.

events <- collect_epocs(block)
head(events)
#> # A tibble: 6 × 4
#>   store onset offset value
#>   <chr> <dbl>  <dbl> <dbl>
#> 1 Cam1  0.112  0.150     1
#> 2 Cam1  0.150  0.189     2
#> 3 Cam1  0.189  0.229     3
#> 4 Cam1  0.229  0.269     4
#> 5 Cam1  0.269  0.309     5
#> 6 Cam1  0.309  0.349     6

Filter by store when you only need one event source.

ticks <- collect_epocs(block, store = "Tick")
ticks
#> # A tibble: 5 × 4
#>   store    onset offset value
#>   <chr>    <dbl>  <dbl> <dbl>
#> 1 Tick  0.000164   1.00     0
#> 2 Tick  1.00       2.00     1
#> 3 Tick  2.00       3.00     2
#> 4 Tick  3.00       4.00     3
#> 5 Tick  4.00       5        4

Events use seconds relative to block start. You can build analysis windows from event onsets with ranges_from_epocs().

ranges_from_epocs(ticks, pre = -0.25, post = 0.75)
#>           [,1]     [,2]     [,3]     [,4]
#> [1,] 0.7502432 1.750323 2.750402 3.750481
#> [2,] 1.7502432 2.750323 3.750402 4.750481

Work With Streams

Collect one selected stream when you want signal values in R.

signal <- collect_stream(block, "_465A")

dim(signal)
#> [1] 5087    1
head(signal)
#>           [,1]
#> [1,] 0.3687995
#> [2,] 0.3711895
#> [3,] 0.3735043
#> [4,] 0.3757292
#> [5,] 0.3778504
#> [6,] 0.3798554

Stream matrices are always samples by channels.

c(samples = nrow(signal), channels = ncol(signal))
#>  samples channels 
#>     5087        1

Use include_time = TRUE when you want sample times alongside the data.

signal_with_time <- collect_stream(
  block,
  "_465A",
  as = "list",
  include_time = TRUE
)

head(signal_with_time$time)
#> [1] 0.00000000 0.00098304 0.00196608 0.00294912 0.00393216 0.00491520
head(signal_with_time$data)
#>           [,1]
#> [1,] 0.3687995
#> [2,] 0.3711895
#> [3,] 0.3735043
#> [4,] 0.3757292
#> [5,] 0.3778504
#> [6,] 0.3798554

For small or bounded views, a tibble can be convenient.

as_tibble_stream(block, "_465A", max_rows = 8)
#> # A tibble: 8 × 3
#>       time channel   value
#>      <dbl> <chr>     <dbl>
#> 1 0        channel_1 0.369
#> 2 0.000983 channel_1 0.371
#> 3 0.00197  channel_1 0.374
#> 4 0.00295  channel_1 0.376
#> 5 0.00393  channel_1 0.378
#> 6 0.00492  channel_1 0.380
#> 7 0.00590  channel_1 0.382
#> 8 0.00688  channel_1 0.383

Avoid turning long high-frequency streams into large tibbles by accident. Use a time window, downsampling, or max_rows for table views.

as_tibble_stream(
  block,
  "Fi1r",
  window = c(1, 1.01),
  max_rows = 8
)
#> # A tibble: 8 × 3
#>    time channel    value
#>   <dbl> <chr>      <dbl>
#> 1  1.00 channel_1 0.0558
#> 2  1.00 channel_1 0.0626
#> 3  1.00 channel_1 0.0577
#> 4  1.00 channel_1 0.0684
#> 5  1.00 channel_1 0.0894
#> 6  1.00 channel_1 0.116 
#> 7  1.00 channel_1 0.114 
#> 8  1.00 channel_1 0.123

Where To Go Next

The high-level interface is intentionally focused on common inspect-and-collect workflows. When you need reader controls that map directly to Python tdt, read-time store filtering, large-block memory checks, save/reload guidance, or direct access to live Python objects, see the advanced usage vignette.