---
title: "From raw accelerometry to PIM/TAT/ZCM activity counts"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{From raw accelerometry to PIM/TAT/ZCM activity counts}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse  = TRUE,
  comment   = "#>",
  fig.width = 9,
  fig.height = 4.5,
  dpi       = 150,
  out.width = "100%"
)
library(zeitR)

has_mrpheus <- requireNamespace("mrpheus", quietly = TRUE)
has_ggplot2 <- requireNamespace("ggplot2", quietly = TRUE)

if (!has_mrpheus) {
  knitr::opts_chunk$set(eval = FALSE)
  message(
    "mrpheus is required to run compute_activity_counts() in this vignette. ",
    "Install from r-universe: install.packages(\"mrpheus\", repos = c(",
    "\"https://circadia-bio.r-universe.dev\", \"https://cloud.r-project.org\"))"
  )
} else {
  library(mrpheus)
}
if (has_ggplot2) library(ggplot2)
```

`read_acttrust()` reads the PIM/TAT/ZCM counts an ActTrust device already
computed onboard. But some devices and pipelines only ever hand you raw
triaxial acceleration -- a GT3X+ raw export, a research-grade IMU, a signal
you've captured yourself. `compute_activity_counts()` bridges that gap:
raw `x`/`y`/`z` samples in, epoch-level PIM/TAT/ZCM counts out, ready for
[`estimate_ee()`](#from-counts-to-pa-intensity)/[`classify_pa_counts()`](#from-counts-to-pa-intensity)
the same way `read_acttrust()`'s output already is.

**Read `?compute_activity_counts` before using this on real data.** Unlike
most of zeitR, this isn't a validated port of a reference implementation --
there is nothing to validate it against (Condor's and ActiGraph's onboard
firmware is proprietary). The help page is explicit about what's borrowed
from a validated source (the band-pass filter, via mrpheus) and what isn't
(the PIM/TAT/ZCM aggregation logic itself). **Prefer device-computed counts
whenever they're available** -- reach for this function only when raw
samples are genuinely all you have.

---

## The processing chain

1. Each axis has its DC offset removed ([`mrpheus::remove_dc()`](https://circadia-bio.r-universe.dev/mrpheus)) --
   important for whichever axis is carrying gravity, typically ~1 g -- then
   is band-pass filtered zero-phase ([`mrpheus::bandpass_filter()`](https://circadia-bio.r-universe.dev/mrpheus)).
   Both are the same filter primitives already validated as part of
   mrpheus's YASA-parity PSG pipeline, reused here rather than
   reimplemented.
2. A vector norm is computed per sample from the *filtered* axes:
   `sqrt(x^2 + y^2 + z^2)` -- filter first, then combine, matching the order
   Batista et al. (2026) describe for ActTrust.
3. Samples are grouped into non-overlapping epochs
   (`epoch_sec * sampling_rate` samples each).
4. Per epoch: **PIM** (sum of the absolute filtered norm), **TAT** (seconds
   above `tat_threshold`), and **ZCM** (sign changes beyond a
   `zcm_threshold` dead-band, summed across the three axes).

---

## A synthetic recording

There's no bundled raw-accelerometer dataset in zeitR (`inst/extdata`'s
ActTrust file is already epoch-level, same as everything `read_acttrust()`
produces) -- so this vignette simulates one: 2 minutes of near-still noise,
followed by 2 minutes of clear 1 Hz movement, at a GT3X+-like 25 Hz.

```{r simulate}
sr        <- 25   # Hz
epoch_sec <- 60
n         <- sr * epoch_sec * 4  # 4 epochs total
t         <- seq(0, (n - 1) / sr, by = 1 / sr)

set.seed(42)
quiet  <- rnorm(sr * epoch_sec * 2, sd = 0.001)
active <- sin(2 * pi * 1 * t[(sr * epoch_sec * 2 + 1):n]) * 0.5

x <- c(quiet, active)               # all simulated movement on the x-axis
y <- rep(0, n)
z <- rnorm(n, sd = 0.01) + 1         # ~1 g gravity offset, small sensor noise
```

```{r compute}
counts <- compute_activity_counts(x, y, z, sampling_rate = sr, epoch_sec = epoch_sec)
counts
```

The first two (quiet) epochs stay near zero on every metric; the last two
(active) epochs show clear PIM, TAT, and ZCM elevation -- the sinusoid
crosses zero roughly twice per second, so ~120 crossings/epoch on the
active x-axis is the expected order of magnitude.

```{r plot, eval = has_ggplot2 && has_mrpheus}
ggplot(counts, aes(x = factor(epoch), y = PIM)) +
  geom_col(fill = "#C25E2A") +
  labs(x = "Epoch", y = "PIM", title = "PIM per epoch: quiet -> active") +
  theme_minimal(base_size = 12)
```

---

## GT3X+-style filtering instead of ActTrust-style

`filter_low`/`filter_high` default to `0.5`/`2.7` Hz (ActTrust-style, per
Batista et al. 2026). Swap in `0.25`/`2.5` Hz for GT3X+-style processing --
there's no separate code path, just different arguments:

```{r gt3x}
counts_gt3x <- compute_activity_counts(
  x, y, z,
  sampling_rate = sr, epoch_sec = epoch_sec,
  filter_low = 0.25, filter_high = 2.5
)
counts_gt3x
```

---

## From counts to PA intensity {#from-counts-to-pa-intensity}

The whole point of matching `read_acttrust()`'s output shape is that
everything downstream stays the same. Feed the `PIM` column straight into
[`estimate_ee()`](../reference/estimate_ee.html)/[`classify_pa_counts()`](../reference/classify_pa_counts.html)
exactly as you would with device-computed counts (see
`vignette("physical-activity")` for the full discussion and caveats of that
step):

```{r pa-intensity}
counts$mets         <- estimate_ee(counts$PIM, device = "ACTT", placement = "wrist")
counts$pa_intensity <- classify_pa_intensity(counts$mets)
counts
```

---

## Tuning `zcm_threshold` / `tat_threshold`

Both default to small, physically motivated values (`0.01`, `0.05`) rather
than a calibrated constant -- there is no published reference value to
default to. If your sensor's noise floor sits close to either threshold,
you'll see it directly: a noisy "quiet" epoch registering non-zero ZCM/TAT
is a sign the threshold needs raising for your specific hardware, not that
the function is broken.

```{r noise-floor}
noisy_quiet <- rnorm(sr * epoch_sec, sd = 0.02)  # noisier than the earlier "quiet" segment
compute_activity_counts(
  noisy_quiet, rep(0, length(noisy_quiet)), rep(0, length(noisy_quiet)),
  sampling_rate = sr, epoch_sec = epoch_sec
)
```

---

## Next steps

- `?compute_activity_counts` -- full argument reference and the
  what-this-is-and-isn't discussion
- `?mrpheus::bandpass_filter`, `?mrpheus::remove_dc` -- the underlying
  filter primitives
- `vignette("physical-activity")` -- what happens after you have PIM
  counts, and the generalisability caveats of the equations that consume
  them
