Skip to contents

Biologging devices often record information about an animal’s behavior using an inertial measurement unit (IMU) that collects acceleration, magnetometer, and/or orientation data. Because collecting these data can be battery- and storage-intensive, they are often recorded intermittently in rapid sets of samples at a fixed frequency, which we will refer to as bursts. These bursts of IMU measurements provide regular snapshots of an animal’s behavior and activity.

(Note that while some IMUs allow you to record data continuously for longer periods of time, we still use the term burst to refer to a distinct set of fixed-frequency IMU data samples, regardless of how long such a “burst” lasts.)

move2imu provides an R vector representation of these bursts so that they can be easily manipulated, analyzed, and linked to other tracking data—for instance, those stored in a move2 object. The package currently supports three sensor types:

  • acc() for accelerometer bursts
  • mag() for magnetometer bursts
  • gyro() for gyroscope bursts

Each of these shares the same underlying structure. For the rest of this vignette, we will use acceleration data as an example.

Burst structure

You can create an acc vector from scratch by providing a list of burst matrices (containing the raw acceleration data) and the associated sampling frequency for those observations. Each burst can contain up to three recording axes: X, Y, and/or Z. Note that the frequency must be provided in frequency units (in this case, "Hz"). move2imu uses the units package to support unit manipulation.

raw_acc <- cbind(
  X = sin(1:30 / 10),
  Y = cos(1:30 / 10),
  Z = 1:30 / 10
)

a1 <- acc(
  bursts = list(raw_acc),
  frequency = units::as_units(40, "Hz")
)

a1
#> <acceleration[1]>
#> [1] (0.67 0.01 1.55)
#> # frequency: 40 [Hz]

The printed output of the vector shows the number of bursts as well as the mean value per axis for each burst. Internally, the raw data matrix is preserved.

Bursts can also be associated with a timestamp marking the start time of the burst:

a2 <- acc(
  bursts = list(
    cbind(X = sin(1:20 / 10), Y = cos(1:20 / 10)),
    cbind(X = sin(1:20 / 10 + 2), Y = cos(1:20 / 10 + 3))
  ),
  frequency = units::as_units(30, "Hz"),
  start = as.POSIXct(c("2026-06-01 00:00:00", "2026-06-01 00:01:00"), tz = "UTC")
)

Multiple acc vectors can be combined with c(), even if their sampling frequency, axes, or other properties vary:

a <- c(a1, a2)

a
#> <acceleration[3]>
#> [1] (0.67 0.01 1.55) (0.73 0.42)      (0.08 -0.52)    
#> # frequency: 30 [Hz] - 40 [Hz]

Loading IMU data

Of course, in most cases you won’t be building bursts from scratch, but will load these data from external sources. move2imu has been designed to work in tandem with move2 and Movebank and will automatically parse IMU data stored in one of Movebank’s standard storage formats (see movebank_acc_colsets()).

Typically, data will be stored in a move2 object, which contains the necessary timestamp and track ID metadata to correctly parse IMU bursts. (You can also provide data as a data.frame, but you will need to manually provide timestamp and track ID metadata.)

For example, we’ll load a sample dataset of albatross tracks stored as a move2:

# Load sample Movebank data into a `move2`
alb <- albatrosses()

