From aba344ca15d6fee6df1e094c8bc4f4bca116a854 Mon Sep 17 00:00:00 2001 From: jhmigueles Date: Tue, 3 Nov 2026 10:55:58 +0100 Subject: [PATCH 1/4] readAxivity: keep one time grid across chunk borders --- R/RcppExports.R | 4 ++-- R/readAxivity.R | 20 ++++++++++++++++---- src/RcppExports.cpp | 8 +++++--- src/init.c | 5 +++-- src/processAxivityBlocks.cpp | 26 +++++++++++++++++++++++++- 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/R/RcppExports.R b/R/RcppExports.R index e5300dd..4ad563a 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -17,8 +17,8 @@ find_matrix_packet_start <- function(file_path, pattern, chunk_size = 1048576L) .Call(`_GGIRread_find_matrix_packet_start`, file_path, pattern, chunk_size) } -processAxivityBlocks <- function(readBlocks, calendar, progress, first, time, start, end, numDBlocks, frequency_tol, maxAllowedCorruptBlocks, loadbattery, interpolationType) { - .Call(`_GGIRread_processAxivityBlocks`, readBlocks, calendar, progress, first, time, start, end, numDBlocks, frequency_tol, maxAllowedCorruptBlocks, loadbattery, interpolationType) +processAxivityBlocks <- function(readBlocks, calendar, progress, first, time, start, end, numDBlocks, frequency_tol, maxAllowedCorruptBlocks, loadbattery, interpolationType, fillToNext = FALSE, gridTolerance = 0) { + .Call(`_GGIRread_processAxivityBlocks`, readBlocks, calendar, progress, first, time, start, end, numDBlocks, frequency_tol, maxAllowedCorruptBlocks, loadbattery, interpolationType, fillToNext, gridTolerance) } resample <- function(raw, rawTime, time, stop, type = 1L) { diff --git a/R/readAxivity.R b/R/readAxivity.R index 92cff46..d18b71d 100755 --- a/R/readAxivity.R +++ b/R/readAxivity.R @@ -329,9 +329,20 @@ readAxivity = function(filename, start = 0, end = 0, progressBar = FALSE, desire startTimestamp = as.numeric(prevRaw$start) # allocate memory for results - timeRes = seq(startTimestamp, endTimestamp, step) - nr = length(timeRes) - 1 - timeRes = as.vector(timeRes[1:nr]) + # Output samples lie on one time grid for the whole file, anchored at the + # start time in the file header: header$start + k * step. The first sample is + # the first grid point at or after the first sample of the start block, and + # the last one is the last grid point before the start of the end block + # (filled in processAxivityBlocks). Calls for consecutive block ranges + # therefore join without lost or repeated samples, and give the same result + # as reading those blocks in one call. + gridStart = as.numeric(header$start) + # tolerance (s) for deciding on which side of a block border a grid point is + gridTolerance = 0.001 * step + k0 = ceiling((startTimestamp - gridTolerance - gridStart) / step) + k1 = ceiling((endTimestamp - gridStart) / step) + 1 + timeRes = gridStart + (k0:k1) * step + nr = length(timeRes) # Keep the R connection for platform-independent filenames and cleanup. The # native loop requests at most 128 packets at a time, independent of file size. readBlocks = function() readBin(fid, raw(), n = blockBytes * 128) @@ -345,7 +356,8 @@ readAxivity = function(filename, start = 0, end = 0, progressBar = FALSE, desire } result = processAxivityBlocks(readBlocks, calendar, progress, prevRaw, timeRes, start, end, numDBlocks, frequency_tol, - maxAllowedCorruptBlocks, loadbattery, interpolationType) + maxAllowedCorruptBlocks, loadbattery, interpolationType, + end < numDBlocks, gridTolerance) accelRes = result$accel temp = result$temp battery = result$battery diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 32f44b1..cacdabc 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -63,8 +63,8 @@ BEGIN_RCPP END_RCPP } // processAxivityBlocks -Rcpp::List processAxivityBlocks(Rcpp::Function readBlocks, Rcpp::Function calendar, Rcpp::Nullable progress, Rcpp::List first, Rcpp::NumericVector time, double start, double end, double numDBlocks, double frequency_tol, int maxAllowedCorruptBlocks, bool loadbattery, int interpolationType); -RcppExport SEXP _GGIRread_processAxivityBlocks(SEXP readBlocksSEXP, SEXP calendarSEXP, SEXP progressSEXP, SEXP firstSEXP, SEXP timeSEXP, SEXP startSEXP, SEXP endSEXP, SEXP numDBlocksSEXP, SEXP frequency_tolSEXP, SEXP maxAllowedCorruptBlocksSEXP, SEXP loadbatterySEXP, SEXP interpolationTypeSEXP) { +Rcpp::List processAxivityBlocks(Rcpp::Function readBlocks, Rcpp::Function calendar, Rcpp::Nullable progress, Rcpp::List first, Rcpp::NumericVector time, double start, double end, double numDBlocks, double frequency_tol, int maxAllowedCorruptBlocks, bool loadbattery, int interpolationType, bool fillToNext, double gridTolerance); +RcppExport SEXP _GGIRread_processAxivityBlocks(SEXP readBlocksSEXP, SEXP calendarSEXP, SEXP progressSEXP, SEXP firstSEXP, SEXP timeSEXP, SEXP startSEXP, SEXP endSEXP, SEXP numDBlocksSEXP, SEXP frequency_tolSEXP, SEXP maxAllowedCorruptBlocksSEXP, SEXP loadbatterySEXP, SEXP interpolationTypeSEXP, SEXP fillToNextSEXP, SEXP gridToleranceSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -80,7 +80,9 @@ BEGIN_RCPP Rcpp::traits::input_parameter< int >::type maxAllowedCorruptBlocks(maxAllowedCorruptBlocksSEXP); Rcpp::traits::input_parameter< bool >::type loadbattery(loadbatterySEXP); Rcpp::traits::input_parameter< int >::type interpolationType(interpolationTypeSEXP); - rcpp_result_gen = Rcpp::wrap(processAxivityBlocks(readBlocks, calendar, progress, first, time, start, end, numDBlocks, frequency_tol, maxAllowedCorruptBlocks, loadbattery, interpolationType)); + Rcpp::traits::input_parameter< bool >::type fillToNext(fillToNextSEXP); + Rcpp::traits::input_parameter< double >::type gridTolerance(gridToleranceSEXP); + rcpp_result_gen = Rcpp::wrap(processAxivityBlocks(readBlocks, calendar, progress, first, time, start, end, numDBlocks, frequency_tol, maxAllowedCorruptBlocks, loadbattery, interpolationType, fillToNext, gridTolerance)); return rcpp_result_gen; END_RCPP } diff --git a/src/init.c b/src/init.c index 5b34752..420a8b2 100644 --- a/src/init.c +++ b/src/init.c @@ -6,7 +6,8 @@ extern SEXP _GGIRread_AxivityNumUnpack(SEXP); extern SEXP _GGIRread_decodeAxivityBlock(SEXP, SEXP, SEXP, SEXP); extern SEXP _GGIRread_processAxivityBlocks(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, - SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); + SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, + SEXP, SEXP); extern SEXP _GGIRread_resample(SEXP, SEXP, SEXP, SEXP, SEXP); extern SEXP _GGIRread_GENEActivReader(SEXP, SEXP, SEXP, SEXP); extern SEXP _GGIRread_find_matrix_packet_start(SEXP, SEXP, SEXP); @@ -14,7 +15,7 @@ extern SEXP _GGIRread_find_matrix_packet_start(SEXP, SEXP, SEXP); static const R_CallMethodDef CallEntries[] = { {"_GGIRread_AxivityNumUnpack", (DL_FUNC) &_GGIRread_AxivityNumUnpack, 1}, {"_GGIRread_decodeAxivityBlock", (DL_FUNC) &_GGIRread_decodeAxivityBlock, 4}, - {"_GGIRread_processAxivityBlocks", (DL_FUNC) &_GGIRread_processAxivityBlocks, 12}, + {"_GGIRread_processAxivityBlocks", (DL_FUNC) &_GGIRread_processAxivityBlocks, 14}, {"_GGIRread_GENEActivReader", (DL_FUNC) &_GGIRread_GENEActivReader, 4}, {"_GGIRread_resample", (DL_FUNC) &_GGIRread_resample, 5}, {"_GGIRread_find_matrix_packet_start", (DL_FUNC) &_GGIRread_find_matrix_packet_start, 3}, diff --git a/src/processAxivityBlocks.cpp b/src/processAxivityBlocks.cpp index 0193cce..eecdd9d 100644 --- a/src/processAxivityBlocks.cpp +++ b/src/processAxivityBlocks.cpp @@ -66,7 +66,8 @@ Rcpp::List processAxivityBlocks(Rcpp::Function readBlocks, Rcpp::Function calend Rcpp::List first, Rcpp::NumericVector time, double start, double end, double numDBlocks, double frequency_tol, int maxAllowedCorruptBlocks, - bool loadbattery, int interpolationType) { + bool loadbattery, int interpolationType, + bool fillToNext = false, double gridTolerance = 0) { using namespace Rcpp; if (time.size() == 0 || time.size() > std::numeric_limits::max()) { stop("Invalid number of samples in Axivity chunk."); @@ -259,6 +260,29 @@ Rcpp::List processAxivityBlocks(Rcpp::Function readBlocks, Rcpp::Function calend interpolationAnchor[axis] = previous.data(length - 1, axis); } } + if (fillToNext && ii == end && ii < numDBlocks) { + // Last iteration of a chunk that ends before the end of the file. The + // lookahead packet (current) is not part of the output, but the grid + // points before its start belong to this chunk. Fill them as the next + // iteration would when the whole file is read: interpolate between the + // interpolation anchor and the first sample of the lookahead packet, + // with the auxiliary values of that packet. The next chunk then starts + // at the first grid point at or after current.start. + while (position < nr && time[position] < current.start - gridTolerance) { + const double weight = (time[position] - interpolationTime) / + (current.start - interpolationTime); + for (int axis = 0; axis < axes; ++axis) { + const double left = interpolationAnchor[axis]; + const double right = current.data(0, axis); + if (interpolationType == 1) accel(position, axis) = weight * (right - left) + left; + else if (interpolationType == 2) accel(position, axis) = std::ceil(weight - 0.5) * (right - left) + left; + } + temperature[position] = current.temperature; + battery[position] = current.battery; + light[position] = current.light; + ++position; + } + } previous = current; if (progress.isNotNull()) Function(progress.get())(position + 1); } From 155d2ae22224f4dc190d2c4e0c8ca0d1c0824b16 Mon Sep 17 00:00:00 2001 From: jhmigueles Date: Tue, 3 Nov 2026 10:56:12 +0100 Subject: [PATCH 2/4] Document the time grid of readAxivity --- man/readAxivity.Rd | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/man/readAxivity.Rd b/man/readAxivity.Rd index 9940677..21171e2 100644 --- a/man/readAxivity.Rd +++ b/man/readAxivity.Rd @@ -123,6 +123,16 @@ when its magnitude lies between 0.8 and 1.2 g, otherwise with \code{c(0, 0, 1)}. Gyroscope channels are filled with zero during imputation. + Output samples lie on one time grid for the whole file, starting at the + start time in the file header and spaced 1 / frequency. A call returns the + grid points from the first sample of block \code{start} up to, but not + including, the start of block \code{end}. Reading consecutive block ranges + in separate calls therefore gives the same samples as reading them in one + call. Next to corrupt blocks, a chunk that starts or ends next to a corrupt + interval, or that contains only the last valid block of the file, can still + differ slightly from a single read, because the frequency check and the + duration of the last block are estimated from fewer blocks. + For failed-checksum rows in \code{QClog}, the packet's contents cannot be trusted: block IDs are inferred from file positions and timing and frequency fields are zero. Such rows have \code{imputed = FALSE}; a separate row records @@ -132,6 +142,10 @@ \donttest{ cwafile = system.file("testfiles/ax3_testfile.cwa", package = "GGIRread")[1] AX3 = readAxivity(filename = cwafile, desiredtz = "Europe/Berlin", start = 1, end = 2) + # read in two chunks that join seamlessly + header = readAxivity(cwafile, start = 0, end = 0)$header + part1 = readAxivity(cwafile, start = 0, end = 50, header = header) + part2 = readAxivity(cwafile, start = 50, end = 100, header = header) } } \author{ From 7afcb4cfe9d22688d8ffe6df282d931036edde0f Mon Sep 17 00:00:00 2001 From: jhmigueles Date: Tue, 3 Nov 2026 10:56:31 +0100 Subject: [PATCH 3/4] Update readAxivity tests for the file-wide time grid --- tests/testthat/test_processAxivityBlocks.R | 9 +- tests/testthat/test_readAxivity.R | 137 +++++++++++++++------ 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/tests/testthat/test_processAxivityBlocks.R b/tests/testthat/test_processAxivityBlocks.R index 135a68d..a1c06c0 100644 --- a/tests/testthat/test_processAxivityBlocks.R +++ b/tests/testthat/test_processAxivityBlocks.R @@ -81,9 +81,12 @@ test_that("packet boundaries interpolate through raw samples on every axis", { } bytes[37] = as.raw(10) writeBin(c(bytes[1:1024], unlist(packets)), filename) - rawTime = origin + rep((0:2) * 0.375, each = count) + - rep((0:(count - 1)) * (0.375 / count), 3) - rawValues = do.call(rbind, values[1:3]) + # Blocks 0-2 are read; the output continues up to the start of block 3, + # interpolating towards its first sample. + rawTime = c(origin + rep((0:2) * 0.375, each = count) + + rep((0:(count - 1)) * (0.375 / count), 3), + origin + 3 * 0.375) + rawValues = rbind(do.call(rbind, values[1:3]), values[[4]][1, ]) for (interpolation in 1:2) { result = readAxivity(filename, start = 0, end = 3, configtz = "UTC", interpolationType = interpolation, frequency_tol = 0.5) diff --git a/tests/testthat/test_readAxivity.R b/tests/testthat/test_readAxivity.R index 17cd09b..a4017f9 100644 --- a/tests/testthat/test_readAxivity.R +++ b/tests/testthat/test_readAxivity.R @@ -1,6 +1,27 @@ library(GGIRread) context("reading Axivity .cwa data") +# Start time of a raw packet, decoded as readAxivity does (UTC clock). +packetStart = function(packet) { + decoded = GGIRread:::decodeAxivityBlock(packet, TRUE, NULL, FALSE) + coded = decoded$timeStamp + start = ISOdatetime(bitwAnd(bitwShiftR(coded, 26), 0x3fL) + 2000, + bitwAnd(bitwShiftR(coded, 22), 0x0fL), + bitwAnd(bitwShiftR(coded, 17), 0x1f), + bitwAnd(bitwShiftR(coded, 12), 0x1fL), + bitwAnd(bitwShiftR(coded, 6), 0x3fL), + bitwAnd(coded, 0x3fL), tz = "UTC") + as.numeric(start) + decoded$fractional / 65536 + decoded$shift +} + +# Reference for resample(): linear (1) or nearest neighbour (2) interpolation. +resampleReference = function(rawTime, raw, time, type) { + i = pmax(1, findInterval(time, rawTime, left.open = TRUE)) + u = (time - rawTime[i]) / (rawTime[i + 1] - rawTime[i]) + if (type == 2) u = ceiling(u - 0.5) + raw[i] + u * (raw[i + 1] - raw[i]) +} + test_that("one-sample recordings retain sensor columns and one output row", { filename = tempfile(fileext = ".cwa") on.exit(unlink(filename)) @@ -148,32 +169,35 @@ test_that("readAxivity returns the final block when it is the only requested blo package = "GGIRread") n = (file.size(cwafile) - 1024) / 512 header = readAxivity(cwafile, desiredtz = "UTC")$header + bytes = readBin(cwafile, "raw", n = file.size(cwafile)) + packet = bytes[1024 + (n - 1) * 512 + seq_len(512)] + decoded = GGIRread:::decodeAxivityBlock(packet, TRUE, NULL, FALSE) + # A single packet is placed on its nominal frequency. + rawTime = packetStart(packet) + (seq_len(decoded$length) - 1) / header$frequency + # Output samples are the points of the file's time grid within the packet. + step = 1 / header$frequency + gridStart = as.numeric(header$start) + k = ceiling((rawTime[1] - 0.001 * step - gridStart) / step): + floor((max(rawTime) - gridStart) / step) for (interpolationType in c(1, 2)) { result = readAxivity(cwafile, start = n - 1, end = n, desiredtz = "UTC", header = header, interpolationType = interpolationType, loadbattery = TRUE, frequency_tol = 0) - expect_equal(nrow(result$data), header$blockLength) + expect_equal(result$data$time, gridStart + k * step, tolerance = 1e-6, scale = 1) + expect_gte(nrow(result$data), header$blockLength - 1) expect_equal(ncol(result$data), if (device == "ax3") 7L else 10L) - expect_equal(diff(result$data$time), - rep(1 / header$frequency, header$blockLength - 1), - tolerance = 1e-6) expect_true(all(result$data$battery > 0)) expect_null(result$QClog) - # First and last accelerometer samples from the fixture's final packet. - expected = if (device == "ax3") { - rbind(c(0.59375, 0.328125, 0.59375), c(-0.0625, -0.84375, 0.265625)) - } else { - rbind(c(104, 2007, 15), c(98, 2010, 23)) / 2048 + for (axis in c("x", "y", "z")) { + expected = resampleReference(rawTime, decoded$data[, axis], + result$data$time, interpolationType) + expect_equal(result$data[[axis]], expected, tolerance = 1e-6, scale = 1) } - expect_equal(unname(as.matrix(result$data[c(1, header$blockLength), - c("x", "y", "z")])), expected, - tolerance = 1e-6) } # The same path is reached when a corrupt start packet is skipped, # leaving just the final valid packet in the requested chunk. - bytes = readBin(cwafile, "raw", n = file.size(cwafile)) offset = 1024 + (n - 2) * 512 + 1 bytes[offset] = as.raw(bitwXor(as.integer(bytes[offset]), 1L)) corruptfile = tempfile(fileext = ".cwa") @@ -197,12 +221,12 @@ test_that("multi-block reads place the final packet on its own time axis", { n = (length(bytes) - 1024) / 512 packet = bytes[1024 + (n - 1) * 512 + seq_len(512)] decoded = GGIRread:::decodeAxivityBlock(packet, TRUE, NULL, FALSE) - single = readAxivity(cwafile, start = n - 1, end = n, desiredtz = "UTC") - preceding = readAxivity(cwafile, start = n - 2, end = n - 1, desiredtz = "UTC") - finalStart = single$data$time[1] + finalStart = packetStart(packet) + precedingStart = packetStart(bytes[1024 + (n - 2) * 512 + seq_len(512)]) # Multi-block reads estimate spacing from the preceding packet, whereas # the single-block path must use the nominal frequency. - spacing = (finalStart - preceding$data$time[1]) / preceding$header$blockLength + header = readAxivity(cwafile, desiredtz = "UTC")$header + spacing = (finalStart - precedingStart) / header$blockLength rawTime = finalStart + (seq_len(decoded$length) - 1) * spacing axes = colnames(decoded$data) for (start in c(0, n - 2)) { @@ -224,10 +248,10 @@ test_that("readAxivity reads data from AX3 file correctly", { expect_equal(AX3$header$device, "Axivity") expect_equal(nrow(AX3$data), 364) expect_equal(ncol(AX3$data), 7) - expect_equal(AX3$data$time[5], 1551174907.255, tolerance = .001, scale = 1) + expect_equal(AX3$data$time[5], 1551174907.2605, tolerance = .001, scale = 1) expect_equal(AX3$data$temp[3], 26.46484, tolerance = 0.0001) - expect_equal(floor(sum(abs(AX3$data[,2:4]))), 603) - expect_equal(AX3$data[1,2], 0.7656, tolerance = .0001, scale = 1) + expect_equal(floor(sum(abs(AX3$data[,2:4]))), 604) + expect_equal(AX3$data[1,2], 0.7491, tolerance = .0001, scale = 1) expect_equal(AX3$data[4,3], -0.3125, tolerance = .0001, scale = 1) expect_true(is.null(AX3$QClog)) @@ -263,8 +287,11 @@ test_that("readAxivity handles failed checksums correctly", { expect_warning(AX3_0_12 <- readAxivity(filename = cwafile, desiredtz = "Europe/Berlin", start = 0, end = 12), "Skipping corrupt block #0") expect_equal(AX3_0_12$header$device, "Axivity") - expect_equal(nrow(AX3_0_12$data), nrow(AX3$data)) - expect_equal(floor(sum(abs(AX3_0_12$data[,2:4]))), floor(sum(abs(AX3$data[,2:4])))) + # The time grid starts at the first valid block: block 0 in the intact file and + # block 1 here, so the two grids are offset by a fraction of a sample. + expect_lte(abs(nrow(AX3_0_12$data) - nrow(AX3$data)), 1) + expect_equal(mean(abs(as.matrix(AX3_0_12$data[,2:4]))), + mean(abs(as.matrix(AX3$data[,2:4]))), tolerance = .001) expect_false(is.null(AX3_0_12$QClog)) expect_equal(nrow(AX3_0_12$QClog),1) @@ -279,17 +306,16 @@ test_that("readAxivity handles failed checksums correctly", { "Skipping corrupt block #0") # the warning is there b/c block 0 is always read, and it's corrupt expect_equal(AX3_1_12$header$device, "Axivity") - expect_equal(nrow(AX3_1_12$data), nrow(AX3$data)) - expect_equal(floor(sum(abs(AX3_1_12$data[,2:4]))), floor(sum(abs(AX3$data[,2:4])))) + expect_equal(AX3_1_12$data, AX3_0_12$data) expect_true(is.null(AX3_1_12$QClog)) # Now read a bigger chunk of the file, so that the corrupt blocks 13-14 fall in the middle expect_warning(AX3_1_20 <- readAxivity(filename = cwafile, desiredtz = "Europe/Berlin", start = 1, end = 20), "Skipping corrupt block #13") expect_equal(AX3_1_20$header$device, "Axivity") - expect_equal(nrow(AX3_1_20$data), 2306) + expect_equal(nrow(AX3_1_20$data), 2307) expect_equal(ncol(AX3_1_20$data), 7) - expect_equal(floor(sum(abs(AX3_1_20$data[,2:4]))), 3244) + expect_equal(floor(sum(abs(AX3_1_20$data[,2:4]))), 3245) # there should be 3 entries in QClog: one each for blocks 13 and 14 with failed checksums (these initially get skipped, not imputed), # and one for block 12 that preceeds the corrupt blocks, because data needs to be imputed from block 12 (inclusive) to 15 (exclusive) @@ -313,7 +339,9 @@ test_that("readAxivity handles failed checksums correctly", { # check that data in blocks 12-14 got imputed with the correct values - nrow11 = nrow(AX3_1_12$data) + # The last row of AX3_1_12 is filled up to the start of block 12, so the last + # sample of block 11 is the one before it. + nrow11 = nrow(AX3_1_12$data) - 1 imputedValues = AX3_1_12$data[nrow11,2:4] VectorG = sqrt(sum(imputedValues^2)) imputedValues = imputedValues / VectorG @@ -484,13 +512,13 @@ test_that("readAxivity reads data from AX6 file correctly", { expect_equal(AX6$header$device, "Axivity") expect_equal(nrow(AX6$data), 121) expect_equal(ncol(AX6$data), 10) - expect_equal(AX6$data$time[5], 1577131447.144, tolerance = .001, scale = 1) + expect_equal(AX6$data$time[5], 1577131447.150, tolerance = .001, scale = 1) expect_equal(AX6$data$temp[3], 27.34375, tolerance = 0.0001) expect_equal(floor(sum(abs(AX6$data[,2:4]))), 2007) expect_equal(floor(sum(abs(AX6$data[,5:7]))), 10) - expect_equal(AX6$data[1,2], 0.2670288, tolerance = 0.0000001, scale = 1) - expect_equal(AX6$data[4,3], -0.5026558, tolerance = 0.0000001, scale = 1) - expect_equal(AX6$data[30,5], -0.009452909, tolerance = 0.0000001, scale = 1) + expect_equal(AX6$data[1,2], 0.2899582, tolerance = 0.0000001, scale = 1) + expect_equal(AX6$data[4,3], -0.5122697, tolerance = 0.0000001, scale = 1) + expect_equal(AX6$data[30,5], -0.008252002, tolerance = 0.0000001, scale = 1) expect_true(is.null(AX6$QClog)) @@ -515,20 +543,55 @@ test_that("readAxivity reads timezones correctly", { tzLon = "Europe/London" # desiredtz == configtz tzequal = readAxivity(filename = cwafile, desiredtz = tzLon, configtz = tzLon, start = 1, end = 4) - expect_equal(tzequal$data$time[1], 1551178507.215, tolerance = .001, scale = 1) + expect_equal(tzequal$data$time[1], 1551178507.2205, tolerance = .001, scale = 1) expect_equal(format(as.POSIXlt(tzequal$data$time[1], tz = tzLon, - origin = "1970-01-01")), "2019-02-26 10:55:07.215") + origin = "1970-01-01")), "2019-02-26 10:55:07.220") # desiredtz < configtz tzwest = readAxivity(filename = cwafile, desiredtz = tzLon, configtz = tzAms, start = 1, end = 4) - expect_equal(tzwest$data$time[1], 1551174907.215, tolerance = .001, scale = 1) + expect_equal(tzwest$data$time[1], 1551174907.2205, tolerance = .001, scale = 1) expect_equal(format(as.POSIXlt(tzwest$data$time[1], tz = tzLon, - origin = "1970-01-01")), "2019-02-26 09:55:07.215") + origin = "1970-01-01")), "2019-02-26 09:55:07.220") # desiredtz > configtz tzeast = readAxivity(filename = cwafile, desiredtz = tzAms, configtz = tzLon, start = 1, end = 4) - expect_equal(tzeast$data$time[1], 1551178507.215, tolerance = .001, scale = 1) + expect_equal(tzeast$data$time[1], 1551178507.2205, tolerance = .001, scale = 1) expect_equal(format(as.POSIXlt(tzeast$data$time[1], tz = tzAms, - origin = "1970-01-01")), "2019-02-26 11:55:07.215") + origin = "1970-01-01")), "2019-02-26 11:55:07.220") options(old) }) + +test_that("chunks of consecutive blocks join like a single read", { + for (device in c("ax3", "ax6")) { + cwafile = system.file(paste0("testfiles/", device, "_testfile.cwa"), package = "GGIRread") + header = readAxivity(cwafile, start = 0, end = 0, desiredtz = "Europe/London")$header + for (interpolation in 1:2) { + whole = readAxivity(cwafile, start = 0, end = Inf, header = header, + desiredtz = "Europe/London", + interpolationType = interpolation)$data + # all output samples on the grid + k = (whole$time - as.numeric(header$start)) * header$frequency + expect_lt(max(abs(k - round(k))), 1e-4) + for (cut in c(10, 50, 100)) { + part1 = readAxivity(cwafile, start = 0, end = cut, header = header, + desiredtz = "Europe/London", + interpolationType = interpolation)$data + part2 = readAxivity(cwafile, start = cut, end = Inf, header = header, + desiredtz = "Europe/London", + interpolationType = interpolation)$data + joined = rbind(part1, part2) + rownames(joined) = rownames(whole) = NULL + expect_equal(joined, whole, tolerance = 0) + } + # three chunks + parts = lapply(list(c(0, 20), c(20, 40), c(40, Inf)), function(w) { + readAxivity(cwafile, start = w[1], end = w[2], header = header, + desiredtz = "Europe/London", + interpolationType = interpolation)$data + }) + joined = do.call(rbind, parts) + rownames(joined) = NULL + expect_equal(joined, whole, tolerance = 0) + } + } +}) From 5ab446fa271e851f564d1b69bed8d2b4fce9301d Mon Sep 17 00:00:00 2001 From: jhmigueles Date: Tue, 3 Nov 2026 10:57:42 +0100 Subject: [PATCH 4/4] update NEWS --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index 16db855..b939348 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# Changes in version 1.2.1 (release date:??-11-2026) + +- `readAxivity()`: output samples now lie on one time grid for the whole file, anchored at the start time in the file header, and each call returns the grid points up to the start of block `end`. Reading a file in consecutive chunks (as GGIR does) now gives the same samples as a single read. Previously each call started its own grid at its first block and stopped at the last raw sample of the chunk, so the grid of the next chunk was shifted by a fraction of a sample and one sample could be lost at each border. This changes the output slightly: timestamps can shift by less than one sample period, values are interpolated at those times, and the number of rows per call can change by one. + # Changes in version 1.2.0 (release date:28-10-2026) All changes below concern `readAxivity()`.