PhysioML provides reproducible feature transforms and leakage-aware reference
deep-learning workflows for PhysioExperiment time series. Its toy fixtures
validate mechanics only; they do not select or clinically validate a model.
install.packages(
"PhysioML",
repos = c("https://x-biosignal.r-universe.dev", BiocManager::repositories())
)
Rcatch22 is optional for catch22(). ROCKET and MiniRocket require the
optional R package reticulate and a caller-managed Python environment with
NumPy and aeon. PhysioML never installs or selects a Python environment at
runtime.
Deep-learning functions require the optional R packages torch and luz plus
a caller-installed LibTorch runtime. Install those dependencies explicitly
before use. PhysioML never calls torch::install_torch(), downloads weights,
or selects an accelerator from package code.
PhysioML reads assays as time x channel x case, preserving channel labels and
case IDs. Building that input contract needs no optional backend:
library(PhysioML)
set.seed(1)
peCases <- function(n_case) {
a <- array(rnorm(256 * 2 * n_case), dim = c(256, 2, n_case))
PhysioExperiment(
assays = list(raw = a), samplingRate = 128,
colData = S4Vectors::DataFrame(label = c("C3", "C4"))
)
}
train_pe <- peCases(6)
test_pe <- peCases(3)
train_labels <- factor(rep(c("rest", "task"), length.out = 6))
valid_labels <- factor(rep(c("rest", "task"), length.out = 3))
train_pe
The feature, dataset, model, and export workflows below call the optional
backends noted above (reticulate with aeon, or torch/luz). Each is guarded
so it runs only when that backend is installed, and reuses the objects built
here.
PhysioML interprets assays as time x channel for one case or
time x channel x case for multiple cases. Channel labels and case IDs are
preserved exactly. Inputs must be numeric, finite, equal length, and at least
10 samples for catch22 or nine samples for convolution transforms. Missing
values, unequal-length series, implicit resampling, padding, and channel
averaging are not supported.
catch22() returns the 22 canonical Rcatch22 features for every case-channel
series. Set catch24 = TRUE to append the upstream DN_Mean and
DN_Spread_Std values. peReducedFeatures() widens these to a case-by-feature
matrix without adding an outcome.
ROCKET returns alternating maximum and proportion-positive-value features. MiniRocket returns proportion-positive-value features. Both results record the requested kernel count and actual backend feature width; MiniRocket may adjust the requested width to its kernel family.
Fit a convolution transform on training cases, then reuse its persistent model for validation or test cases:
if (requireNamespace("reticulate", quietly = TRUE)) {
fit <- minirocket(train_pe, seed = 42)
test_features <- minirocket(test_pe, model = fit$model)
}
Do not fit the transform on all cases before cross-validation.
Split patients or cases before windowing or normalization. Fit a training Dataset, then reuse its frozen channel-wise statistics:
if (requireNamespace("torch", quietly = TRUE)) {
train <- peDataset(
train_pe,
targets = train_labels,
window_samples = 256,
stride_samples = 128,
normalization = "zscore"
)
valid <- peDataset(
test_pe,
targets = valid_labels,
window_samples = 256,
stride_samples = 128,
normalization = "zscore",
normalization_stats = train$contract$normalization_stats,
class_levels = train$contract$class_levels
)
}
Items are channel x time; DataLoader batches are
batch x channel x time, including a batch of one. Windows are complete,
case-major, and never cross a case boundary. Classification targets are
zero-based torch_long values with a reversible label table. Regression
targets and inputs are torch_float32. Targets are never appended to inputs.
Z-score scale is the population standard deviation. Robust scale is
1.4826 * median(abs(x - median(x))). A constant training channel receives
scale one and a typed diagnostic. Passing all cases to peDataset() and
splitting its windows afterward leaks normalization information and can put
one patient's windows in both training and validation.
peModel() constructs four compact modules with a common
batch x channel x time input and batch x output result:
1 + 2 * (kernel_size - 1) * sum(dilations).batch x time x channel and uses the final
hidden representation. Bidirectionality must be requested explicitly.Classification modules return logits; softmax is applied only for probability
prediction. trainModel() uses Adam through luz, requires caller-supplied
case-disjoint validation, never creates a row-level split, and rejects
independently fitted validation normalization. The initial implementation uses
CPU and num_workers = 0; it restores caller R and torch RNG states on success
and error. Accelerator runs are not claimed to be bitwise reproducible.
if (requireNamespace("torch", quietly = TRUE) &&
requireNamespace("luz", quietly = TRUE)) {
fit <- trainModel(
train,
valid,
model = "cnn1d",
epochs = 20,
seed = 42
)
probability <- predictModel(fit, valid, type = "probability")
}
Prediction runs with gradients disabled and evaluation mode enabled. Channel order, time width, task, dtype, class mapping, and frozen normalization must match training exactly.
exportONNX() crosses one explicit inference-only boundary:
R torch module -> TorchScript -> Python PyTorch -> ONNX
The initial exporter uses PyTorch's legacy TorchScript ONNX path deliberately;
it does not rebuild an R architecture in Python. R LibTorch and Python torch
must have the same major/minor version. Export uses float32
batch x channel x time input named signal and float32
batch x output logits named output, opset 17 by default. Channel, time, and
output dimensions remain fixed. EEGNet, CNN1D, and TCN support a dynamic batch
axis. R torch's implicit LSTM initial state is traced at a fixed batch width,
so LSTM export rejects dynamic_batch = TRUE and supports the validated fixed
batch width of two.
Each graph is checked with ONNX, run through ONNX Runtime's
CPUExecutionProvider with provider fallback disabled, and compared to the R
torch logits before landing. The maximum absolute and relative errors must
both be at most 1e-4. The graph and canonical JSON model card are landed as
a pair:
if (requireNamespace("torch", quietly = TRUE) &&
requireNamespace("luz", quietly = TRUE) &&
requireNamespace("reticulate", quietly = TRUE)) {
onnx_path <- file.path(tempdir(), "model.onnx")
exportONNX(fit, onnx_path)
portable <- importONNX(onnx_path)
logits <- onnxPredict(portable, test_pe, type = "logit")
}
importONNX() retains graph bytes and the validated card, not a Python
InferenceSession, so the result survives saveRDS() and a fresh R process.
onnxPredict() recreates a CPU session for each call and requires the exact
training channel order, time width, task, class mapping, dtype, and frozen
normalization statistics. It never fits normalization or sends targets to
ONNX Runtime.
Python torch, ONNX, and ONNX Runtime are caller-managed optional dependencies. PhysioML does not install them or select a Python environment. ONNX avoids Python pickle loading during import, but an untrusted graph can still target parser/runtime vulnerabilities or consume excessive resources. The model-card SHA-256 proves byte integrity, not trust or clinical validity.
freezeLayers() clones a fitted R torch model and selects parameters by their
exact names. Prefix selection is component-aware. The default architecture
registry freezes the feature extractor and leaves only classifier.*
trainable. fineTune() passes exactly the resulting trainable tensors to Adam.
Frozen modules remain in evaluation mode, so batch-normalization buffers do
not drift during a linear probe.
if (requireNamespace("torch", quietly = TRUE) &&
requireNamespace("luz", quietly = TRUE)) {
frozen <- freezeLayers(fit)
adapted <- fineTune(
frozen,
train,
valid,
strategy = "partial",
unfreeze = c("conv2", "bn2")
)
}
Target normalization is fitted only on the target training split and then reused unchanged by validation and test datasets. Case IDs must be disjoint. The source model and caller RNG states are not mutated.
domainAdapt() implements CORAL for a training-source matrix Xs and an
explicitly designated unlabeled target adaptation matrix Xt:
A = (cov(Xs) + lambda I)^(-1/2) (cov(Xt) + lambda I)^(1/2)
Xs' = (Xs - mean(Xs)) A + mean(Xt)
Matrix powers use symmetric eigendecomposition with an explicit eigenvalue floor. Target validation/test rows and labels do not belong in the fit. Matching second-order statistics does not guarantee accuracy, fairness, causal invariance, class-conditional alignment, or clinical validity.
foundationEmbed() resolves the same case-major, channel-ordered complete
windows as the feature and torch APIs. Every result records sampling rate,
units, axes, channel order, model ID, immutable 40-character revision, complete
manifest SHA-256, preprocessing, and backend versions. CPU execution is the
only public device.
cacheFoundationModel() requires explicit first-download consent, license
acceptance, an immutable revision, and a predeclared canonical manifest hash.
It verifies the entire declared file inventory, sizes, hashes, READY marker,
path containment, and symlink absence before use. A cache hit performs no
download. Safetensors is the supported checkpoint boundary;
trust_remote_code, unrestricted pickle loading, package installation, and
environment selection are not used. Checksums establish integrity, not trust.
MOMENT, Chronos-2, LaBraM, and BENDR have distinct adapter contracts. The package-owned tiny safetensors fixture keeps routine acceptance reproducible offline. An official adapter is enabled only after its exact revision, preprocessing, output, repeatability, CPU behavior, and license pass a managed preflight. Chronos-2 currently lacks a documented fixed-width embedding method; LaBraM and BENDR official repository-code/pickle checkpoints do not meet this safe generic loader boundary. Those adapters fail explicitly rather than guessing an intermediate representation or montage.
Fitted convolution models contain a Python pickle serialized into an R raw
vector, plus backend versions and a SHA-256 checksum. saveRDS() and
readRDS() preserve the model without a live Python pointer. A checksum
detects accidental corruption but does not establish trust: loading a pickle
from an untrusted source can execute code. Reuse only model objects obtained
from a trusted source and with the recorded aeon version.
Every result records exact package, Python, NumPy, and aeon provenance. These generic transforms do not establish clinical validity for a downstream model.
A fitted physio_dl_model contains live torch/luz objects for the current
session. Upstream torch serialization is not promised here as durable across
versions. The plain model spec, input contract, class mapping, normalization
statistics, and backend versions are retained for governed export work. Never
deserialize an untrusted model file.
The feature definitions follow Lubba et al. (2019), ROCKET follows Dempster, Petitjean and Webb (2020), MiniRocket follows Dempster, Schmidt and Webb (2021), EEGNet follows Lawhern et al. (2018), and the causal TCN follows Bai, Kolter and Koltun (2018). PhysioML is MIT-licensed and calls separately installed maintained backends at runtime; it does not vendor their source.
See the x-biosignal organization and x-biosignal.r-universe.dev for the full package suite.