alb
#> A <move2> with `track_id_column` "individual_local_identifier" and
#> `time_column` "timestamp"
#> Containing 9 tracks lasting on average 59.3 mins in a
#> Simple feature collection with 54 features and 21 fields (with 45 geometries empty)
#> Geometry type: POINT
#> Dimension:     XY
#> Bounding box:  xmin: -89.67882 ymin: -9.06721 xmax: -78.67636 ymax: -1.025846
#> Geodetic CRS:  WGS 84
#> # A tibble: 54 × 22
#>    sensor_type_id individual_local_identifier eobs_battery_voltage
#>  *        <int64> <fct>                                       [mV]
#>  1            653 4266-84831108                               3759
#>  2        2365683 4266-84831108                                 NA
#>  3        2365683 4266-84831108                                 NA
#>  4        2365683 4266-84831108                                 NA
#>  5        2365683 4266-84831108                                 NA
#>  6        2365683 4266-84831108                                 NA
#>  7            653 4261-2228                                   3720
#>  8        2365683 4261-2228                                     NA
#>  9        2365683 4261-2228                                     NA
#> 10        2365683 4261-2228                                     NA
#> # ℹ 44 more rows
#> # ℹ 19 more variables: eobs_fix_battery_voltage [mV],
#> #   eobs_horizontal_accuracy_estimate [m], eobs_key_bin_checksum <int64>,
#> #   eobs_speed_accuracy_estimate [m/s], eobs_start_timestamp <dttm>,
#> #   eobs_status <ord>, eobs_temperature [°C], eobs_type_of_fix <fct>,
#> #   eobs_used_time_to_get_fix [s], ground_speed [m/s], heading [°],
#> #   height_above_ellipsoid [m], timestamp <dttm>, visible <lgl>, …
#> Track features:
#> # A tibble: 9 × 52
#>   deployment_id  tag_id individual_id animal_life_stage attachment_type
#>         <int64> <int64>       <int64> <fct>             <fct>          
#> 1       9472222 2911134       2911065 adult             tape           
#> 2       9472220 2911111       2911067 adult             tape           
#> 3       9472218 2911109       2911060 adult             tape           
#> 4       9472214 2911130       2911066 adult             tape           
#> 5       9472208 2911108       2911074 adult             tape           
#> 6       2911178 2911132       2911094 adult             tape           
#> 7       2911168 2911129       2911093 adult             tape           
#> 8       2911167 2911127       2911092 adult             tape           
#> 9       2911150 2911126       2911091 adult             tape           
#> # ℹ 47 more variables: deployment_comments <chr>, deploy_on_timestamp <dttm>,
#> #   duty_cycle <chr>, deployment_local_identifier <fct>,
#> #   manipulation_type <fct>, study_site <chr>, tag_readout_method <fct>,
#> #   sensor_type_ids <chr>, capture_location <POINT [°]>,
#> #   deploy_on_location <POINT [°]>, deploy_off_location <POINT [°]>,
#> #   individual_comments <chr>, individual_local_identifier <fct>,
#> #   taxon_canonical_name <fct>, individual_number_of_deployments <int>, …

Then, to extract the acceleration data from this object, we simply call as_acc():

a <- as_acc(alb)

a
#> <acceleration[54]>
#>  [1] <NA>              (1824.17 1913.83) (1904.3 1926.5)   (1823.27 1913.42)
#>  [5] (1826.7 1915.7)   (1719.07 1908.8)  <NA>              (1943.47 2028.05)
#>  [9] (1927.98 2013.72) (1926.7 2008.82)  (1940.97 2023.93) (1940.02 2021.87)
#> [13] (1969.07 2044.2)  <NA>              (1962.88 2033.35) (2062.08 2023.48)
#> [17] (2090.73 2025.83) (2083.92 2051.48) (1887.58 1927.13) <NA>             
#> [21] (1846.38 1948.13) (1881.15 1938.03) (1841.58 2093.23) (1778.92 2129.55)
#> [25] (1812.17 1952.13) <NA>              (1790.4 1927.4)   (1811.85 1923.43)
#> [29] (1804.47 1935.05) (1811.45 1937.57) (2012.53 2227.17) <NA>             
#> [33] (1970.52 2238.62) (2130.75 2082.43) (1997.18 2074.33) (2049.4 2060.38) 
#> [37] (1975.93 1868.22) <NA>              (1920.02 2128.58) (1879.35 1920.62)
#> [41] (1927.5 1941.72)  (1949.27 1962.57) (1936.87 1918.93) <NA>             
#> [45] (1672.52 1865.77) (1942.15 1922.33) (2072.85 1952.1)  (1617.97 1944.63)
#> [49] (1845.87 1921.13) <NA>              (1838.77 1929.6)  (1843.57 1929.52)
#> [53] (1852.25 1929.65) (1829.97 1932.28)
#> # frequency: 5 [Hz]

