---
title: "The Vallim pipeline: native sleep classification"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{The Vallim pipeline: native sleep classification}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

This vignette introduces `run_pipeline_native()`, the **Vallim pipeline** — a
sleep classification layer developed by Julia Ribeiro da Silva Vallim that
addresses limitations of the base CSPD pipeline in detecting and classifying
sleep episodes from actigraphy data.

---

## Background

The Condor CSPD pipeline (`run_pipeline()`) detects sleep periods using an
adaptive median filter and returns one row per period. It works well for
clear-cut recordings but can misclassify episodes in several edge cases:

- Daytime naps adjacent to a long main sleep (date collision)
- Recordings ending mid-sleep (truncated episodes)
- Sleep periods fragmented across multiple short blocks
- Unusually long in-bed periods (TBT > 14 h) that should be split

The Vallim pipeline keeps the same off-wrist detection and CSPD epoch scorer
as `run_pipeline()` but replaces the final classification step with a 7-rule
adaptive system ported from Julia Vallim's Python notebook
(`vs_condor_py_pipeline_fix29_jrsv.ipynb`).

---

## The rule set

The classification follows this fixed execution order (Fix 27):

| Step | Fix/Rule | Description |
|------|----------|-------------|
| 1 | Fix 25 | Exclude truncated episodes at the recording end |
| 2 | Fix 26a | Infer adaptive nocturnal window from wrist temperature and light |
| 3 | Fix 29 / Rule 2 | Split TBT 14–16 h episodes; exclude TBT > 16 h |
| 4 | Fix 26c | Recover fragmented sleep nights missed by the scorer |
| 5 | Rules 3–5 | Classify as main or secondary using the nocturnal window |
| 6 | Fix 26b | Resolve sleep-date collisions from the noon threshold |
| 7 | Rule 6 | Keep the longest main episode per sleep date |
| 8 | Rule 7 | Exclude all episodes on days with no main sleep |

The nocturnal window (Fix 26a) is inferred adaptively per participant rather
than fixed at 18:00–06:00. Episodes where wrist temperature ≥ 28 °C and
ambient light ≤ 5 lux are used as candidates; the circular mean of their
onsets becomes the window anchor (±4 h).

---

## Basic usage

```{r run}
FILE <- system.file("extdata", "input1.txt", package = "zeitR")
TZ   <- "America/Sao_Paulo"

result <- run_pipeline_native(FILE, tz = TZ, quiet = TRUE)
result
```

The `nights` table has the same schema as `run_pipeline()` but adds a
`sleep_type` column:

```{r nights}
result$nights |>
  mutate(
    bed_time    = format(bed_time,    "%Y-%m-%d %H:%M"),
    get_up_time = format(get_up_time, "%Y-%m-%d %H:%M"),
    tbt_h       = round(tbt / 60, 1),
    tst_h       = round(tst / 60, 1),
    eff_pct     = round(eff * 100, 1)
  ) |>
  select(night, sleep_type, bed_time, get_up_time, tbt_h, tst_h, waso, sol, eff_pct) |>
  head(10) |>
  knitr::kable(
    col.names = c("Night", "Type", "Bed time", "Get-up time",
                  "TBT (h)", "TST (h)", "WASO (min)", "SOL (min)", "Eff (%)"),
    align = "clllrrrrr"
  )
```

---

## Comparing the two pipelines

Running both pipelines on the same recording lets you see exactly what the
Vallim rule set changes:

```{r compare}
cspd   <- run_pipeline(FILE, tz = TZ, quiet = TRUE)
native <- run_pipeline_native(FILE, tz = TZ, quiet = TRUE)

cat("CSPD nights:  ", nrow(cspd$nights),   "\n")
cat("Vallim nights:", nrow(native$nights), "\n")
cat("  main:      ", sum(native$nights$sleep_type == "main"),      "\n")
cat("  secondary: ", sum(native$nights$sleep_type == "secondary"), "\n")
```

