From 8c6713478367f6da1df0d3a363e4679f109e3b10 Mon Sep 17 00:00:00 2001 From: benjaminfreyuu Date: Tue, 30 Jun 2026 18:29:54 +0200 Subject: [PATCH 1/8] Add SenKin CITE-seq method (LightGBM + GRU ensemble) --- src/methods/senkin/senkin/config.vsh.yaml | 27 ++ src/methods/senkin/senkin/main.nf | 18 ++ .../senkin/senkin_predict/config.vsh.yaml | 24 ++ src/methods/senkin/senkin_predict/script.py | 41 +++ .../senkin/senkin_train/config.vsh.yaml | 45 +++ src/methods/senkin/senkin_train/script.py | 256 ++++++++++++++++++ src/workflows/run_benchmark/main.nf | 3 +- 7 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 src/methods/senkin/senkin/config.vsh.yaml create mode 100644 src/methods/senkin/senkin/main.nf create mode 100644 src/methods/senkin/senkin_predict/config.vsh.yaml create mode 100644 src/methods/senkin/senkin_predict/script.py create mode 100644 src/methods/senkin/senkin_train/config.vsh.yaml create mode 100644 src/methods/senkin/senkin_train/script.py diff --git a/src/methods/senkin/senkin/config.vsh.yaml b/src/methods/senkin/senkin/config.vsh.yaml new file mode 100644 index 00000000..b075bc86 --- /dev/null +++ b/src/methods/senkin/senkin/config.vsh.yaml @@ -0,0 +1,27 @@ +__merge__: ../../../api/comp_method.yaml +name: senkin +label: SenKin +summary: "LightGBM + bidirectional GRU ensemble for CITE-seq protein prediction (OpenProblems 2022 2nd place)" +description: | + Two-stage method from the OpenProblems NeurIPS 2021 competition. Stage 1 trains four + LightGBM models on different RNA feature representations (log-normalized, CLR-TSVD, + custom sqrt-normalized, and raw counts). Stage 2 refines predictions with two neural + network architectures: a bidirectional GRU with cosine-similarity loss and a dense + bidirectional GRU with MSE loss. Final predictions are a weighted blend (55% cosine, + 45% MSE) of per-fold averaged outputs. +references: + doi: + - 10.1101/2022.04.11.487796 +links: + repository: https://github.com/lueckenlab/senkin-tmp-cite-pred +info: + preferred_normalization: log_cp10k +resources: + - path: main.nf + type: nextflow_script + entrypoint: run_wf +dependencies: + - name: methods/senkin_train + - name: methods/senkin_predict +runners: + - type: nextflow diff --git a/src/methods/senkin/senkin/main.nf b/src/methods/senkin/senkin/main.nf new file mode 100644 index 00000000..f5f7479c --- /dev/null +++ b/src/methods/senkin/senkin/main.nf @@ -0,0 +1,18 @@ +workflow run_wf { + take: input_ch + main: + output_ch = input_ch + | senkin_train.run( + fromState: ["input_train_mod1", "input_train_mod2", "input_test_mod1"], + toState: ["input_model": "output"] + ) + | senkin_predict.run( + fromState: ["input_test_mod1", "input_train_mod2", "input_model"], + toState: ["output": "output"] + ) + | map { tup -> + [tup[0], [output: tup[1].output]] + } + + emit: output_ch +} diff --git a/src/methods/senkin/senkin_predict/config.vsh.yaml b/src/methods/senkin/senkin_predict/config.vsh.yaml new file mode 100644 index 00000000..eb564fda --- /dev/null +++ b/src/methods/senkin/senkin_predict/config.vsh.yaml @@ -0,0 +1,24 @@ +__merge__: ../../../api/comp_method_predict.yaml +name: senkin_predict +resources: + - path: script.py + type: python_script +engines: + - type: docker + image: openproblems/base_pytorch_nvidia:1 + setup: + - type: docker + run: pip install --no-cache-dir --no-deps git+https://github.com/lueckenlab/senkin-tmp-cite-pred.git + - type: python + packages: + - lightgbm>=4.0 + - tensorflow>=2.12 + - scikit-learn>=1.1 + - mudata>=0.2 + - muon>=0.1 + - fast-array-utils +runners: + - type: executable + - type: nextflow + directives: + label: [highmem, hightime, midcpu, gpu] diff --git a/src/methods/senkin/senkin_predict/script.py b/src/methods/senkin/senkin_predict/script.py new file mode 100644 index 00000000..e67e9809 --- /dev/null +++ b/src/methods/senkin/senkin_predict/script.py @@ -0,0 +1,41 @@ +import logging +import pickle + +import anndata as ad +import numpy as np +from scipy.sparse import csc_matrix + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +## VIASH START +par = { + "input_test_mod1": "resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/test_mod1.h5ad", + "input_train_mod2": "resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod2.h5ad", + "input_model": "output_model.pkl", + "output": "output_pred.h5ad", +} +meta = {"name": "senkin"} +## VIASH END + +logger.info("Reading input files...") +adata_rna_test = ad.read_h5ad(par["input_test_mod1"]) +adata_prot_train = ad.read_h5ad(par["input_train_mod2"]) + +logger.info("Loading model bundle...") +with open(par["input_model"], "rb") as f: + bundle = pickle.load(f) + +logger.info("Writing predictions...") +adata_out = ad.AnnData( + layers={"normalized": csc_matrix(bundle["test_predictions"])}, + obs=adata_rna_test.obs, + var=adata_prot_train.var, + uns={ + "dataset_id": adata_rna_test.uns.get("dataset_id", bundle.get("dataset_id", "")), + "method_id": meta["name"], + }, +) + +adata_out.write_h5ad(par["output"], compression="gzip") +logger.info("Predictions saved to %s", par["output"]) diff --git a/src/methods/senkin/senkin_train/config.vsh.yaml b/src/methods/senkin/senkin_train/config.vsh.yaml new file mode 100644 index 00000000..098d023a --- /dev/null +++ b/src/methods/senkin/senkin_train/config.vsh.yaml @@ -0,0 +1,45 @@ +__merge__: ../../../api/comp_method_train.yaml +name: senkin_train +resources: + - path: script.py + type: python_script +arguments: + - name: "--n_folds" + type: integer + default: 5 + description: Number of cross-validation folds for LightGBM and neural network training. + - name: "--lgbm_boost_rounds" + type: integer + default: 10000 + description: Maximum LightGBM boosting rounds (early stopping applies). + - name: "--lgbm_early_stopping" + type: integer + default: 100 + description: LightGBM early stopping patience (rounds without improvement). + - name: "--nn_epochs" + type: integer + default: 100 + description: Maximum neural network training epochs (early stopping applies). + - name: "--n_tsvd_components" + type: integer + default: 100 + description: TSVD components for reducing LightGBM predictions before NN input. +engines: + - type: docker + image: openproblems/base_pytorch_nvidia:1 + setup: + - type: docker + run: pip install --no-cache-dir --no-deps git+https://github.com/lueckenlab/senkin-tmp-cite-pred.git + - type: python + packages: + - lightgbm>=4.0 + - tensorflow>=2.12 + - scikit-learn>=1.1 + - mudata>=0.2 + - muon>=0.1 + - fast-array-utils +runners: + - type: executable + - type: nextflow + directives: + label: [highmem, hightime, midcpu, gpu] diff --git a/src/methods/senkin/senkin_train/script.py b/src/methods/senkin/senkin_train/script.py new file mode 100644 index 00000000..d8ac21c9 --- /dev/null +++ b/src/methods/senkin/senkin_train/script.py @@ -0,0 +1,256 @@ +import gc +import logging +import pickle + +import anndata as ad +import numpy as np +from scipy.sparse import issparse +from sklearn.decomposition import PCA +from sklearn.model_selection import KFold +import tensorflow as tf + +from senkin_tmp_cite_pred.preprocess import remove_constant_vars, senkin_normalize, get_top_correlated_features +from senkin_tmp_cite_pred.lgbm_models import get_lgbm_predictions, lgbm_params_1, lgbm_params_2, lgbm_params_3, lgbm_params_4 +from senkin_tmp_cite_pred.nn_models import cite_cos_sim_model, cite_mse_model, nn_kfold, zscore +from senkin_tmp_cite_pred.metrics import cosine_similarity_loss + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +## VIASH START +par = { + "input_train_mod1": "resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod1.h5ad", + "input_train_mod2": "resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/train_mod2.h5ad", + "input_test_mod1": "resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/swap/test_mod1.h5ad", + "output": "output_model.pkl", + "n_folds": 5, + "lgbm_boost_rounds": 10000, + "lgbm_early_stopping": 100, + "nn_epochs": 100, + "n_tsvd_components": 100, +} +meta = {"name": "senkin"} +## VIASH END + + +def _to_dense(X): + return X.toarray() if issparse(X) else np.array(X) + + +def _clr_tsvd_fitted(adata, n_components=200): + """CLR then TSVD, returning (transformed_array, fitted_tsvd).""" + from sklearn.decomposition import TruncatedSVD + from muon import prot as pt + n_comp = min(n_components, min(adata.shape) - 1) + tsvd = TruncatedSVD(n_components=n_comp, algorithm="arpack", random_state=42) + clr = pt.pp.clr(adata, inplace=False).X + return tsvd.fit_transform(clr), tsvd + + +def _parse_batch(obs, batch_col="batch"): + if batch_col not in obs.columns: + obs["day"] = "unknown" + obs["donor"] = "unknown" + return obs + def _split(b): + b = str(b) + d_idx = b.find("d") + if d_idx > 0: + return b[1:d_idx], b[d_idx + 1:] + return b, b + obs["day"], obs["donor"] = zip(*obs[batch_col].astype(str).map(_split)) + return obs + + +def _lognorm(X_counts): + X = _to_dense(X_counts).astype(np.float64) + row_sums = X.sum(axis=1, keepdims=True) + row_sums[row_sums == 0] = 1 + return np.log1p(X / row_sums * 1e4) + + +# --------------------------------------------------------------------------- +# Load data +# --------------------------------------------------------------------------- +logger.info("Reading input files...") +adata_rna_train = ad.read_h5ad(par["input_train_mod1"]) +adata_prot_train = ad.read_h5ad(par["input_train_mod2"]) +adata_rna_test = ad.read_h5ad(par["input_test_mod1"]) + +adata_rna_train.obs = _parse_batch(adata_rna_train.obs) +adata_prot_train.obs = _parse_batch(adata_prot_train.obs) +adata_rna_test.obs = _parse_batch(adata_rna_test.obs) + +# Mark split membership before concatenating +adata_rna_train.obs["split"] = "train" +adata_rna_test.obs["split"] = "test" + +# Concatenate train + test RNA so the original pipeline sees both at once +import scanpy as sc +adata_rna_all = sc.concat([adata_rna_train, adata_rna_test], axis=0) +adata_rna_all.X = adata_rna_all.layers["counts"] + +# --------------------------------------------------------------------------- +# Preprocessing on combined train+test RNA +# --------------------------------------------------------------------------- +logger.info("Preprocessing RNA (train + test combined)...") + +adata_rna_all_filt = remove_constant_vars(adata_rna_all) +X_counts_all = _to_dense(adata_rna_all_filt.layers.get("counts", adata_rna_all_filt.X)) + +train_mask = adata_rna_all_filt.obs["split"] == "train" +test_mask = adata_rna_all_filt.obs["split"] == "test" + +# Log-normalize +X_lognorm_all = _lognorm(X_counts_all) + +# CLR-TSVD — fit on all, keep fitted object for predict-time use +logger.info("Computing CLR-TSVD...") +X_clr_tsvd_all, clr_tsvd_fitted = _clr_tsvd_fitted(adata_rna_all_filt, n_components=200) + +# SenKin normalization + PCA — fit on all +logger.info("Computing SenKin normalization and PCA...") +X_sqrt_norm_all = np.asarray(senkin_normalize(adata_rna_all_filt, batch_key="day")) +n_pca = min(100, min(X_sqrt_norm_all.shape) - 1) +pca_model = PCA(n_components=n_pca, random_state=42) +X_pca_all = pca_model.fit_transform(X_sqrt_norm_all) + +# Correlated gene selection — computed on train cells only (no label leakage) +logger.info("Selecting correlated features...") +adata_rna_train_filt = adata_rna_all_filt[train_mask] +Y_prot_train = _to_dense(adata_prot_train.layers.get("normalized", adata_prot_train.X)).astype(np.float64) +# get_top_correlated_features hardcodes .layers["dsb"]; alias our normalized layer +_added_dsb = "dsb" not in adata_prot_train.layers +if _added_dsb: + adata_prot_train.layers["dsb"] = adata_prot_train.layers.get("normalized", adata_prot_train.X) +# Use "day" as group key — benchmark data has no "donor" column +_group_key = "donor" if "donor" in adata_rna_train_filt.obs.columns else "day" +top_corr_genes = get_top_correlated_features(adata_rna_train_filt, adata_prot_train, group_key=_group_key) +if _added_dsb: + del adata_prot_train.layers["dsb"] +all_var_names = list(adata_rna_all_filt.var_names) +selected_gene_idxs = [all_var_names.index(g) for g in top_corr_genes if g in all_var_names] +X_raw_selected_all = X_counts_all[:, selected_gene_idxs].astype(np.float64) + +# Split back into train / test portions +X_lognorm_train = X_lognorm_all[train_mask] +X_lognorm_test = X_lognorm_all[test_mask] +X_clr_tsvd_train = X_clr_tsvd_all[train_mask] +X_clr_tsvd_test = X_clr_tsvd_all[test_mask] +X_pca_train = X_pca_all[train_mask] +X_pca_test = X_pca_all[test_mask] +X_raw_sel_train = X_raw_selected_all[train_mask] +X_raw_sel_test = X_raw_selected_all[test_mask] +X_counts_train = X_counts_all[train_mask] +X_counts_test = X_counts_all[test_mask] + +folds = KFold(n_splits=par["n_folds"], shuffle=True, random_state=666) +n_tsvd = par["n_tsvd_components"] +boost_rounds = par["lgbm_boost_rounds"] +early_stop = par["lgbm_early_stopping"] + +# Protein targets +Y_prot_raw = _to_dense(adata_prot_train.layers.get("counts", adata_prot_train.X)).astype(np.float64) + +# --------------------------------------------------------------------------- +# LightGBM — 4 models, train+test passed together (original design) +# get_lgbm_predictions concatenates train+test, fits TSVD on combined array +# --------------------------------------------------------------------------- +logger.info("Training LightGBM model 1 (log-norm → proteins)...") +lgbm1_svd_all = get_lgbm_predictions( + X_lognorm_train, Y_prot_train, X_lognorm_test, + folds, lgbm_params_1, + n_tsvd_components=n_tsvd, + num_boost_round=boost_rounds, + early_stopping_rounds=early_stop, +) + +logger.info("Training LightGBM model 2 (combined → proteins)...") +X_comb_train = np.concatenate([X_clr_tsvd_train, X_raw_sel_train, X_pca_train], axis=1) +X_comb_test = np.concatenate([X_clr_tsvd_test, X_raw_sel_test, X_pca_test], axis=1) +lgbm2_svd_all = get_lgbm_predictions( + X_comb_train, Y_prot_train, X_comb_test, + folds, lgbm_params_2, + n_tsvd_components=n_tsvd, + num_boost_round=boost_rounds, + early_stopping_rounds=early_stop, +) + +logger.info("Training LightGBM model 3 (raw counts → proteins)...") +lgbm3_svd_all = get_lgbm_predictions( + X_counts_train, Y_prot_train, X_counts_test, + folds, lgbm_params_3, + n_tsvd_components=n_tsvd, + num_boost_round=boost_rounds, + early_stopping_rounds=early_stop, +) + +logger.info("Training LightGBM model 4 (raw counts → raw proteins)...") +lgbm4_svd_all = get_lgbm_predictions( + X_counts_train, Y_prot_raw, X_counts_test, + folds, lgbm_params_4, + n_tsvd_components=n_tsvd, + num_boost_round=boost_rounds, + early_stopping_rounds=early_stop, +) + +# get_lgbm_predictions returns shape (n_train + n_test, n_tsvd) +n_train = X_lognorm_train.shape[0] +n_test = X_lognorm_test.shape[0] + +lgbm1_tr = lgbm1_svd_all[:n_train]; lgbm1_te = lgbm1_svd_all[n_train:] +lgbm2_tr = lgbm2_svd_all[:n_train]; lgbm2_te = lgbm2_svd_all[n_train:] +lgbm3_tr = lgbm3_svd_all[:n_train]; lgbm3_te = lgbm3_svd_all[n_train:] +lgbm4_tr = lgbm4_svd_all[:n_train]; lgbm4_te = lgbm4_svd_all[n_train:] + +# Build NN inputs +nn_X_train = np.concatenate([X_clr_tsvd_train, X_pca_train, X_raw_sel_train, lgbm1_tr, lgbm2_tr, lgbm3_tr, lgbm4_tr], axis=1).astype(np.float32) +nn_X_test = np.concatenate([X_clr_tsvd_test, X_pca_test, X_raw_sel_test, lgbm1_te, lgbm2_te, lgbm3_te, lgbm4_te], axis=1).astype(np.float32) +nn_y_train = Y_prot_train.astype(np.float32) + +train_cell_ids = np.array(adata_rna_all_filt.obs_names[train_mask]) +test_cell_ids = np.array(adata_rna_all_filt.obs_names[test_mask]) + +# --------------------------------------------------------------------------- +# Neural network — use original nn_kfold which saves checkpoint weights +# --------------------------------------------------------------------------- +logger.info("Training neural network (cosine model)...") +import os +os.makedirs("models", exist_ok=True) + +train_preds_cos, test_preds_cos = nn_kfold( + train_cell_ids, nn_X_train, nn_y_train, + test_cell_ids, nn_X_test, + cite_cos_sim_model, folds, + model_name="cite_cos_model", + BATCH_SIZE=620, EPOCHS=par["nn_epochs"], LR_FACTOR=0.05, +) + +logger.info("Training neural network (MSE model)...") +nn_y_train_z = zscore(nn_y_train) +train_preds_mse, test_preds_mse = nn_kfold( + train_cell_ids, nn_X_train, nn_y_train_z, + test_cell_ids, nn_X_test, + cite_mse_model, folds, + model_name="cite_mse_model", + BATCH_SIZE=600, EPOCHS=par["nn_epochs"], LR_FACTOR=0.1, +) + +# Blend — identical to original train_nn_models +test_preds = zscore(test_preds_cos) * 0.55 + zscore(test_preds_mse) * 0.45 + +# --------------------------------------------------------------------------- +# Save bundle — test predictions stored directly, predict script just reads them +# --------------------------------------------------------------------------- +logger.info("Saving model bundle...") +bundle = { + "test_predictions": test_preds.astype(np.float32), # (n_test, n_proteins) + "test_obs": adata_rna_test.obs, + "prot_var": adata_prot_train.var, + "dataset_id": adata_rna_train.uns.get("dataset_id", ""), +} + +with open(par["output"], "wb") as f: + pickle.dump(bundle, f, protocol=4) + +logger.info("Training complete. Model saved to %s", par["output"]) diff --git a/src/workflows/run_benchmark/main.nf b/src/workflows/run_benchmark/main.nf index 6a7989d4..044eb0cc 100644 --- a/src/workflows/run_benchmark/main.nf +++ b/src/workflows/run_benchmark/main.nf @@ -18,7 +18,8 @@ methods = [ lm, guanlab_dengkw_pm, novel, - simple_mlp + simple_mlp, + senkin ] // construct list of metrics From 4ef165df68725c8e18b768629a8e0e0fba5aa2a0 Mon Sep 17 00:00:00 2001 From: benjaminfreyuu Date: Wed, 29 Jul 2026 10:52:54 +0200 Subject: [PATCH 2/8] Use senkin_tmp_cite_pred log_normalize and clr_tsvd helpers Replace the inline _lognorm and _clr_tsvd_fitted duplicates in senkin_train with the now-published library functions log_normalize() and clr_tsvd() from senkin_tmp_cite_pred.preprocess. Behaviour is unchanged: log_normalize uses target_sum=1e4 (CP10K + log1p) matching the old _lognorm, and clr_tsvd uses random_state=42 for reproducible components. The previously-kept fitted TSVD object was unused and is dropped. --- src/methods/senkin/senkin_train/script.py | 27 +++++------------------ 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/methods/senkin/senkin_train/script.py b/src/methods/senkin/senkin_train/script.py index d8ac21c9..f87b6936 100644 --- a/src/methods/senkin/senkin_train/script.py +++ b/src/methods/senkin/senkin_train/script.py @@ -9,7 +9,7 @@ from sklearn.model_selection import KFold import tensorflow as tf -from senkin_tmp_cite_pred.preprocess import remove_constant_vars, senkin_normalize, get_top_correlated_features +from senkin_tmp_cite_pred.preprocess import remove_constant_vars, senkin_normalize, get_top_correlated_features, log_normalize, clr_tsvd from senkin_tmp_cite_pred.lgbm_models import get_lgbm_predictions, lgbm_params_1, lgbm_params_2, lgbm_params_3, lgbm_params_4 from senkin_tmp_cite_pred.nn_models import cite_cos_sim_model, cite_mse_model, nn_kfold, zscore from senkin_tmp_cite_pred.metrics import cosine_similarity_loss @@ -37,16 +37,6 @@ def _to_dense(X): return X.toarray() if issparse(X) else np.array(X) -def _clr_tsvd_fitted(adata, n_components=200): - """CLR then TSVD, returning (transformed_array, fitted_tsvd).""" - from sklearn.decomposition import TruncatedSVD - from muon import prot as pt - n_comp = min(n_components, min(adata.shape) - 1) - tsvd = TruncatedSVD(n_components=n_comp, algorithm="arpack", random_state=42) - clr = pt.pp.clr(adata, inplace=False).X - return tsvd.fit_transform(clr), tsvd - - def _parse_batch(obs, batch_col="batch"): if batch_col not in obs.columns: obs["day"] = "unknown" @@ -62,13 +52,6 @@ def _split(b): return obs -def _lognorm(X_counts): - X = _to_dense(X_counts).astype(np.float64) - row_sums = X.sum(axis=1, keepdims=True) - row_sums[row_sums == 0] = 1 - return np.log1p(X / row_sums * 1e4) - - # --------------------------------------------------------------------------- # Load data # --------------------------------------------------------------------------- @@ -101,12 +84,12 @@ def _lognorm(X_counts): train_mask = adata_rna_all_filt.obs["split"] == "train" test_mask = adata_rna_all_filt.obs["split"] == "test" -# Log-normalize -X_lognorm_all = _lognorm(X_counts_all) +# Log-normalize (CP10K + log1p) via the senkin_tmp_cite_pred library helper +X_lognorm_all = np.asarray(log_normalize(adata_rna_all_filt, target_sum=1e4)) -# CLR-TSVD — fit on all, keep fitted object for predict-time use +# CLR-TSVD via the library helper (random_state=42 for reproducible components) logger.info("Computing CLR-TSVD...") -X_clr_tsvd_all, clr_tsvd_fitted = _clr_tsvd_fitted(adata_rna_all_filt, n_components=200) +X_clr_tsvd_all = clr_tsvd(adata_rna_all_filt, n_components=200, random_state=42) # SenKin normalization + PCA — fit on all logger.info("Computing SenKin normalization and PCA...") From 63950542984f05e670e499d53e6a33999dbf71e2 Mon Sep 17 00:00:00 2001 From: benjaminfreyuu Date: Wed, 29 Jul 2026 11:29:41 +0200 Subject: [PATCH 3/8] Densify log_normalize output before array slicing log_normalize() preserves the input sparsity, so on the sparse counts layer it returns a sparse matrix. np.asarray() on a sparse matrix yields a 0-d object array, which crashed at X_lognorm_all[train_mask] with an IndexError. Use the existing _to_dense() helper (and restore float64) so the downstream boolean-mask slicing and concatenation work, matching the previous inline _lognorm behaviour. Verified: viash test src/methods/senkin/senkin_train passes (1/1). Co-Authored-By: Claude Opus 4.8 --- src/methods/senkin/senkin_train/script.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/methods/senkin/senkin_train/script.py b/src/methods/senkin/senkin_train/script.py index f87b6936..71ee39b5 100644 --- a/src/methods/senkin/senkin_train/script.py +++ b/src/methods/senkin/senkin_train/script.py @@ -84,8 +84,9 @@ def _split(b): train_mask = adata_rna_all_filt.obs["split"] == "train" test_mask = adata_rna_all_filt.obs["split"] == "test" -# Log-normalize (CP10K + log1p) via the senkin_tmp_cite_pred library helper -X_lognorm_all = np.asarray(log_normalize(adata_rna_all_filt, target_sum=1e4)) +# Log-normalize (CP10K + log1p) via the senkin_tmp_cite_pred library helper. +# log_normalize preserves the input sparsity, so densify for the array math below. +X_lognorm_all = _to_dense(log_normalize(adata_rna_all_filt, target_sum=1e4)).astype(np.float64) # CLR-TSVD via the library helper (random_state=42 for reproducible components) logger.info("Computing CLR-TSVD...") From ce5658ed9672efde7538a0afe1a873747e3b869d Mon Sep 17 00:00:00 2001 From: Robrecht Cannoodt Date: Thu, 30 Jul 2026 08:48:14 +0200 Subject: [PATCH 4/8] register senkin, fix its method id, and drop the unused gpu label * Add methods/senkin to run_benchmark's dependencies -- it was in the methods list but not declared, so the generated workflow referenced an undefined variable and the whole run died at startup * Report "senkin" as the method id rather than senkin_predict; the metrics copy uns["method_id"] straight into the score * Drop the gpu label: measured on a T4, senkin runs entirely on CPU (LightGBM's pip wheel is CPU-only and dominates the runtime, and TensorFlow cannot load CUDA in this image) * Give senkin_predict a test_setup and generate its model in test_resources.sh, so viash test has an --input_model to use --- scripts/create_datasets/test_resources.sh | 12 ++++++++++++ src/methods/senkin/senkin_predict/config.vsh.yaml | 9 ++++++++- src/methods/senkin/senkin_predict/script.py | 2 +- src/methods/senkin/senkin_train/config.vsh.yaml | 2 +- src/workflows/run_benchmark/config.vsh.yaml | 1 + 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/scripts/create_datasets/test_resources.sh b/scripts/create_datasets/test_resources.sh index 767ba1b8..6ee0c090 100755 --- a/scripts/create_datasets/test_resources.sh +++ b/scripts/create_datasets/test_resources.sh @@ -46,6 +46,18 @@ for name in bmmc_cite/normal bmmc_cite/swap bmmc_multiome/normal bmmc_multiome/s --input_test_mod1 $OUTPUT_DIR/openproblems_neurips2021/$name/test_mod1.h5ad \ --output $OUTPUT_DIR/openproblems_neurips2021/$name/models/simple_mlp/ + # senkin is CITE-only + if [[ "$name" == bmmc_cite/normal ]]; then + echo "pre-train senkin on $name" + [ -d $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin/ ] && rm -r $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin/ + mkdir -p $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin/ + viash run src/methods/senkin/senkin_train/config.vsh.yaml -- \ + --input_train_mod1 $OUTPUT_DIR/openproblems_neurips2021/$name/train_mod1.h5ad \ + --input_train_mod2 $OUTPUT_DIR/openproblems_neurips2021/$name/train_mod2.h5ad \ + --input_test_mod1 $OUTPUT_DIR/openproblems_neurips2021/$name/test_mod1.h5ad \ + --output $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin/model.pkl + fi + echo "pre-train novel on $name" [ -d $OUTPUT_DIR/openproblems_neurips2021/$name/models/novel/ ] && rm -r $OUTPUT_DIR/openproblems_neurips2021/$name/models/novel/ mkdir -p $OUTPUT_DIR/openproblems_neurips2021/$name/models/novel/ diff --git a/src/methods/senkin/senkin_predict/config.vsh.yaml b/src/methods/senkin/senkin_predict/config.vsh.yaml index eb564fda..e123a97f 100644 --- a/src/methods/senkin/senkin_predict/config.vsh.yaml +++ b/src/methods/senkin/senkin_predict/config.vsh.yaml @@ -1,5 +1,12 @@ __merge__: ../../../api/comp_method_predict.yaml name: senkin_predict + +info: + test_setup: + with_model: + input_test_mod1: resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/test_mod1.h5ad + input_train_mod2: resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/train_mod2.h5ad + input_model: resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/models/senkin/model.pkl resources: - path: script.py type: python_script @@ -21,4 +28,4 @@ runners: - type: executable - type: nextflow directives: - label: [highmem, hightime, midcpu, gpu] + label: [highmem, hightime, midcpu] diff --git a/src/methods/senkin/senkin_predict/script.py b/src/methods/senkin/senkin_predict/script.py index e67e9809..6dfaf97f 100644 --- a/src/methods/senkin/senkin_predict/script.py +++ b/src/methods/senkin/senkin_predict/script.py @@ -33,7 +33,7 @@ var=adata_prot_train.var, uns={ "dataset_id": adata_rna_test.uns.get("dataset_id", bundle.get("dataset_id", "")), - "method_id": meta["name"], + "method_id": "senkin", }, ) diff --git a/src/methods/senkin/senkin_train/config.vsh.yaml b/src/methods/senkin/senkin_train/config.vsh.yaml index 098d023a..68a6e745 100644 --- a/src/methods/senkin/senkin_train/config.vsh.yaml +++ b/src/methods/senkin/senkin_train/config.vsh.yaml @@ -42,4 +42,4 @@ runners: - type: executable - type: nextflow directives: - label: [highmem, hightime, midcpu, gpu] + label: [highmem, hightime, midcpu] diff --git a/src/workflows/run_benchmark/config.vsh.yaml b/src/workflows/run_benchmark/config.vsh.yaml index ea4396c3..0937c979 100644 --- a/src/workflows/run_benchmark/config.vsh.yaml +++ b/src/workflows/run_benchmark/config.vsh.yaml @@ -74,6 +74,7 @@ dependencies: - name: methods/guanlab_dengkw_pm - name: methods/novel - name: methods/simple_mlp + - name: methods/senkin - name: metrics/correlation - name: metrics/mse runners: From ec20b3c514102833cb654e4d9920836399753a03 Mon Sep 17 00:00:00 2001 From: Robrecht Cannoodt Date: Thu, 30 Jul 2026 09:23:00 +0200 Subject: [PATCH 5/8] rename senkin to senkin_tmp, update doi * Rename the method and both sub-components to `senkin_tmp`, so the name credits both team members * Point the doi at the 2026 competition paper (10.64898/2026.02.24.707614) Co-authored-by: Vladimir Shitov <35199218+VladimirShitov@users.noreply.github.com> --- scripts/create_datasets/test_resources.sh | 12 ++++++------ .../senkin => senkin_tmp/senkin_tmp}/config.vsh.yaml | 10 +++++----- .../{senkin/senkin => senkin_tmp/senkin_tmp}/main.nf | 4 ++-- .../senkin_tmp_predict}/config.vsh.yaml | 4 ++-- .../senkin_tmp_predict}/script.py | 4 ++-- .../senkin_tmp_train}/config.vsh.yaml | 2 +- .../senkin_tmp_train}/script.py | 2 +- src/workflows/run_benchmark/config.vsh.yaml | 2 +- src/workflows/run_benchmark/main.nf | 2 +- 9 files changed, 21 insertions(+), 21 deletions(-) rename src/methods/{senkin/senkin => senkin_tmp/senkin_tmp}/config.vsh.yaml (86%) rename src/methods/{senkin/senkin => senkin_tmp/senkin_tmp}/main.nf (87%) rename src/methods/{senkin/senkin_predict => senkin_tmp/senkin_tmp_predict}/config.vsh.yaml (91%) rename src/methods/{senkin/senkin_predict => senkin_tmp/senkin_tmp_predict}/script.py (94%) rename src/methods/{senkin/senkin_train => senkin_tmp/senkin_tmp_train}/config.vsh.yaml (98%) rename src/methods/{senkin/senkin_train => senkin_tmp/senkin_tmp_train}/script.py (99%) diff --git a/scripts/create_datasets/test_resources.sh b/scripts/create_datasets/test_resources.sh index 6ee0c090..ed99d7aa 100755 --- a/scripts/create_datasets/test_resources.sh +++ b/scripts/create_datasets/test_resources.sh @@ -46,16 +46,16 @@ for name in bmmc_cite/normal bmmc_cite/swap bmmc_multiome/normal bmmc_multiome/s --input_test_mod1 $OUTPUT_DIR/openproblems_neurips2021/$name/test_mod1.h5ad \ --output $OUTPUT_DIR/openproblems_neurips2021/$name/models/simple_mlp/ - # senkin is CITE-only + # senkin_tmp is CITE-only if [[ "$name" == bmmc_cite/normal ]]; then - echo "pre-train senkin on $name" - [ -d $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin/ ] && rm -r $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin/ - mkdir -p $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin/ - viash run src/methods/senkin/senkin_train/config.vsh.yaml -- \ + echo "pre-train senkin_tmp on $name" + [ -d $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin_tmp/ ] && rm -r $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin_tmp/ + mkdir -p $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin_tmp/ + viash run src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml -- \ --input_train_mod1 $OUTPUT_DIR/openproblems_neurips2021/$name/train_mod1.h5ad \ --input_train_mod2 $OUTPUT_DIR/openproblems_neurips2021/$name/train_mod2.h5ad \ --input_test_mod1 $OUTPUT_DIR/openproblems_neurips2021/$name/test_mod1.h5ad \ - --output $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin/model.pkl + --output $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin_tmp/model.pkl fi echo "pre-train novel on $name" diff --git a/src/methods/senkin/senkin/config.vsh.yaml b/src/methods/senkin_tmp/senkin_tmp/config.vsh.yaml similarity index 86% rename from src/methods/senkin/senkin/config.vsh.yaml rename to src/methods/senkin_tmp/senkin_tmp/config.vsh.yaml index b075bc86..9ee16355 100644 --- a/src/methods/senkin/senkin/config.vsh.yaml +++ b/src/methods/senkin_tmp/senkin_tmp/config.vsh.yaml @@ -1,6 +1,6 @@ __merge__: ../../../api/comp_method.yaml -name: senkin -label: SenKin +name: senkin_tmp +label: SenKin_tmp summary: "LightGBM + bidirectional GRU ensemble for CITE-seq protein prediction (OpenProblems 2022 2nd place)" description: | Two-stage method from the OpenProblems NeurIPS 2021 competition. Stage 1 trains four @@ -11,7 +11,7 @@ description: | 45% MSE) of per-fold averaged outputs. references: doi: - - 10.1101/2022.04.11.487796 + - 10.64898/2026.02.24.707614 links: repository: https://github.com/lueckenlab/senkin-tmp-cite-pred info: @@ -21,7 +21,7 @@ resources: type: nextflow_script entrypoint: run_wf dependencies: - - name: methods/senkin_train - - name: methods/senkin_predict + - name: methods/senkin_tmp_train + - name: methods/senkin_tmp_predict runners: - type: nextflow diff --git a/src/methods/senkin/senkin/main.nf b/src/methods/senkin_tmp/senkin_tmp/main.nf similarity index 87% rename from src/methods/senkin/senkin/main.nf rename to src/methods/senkin_tmp/senkin_tmp/main.nf index f5f7479c..d527c816 100644 --- a/src/methods/senkin/senkin/main.nf +++ b/src/methods/senkin_tmp/senkin_tmp/main.nf @@ -2,11 +2,11 @@ workflow run_wf { take: input_ch main: output_ch = input_ch - | senkin_train.run( + | senkin_tmp_train.run( fromState: ["input_train_mod1", "input_train_mod2", "input_test_mod1"], toState: ["input_model": "output"] ) - | senkin_predict.run( + | senkin_tmp_predict.run( fromState: ["input_test_mod1", "input_train_mod2", "input_model"], toState: ["output": "output"] ) diff --git a/src/methods/senkin/senkin_predict/config.vsh.yaml b/src/methods/senkin_tmp/senkin_tmp_predict/config.vsh.yaml similarity index 91% rename from src/methods/senkin/senkin_predict/config.vsh.yaml rename to src/methods/senkin_tmp/senkin_tmp_predict/config.vsh.yaml index e123a97f..107cd7dd 100644 --- a/src/methods/senkin/senkin_predict/config.vsh.yaml +++ b/src/methods/senkin_tmp/senkin_tmp_predict/config.vsh.yaml @@ -1,12 +1,12 @@ __merge__: ../../../api/comp_method_predict.yaml -name: senkin_predict +name: senkin_tmp_predict info: test_setup: with_model: input_test_mod1: resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/test_mod1.h5ad input_train_mod2: resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/train_mod2.h5ad - input_model: resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/models/senkin/model.pkl + input_model: resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal/models/senkin_tmp/model.pkl resources: - path: script.py type: python_script diff --git a/src/methods/senkin/senkin_predict/script.py b/src/methods/senkin_tmp/senkin_tmp_predict/script.py similarity index 94% rename from src/methods/senkin/senkin_predict/script.py rename to src/methods/senkin_tmp/senkin_tmp_predict/script.py index 6dfaf97f..8529f85d 100644 --- a/src/methods/senkin/senkin_predict/script.py +++ b/src/methods/senkin_tmp/senkin_tmp_predict/script.py @@ -15,7 +15,7 @@ "input_model": "output_model.pkl", "output": "output_pred.h5ad", } -meta = {"name": "senkin"} +meta = {"name": "senkin_tmp"} ## VIASH END logger.info("Reading input files...") @@ -33,7 +33,7 @@ var=adata_prot_train.var, uns={ "dataset_id": adata_rna_test.uns.get("dataset_id", bundle.get("dataset_id", "")), - "method_id": "senkin", + "method_id": "senkin_tmp", }, ) diff --git a/src/methods/senkin/senkin_train/config.vsh.yaml b/src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml similarity index 98% rename from src/methods/senkin/senkin_train/config.vsh.yaml rename to src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml index 68a6e745..2eca41a4 100644 --- a/src/methods/senkin/senkin_train/config.vsh.yaml +++ b/src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml @@ -1,5 +1,5 @@ __merge__: ../../../api/comp_method_train.yaml -name: senkin_train +name: senkin_tmp_train resources: - path: script.py type: python_script diff --git a/src/methods/senkin/senkin_train/script.py b/src/methods/senkin_tmp/senkin_tmp_train/script.py similarity index 99% rename from src/methods/senkin/senkin_train/script.py rename to src/methods/senkin_tmp/senkin_tmp_train/script.py index 71ee39b5..11760cb5 100644 --- a/src/methods/senkin/senkin_train/script.py +++ b/src/methods/senkin_tmp/senkin_tmp_train/script.py @@ -29,7 +29,7 @@ "nn_epochs": 100, "n_tsvd_components": 100, } -meta = {"name": "senkin"} +meta = {"name": "senkin_tmp"} ## VIASH END diff --git a/src/workflows/run_benchmark/config.vsh.yaml b/src/workflows/run_benchmark/config.vsh.yaml index 0937c979..514ba8b6 100644 --- a/src/workflows/run_benchmark/config.vsh.yaml +++ b/src/workflows/run_benchmark/config.vsh.yaml @@ -74,7 +74,7 @@ dependencies: - name: methods/guanlab_dengkw_pm - name: methods/novel - name: methods/simple_mlp - - name: methods/senkin + - name: methods/senkin_tmp - name: metrics/correlation - name: metrics/mse runners: diff --git a/src/workflows/run_benchmark/main.nf b/src/workflows/run_benchmark/main.nf index 47c64862..890d8b1f 100644 --- a/src/workflows/run_benchmark/main.nf +++ b/src/workflows/run_benchmark/main.nf @@ -19,7 +19,7 @@ methods = [ guanlab_dengkw_pm, novel, simple_mlp, - senkin + senkin_tmp ] // construct list of metrics From afc9aad4fe21a64fb1dcd32b26297cc613da8d0b Mon Sep 17 00:00:00 2001 From: Robrecht Cannoodt Date: Thu, 30 Jul 2026 11:56:22 +0200 Subject: [PATCH 6/8] cap senkin_tmp's training settings during viash test * Cap the boosting rounds, early stopping patience and nn epochs with `info.test_default`, the way #57 did for the other two train components * Fold the senkin_tmp fixture into the skip-if-up-to-date structure that came in with #57 --- scripts/create_datasets/test_resources.sh | 20 ++++++++++++------- .../senkin_tmp_train/config.vsh.yaml | 6 ++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/scripts/create_datasets/test_resources.sh b/scripts/create_datasets/test_resources.sh index 07d79d10..00b393d7 100755 --- a/scripts/create_datasets/test_resources.sh +++ b/scripts/create_datasets/test_resources.sh @@ -93,13 +93,19 @@ for name in bmmc_cite/normal bmmc_cite/swap bmmc_multiome/normal bmmc_multiome/s # senkin_tmp is CITE-only if [[ "$name" == bmmc_cite/normal ]]; then echo "pre-train senkin_tmp on $name" - [ -d $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin_tmp/ ] && rm -r $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin_tmp/ - mkdir -p $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin_tmp/ - viash run src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml -- \ - --input_train_mod1 $OUTPUT_DIR/openproblems_neurips2021/$name/train_mod1.h5ad \ - --input_train_mod2 $OUTPUT_DIR/openproblems_neurips2021/$name/train_mod2.h5ad \ - --input_test_mod1 $OUTPUT_DIR/openproblems_neurips2021/$name/test_mod1.h5ad \ - --output $OUTPUT_DIR/openproblems_neurips2021/$name/models/senkin_tmp/model.pkl + if up_to_date $DATASET_DIR/$name/models/senkin_tmp/model.pkl $STATE; then + echo " already up to date, skipping" + else + mkdir -p $DATASET_DIR/$name/models/senkin_tmp/ + viash run src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml -- \ + --input_train_mod1 $DATASET_DIR/$name/train_mod1.h5ad \ + --input_train_mod2 $DATASET_DIR/$name/train_mod2.h5ad \ + --input_test_mod1 $DATASET_DIR/$name/test_mod1.h5ad \ + --lgbm_boost_rounds 50 \ + --lgbm_early_stopping 10 \ + --nn_epochs 2 \ + --output $DATASET_DIR/$name/models/senkin_tmp/model.pkl + fi fi echo "pre-train novel on $name" diff --git a/src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml b/src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml index 2eca41a4..dec48570 100644 --- a/src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml +++ b/src/methods/senkin_tmp/senkin_tmp_train/config.vsh.yaml @@ -12,14 +12,20 @@ arguments: type: integer default: 10000 description: Maximum LightGBM boosting rounds (early stopping applies). + info: + test_default: 50 - name: "--lgbm_early_stopping" type: integer default: 100 description: LightGBM early stopping patience (rounds without improvement). + info: + test_default: 10 - name: "--nn_epochs" type: integer default: 100 description: Maximum neural network training epochs (early stopping applies). + info: + test_default: 2 - name: "--n_tsvd_components" type: integer default: 100 From 1bb043b40c76d74a18df81a344519c8424ce682f Mon Sep 17 00:00:00 2001 From: Robrecht Cannoodt Date: Thu, 30 Jul 2026 11:56:31 +0200 Subject: [PATCH 7/8] update label --- src/methods/senkin_tmp/senkin_tmp/config.vsh.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/methods/senkin_tmp/senkin_tmp/config.vsh.yaml b/src/methods/senkin_tmp/senkin_tmp/config.vsh.yaml index 9ee16355..8f8885d9 100644 --- a/src/methods/senkin_tmp/senkin_tmp/config.vsh.yaml +++ b/src/methods/senkin_tmp/senkin_tmp/config.vsh.yaml @@ -1,6 +1,6 @@ __merge__: ../../../api/comp_method.yaml name: senkin_tmp -label: SenKin_tmp +label: "senkin & tmp" summary: "LightGBM + bidirectional GRU ensemble for CITE-seq protein prediction (OpenProblems 2022 2nd place)" description: | Two-stage method from the OpenProblems NeurIPS 2021 competition. Stage 1 trains four From a7b44a26dfe99ea5febd02115b230635d0cdc9ef Mon Sep 17 00:00:00 2001 From: Robrecht Cannoodt Date: Thu, 30 Jul 2026 15:11:52 +0200 Subject: [PATCH 8/8] Add test resources --- src/methods/senkin_tmp/senkin_tmp_predict/config.vsh.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/methods/senkin_tmp/senkin_tmp_predict/config.vsh.yaml b/src/methods/senkin_tmp/senkin_tmp_predict/config.vsh.yaml index 107cd7dd..e24c556c 100644 --- a/src/methods/senkin_tmp/senkin_tmp_predict/config.vsh.yaml +++ b/src/methods/senkin_tmp/senkin_tmp_predict/config.vsh.yaml @@ -10,6 +10,9 @@ info: resources: - path: script.py type: python_script +test_resources: + - path: /resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal + dest: resources_test/task_predict_modality/openproblems_neurips2021/bmmc_cite/normal engines: - type: docker image: openproblems/base_pytorch_nvidia:1