as_acc() automatically identifies the relevant acceleration data in the albatross data and converts them into bursts with associated frequencies and start timestamps.

By default, rows in the move2 with no acceleration data receive missing (NA) acc bursts. This makes it easy to keep acceleration bursts linked to other metadata stored in the move2, like GPS location:

alb |>
  mutate(acceleration = a) |>
  select(acceleration)
#> A <move2> with `track_id_column` "individual_local_identifier" and
#> `time_column` "timestamp"
#> Containing 9 tracks lasting on average 59.3 mins in a
#> Simple feature collection with 54 features and 3 fields (with 45 geometries empty)
#> Geometry type: POINT
#> Dimension:     XY
#> Bounding box:  xmin: -89.67882 ymin: -9.06721 xmax: -78.67636 ymax: -1.025846
#> Geodetic CRS:  WGS 84
#> # A tibble: 54 × 4
#>         acceleration              geometry timestamp          
#>                <acc>           <POINT [°]> <dttm>             
#>  1                NA  (-78.67636 -9.06721) 2008-07-27 00:00:56
#>  2 (1824.17 1913.83)                 EMPTY 2008-07-27 00:00:56
#>  3   (1904.3 1926.5)                 EMPTY 2008-07-27 00:15:00
#>  4 (1823.27 1913.42)                 EMPTY 2008-07-27 00:30:00
#>  5   (1826.7 1915.7)                 EMPTY 2008-07-27 00:45:00
#>  6  (1719.07 1908.8)                 EMPTY 2008-07-27 01:00:00
#>  7                NA (-89.45139 -2.083909) 2008-07-27 00:00:15
#>  8 (1943.47 2028.05)                 EMPTY 2008-07-27 00:00:15
#>  9 (1927.98 2013.72)                 EMPTY 2008-07-27 00:15:00
#> 10  (1926.7 2008.82)                 EMPTY 2008-07-27 00:30:00
#> # ℹ 44 more rows
#> # ℹ 1 more variable: individual_local_identifier <fct>
#> Track features:
#> # A tibble: 9 × 52
#>   deployment_id  tag_id individual_id animal_life_stage attachment_type
#>         <int64> <int64>       <int64> <fct>             <fct>          
#> 1       9472222 2911134       2911065 adult             tape           
#> 2       9472220 2911111       2911067 adult             tape           
#> 3       9472218 2911109       2911060 adult             tape           
#> 4       9472214 2911130       2911066 adult             tape           
#> 5       9472208 2911108       2911074 adult             tape           
#> 6       2911178 2911132       2911094 adult             tape           
#> 7       2911168 2911129       2911093 adult             tape           
#> 8       2911167 2911127       2911092 adult             tape           
#> 9       2911150 2911126       2911091 adult             tape           
#> # ℹ 47 more variables: deployment_comments <chr>, deploy_on_timestamp <dttm>,
#> #   duty_cycle <chr>, deployment_local_identifier <fct>,
#> #   manipulation_type <fct>, study_site <chr>, tag_readout_method <fct>,
#> #   sensor_type_ids <chr>, capture_location <POINT [°]>,
#> #   deploy_on_location <POINT [°]>, deploy_off_location <POINT [°]>,
#> #   individual_comments <chr>, individual_local_identifier <fct>,
#> #   taxon_canonical_name <fct>, individual_number_of_deployments <int>, …

Input data formats

The Movebank data model supports several storage formats for acceleration data. However, move2imu handles this heterogeneity out of the box:

# Sample Movebank data
gul <- gulls()

as_acc(gul)
#> <acceleration[1239]>
#>    [1] <NA>                     (-97.75 323.55 1963.95) 
#>    [3] <NA>                     <NA>                    
#>    [5] <NA>                     <NA>                    
#>    [7] <NA>                     <NA>                    
#>    [9] <NA>                     <NA>                    
#>   [11] <NA>                     <NA>                    
#>   [13] <NA>                     <NA>                    
#>   [15] <NA>                     <NA>                    
#>   [17] <NA>                     <NA>                    
#> ...