The Vallim pipeline consolidates duplicate-date episodes (Rule 6) and
discards dates where no valid main sleep exists (Rule 7), resulting in a
cleaner set of main nights.

---

## Sleep onset and offset statistics

`circ_mean_h()` and `circ_sd_h()` compute circular statistics for clock-time
variables. Using standard arithmetic mean for times like sleep onset (which
often straddles midnight) inflates variability estimates and produces biased
means; the circular mean handles the wrap correctly.

```{r circ}
main <- native$nights |> filter(sleep_type == "main")

# Decimal hours of bed time and get-up time
onset_h  <- as.numeric(format(main$bed_time,    "%H", tz = TZ)) +
            as.numeric(format(main$bed_time,    "%M", tz = TZ)) / 60
offset_h <- as.numeric(format(main$get_up_time, "%H", tz = TZ)) +
            as.numeric(format(main$get_up_time, "%M", tz = TZ)) / 60

cat(sprintf("Mean sleep onset:  %05.2f h  (SD = %.2f h)\n",
            circ_mean_h(onset_h),  circ_sd_h(onset_h)))
cat(sprintf("Mean sleep offset: %05.2f h  (SD = %.2f h)\n",
            circ_mean_h(offset_h), circ_sd_h(offset_h)))
```

The `circ_mean_h()` function returns decimal hours in [0, 24). To display as
HH:MM:

```{r fmt}
fmt_h <- function(h) sprintf("%02d:%02d", as.integer(h) %% 24L,
                              as.integer((h %% 1) * 60))

cat("Mean sleep onset: ", fmt_h(circ_mean_h(onset_h)),  "\n")
cat("Mean get-up time: ", fmt_h(circ_mean_h(offset_h)), "\n")
```

---

## Advanced: running each step directly

For custom workflows, `extract_sleep_episodes()` and
`classify_sleep_episodes()` expose the two stages independently.

```{r advanced, eval = FALSE}
# Step 1: read, prepare, detect off-wrist and CSPD-score epochs
rec  <- read_acttrust(FILE, tz = TZ)
prep <- prepare_actigraphy(rec)
prep <- detect_offwrist_bimodal(prep)
prep <- detect_sleep_crespo(prep)

# Step 2: extract per-episode statistics from the CSPD state
episodes_raw <- extract_sleep_episodes(prep)

# Step 3: apply the Vallim rule set
episodes <- classify_sleep_episodes(
  episodes = episodes_raw,
  data     = prep,
  verbose  = TRUE           # print each fix as it fires
)

episodes |> filter(sleep_type == "main")
```

The `verbose = TRUE` argument prints a trace of each rule:

```
[Fix 26a] anchor=23.8 h -> nocturnal window 19.8 h-3.8 h
[Fix 26c] No fragmented episodes recovered.
[Fix 26b] No sleep-date collisions detected.
```

---

## Batch processing

`run_pipeline_native_batch()` works identically to `run_pipeline_batch()`:

```{r batch, eval = FALSE}
results <- run_pipeline_native_batch(
  "recordings/",
  tz    = "America/Sao_Paulo",
  quiet = TRUE
)

# Combine all main nights
main_all <- bind_rows(
  lapply(names(results), function(id) {
    results[[id]]$nights |>
      filter(sleep_type == "main") |>
      mutate(subject_id = id)
  })
)
```

---

## Next steps

- `vignette("sleep-analysis")` — CSPD pipeline walkthrough with actogram
- `?export_hypnogram` — export results to `hypnoR` for sleep architecture metrics
- `?compute_sleep_metrics`, `?compute_cpd_metrics` — day-type sleep summary and chronotype
- `?classify_sleep_episodes` — full parameter reference for the rule set
- `?circ_mean_h`, `?circ_sd_h` — circular statistics for clock-time variables
- `?acttrust_params` — device parameter defaults
