Reproducible benchmark of wavelet-enhanced intrabody communication biometric identification.
The 14 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
- [1] § Methods › Classifiers ↔ ibc_benchmark/models.py, lines 22–75 · score 0.91 · convolutional blocks, pooling layer, max pooling, batch normalisation, ReLU, SpectralCNN
- [2] § Results › Additional learned-model analysis ↔ experiments/mlp_raw_vs_combined_clean.py, lines 1–28 · score 0.74 · feature fusion, Raw MLP, model capacity, Combined MLP, raw spectra, seed
- [3] § Methods › Performance metrics ↔ notebooks/06_synthetic_roc_from_experiments.ipynb, the whole file · a weak match · score 0.71 · synthetic scores, ROC curves, calibrated, KNN, metrics, accuracy
- [4] § Methods › Classifiers ↔ notebooks/05_embedded_profiling_and_reports.ipynb, lines 341–419 · score 0.69 · AdamW, weight decay, ReLU, lightweight, dropout, batch
- [5] § Methods › Feature extraction ↔ ibc_benchmark/features.py, lines 35–80 · score 0.69 · discrete wavelet transform, standard deviation, coefficient, entropy, periodisation, energy
- [6] § Methods › Classifiers ↔ notebooks/05_embedded_profiling_and_reports.ipynb, lines 341–419 · score 0.67 · AdamW, weight decay, ReLU, hidden, Dropout, MLP
- [7] § Results › Additional learned-model analysis ↔ experiments/mlp_raw_vs_combined.py, lines 1–29 · score 0.65 · feature fusion, model capacity, raw spectra, matching, MLP, seed
- [8] § Methods › Classifiers ↔ ibc_benchmark/models.py, lines 78–101 · score 0.63 · hidden layer, ReLU, activations, network, Dropout, MLP
- [9] § Methods › Performance metrics ↔ models/train_evaluate.py, lines 50–82 · score 0.63 · ROC curves, FN, FP, TN, TP, EER
- [10] § Methods › Pre-processing ↔ ibc_benchmark/data.py, lines 79–144 · score 0.63 · experiment_id, remove rows, outliers, freq, filtered
- [11] § Methods › Classifiers ↔ models.py, lines 1–31 · score 0.63 · LightGBM, SpectralCNN, Random Forest, KNN, embedded, MLP
- [12] § Methods › MCU latency and energy profiling ↔ notebooks/05_embedded_profiling_and_reports.ipynb, lines 1–79 · score 0.62 · Cortex M4, board, voltage, profile, MCU, device
- [13] § Methods › Leakage-free evaluation protocol ↔ experiments/mlp_raw_vs_combined.py, lines 1–29 · score 0.57 · subject_id, enrollment, protocol, query, seeds, training
- [14] § Methods › Leakage-free evaluation protocol ↔ ibc_benchmark/data.py, lines 147–173 · score 0.55 · GroupShuffleSplit, subject_id, seeds, Leakage, training
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Jupyter notebook · 444 lines · 17 KB · MIT · 3 matches
- # %%
- # Notebook: Frequency Analysis & On-Device Simulation (Cortex-M4) — Improved
- # Python >= 3.9
- # Requires: numpy, pandas, pywt, matplotlib, seaborn, scikit-learn, scipy, (optional) torch
- # =========================
- # Cell 0: Environment & Config
- # =========================
- import os
- from pathlib import Path
- from typing import Tuple, Dict, List, Optional
- import numpy as np
- import pandas as pd
- import matplotlib.pyplot as plt
- import seaborn as sns
- import pywt
- from sklearn.ensemble import RandomForestClassifier
- from sklearn.model_selection import StratifiedShuffleSplit
- from sklearn.preprocessing import StandardScaler
- from sklearn.decomposition import PCA
- from sklearn.metrics import accuracy_score
- from scipy import signal
- sns.set_context("talk")
- sns.set_style("whitegrid")
- SEED = 42
- rng = np.random.default_rng(SEED)
- CONFIG = {
- # Data & processing
- "data_dir": "data",
- "proc_dir": "data/processed",
- "results_dir": "results",
- "fig_dir": "results/figures",
- "out_len": 256, # resampled spectrum length
- "downsample_to": None, # e.g., 128 or None
- "standardize": True, # z-score with train stats
- "pca_components": None, # e.g., 128 or None (applied to raw only)
- "val_ratio": 0.2,
- # DWT
- "dwt_wavelet": "db4",
- "dwt_level": 2,
- "cache_dwt": True,
- # Visualization
- "scalogram_num": 6, # how many validation samples to visualize
- "scalogram_scales": 64,
- "scalogram_wavelet": "morl",
- "topk_importance": 20,
- # Baseline models
- "run_rf": True,
- "rf_n_estimators": 500,
- "rf_n_jobs": -1,
- "run_mlp": False, # optional quick MLP on combined features
- "mlp_hidden": 256,
- "mlp_epochs": 300,
- "mlp_lr": 5e-4,
- "mlp_wd": 1e-3,
- # MCU simulation (Cortex-M4 defaults; tune per board)
- "mcu_clock_hz": 96e6,
- "mcu_voltage_v": 3.3,
- "mcu_current_a": 0.035,
- # Reproducibility
- "seed": SEED,
- }
- DATA_DIR = Path(CONFIG["data_dir"])
- PROC_DIR = Path(CONFIG["proc_dir"])
- RES_DIR = Path(CONFIG["results_dir"])
- FIG_DIR = Path(CONFIG["fig_dir"])
- for p in [PROC_DIR, RES_DIR, FIG_DIR]:
- p.mkdir(parents=True, exist_ok=True)
- # =========================
- # Cell 1: Data Loading with Fallback
- # =========================
- def parse_gain_columns(df: pd.DataFrame, family_prefix: str) -> Tuple[np.ndarray, List[str]]:
- """Parse rx_gain columns and return (freqs, column_names) sorted by frequency."""
- cols, freqs = [], []
- for c in df.columns:
- if c.startswith(family_prefix + "_f_"):
- f = c.split("_f_")[-1]
- if f.isdigit():
- freqs.append(float(f)); cols.append(c)
- order = np.argsort(freqs)
- return np.asarray(freqs, dtype=float)[order], [cols[i] for i in order]
- def select_gain_family(df: pd.DataFrame) -> Tuple[str, np.ndarray, List[str]]:
- """Choose gain family with lower zero/NaN ratio across rows."""
- f1, c1 = parse_gain_columns(df, "rx_gain_50")
- f2, c2 = parse_gain_columns(df, "rx_gain_1M")
- def score(cols):
- if len(cols) == 0: return np.inf
- vals = df[cols].replace([np.inf, -np.inf], np.nan)
- return float(((vals == 0) | vals.isna()).sum(axis=1).mean())
- s1, s2 = score(c1), score(c2)
- if s1 < s2 and len(c1) > 0:
- return "rx_gain_50", f1, c1
- return "rx_gain_1M", f2, c2
- def resample_spectrum(row_vals: np.ndarray, in_freqs: np.ndarray, out_len: int = 256) -> np.ndarray:
- """Linear interpolation to fixed length; expects dB-domain magnitudes."""
- fmin, fmax = float(in_freqs.min()), float(in_freqs.max())
- f_out = np.linspace(fmin, fmax, out_len, dtype=float)
- return np.interp(f_out, in_freqs, row_vals.astype(float))
- def load_or_build_processed(out_len: int = 256) -> Tuple[np.ndarray, np.ndarray]:
- """Load processed X(out_len) and y, or build from all_measurements.csv."""
- x_csv = PROC_DIR / "ibc_processed.csv"
- y_csv = DATA_DIR / "labels_filtered.csv"
- if x_csv.exists() and y_csv.exists():
- X = pd.read_csv(x_csv).values.astype(np.float32)
- y = pd.read_csv(y_csv)["subject_id"].astype(int).values
- return X, y
- raw_csvs = list(DATA_DIR.glob("all_measurements.csv"))
- if not raw_csvs:
- raise FileNotFoundError("Provide data/processed/ibc_processed.csv or data/all_measurements.csv")
- df = pd.read_csv(raw_csvs[0])
- assert "subject_id" in df.columns, "subject_id missing in all_measurements.csv"
- family, freqs, cols = select_gain_family(df)
- vals = df[cols].replace([np.inf, -np.inf], np.nan)
- mask = ~vals.isna().any(axis=1)
- df = df.loc[mask].reset_index(drop=True)
- vals = vals.loc[mask].reset_index(drop=True)
- y = df["subject_id"].astype(int).values
- X = np.vstack([resample_spectrum(vals.iloc[i].values, freqs, out_len=out_len)
- for i in range(len(vals))]).astype(np.float32)
- pd.DataFrame(X, columns=[f"f{i}" for i in range(X.shape[1])]).to_csv(x_csv, index=False)
- pd.DataFrame({"subject_id": y}).to_csv(y_csv, index=False)
- return X, y
- X_raw, y = load_or_build_processed(out_len=CONFIG["out_len"])
- print("Processed:", X_raw.shape, y.shape)
- # =========================
- # Cell 2: Split & Preprocess (leakage-free)
- # =========================
- sss = StratifiedShuffleSplit(n_splits=1, test_size=CONFIG["val_ratio"], random_state=CONFIG["seed"])
- tr_idx, va_idx = next(sss.split(X_raw, y))
- X_tr_raw, X_va_raw = X_raw[tr_idx], X_raw[va_idx]
- y_tr, y_va = y[tr_idx], y[va_idx]
- print("Split:", X_tr_raw.shape, X_va_raw.shape, len(np.unique(y_tr)), len(np.unique(y_va)))
- # Optional downsample (e.g., 256 -> 128) BEFORE standardization
- def maybe_downsample(X: np.ndarray, to_len: Optional[int]) -> np.ndarray:
- if to_len is None or to_len == X.shape[1]:
- return X
- # Use Fourier resampling for smooth decimation
- return signal.resample(X, num=to_len, axis=1)
- X_tr_ds = maybe_downsample(X_tr_raw, CONFIG["downsample_to"])
- X_va_ds = maybe_downsample(X_va_raw, CONFIG["downsample_to"])
- print("After downsample:", X_tr_ds.shape, X_va_ds.shape)
- # Standardization with train stats
- if CONFIG["standardize"]:
- scaler = StandardScaler(with_mean=True, with_std=True)
- X_tr = scaler.fit_transform(X_tr_ds)
- X_va = scaler.transform(X_va_ds)
- else:
- X_tr, X_va = X_tr_ds, X_va_ds
- # Optional PCA on RAW (retain DWT over standardized raw)
- def maybe_pca_fit_transform(Xtr: np.ndarray, Xva: np.ndarray, n_comp: Optional[int]):
- if n_comp is None:
- return Xtr, Xva, None
- pca = PCA(n_components=n_comp, random_state=CONFIG["seed"])
- return pca.fit_transform(Xtr), pca.transform(Xva), pca
- X_tr_raw_pca, X_va_raw_pca, pca_model = maybe_pca_fit_transform(X_tr, X_va, CONFIG["pca_components"])
- print("Raw/PCA shapes:", X_tr.shape, X_va.shape, X_tr_raw_pca.shape, X_va_raw_pca.shape)
- # =========================
- # Cell 3: DWT(db4, L2) feature extraction (with caching)
- # =========================
- def dwt_stats(X: np.ndarray, wavelet: str = "db4", level: int = 2) -> np.ndarray:
- """Compute DWT stats per sample: energy, entropy, mean, std for each coeff array."""
- feats = []
- for row in X:
- coeffs = pywt.wavedec(row, wavelet, level=level, mode="periodization")
- fvec = []
- for c in coeffs:
- e = float(np.sum(c**2))
- p = (c**2) / (e + 1e-12)
- ent = float(-np.sum(p * np.log2(np.clip(p, 1e-12, 1.0))))
- fvec.extend([e, ent, float(np.mean(c)), float(np.std(c))])
- feats.append(fvec)
- return np.asarray(feats, dtype=np.float32)
- def dwt_cached(Xtr: np.ndarray, Xva: np.ndarray, wavelet: str, level: int) -> Tuple[np.ndarray, np.ndarray]:
- key = f"dwt_{wavelet}_L{level}_std{int(CONFIG['standardize'])}_len{Xtr.shape[1]}"
- ftr = PROC_DIR / f"{key}_train.npy"
- fva = PROC_DIR / f"{key}_val.npy"
- if CONFIG["cache_dwt"] and ftr.exists() and fva.exists():
- return np.load(ftr), np.load(fva)
- X_tr_dwt = dwt_stats(Xtr, wavelet=wavelet, level=level)
- X_va_dwt = dwt_stats(Xva, wavelet=wavelet, level=level)
- if CONFIG["cache_dwt"]:
- np.save(ftr, X_tr_dwt); np.save(fva, X_va_dwt)
- return X_tr_dwt, X_va_dwt
- X_tr_dwt, X_va_dwt = dwt_cached(X_tr, X_va, CONFIG["dwt_wavelet"], CONFIG["dwt_level"])
- print("DWT shapes:", X_tr_dwt.shape, X_va_dwt.shape)
- # Combined features (raw/PCA + DWT)
- X_tr_comb = np.hstack([X_tr_raw_pca, X_tr_dwt])
- X_va_comb = np.hstack([X_va_raw_pca, X_va_dwt])
- print("Combined shapes:", X_tr_comb.shape, X_va_comb.shape)
- # =========================
- # Cell 4: CWT Scalogram Grid
- # =========================
- def cwt_scalogram(x: np.ndarray, wavelet: str = "morl", num_scales: int = 64) -> np.ndarray:
- scales = np.linspace(1, num_scales, num_scales)
- coef, freqs = pywt.cwt(x, scales, wavelet)
- power = np.abs(coef)**2
- return power.astype(np.float32)
- def save_scalogram_grid(Xv: np.ndarray, k: int, wavelet: str, num_scales: int, out_path: Path):
- idx = rng.choice(np.arange(Xv.shape[0]), size=min(k, Xv.shape[0]), replace=False)
- n = len(idx); cols = min(3, n); rows = int(np.ceil(n / cols))
- fig, axes = plt.subplots(rows, cols, figsize=(4*cols, 3.2*rows), squeeze=False)
- for ax, i in zip(axes.flat, idx):
- S = cwt_scalogram(Xv[i], wavelet=wavelet, num_scales=num_scales)
- sns.heatmap(S, cmap="magma", cbar=False, ax=ax)
- ax.set_title(f"val idx {i}")
- ax.set_xlabel("Freq idx"); ax.set_ylabel("Scales")
- for ax in axes.flat[n:]:
- ax.axis("off")
- fig.suptitle("CWT Scalogram Grid")
- fig.tight_layout()
- fig.savefig(out_path, dpi=200)
- plt.close(fig)
- save_scalogram_grid(X_va, CONFIG["scalogram_num"], CONFIG["scalogram_wavelet"],
- CONFIG["scalogram_scales"], FIG_DIR / "scalogram_grid.png")
- # =========================
- # Cell 5: RF Feature Importance on DWT stats
- # =========================
- if CONFIG["run_rf"]:
- rf = RandomForestClassifier(
- n_estimators=CONFIG["rf_n_estimators"], max_depth=None,
- random_state=SEED, n_jobs=CONFIG["rf_n_jobs"]
- )
- rf.fit(X_tr_dwt, y_tr)
- imp = rf.feature_importances_
- order = np.argsort(imp)[::-1]
- topk = CONFIG["topk_importance"]
- # Bar plot
- plt.figure(figsize=(10, 4))
- plt.bar(np.arange(min(topk, len(imp))), imp[order][:topk])
- plt.title(f"Top-{topk} DWT Feature Importances (RF Gini)")
- plt.xlabel("DWT feature index (energy/entropy/mean/std per coeff)")
- plt.ylabel("Gini importance")
- plt.tight_layout()
- plt.savefig(FIG_DIR / "dwt_importances_topk.png", dpi=200)
- plt.close()
- # CSV of importances
- pd.DataFrame({
- "feat_idx": order,
- "importance": imp[order]
- }).to_csv(RES_DIR / "dwt_feature_importances.csv", index=False)
- # =========================
- # Cell 6: MCU On-Device Simulation (Cortex-M4) — Stage-wise
- # =========================
- def cycles_zscore(n: int, d: int) -> int:
- # simple model: subtract mean, divide std, load/store per element
- C_ADD, C_DIV, C_MEM = 1, 3, 2
- return int(n * d * (C_ADD + C_DIV + C_MEM))
- def cycles_dwt_db4_l2(n: int, d: int) -> int:
- # first-order estimator: lifting-like passes across ~2d with aggregated constants
- C_MUL, C_ADD, C_MEM = 1, 1, 2
- a = 20 # aggregated factor (tunable)
- dwt = int(a * 2 * d * (C_MUL + C_ADD + C_MEM))
- stats = int(10 * d * (C_ADD + C_MUL + C_MEM)) # energy/entropy/mean/std
- return (dwt + stats) * n
- def time_energy(cycles: int, f_hz: float, v: float, i: float) -> Tuple[float, float]:
- t = cycles / f_hz
- e = v * i * t
- return t, e
- def simulate_pipeline(n_samples: int, d_spec: int, clk: float, v: float, i: float) -> Dict[str, float]:
- c_z = cycles_zscore(n_samples, d_spec) if CONFIG["standardize"] else 0
- c_dwt = cycles_dwt_db4_l2(n_samples, d_spec)
- c_tot = c_z + c_dwt
- t, e = time_energy(c_tot, clk, v, i)
- return {
- "cycles_total": float(c_tot),
- "time_s": float(t),
- "energy_J": float(e),
- "cycles_zscore": float(c_z),
- "cycles_dwt": float(c_dwt),
- "clock_hz": float(clk),
- "voltage_v": float(v),
- "current_a": float(i),
- "n_samples": int(n_samples),
- "d_spec": int(d_spec),
- }
- mcu = simulate_pipeline(
- n_samples=1,
- d_spec=X_tr.shape[1],
- clk=CONFIG["mcu_clock_hz"],
- v=CONFIG["mcu_voltage_v"],
- i=CONFIG["mcu_current_a"],
- )
- print("MCU Simulation (1 sample):", mcu)
- # Also simulate a small batch (e.g., 50 samples)
- mcu_batch = simulate_pipeline(
- n_samples=50,
- d_spec=X_tr.shape[1],
- clk=CONFIG["mcu_clock_hz"],
- v=CONFIG["mcu_voltage_v"],
- i=CONFIG["mcu_current_a"],
- )
- # =========================
- # Cell 7: Lightweight Baselines (RF combined, optional MLP)
- # =========================
- results = []
- if CONFIG["run_rf"]:
- clf = RandomForestClassifier(n_estimators=CONFIG["rf_n_estimators"], random_state=SEED, n_jobs=CONFIG["rf_n_jobs"])
- clf.fit(X_tr_comb, y_tr)
- y_pred = clf.predict(X_va_comb)
- acc = accuracy_score(y_va, y_pred)
- print("RF(combined) val accuracy:", acc)
- results.append({"model": "rf_combined", "val_acc": float(acc)})
- if CONFIG["run_mlp"]:
- try:
- import torch, torch.nn as nn, torch.nn.functional as F
- from torch.utils.data import TensorDataset, DataLoader
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
- class MLP(nn.Module):
- def __init__(self, in_dim, n_classes, hidden=256, p=0.3):
- super().__init__()
- self.net = nn.Sequential(
- nn.Linear(in_dim, hidden),
- nn.ReLU(inplace=True),
- nn.Dropout(p),
- nn.Linear(hidden, hidden),
- nn.ReLU(inplace=True),
- nn.Dropout(p),
- nn.Linear(hidden, n_classes),
- )
- def forward(self, x): return self.net(x)
- C = len(np.unique(y_tr))
- D = X_tr_comb.shape[1]
- model = MLP(D, C, hidden=CONFIG["mlp_hidden"], p=0.3).to(DEVICE)
- opt = torch.optim.AdamW(model.parameters(), lr=CONFIG["mlp_lr"], weight_decay=CONFIG["mlp_wd"])
- crit = nn.CrossEntropyLoss()
- Xt = torch.from_numpy(X_tr_comb).float().to(DEVICE)
- yt = torch.from_numpy(pd.factorize(y_tr)[0]).long().to(DEVICE)
- Xv = torch.from_numpy(X_va_comb).float().to(DEVICE)
- yv = torch.from_numpy(pd.factorize(y_va, sort=True)[0]).long().to(DEVICE)
- # Align val encoding to train classes
- # Build mapping from original labels to 0..C-1 using train labels
- classes = np.unique(y_tr)
- to_index = {int(c): i for i, c in enumerate(classes)}
- y_tr_enc = np.vectorize(lambda t: to_index[int(t)])(y_tr)
- y_va_enc = np.vectorize(lambda t: to_index[int(t)])(y_va)
- yt = torch.from_numpy(y_tr_enc).long().to(DEVICE)
- yv = torch.from_numpy(y_va_enc).long().to(DEVICE)
- dl = DataLoader(TensorDataset(Xt, yt), batch_size=64, shuffle=True, drop_last=False)
- best_val = 1e9; best_state=None
- for epoch in range(1, CONFIG["mlp_epochs"]+1):
- model.train(); run=0.0
- for xb, yb in dl:
- opt.zero_grad(); logits = model(xb); loss = crit(logits, yb)
- loss.backward(); opt.step(); run += loss.item() * xb.size(0)
- tr_loss = run / len(dl.dataset)
- model.eval()
- with torch.no_grad():
- va_loss = crit(model(Xv), yv).item()
- preds = model(Xv).argmax(dim=1).cpu().numpy()
- va_acc = (preds == y_va_enc).mean()
- if va_loss < best_val:
- best_val = va_loss
- best_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}
- if epoch % 50 == 0 or epoch == 1:
- print(f"[MLP {epoch:03d}] tr={tr_loss:.4f} val={va_loss:.4f} acc={va_acc:.4f}")
- if best_state is not None:
- model.load_state_dict(best_state)
- with torch.no_grad():
- preds = model(Xv).argmax(dim=1).cpu().numpy()
- acc = (preds == y_va_enc).mean()
- print("MLP(combined) val accuracy:", acc)
- results.append({"model": "mlp_combined", "val_acc": float(acc)})
- except Exception as e:
- print("MLP failed:", e)
- # =========================
- # Cell 8: Save Reports
- # =========================
- report = {
- "n_train": int(X_tr.shape[0]), "n_val": int(X_va.shape[0]),
- "d_raw": int(X_tr.shape[1]), "d_dwt": int(X_tr_dwt.shape[1]),
- "d_combined": int(X_tr_comb.shape[1]),
- "standardize": bool(CONFIG["standardize"]),
- "downsample_to": int(CONFIG["downsample_to"]) if CONFIG["downsample_to"] is not None else None,
- "pca_components": int(CONFIG["pca_components"]) if CONFIG["pca_components"] is not None else None,
- "dwt_wavelet": CONFIG["dwt_wavelet"], "dwt_level": int(CONFIG["dwt_level"]),
- "rf_ran": bool(CONFIG["run_rf"]), "mlp_ran": bool(CONFIG["run_mlp"]),
- "mcu_sim_1": mcu, "mcu_sim_50": mcu_batch,
- }
- pd.DataFrame(results).to_csv(RES_DIR / "baseline_results.csv", index=False)
- pd.Series(report, dtype=object).to_json(RES_DIR / "ondevice_simulation_report.json", indent=2)
- print("Saved:")
- print(" - Figures: scalogram_grid.png, dwt_importances_topk.png")
- print(" - Reports: dwt_feature_importances.csv, baseline_results.csv, ondevice_simulation_report.json")
- # %%
05_embedded_profiling_and_reports.ipynb at commit 18235d5, under MIT · at the source
Overview
Abstract
Intrabody communication (IBC) channels offer physiological diversity that may support future wearable biometric identification. Recent reports of over 99 per cent identification accuracy have frequently resulted from data leakage, where samples from the same subject are seen in both training and evaluation, yielding inflated and unreliable metrics. In this work, we establish a public, leakage-free benchmark for IBC biometrics built on a 30-subject open dataset, using strict subject-wise 80/
Supplementary Information: The online version contains supplementary material available at https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 14 matches between paragraphs and lines of code.
dryjins/ibc-wavelet-benchmark
18235d5e195935711400e9c02754e5241f83efcd, 14 May 2026Availability: 1 check, the latest on 28 September 2026: the link answers
- 28 September 2026: the link answers
30 files
- analysis_and_visualizati
on.ipynb , Jupyter, 166 lines - data/
download_dataset.py , Python, 24 lines - data/
raw/ , Python, 31 linescommon.py - data/
raw/ , Python, 62 linesfilter_results.py - data/
raw/ , Python, 63 linesplot_measurement_group.p y - data/
raw/ , Python, 102 linesplot_sample_curves.py - data/
raw/ , Python, 455 linesprocess_json_files.py - eval_utils.py, Python, 51 lines
- experiments/
mlp_quick.py , Python, 205 lines - experiments/
mlp_raw_vs_combined.py , Python, 277 lines, 2 matches - experiments/
mlp_raw_vs_combined_clea , Python, 250 lines, 1 matchn.py - features.py, Python, 110 lines
- ibc_benchmark/
__init__.py , Python, 1 line - ibc_benchmark/
data.py , Python, 187 lines, 2 matches - ibc_benchmark/
evaluate.py , Python, 90 lines - ibc_benchmark/
features.py , Python, 132 lines, 1 match - ibc_benchmark/
models.py , Python, 160 lines, 2 matches - ibc_benchmark/
trainer.py , Python, 127 lines - models.py, Python, 392 lines, 1 match
- models/
train_evaluate.py , Python, 133 lines, 1 match - notebooks/
01_preprocess_and_knn_ba , Jupyter, 627 linesseline.ipynb - notebooks/
02_subjectwise_sklearn_b , Jupyter, 295 linesenchmarks.ipynb - notebooks/
03_processed_dataset_bui , Jupyter, 395 lineslder.ipynb - notebooks/
04_closed_set_neural_bas , Jupyter, 533 lineselines.ipynb - notebooks/
05_embedded_profiling_an , Jupyter, 444 lines, 3 matchesd_reports.ipynb - notebooks/
06_synthetic_roc_from_ex , Jupyter, 126 lines, 1 matchperiments.ipynb - notebooks/
archive/ , Jupyter, 459 lineslegacy_download_preproce ss_attempt.ipynb - run_benchmark.py, Python, 172 lines
- LICENSE, License, 21 lines
- README.md, Text, 148 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 28 scripts, each with its path and the digest of its content;
- 14 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
Datasets cited
- zenodo:8214497, at Zenodo; found in “Data availability”
Data availability
The dataset analysed during this study is publicly available in the Zenodo repository at https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 28 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 3 keywords, 8 MeSH terms, 1 funder, 27 references.
Cite
This paper
Jin, S., & Komarov, M. M. (2026). Reproducible benchmark of wavelet-enhanced intrabody communication biometric identification. Scientific reports, 16(1), 24421. https://
BibTeX
@article{jin2026reproduc
author = {Jin, Seungmin and Komarov, Mikhail M.},
title = {{Reproducible benchmark of wavelet-enhanced intrabody communication biometric identification}},
journal = {Scientific reports},
year = {2026},
month = may,
volume = {16},
number = {1},
pages = {24421},
publisher = {Nature Publishing Group},
issn = {2045-2322},
doi = {10.1038/
url = {https://
pmid = {42209669},
pmcid = {PMC13448553}
}
RIS
TY - JOUR
AU - Jin, Seungmin
AU - Komarov, Mikhail M.
TI - Reproducible benchmark of wavelet-enhanced intrabody communication biometric identification
T2 - Scientific reports
J2 - Sci Rep
PY - 2026
DA - 2026/
VL - 16
IS - 1
SP - 24421
SN - 2045-2322
PB - Nature Publishing Group
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"type": "article-journal",
"title": "Reproducible benchmark of wavelet-enhanced intrabody communication biometric identification",
"container-title": "Scientific reports",
"author": [
{
"family": "Jin",
"given": "Seungmin"
},
{
"family": "Komarov",
"given": "Mikhail M."
}
],
"container-title-short":
"volume": "16",
"issue": "1",
"page": "24421",
"DOI": "10.1038/
"PMID": "42209669",
"PMCID": "PMC13448553",
"ISSN": "2045-2322",
"publisher": "Nature Publishing Group",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
28
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1371/journal.pgen.1012242 [code]
- Wiz regulates clustered protocadherin genes by restricting CTCF/
cohesin loop extrusion in a genomic-distance biased manner. Journal: PLoS geneticsIn common: LightGBM, PyTorch, seaborn, 5 other tools, 1 reference - [2] doi:10.1371/journal.pone.0354510 [code]
- Personalized adaptive virtual reality experience driven by electroencephalography-b
ased pain recognition. Journal: PloS oneIn common: LightGBM, PyWavelets, seaborn, 4 other tools - [3] doi:10.7554/elife.110588 [code]
- Opening the black box toward a modular approach to spike sorting.Journal: eLifeIn common: LightGBM, PyTorch, seaborn, 5 other tools, methods / tools
- [4] doi:10.1002/dad2.70430 [code]
- Benchmarking privacy and utility in synthetic tabular cohorts for Alzheimer's disease research.Journal: Alzheimer's & dementia (Amsterdam, Netherlands)In common: LightGBM, PyTorch, seaborn, 5 other tools, methods / tools
- [5] doi:10.1038/s41598-026-68186-2 [code]
- NeuroStream: spectral-spatio-temporal
deep learning for visual stimulus classification from EEG. Journal: Scientific reportsIn common: PyWavelets, PyTorch, seaborn, 5 other tools, methods / tools - [6] doi:10.1038/s41592-026-03057-2 [code]
- CREsted: modeling genomic and synthetic cell-type-specific enhancers across tissues and species.Journal: Nature methodsIn common: PyWavelets, PyTorch, seaborn, 5 other tools, methods / tools
- [7] doi:10.1186/s13244-026-02365-7 [code]
- Super-resolution MRI and 2.5D deep learning for intratumoral-peritumoral
radiomics in preoperative prediction of rectal cancer perineural invasion. Journal: Insights into imagingIn common: LightGBM, PyTorch, seaborn, 5 other tools - [8] doi:10.1038/s41467-026-73996-z [code]
- Genetic architecture of white matter microstructure captured by unsupervised deep representation learning of fractional anisotropy maps.Journal: Nature communicationsIn common: LightGBM, PyTorch, seaborn, 5 other tools
- [9] doi:10.1016/j.isci.2026.116055 [code]
- Mapping the transcriptional diversity of calcium signaling in the mouse and human brain.Journal: iScienceIn common: LightGBM, PyTorch, seaborn, 5 other tools
- [10] doi:10.1093/bib/bbag118 [code]
- Drug screening for α-synuclein aggregation inhibitors via multimodal graph neural network.Journal: Briefings in bioinformaticsIn common: LightGBM, PyTorch, seaborn, 5 other tools
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 28 scripts, and 14 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:e3ce27a7ac270b79…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