Even though the albatross and gulls data sources store their acceleration data differently, because each adheres to the Movebank data model, as_acc() seamlessly parses the data into a set of acc burst matrices.

For more details about the ways Movebank stores acceleration data, see movebank_acc_colsets().

Fine-tuning data import

move2imu also supports IMU data extraction from data.frame objects. The workflow is identical to that for move2 objects, except that you will need to specify a vector of timestamps and track IDs separately (move2 objects store this metadata and provide it to as_*() directly).

x <- data.frame(
  acceleration_x = c(1, 2, 3, 4),
  acceleration_y = c(5, 6, 7, 8),
  acceleration_z = c(9, 10, 11, 12),
  timestamp = as.POSIXct("2024-01-01", tz = "UTC") + seq(0, 0.3, by = 0.1),
  id = "tag_1"
)

as_acc(x, timestamp = x$timestamp, track_id = x$id)
#> <acceleration[4]>
#> [1] (2.5 6.5 10.5) <NA>           <NA>           <NA>          
#> # frequency: 10 [Hz]

Further, if your data are stored using column names that don’t adhere to the Movebank data model, you can still use as_acc() to load them, provided that the data themselves are stored in a recognizable format (see imu_colset()). Use imu_colset() to define the column names that correspond to each axis of acceleration data in your input data source:

# The same samples, under custom column names
names(x)[1:3] <- c("acc_x", "acc_y", "acc_z")

as_acc(
  x,
  colset = imu_colset(x = "acc_x", y = "acc_y", z = "acc_z"),
  timestamp = x$timestamp,
  track_id = x$id
)
#> <acceleration[4]>
#> [1] (2.5 6.5 10.5) <NA>           <NA>           <NA>          
#> # frequency: 10 [Hz]

Exploring bursts

For a quick summary of an IMU vector, use summary(). This shows the range of several key burst properties as well as the quantiles of the inter-burst time intervals and the actual recorded burst values.

a <- as_acc(albatrosses())

summary(a)
#> 54 acc bursts (9 NA)
#> from 2008-07-27 00:00:14 to 2008-07-27 01:00:00 UTC 
#> 
#> Axes: XY (45) 
#> Frequencies: 5 -- 5 [Hz] 
#> Samples per burst: 60 -- 60 
#> Durations: 12 -- 12 [s] 
#> Intervals: [ -3598 / 833 / 888 / 888 / 891 ] [s]  (min/Q1/med/Q3/max) 
#> 
#> Values:  [ 1462 / 1875 / 1936 / 2010 / 2988 ]  (min/Q1/med/Q3/max) 
#> Units:   NULL

For a more detailed look at these properties, you can use move2imu’s dedicated accessors. For instance:

# Number of samples per burst
n_samples(a)
#>  [1] NA 60 60 60 60 60 NA 60 60 60 60 60 60 NA 60 60 60 60 60 NA 60 60 60 60 60
#> [26] NA 60 60 60 60 60 NA 60 60 60 60 60 NA 60 60 60 60 60 NA 60 60 60 60 60 NA
#> [51] 60 60 60 60

# Burst duration, in seconds
burst_dur(a)
#> Units: [s]
#>  [1] NA 12 12 12 12 12 NA 12 12 12 12 12 12 NA 12 12 12 12 12 NA 12 12 12 12 12
#> [26] NA 12 12 12 12 12 NA 12 12 12 12 12 NA 12 12 12 12 12 NA 12 12 12 12 12 NA
#> [51] 12 12 12 12

# Check whether all bursts have same frequency, samples, axes, units
is_uniform(a)
#> [1] TRUE

# and more...

If you need to operate on the burst data or metadata directly, they can easily be accessed as well:

