Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.4 #64

Merged
merged 14 commits into from
Nov 12, 2024
Merged
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: animovement
Type: Package
Title: An R toolbox for analysing animal movement across space and time
Version: 0.3.0
Version: 0.4.0
Authors@R:
person(
"Mikkel",
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export(calculate_kinematics)
export(calculate_statistics)
export(calculate_straightness)
export(clean_kinematics)
export(does_file_have_expected_headers)
export(ensure_file_has_expected_headers)
export(ensure_file_has_headers)
export(group_every)
Expand All @@ -28,6 +29,7 @@ import(dplyr)
import(rhdf5)
import(tidyr)
import(tidyselect)
import(vroom)
importFrom(circular,circular)
importFrom(circular,is.circular)
importFrom(cli,cli_abort)
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# animovement 0.4.0

Added readers for AnimalTA (`read_animalta`) and idtracker.ai (`read_idtracker`).

# animovement 0.3.0

Has added the ability to read centroid tracking from Bonsai files through `read_bonsai()`.
Expand Down
71 changes: 69 additions & 2 deletions R/read_animalta.R
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,76 @@
#' `r lifecycle::badge('experimental')`
#'
#' @param path An AnimalTA data frame
#' @param with_roi Were one or more ROIs used?
#'
#' @import dplyr
#' @import vroom
#' @importFrom janitor clean_names
#'
#' @return a movement dataframe
#' @export
read_animalta <- function(path) {
cli::cli_abort("`read_animalta` has not yet been implemented. Coming soon!")
read_animalta <- function(path, with_roi = FALSE) {
# Inspect headers
if (with_roi == FALSE){
validate_files(
path,
expected_suffix = "csv",
expected_headers = c("X", "Y", "Time")
)
data <- read_animalta_no_roi(path)
} else {
validate_files(
path,
expected_suffix = "csv",
expected_headers = c("Time", "X_Arena0_Ind0", "Y_Arena0_Ind0")
)
data <- read_animalta_with_roi(path)
}
data <- data |>
dplyr::mutate(keypoint = factor("centroid")) |>
dplyr::relocate("keypoint", .after = "individual")
return(data)
}

#' @inheritParams read_animalta
#' @keywords internal
read_animalta_no_roi <- function(path){
data <- vroom::vroom(
path,
delim = ";",
show_col_types = FALSE
) |>
janitor::clean_names() |>
dplyr::mutate(frame = as.numeric(.data$frame),
time = as.numeric(.data$time)) |>
dplyr::rename(individual = "ind") |>
dplyr::mutate(individual = factor(.data$individual)) |>
dplyr::select(-c("frame", "arena"))
attributes(data)$spec <- NULL
attributes(data)$problems <- NULL
return(data)
}

#' @inheritParams read_animalta
#' @import tidyr
#' @keywords internal
read_animalta_with_roi <- function(path){
data <- vroom::vroom(
path,
delim = ";",
show_col_types = FALSE
) |>
janitor::clean_names()

data <- data |>
tidyr::pivot_longer(cols = 3:ncol(data),
names_to = c("coordinate", "individual", "arena"),
names_sep = "_",
values_to = "val") |>
tidyr::pivot_wider(id_cols = c("time", "individual", "arena"),
names_from = "coordinate",
values_from = "val") |>
tidyr::unite("individual", c("individual", "arena")) |>
dplyr::mutate(individual = factor(.data$individual))
return(data)
}
128 changes: 125 additions & 3 deletions R/read_idtracker.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,132 @@
#' @description
#' `r lifecycle::badge('experimental')`
#'
#' @param data An idtracker.ai data frame
#' @param path Path to an idtracker.ai data frame
#' @param path_probabilities Path to a csv file with probabilities. Only needed if you are reading csv files as they are included in h5 files.
#' @param version idtracker.ai version. Currently only v6 output is implemented
#'
#' @import dplyr
#' @import tidyr
#' @import rhdf5
#' @importFrom vroom vroom
#' @importFrom janitor clean_names
#'
#' @return a movement dataframe
#' @export
read_idtracker <- function(data) {
cli::cli_abort("`read_idtracker` has not yet been implemented. Coming soon!")
read_idtracker <- function(path, path_probabilities = NULL, version = 6) {
# Needs to check the file extension
# If probabilites are given, extension needs to be csv
validate_files(path, expected_suffix = c("csv", "h5"))
if (!is.null(path_probabilities) & .get_file_ext(path) == "h5"){
cli::cli_warn("You supplied a h5 file and probabilities in csv; the h5 data already contains the probabilities, so we only the h5 data.")
}
if (.get_file_ext(path) == "csv"){
data <- read_idtracker_csv(path, path_probabilities, version = version)
} else if(.get_file_ext(path) == "h5"){
data <- read_idtracker_h5(path, version = version)
}

return(data)
}

#' @inheritParams read_idtracker
#' @keywords internal
read_idtracker_csv <- function(path, path_probabilities, version = 6){
data <- vroom::vroom(
path,
delim = ",",
show_col_types = FALSE
) |>
suppressMessages() |>
janitor::clean_names()

data <- data |>
tidyr::pivot_longer(cols = 2:ncol(data),
names_to = c("coordinate", "individual"),
names_sep = "(?<=[A-Za-z])(?=[0-9])",
values_to = "val"
) |>
tidyr::pivot_wider(id_cols = c("seconds", "individual"),
names_from = "coordinate",
values_from = "val") |>
dplyr::rename(time = "seconds") |>
dplyr::mutate(individual = factor(.data$individual))

if (!is.null(path_probabilities)){
probs <- read_idtracker_probabilities(path_probabilities)
data <- dplyr::left_join(data, probs, by = c("individual", "time"))
}

# Convert NaN to NA
data <- data |>
dplyr::mutate(dplyr::across(dplyr::everything(), ~ifelse(is.nan(.), NA, .))) |>
dplyr::mutate(individual = factor(.data$individual),
keypoint = factor("centroid")) |>
dplyr::relocate("keypoint", .after = "individual")

return(data)
}

#' @inheritParams read_idtracker
#' @keywords internal
read_idtracker_probabilities <- function(path){
data <- vroom::vroom(
path,
delim = ",",
show_col_types = FALSE
) |>
suppressMessages() |>
janitor::clean_names()

data <- data |>
tidyr::pivot_longer(cols = 2:ncol(data),
names_to = c("placeholder", "individual"),
names_sep = "(?<=[A-Za-z])(?=[0-9])",
values_to = "confidence"
) |>
dplyr::select(-"placeholder") |>
dplyr::rename(time = "seconds")
}

#' @inheritParams read_idtracker
#' @keywords internal
read_idtracker_h5 <- function(path, version = version){
traj_dimensions <- rhdf5::h5ls(path) |>
dplyr::as_tibble(.name_repair = "unique") |>
dplyr::filter(.data$name == "trajectories") |>
dplyr::pull(dim) |>
strsplit(" x ")

n_individuals <- traj_dimensions[[1]][2] |> as.numeric()

data <- data.frame()
for (i in 1:n_individuals){
trajectories <- rhdf5::h5read(path, "trajectories")[,i,] |>
t() |>
dplyr::as_tibble(.name_repair = "unique") |>
suppressMessages() |>
dplyr::rename(x = "...1",
y = "...2")

probs <- rhdf5::h5read(path, "id_probabilities")[,i,] |>
dplyr::as_tibble(.name_repair = "unique") |>
dplyr::rename(confidence = "value")

data_temp <- dplyr::bind_cols(trajectories, probs) |>
dplyr::mutate(individual = factor(i),
keypoint = factor("centroid"),
time = row_number())

data <- dplyr::bind_rows(data, data_temp)
}

data <- data |>
# Convert NaN to NA
dplyr::mutate(dplyr::across(dplyr::everything(), ~ifelse(is.nan(.), NA, .))) |>
dplyr::relocate("keypoint", .before = "x") |>
dplyr::relocate("individual", .before = "keypoint") |>
dplyr::relocate("time", .before = "individual") |>
dplyr::mutate(individual = factor(.data$individual),
keypoint = factor(.data$keypoint))
return(data)
}
4 changes: 2 additions & 2 deletions R/read_trackball.R
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ read_trackball <- function(
distance_scale = NULL,
distance_unit = NULL,
verbose = FALSE) {
validate_files(paths, expected_suffix = "csv", expected_headers = c("x", "y", "time"))
validate_files(paths, expected_suffix = "csv") #expected_headers = c("x", "y", "time")
validate_trackball(paths, setup, col_time)
n_sensors <- length(paths)

Expand Down Expand Up @@ -80,7 +80,7 @@ read_trackball <- function(
#' @keywords internal
read_opticalflow <- function(path, col_time, verbose = FALSE) {
# Read file
if (ensure_file_has_expected_headers(path, c("x", "y", "time"))) {
if (does_file_have_expected_headers(path, c("x", "y", "time"))) {
data <- vroom::vroom(
path,
delim = ",",
Expand Down
29 changes: 25 additions & 4 deletions R/validator_files.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
expected_permission = "r",
expected_suffix = NULL,
expected_headers = NULL) {

# Perform checks on all supplied paths
for (p in path) {
ensure_is_not_dir(p)
Expand All @@ -32,8 +33,6 @@
if (!is.null(expected_suffix)) {
ensure_file_has_expected_suffix(p, expected_suffix)
}

# Check file headers
if (!is.null(expected_headers)){
ensure_file_has_headers(p)
ensure_file_has_expected_headers(p, expected_headers)
Expand Down Expand Up @@ -94,20 +93,42 @@
df <- vroom::vroom(
path,
n_max = 10,
delim = ",",
# delim = ",",
show_col_types = FALSE,
.name_repair = "unique"
) |>
suppressMessages()
has_headers <- ncol(df) > 1
return(has_headers)
if (has_headers != TRUE) {
cli::cli_abort("Expected file headers(es), but found none.")

Check warning on line 103 in R/validator_files.R

View check run for this annotation

Codecov / codecov/patch

R/validator_files.R#L103

Added line #L103 was not covered by tests
}
# return(has_headers)
}

#' Ensure file has expected headers
#' @inheritParams validate_files
#' @keywords internal
#' @export
ensure_file_has_expected_headers <- function(path, expected_headers = c("x", "y", "time")) {
df <- vroom::vroom(
path,
n_max = 10,
# delim = ",",
show_col_types = FALSE,
.name_repair = "unique"
) |>
suppressMessages()
has_correct_headers <- all(expected_headers %in% names(df))
if (has_correct_headers != TRUE) {
cli::cli_abort("Expected the following file headers: {expected_headers}, but they were not present.")
}
}

#' Check whether file has expected headers
#' @inheritParams validate_files
#' @keywords internal
#' @export
does_file_have_expected_headers <- function(path, expected_headers = c("x", "y", "time")) {
df <- vroom::vroom(
path,
n_max = 10,
Expand Down
3 changes: 2 additions & 1 deletion R/validator_trackball.R
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ ensure_identical_suffix <- function(paths) {
#' @inheritParams validate_trackball
#' @keywords internal
ensure_header_match <- function(path, col_time) {
if (!ensure_file_has_headers(path) & is.character(col_time)) {
does_file_have_expected_headers(path)
if (is.character(col_time)) {
cli::cli_abort("`col_time` is a string ({col_time}), but the file doesn't have named headers. Either use a column number or provide a file with named headers.")
}
}
2 changes: 1 addition & 1 deletion man/calculate_kinematics.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions man/does_file_have_expected_headers.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion man/read_animalta.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions man/read_idtracker.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading