zeitR (development version)
✨ New features
-
New LIDS (Locomotor Inactivity During Sleep) module –
lids_transform(),fit_lids(),detect_lids_bouts(),compute_lids(), andstudy_lids_metrics()– porting the ultradian-rhythm methodology of Winnebeck et al. (2018, Current Biology) and its infant extension in Hammad et al. (2026, SLEEP, https://zenodo.org/records/18199381).lids_transform()applies the100/(1+x)non-linear transform plus Gaussian (Hammad 2026, default) or moving-average (Winnebeck 2018 /pyActigraphy) smoothing;fit_lids()scans candidate periods (30-180 min by default) with an OLS sloped-cosine fit, selecting the period with the highest Munich Rhythmicity Index.compute_lids()is the main entry point: it extracts sleep bouts either from an existing zeitR pipeline’sstatecolumn (bout_source = "state") or via the new standalonedetect_lids_bouts()Roenneberg relative-immobility detector (bout_source = "roenneberg", for raw activity that hasn’t been run throughrun_pipeline()/run_pipeline_native()), fits each bout, and applies the Winnebeck/Hammad quality filter (pearson_r,p_value, offset bounds).study_lids_metrics()is the batch/syncR::sync()-ready counterpart, summarising each participant’s quality-filtered bouts (median +/- IQR period, amplitude, offset, slope) into one row, alongsidestudy_sleep_metrics()andstudy_summary().Ported from a prototype R notebook draft by Mario Leocadio-Miguel (itself adapted from an older MATLAB script); one bug in that draft is fixed here – its bout-fusing step was truncated mid-statement (
fused.a...) and would not have run as written; see?detect_lids_boutsfor the full reimplementation (.fuse_bouts()). Not yet validated againstpyActigraphy’sLIDSclass or an external reference dataset – treat results accordingly until a parity check is run.
zeitR 0.1.6 (2026-07)
✨ New features
- New
read_axivity()– bridges axR’saxivity_read_cwa()raw per-sample output (AX3/AX6.cwafiles) into the same 9-column epoch-level shaperead_acttrust()produces, converting raw triaxial acceleration to PIM/TAT/ZCM viacompute_activity_counts()(GT3X+ filter preset,0.25-2.5Hz, since no validated Axivity-specific preset exists). Wired intoread_actigraphy(device = "axivity").axRadded toSuggestsnow that it’s published on r-universe. Treatactivity/ZCMnfrom this path as an unvalidated approximation – see?read_axivityfor the full caveats (sampling-rate detection, time-zone re-labelling instead of conversion, and what specifically hasn’t been checked against a reference like GGIR). - New
compute_sri()— Sleep Regularity Index (Phillips et al. 2017), ported from Fix 30 of the Python reference pipeline (SRI_vallim). Derives sleep/wake directly from the epoch-levelstatecolumn zeitR’s own pipelines already produce, rather than from a pyActigraphy scoring algorithm – Julia’s concordance analysis against manual reference scoring showed this is substantially more accurate than Sadeh/Cole-Kripke/ Roenneberg/Scripps (ICC 0.82 vs 0.19-0.67, N=404). Off-wrist gaps ≤30 min are interpolated; longer gaps are excluded from the day-to-day comparison rather than counted as a mismatch. -
compute_npcra()gains atrim_to_d1argument (defaultTRUE): the recording is now trimmed to start at 00:00 of D+1 before computing IS, IV, RA, L5, and M10, matching the Python reference pipeline’s convention (which never spans the raw, typically fractional, recording length – the previous default here reported e.g.n_days = 7.5where Python reports a clean7). Settrim_to_d1 = FALSEfor the previous behaviour. Does not replicate Python’s separate 30-min-threshold rule for the M10/L5 windows specifically – only the D+1 window start.
🐛 Bug fixes
-
Fix 25 / Fix 26c interaction:
.recover_fragmented_episodes()(classify_sleep_episodes()’s Fix 26c step) had no knowledge of the boundary Fix 25 uses to exclude episodes truncated by the end of the recording. When Fix 25 correctly excluded such an episode, its date became “uncovered”, and the Fix 26c recovery scan would reconstruct the same episode from the same raw epochs – silently undoing Fix 25 and producing a biologically implausible extra main night (e.g. 8 main nights on a 7-day recording). Root-caused while investigating Julia’s report of this exact symptom (matching the Python pipeline’s now-fixed Fix 29f). Fixed by giving.recover_fragmented_episodes()the same last-day-noon boundary and skipping recovery for any candidate that would itself start at/after noon on the recording’s last calendar day.
🧪 Tests
- New
test-read-axivity.R:read_axivity()’s bridging logic (column shape, per-epoch light/int_temp averaging, epoch-start datetimes, tz re-labelling vs shifting, metadata assembly, dominant-sample-rate detection with an outlier warning, and an exact match against a directcompute_activity_counts()call on the same input) –axivity_read_cwa()itself is mocked throughout viatestthat::local_mocked_bindings(), so these don’t need a real.cwabinary fixture. Skipped viaskip_if_not_installed()foraxR/mrpheus/withrwhere needed.withradded toSuggests. -
test-fix26c.R: regression test reproducing the Fix 25 / Fix 26c interaction above – a short evening sleep-like run right at the file’s end (mirroring the notebook’s ID_0138 case) is no longer recovered. -
test-npcra.R:trim_to_d1default behaviour (exact day removed,n_daysunaffected numerically on the repeating fixture), and the <2-epoch fallback-with-warning path. - New
test-sri.R:compute_sri()on perfectly regular (SRI = 100) and perfectly inverted (SRI = -100) synthetic patterns, off-wrist gap interpolation vs exclusion at the 30-min boundary,zeitr_result/bare data frame input, the <24h fallback-with-warning path, and.interpolate_short_gaps()directly (ffill, start-of-vector bfill, gap spanning the whole vector).
zeitR 0.1.5 (2026-07)
✨ New features
New
pa_equations(),estimate_ee(),classify_pa_intensity(), andclassify_pa_counts()– physical activity intensity classification from ActTrust(R)/GT3X+ activity counts, porting the published cut-points and MET-estimation equations from Batista et al. (2026, PLoS ONE https://doi.org/10.1371/journal.pone.0348631). Extends zeitR beyond sleep staging into the other half of the 24h rest-activity cycle: light, moderate, vigorous, and very vigorous PA bands from hip- or wrist-worn ActTrust(R) or GT3X+ counts. Only the published coefficient/cut-point table is ported (not the original study’s calorimetry-fittedlm()/msmpipeline, which needs data no zeitR user will have) – see?pa_equationsfor the equation-set design and important generalisability caveats: single lab-treadmill validation study (N=56, healthy adults 18-35), GT3X+ (hip) cut-points differ 2-65% from prior published GT3X+ studies which themselves differ from each other by 16-39%, and that spread reflects differing modelling approaches (two-regression, ANN, and this paper’s linear model) as well as sample – diagnosable against Sasaki et al.’s transparent two-regression model, not diagnosable against Santos-Lozano et al.’s ANN, which has no inspectable coefficients to compare against.New
compute_activity_counts()– converts raw triaxial acceleration (x/y/z+sampling_rate) into epoch-level PIM/TAT/ZCM activity counts, for devices or pipelines that only provide raw samples rather than onboard-computed counts (e.g.read_acttrust()’sactivity/ZCMncolumns). Filtering reusesmrpheus::remove_dc()/mrpheus::bandpass_filter()– the same zero-phase Butterworth implementation already validated as part of mrpheus’s YASA-parity PSG pipeline – rather than a new, unvalidated filter written from scratch. This makesmrpheusa cross-packageSuggestsdependency for this function specifically (precedented by hypnoR, which alreadySuggestsbothmrpheusandzeitR); the rest of zeitR stays independent of it. The epoch-level PIM/TAT/ZCM aggregation logic itself has no reference implementation to check against – no raw-to-counts converter exists incondor_pipeline/circadiaBase_Docker(only already-epoched data), and Condor’s/ActiGraph’s exact onboard thresholds are proprietary – see?compute_activity_countsfor what’s built from the general processing description in Batista et al. (2026, PLoS ONE) vs. what’s an open/tunable parameter (zcm_threshold,tat_threshold). Default band-pass cutoffs are ActTrust-style (0.5-2.7Hz); passfilter_low/filter_highfor GT3X+-style (0.25-2.5Hz) processing instead.
🚀 CI
- Fixed mrpheus dependency resolution in CI: added
Additional_repositories: https://circadia-bio.r-universe.devand a repo-root.Rprofilesettingoptions(repos = ...)directly, matching hypnoR’s existing setup for the same mrpheus/zeitR pairing.Additional_repositoriesalone doesn’t automatically wire intopak’s dependency resolution – it’s mainly a documentation/NOTE-suppression field – so the.Rprofileis what actually makesmrpheusresolvable forcompute_activity_counts().
🐛 Bug fixes
-
.zero_crossing_indicator()(internal,compute_activity_counts()) calledrep(FALSE, n - 1)withn = 0for a zero-length input, andrep()rejects a negativetimesargument. Surfaced while writing the degenerate-length test case, not by any real recording. Fixed with an earlyif (n < 2L) return(logical(0))guard.
📚 Documentation
- New
vignette("physical-activity")– walks throughpa_equations(),estimate_ee(),classify_pa_intensity(), andclassify_pa_counts()using the bundled ActTrust recording’s real PIM counts. Flags the extrapolation from treadmill-fitted equations to free-living data inline rather than burying it in a caveats section, and points to?pa_equationsfor the full generalisability discussion instead of duplicating it. - New
vignette("raw-accelerometry")–compute_activity_counts()on a simulated raw triaxial recording (quiet segment, then clear 1 Hz movement), covering ActTrust- vs. GT3X+-style filter cutoffs, tuningzcm_threshold/tat_thresholdagainst sensor noise floor, and closing the full loop intoestimate_ee()/classify_pa_intensity()so the raw signal and PA-intensity vignettes read as one continuous pipeline rather than two unrelated features. - README: added the
r-universebadge and recommendedinstall.packages(..., repos = c("https://circadia-bio.r-universe.dev", ...))install path (GitHubpakinstall kept as the dev-version fallback, matching hypnoR); filled in ten previously undocumentedFeaturesbullets (read_acttrust(),prepare_actigraphy(), the four PA-intensity functions, the four actogram-plotting functions) that were already shipped and in the pkgdown reference index but never listed; added the PA-intensity MET-band table to Computed Variables; brought the Project Structure tree and Dependencies table (now with an Imports/SuggestsTypecolumn) up to date with everything actually inDESCRIPTION.
🧪 Tests
-
test-raw-accelerometry.R:.zero_crossing_indicator()/.epoch_rowsum()directly, pluscompute_activity_counts()input validation, the trailing-incomplete-epoch warning, a flat zero-motion signal (all metrics exactly zero), a clean 1 Hz sinusoid (ZCM ~ 2 x frequency xepoch_secon interior epochs), PIM/TAT increasing with amplitude, andmetricssubsetting. Skipped viaskip_if_not_installed("mrpheus")where that dependency is needed.
zeitR 0.1.4 (2026-07)
✨ New features
- New
study_sleep_metrics()– batch wrapper computingcompute_sleep_metrics()andcompute_cpd_metrics()across every participant in arun_pipeline_batch()/run_pipeline_native_batch()result, stacked into one tibble with aparticipant_idcolumn – the sleep-timing/chronotype (CPD, MSF/MSW, social jetlag) counterpart tostudy_summary()(NPCRA/activity-rhythm variables). Closes a gap found while checkingsyncR::sync()compatibility:compute_sleep_metrics()andcompute_cpd_metrics()each return a single named list per participant with no participant identifier and no batch equivalent, unlikestudy_summary(), so there was previously no way to get these metrics into the one-row-per-participant shapesync()expects without writing manual glue code per study.
📊 Visualisation
-
actogram_colours(): swapped the default"wake"and"off-wrist"colours (wake is now the warm terracotta#C25E2A; off-wrist is now the neutral sand#D9C8A0). Affects the default palette used byplot_actogram(),plot_actogram_double(), andplot_actogram_activity()whenevercoloursis not supplied explicitly. -
plot_actogram_activity()gains alog_scaleargument. WhenTRUE, applies alog1p()transform to the activity signal before capping and normalising bar heights, compressing the dynamic range so structure among low-to-moderate activity epochs is easier to see against a right-skewed raw signal (occasional high bursts no longer dominate the visible range). DefaultFALSEpreserves the existing linear-scale behaviour exactly.
🚀 Performance
- Removed
.adaptive_median_filter(), a dead pure-R fallback insleep_periods.Rthat was superseded byadaptive_median_filter_cpp()and never called. No behaviour change. - Removed four further dead internal helpers from
utils.R–zero_sequences(),quantile_filter(),max_filter(),min_filter()– confirmed unreferenced anywhere inR/,dev/, or the test suite; all superseded by direct calls to the corresponding Rcpp functions. No behaviour change. (rolling_apply()was initially removed too, but is kept – it’s the R reference implementation used by therolling_max_cpp()/rolling_min_cpp()parity tests intest-crespo-cpp-parity.R.) -
run_pipeline_batch()andrun_pipeline_native_batch()gain aparallelargument. WhenTRUE, files are processed concurrently viafuture.apply::future_lapply()under whateverfuture::plan()the caller has set (e.g.future::plan(future::multisession(workers = 4))). Falls back to sequential processing with a warning iffuture.applyis not installed. Default remainsFALSE(sequential), so existing code is unaffected.futureandfuture.applyadded toSuggests.
🐛 Bug fixes
-
zeitr_abort(),zeitr_warn(), andzeitr_inform()(internal message wrappers aroundcli::cli_abort()/cli_warn()/cli_inform()) did not forward.envir, so glue-style interpolation of a variable local to the calling function (e.g.{.val {missing_cols}}) silently failed with “object not found” instead of producing the intended message. Fixed by defaulting.envir = parent.frame()in all three wrappers; no call sites needed to change. - Five call sites (three in
plot_actogram.R, two inexport.R) passed their error/warning message as multiple separate comma-delimited string arguments instead of one string. R does not auto-concatenate adjacent string literals, so the extra arguments were passed through tocli::cli_abort()/cli_warn()as unnamed condition data, whichrlangrejects (“Conditions must have named data fields”). Fixed by merging each into a single message string. None of the five affected error paths had previously been exercised by a test. -
plot_actogram()andplot_actogram_double(): the epoch atmins_since_midnight = 0(midnight, i.e. the first epoch of every calendar day) had its left half clipped bygeom_tile()’s centred tile extending outside a hardscale_x_continuous()limit of exactly0, producing a silently dropped/incomplete tile at the start of every row (and aggplot2“missing values” warning once actually rendered under test). Fixed by widening the x-axis limits by half an epoch on each side in bothplot_actogram()and the shared.scale_x_double()helper (also used byplot_actogram_activity(), which was not affected by the clipping itself since it usesgeom_rect()with explicit epoch boundaries rather than a centred tile). -
export_hypnogram():is.list(result)doesn’t exclude a bare tibble (tibbles are lists too), soresult$subject_idon a bare tibble without that column triggered a spurious “Unknown or uninitialised column” warning. Fixed by adding the missing!is.data.frame(result)guard, matching the pattern already used two lines above it in the same function. -
export_hypnogram(): whenZCMnis absent (a documented, valid use case),zcmisNULLandzcm == 0evaluates tological(0), whichdplyr::case_when()cannot recycle against the other length-n conditions – a hard error, not just a warning. Fixed by precomputing a proper full-lengthzcm_is_zerovector before thecase_when()call. -
compute_npcra(): the sameis.null(x$col)-on-a-tibble pattern for the optionalstatecolumn triggered a spurious “Unknown or uninitialised column” warning wheneverstatewas absent (also a documented, valid use case). Fixed with"state" %in% names(epochs). -
plot_actogram.R’s internal.actogram_title()had the sameis.list()/bare-tibble gap asexport_hypnogram(), not yet triggered by any existing test but the same latent risk. Fixed proactively for consistency with the pattern used elsewhere.
🧪 Tests
-
test-batch-helper.R:.run_pipeline_over_files()– sequential success, partial-failure skip-with-warning, all-failing batch returns empty list, parallel dispatch viafuture_lapply()(skipped iffuture.applyis not installed), sequential fallback whenfuture.applyis unavailable (skipped if it is installed), and a regression guard confirming both exported batch wrappers still default toparallel = FALSE. -
test-actogram-snapshots.R: visual regression snapshots (vdiffr, skipped if not installed) forplot_actogram(),plot_actogram_double(),plot_actogram_activity(), andplot_actogram_activity()with a customactivity_cap_quantileand withlog_scale = TRUE, using a deterministic synthetic 2-day fixture. Also covers (independently ofvdiffr) missing-column errors for all three functions, the missing-activity_colerror, acceptance of azeitr_resultlist as well as a bare tibble, and thatlog_scale = FALSEis byte-identical to the pre-log_scalebehaviour.vdiffradded toSuggests. - New test files bringing five previously 0%-covered files up to full or near-full coverage:
test-circ-utils.R,test-export-hypnogram.R,test-npcra.R,test-study-summary.R,test-read-actigraphy.R. Alsotest-utils.R, covering edge-case branches innorm_01(),zero_prop(),ashman_d(), and%||%not guaranteed to be hit by ordinary pipeline data. Overall coverage moved from 81.1% to 87.9%.covradded toSuggests. -
test-study-sleep-metrics.R: synthetic multi-participant coverage forstudy_sleep_metrics()– both metric sets present with correctn_overall/n_wd/n_fdcounts, holiday forwarding shifting a night between the workday/free-day groups, per-participantholidays/free_daysfallback vs a study-level override, a participant whosecompute_cpd_metrics()call fails (no free days) whilecompute_sleep_metrics()still succeeds for the same participant (only the failing metric set isNA-filled), skipping non-zeitr_resultentries, the empty/all-invalid-batch paths, and thesubject_id-missing fallback to the list name.
zeitR 0.1.3 (2026-07)
📊 Visualisation
-
plot_actogram()– single-column raster actogram. One row per calendar day, time-of-day on the x-axis, filled by sleep/wake state. Oldest day at the top, following standard chronobiology convention. Equivalent to the ad-hoc ggplot2 code in the single-recording vignette but packaged as a reusable function with consistent defaults. -
plot_actogram_double()– classic double-plotted actogram. Each recording day appears twice: in the left column of its own row (x = 00:00 to 24:00) and in the right column of the row above (x = 24:00 to 48:00). Circadian phase drift is visible as a diagonal band across consecutive rows. A dashed vertical line marks the 24 h column boundary. -
plot_actogram_activity()– double-plotted actogram with activity bars. Same row structure asplot_actogram_double()but each epoch is drawn as a vertical bar whose height is proportional to the raw ZCMn activity count. Bars are coloured by sleep/wake state so activity intensity and state classification are read simultaneously. A 99th-percentile cap on bar heights prevents outlier bursts from compressing the rest of the range; a thin baseline stub keeps zero-activity epochs (sleep, off-wrist) faintly visible. -
actogram_colours()– exported helper returning the named hex colour vector used as the default palette across all three actogram functions. Pass the result to anycoloursargument to inspect or partially override defaults. - All three functions accept a
zeitr_resultlist or a bare tibble withdatetimeandstatecolumns.ggplot2remains inSuggests; a clear error is thrown if it is not installed.
🚀 Performance
-
rolling_median_prepadded_cpp()added tosrc/rolling_filters.cpp. Replaces theRcppRoll/zoo/vapplyfallback chain in.estimate_sleep_padded()with a single direct Rcpp call. Off-wrist sleep estimation is now unconditionally fast (O(n * win) in C++) regardless of which optional packages are installed.zooremoved from Imports;RcppRollremoved from Suggests. -
check_consistency()vectorised. Two O(n) Rforloops replaced withwhich()calls. No behaviour change.
📅 Free-day classification
- New
free_daysparameter onrun_pipeline_native(),compute_sleep_metrics(), andcompute_cpd_metrics(). Replaces the hardcoded Saturday + Sunday with any combination of days (English names or ISO integers 1–7). Default isc("Saturday", "Sunday"). Enables non-standard schedules such as Friday–Saturday weekends or compressed work weeks. -
holidaysnow accepts"DD-MM"strings for fixed-date annual holidays (e.g."25-12"for Christmas) in addition toDateobjects and"YYYY-MM-DD"strings. All three forms can be mixed in the same vector. -
compute_sleep_metrics()andcompute_cpd_metrics()are now S3 generics. Passing azeitr_resultdirectly auto-forwardsresult$holidaysandresult$free_days— no need to repeat them manually. - A warning is emitted when
holidays = NULL; suppress withoptions(zeitR.no_holidays_warn = FALSE).
🐛 Bug fixes
- Free-day detection was broken on non-English locales (
weekdays()returns"sabado"onpt_BR). Fixed by using the locale-independent ISO 8601 weekday number. - MSF and MSW now use the circular mean, matching the fix29 notebook. Plain mean gives wrong results when mid-sleep wraps midnight.
-
compute_cpd_metrics()now drops episodes starting after noon on the last recording day (truncated by end of file), matching fix29’s filter.
🧪 Tests
- Free-day classification tests (
test-free-days.R):.parse_free_days()input validation (English names, ISO integers, case insensitivity, range errors),.is_free_day()locale-independent weekday detection (vectorised over a full week, default and custom schedules), all three holiday input forms (Date,"YYYY-MM-DD","DD-MM") including mixed-form vectors, year-specificity of"YYYY-MM-DD"vs recurrence of"DD-MM", thezeitR.no_holidays_warnoption, andzeitr_resultS3 dispatch forwardingfree_daysandholidaysto bothcompute_sleep_metrics()andcompute_cpd_metrics().
📚 Documentation
- New vignette
vignette("actogram")– covers all three plot functions andactogram_colours(): single-column vs double-plotted vs activity-bar formats, colour customisation,date_label_every, extending the returnedggplotobject with additional layers, and working with bare tibbles instead of azeitr_result.
zeitR 0.1.2 (2026-07)
🌙 Vallim native pipeline
-
run_pipeline_native()— full single-recording pipeline using the Vallim (JRSV) rule set developed by Julia Ribeiro da Silva Vallim. Replaces Condor’snights_dfclassification with a 7-step adaptive rule set: Fix 25 (truncated episode exclusion), Fix 26a (adaptive nocturnal window inferred from wrist temperature and ambient light), Fix 26b (sleep-date collision resolution), Fix 26c (fragmented episode recovery), Fix 29 (14–16 h episode splitting), Rules 3–5 (main/secondary classification), Rule 6 (longest-main-per-date selection), Rule 7 (days-without-main exclusion). Validated against the Python reference pipeline: all 52 main nights on the ActTrust validation recording classified identically. -
run_pipeline_native_batch()— directory-level wrapper forrun_pipeline_native(). -
extract_sleep_episodes()— convert a CSPD-scored epoch table into a per-episode tibble with WASO, SOL, SOI, TST, NW, EFF derived via Cole-Kripke epoch scoring. -
classify_sleep_episodes()— apply the full JRSV rule set to a raw episode table and returnsleep_type("main"/"secondary"). -
circ_mean_h()— unit-circle circular mean for clock-time variables (Fix 20; handles midnight wrap correctly). -
circ_sd_h()— circular SD using mean resultant length formula (Fix 24; invariant to the wrap point). - Julia Ribeiro da Silva Vallim (ORCID 0000-0001-8708-8479) added as author in DESCRIPTION,
_pkgdown.yml, and pipeline documentation.
🚀 Performance (Rcpp)
- Five rolling filters replaced with Rcpp implementations — 215× speedup on a 40,000-epoch recording (12 GB → 312 KB memory):
rolling_median_cpp,rolling_mean_cpp,rolling_var_cpp,rolling_zero_prop_cpp,rolling_quantile_cpp. -
diff5()five-point stencil derivative ported todiff5_cpp— replaces the interior Rforloop; used in off-wrist temperature derivative. -
score_epochs_cole_kripke()ported toscore_epochs_cole_kripke_cpp— single-pass O(n) convolution replaces 17 vectorised R additions. -
Rcpp (>= 1.0.0)added toImportsandLinkingTo.
🔧 Other changes
-
mclustmoved fromSuggeststoImports; the GMM fallback warning is no longer emitted for standard ActTrust recordings. -
_pkgdown.ymlupdated with all new exports and Julia Vallim authorship.
🧪 Tests
- Rcpp rolling filter parity tests (
test-rolling-filters-parity.R): all five filters,diff5_cpp, andscore_epochs_cole_kripke_cppvalidated against R reference implementations. - Vallim pipeline classification parity tests (
test-vallim-parity.R): episode count, sleep-date coverage, andsleep_typeclassification locked againstinst/extdata/vallim_nights.csv(generated bydev/parity_vallim.pyon the Python reference pipeline).
📤 hypnoR export
-
export_hypnogram()— converts azeitr_resultinto the tidy hypnogram format expected byhypnoR. Stage mapping:ZCMn == 0within sleep epochs becomes"Quiet sleep"; non-zero sleep activity becomes"Sleep"; wake and off-wrist epochs become"W".subject_idis inferred automatically fromresult$subject_id(set by the pipeline from the filename stem) and can be overridden with an explicit argument. Works in both single-file and batch contexts.
📋 Sleep summary metrics
-
compute_sleep_metrics()— per-night sleep metrics split by day type (overall / workday / free day): SOL, TST, TBT, WASO, sleep efficiency, sleep onset, get-up time, mid-sleep, and within-person SDs. Column names and arithmetic mirror Julia Vallim’scompute_sleep_metrics()inpipeline_functions_fix27.py. -
compute_cpd_metrics()— CPD, MSW, MSF, MSFsc, SJL, and signed SJLa. Portsnights_to_cpd_df()andcompute_cpd_metrics()from the same reference. Both functions accept aholidaysargument for country-specific public holidays beyond weekends.
🐛 Bug fixes
-
Fix 26c (fragment recovery): two bugs closed.
- The Python reference pipeline silently disabled temperature- and light-based gap merging due to column name mismatches (
'TEMPERATURE'/'LIGHT'vs the actual'int_temp'/'light'columns in ActTrust data).pipeline_functions_fix27.pypatched;inst/extdata/vallim_nights.csvregenerated against the corrected Python output and re-verified: 52/52 main nights, all sleep dates and classifications match R. - R’s Fix 26c was using the period-level CSPD
statecolumn to detect sleep runs within the recovery window, which incorrectly treated entire 19+ h CSPD periods as a single sleep run. Fix 26c now uses Cole-Kripke epoch scoring onZCMnto determine sleep/wake within the candidate window, matching the intended behaviour. R is the reference implementation for Fix 26c.
- The Python reference pipeline silently disabled temperature- and light-based gap merging due to column name mismatches (
-
offwrist_refiner.R: fixed scalarFALSEassignment to a 0-row data frame ($valley_peak <- rep(FALSE, nrow(...))) that caused a crash on recordings with no valid off-wrist candidates.
🚀 Performance (Rcpp) — continued
-
Crespo MSP hot paths — five additional Rcpp ports eliminating the remaining R
forloops andvapplycalls in the main sleep detector:-
rolling_max_cpp/rolling_min_cpp— replacerolling_apply(max/min)in.morphological_open_close(). -
zero_mitigation_cpp— replaces the zero-run mitigationforloop (pass 1 of.crespo_msp()). -
mark_invalid_zeros_cpp— replaces the invalid-zero markingforloop (pass 2 of.crespo_msp()). -
adaptive_median_filter_cpp— replaces the variable-window adaptive medianforloop in both.crespo_msp()and.crespo_nap_msp(); this was the single hottest loop in the pipeline. - The coarse median filter
vapplyin.crespo_msp()now reuses the existingrolling_median_cppwith constant padding.
-
🧪 Tests
- Crespo C++ parity tests (
test-crespo-cpp-parity.R):rolling_max_cpp,rolling_min_cpp,zero_mitigation_cpp,mark_invalid_zeros_cpp,adaptive_median_filter_cpp, morphological close/open pair, and end-to-end epoch count lock oninput1.txt. - Fix 26c regression test (
test-fix26c.R): synthetic 1-min epoch recording with a bloated 19 h CSPDstate = 1period containing two Cole-Kripke sleep runs (3 h + 5.5 h) separated by a warm/dark wake gap. Asserts that.recover_fragmented_episodes()merges the CK-derived runs (TBT ~ 9 h) rather than the CSPD state period (TBT ~ 19 h). Runs on CI; no external data required.
zeitR 0.1.0 (2026-06)
🚀 Pipeline
- Full actigraphy pipeline validated epoch-for-epoch (
0 / 76,196mismatches) against the Condor circadiaBase Python reference on an ActTrust recording:detect_offwrist_bimodal(),detect_sleep_crespo(),detect_naps_crespo()(faithfulnap_wrapperport),compute_waso(). -
run_pipeline()gains aparamsargument (defaultacttrust_params()); device-specific defaults are now consolidated in one place and can be overridden without touching individual detector calls.wake_threshis removed from therun_pipeline()signature — it now lives inparams$waso$wake_thresh. -
run_pipeline()gains aquietargument to suppress the timestamp-issue warning (useful in batch and testing contexts).
✨ New functions
-
acttrust_params()— exported device parameter preset consolidating all ActTrust-specific defaults across off-wrist, sleep, nap, and WASO stages. Copy and modify to adapt the pipeline to other devices. -
label_states()— converts the integerstatecolumn to a human-readable ordered factor ("wake","sleep","nap","off-wrist").
🧪 Tests
- End-to-end pipeline parity regression test (
test-pipeline-parity.R): epoch-level state, per-layer counts, and nightly statistics locked againstpython_output.csvandpython_nights.csv. - CSPD refiner parity tests (
test-cspd-refiner-parity.R): stage-1 peak-valley length filter, stage-2 sleep-gap separation, full.cspd_refine_periodsoutput, and bedtime/getuptime indices against Python intermediates. - Sleep Crespo wiring test (
test-sleep-crespo-wiring.R): isolatesdetect_sleep_crespo(refine = TRUE)againstcspd_refined_output.csv. - WASO parity tests (
test-waso-parity.R):.nights_dfboundaries, per-night statistics, and within-night epoch agreement on boundary-matched nights.
🌱 Initial release
- Full package scaffold:
read_acttrust(),read_actigraphy(),read_actigraphy_dir(),prepare_actigraphy(),check_consistency(),score_epochs_cole_kripke(),compute_npcra(),study_summary(),run_pipeline_batch(). - Three vignettes: getting started, NPCRA, study-level analysis.
- pkgdown site with Bootstrap 5 and Circadia Lab branding.