bursts(a)
#> <acc_list[54]>
#> [[1]]
#> NULL
#> 
#> [[2]]
#>          X    Y
#>  [1,] 1856 1900
#>  [2,] 1816 1931
#>  [3,] 1812 1902
#>  [4,] 1826 1920
#> ...

freqs(a)
#> Units: [Hz]
#>  [1] NA  5  5  5  5  5 NA  5  5  5  5  5  5 NA  5  5  5  5  5 NA  5  5  5  5  5
#> [26] NA  5  5  5  5  5 NA  5  5  5  5  5 NA  5  5  5  5  5 NA  5  5  5  5  5 NA
#> [51]  5  5  5  5

starts(a)
#>  [1] NA                        "2008-07-27 00:00:56 UTC"
#>  [3] "2008-07-27 00:15:00 UTC" "2008-07-27 00:30:00 UTC"
#>  [5] "2008-07-27 00:45:00 UTC" "2008-07-27 01:00:00 UTC"
#>  [7] NA                        "2008-07-27 00:00:15 UTC"
#>  [9] "2008-07-27 00:15:00 UTC" "2008-07-27 00:30:00 UTC"
#> [11] "2008-07-27 00:45:00 UTC" "2008-07-27 01:00:00 UTC"
#> [13] "2008-07-27 00:00:55 UTC" NA                       
#> [15] "2008-07-27 00:15:00 UTC" "2008-07-27 00:30:00 UTC"
#> [17] "2008-07-27 00:45:00 UTC" "2008-07-27 01:00:00 UTC"
#> [19] "2008-07-27 00:00:50 UTC" NA                       
#> ...

These can be used to easily explore the distributions of these properties:

# Distribution of inter-burst intervals
hist(burst_intervals(a), main = "Burst intervals (s)")


# All values
hist(unlist(bursts(a)), main = "Sample values")

Plotting sampling effort

Many tags change their sampling settings over the course of a study. To visualize how the IMU data were sampled, use plot_sampling_effort(). This can reveal periods where different sensors or tracks collected data simultaneously, where tags stopped and started collecting data, and how densely certain time ranges were sampled.

track_ids <- mt_track_id(albatrosses())

plot_sampling_effort(acc = a, ids = track_ids)

Modify the width of the aggregation bins and the plot’s time range to explore time periods in more detail:

start <- as.POSIXct("2008-07-27 00:00:00", tz = "UTC")
end <- as.POSIXct("2008-07-27 00:02:00", tz = "UTC")
width <- units::set_units(0.3, "s")

plot_sampling_effort(
  acc = a,
  ids = track_ids,
  bin_width = width,
  from = start,
  to = end
)

plot_sampling_effort() accepts both imu objects as well as timestamp vectors, so you can also plot other sensors based on their recorded timestamps (e.g. GPS coordinates):

# Mask out timestamps to get only those where a GPS location was recorded
gps <- replace(mt_time(albatrosses()), sf::st_is_empty(albatrosses()), NA)

plot_sampling_effort(
  acc = a,
  gps = gps,
  ids = track_ids,
  bin_width = width,
  from = start,
  to = end
)

Plotting IMU traces

You can also plot the values recorded by an IMU vector over time with plot_imu_trace(). This produces an interactive dygraph that you can zoom into with the mouse:

You can also pass a single burst (or a subset) for a focused view — here, the wing beats are clearly visible on the Z axis:

The plot is returned unstyled. Additional arguments are passed to dygraphs::dygraph() and additional dygraphs layers can be added to the output plot. For instance, to add axis labels and ensure times are labeled in UTC instead of the browser’s local time zone:

plot_imu_trace(a[422], main = "Wing beats", ylab = "Acceleration (g)") |>
  dygraphs::dyOptions(labelsUTC = TRUE)

Next steps

If your data are stored as raw ADC values rather than physical units, see the calibration vignette to learn how to convert them.

See the programming with bursts vignette for a demonstration of how to compute values from acc bursts and link them back to GPS coordinates in a move2 object.