From 22b267aca2d3f550d7db4be4b351c88a5d8671f5 Mon Sep 17 00:00:00 2001 From: jranek Date: Tue, 23 Jun 2026 11:27:15 -0700 Subject: [PATCH 1/6] update rng --- sketchKH/kh.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/sketchKH/kh.py b/sketchKH/kh.py index daa7e05..df0d241 100644 --- a/sketchKH/kh.py +++ b/sketchKH/kh.py @@ -1,5 +1,4 @@ import numpy as np -from tqdm import tqdm import anndata from typing import Union import scipy @@ -8,6 +7,7 @@ from numba import njit from joblib import Parallel, delayed from tqdm_joblib import tqdm_joblib +from scipy import sparse def random_feats(X: np.ndarray, gamma: Union[int, float] = 1, @@ -29,17 +29,14 @@ def random_feats(X: np.ndarray, ---------- """ scale = 1 / gamma - - if (frequency_seed is not None): - np.random.seed(frequency_seed) - W = np.random.normal(scale = scale, size = (X.shape[1], 1000)) - else: - W = np.random.normal(scale = scale, size = (X.shape[1], 1000)) + rng = np.random.RandomState(frequency_seed) + W = rng.normal(scale = scale, size = (X.shape[1], 1000)) XW = np.dot(X, W) - sin_XW = np.sin(XW) - cos_XW = np.cos(XW) - phi = np.concatenate((cos_XW, sin_XW), axis=1) + + phi = np.empty((X.shape[0], 2000)) + np.cos(XW, out = phi[:, :1000]) + np.sin(XW, out = phi[:, 1000:]) return phi From 68034f6cb32b11c8a7b7e8c0abbdb3e985511573 Mon Sep 17 00:00:00 2001 From: jranek Date: Tue, 23 Jun 2026 11:29:39 -0700 Subject: [PATCH 2/6] vectorize kh --- sketchKH/kh.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sketchKH/kh.py b/sketchKH/kh.py index df0d241..a78da32 100644 --- a/sketchKH/kh.py +++ b/sketchKH/kh.py @@ -69,13 +69,12 @@ def kernel_herding(phi: np.ndarray, num_subsamples: int): w_0 = np.copy(w_t) for subsample_idx in range(num_subsamples): #find argmax + scores = phi @ w_t max_score = -1e20 new_ind = -1 for cell_idx in range(num_cells): if selected_mask[cell_idx] == 0: - score = 0.0 - for feature_idx in range(num_features): - score += phi[cell_idx, feature_idx] * w_t[feature_idx] + score = scores[cell_idx] if score > max_score: max_score = score new_ind = cell_idx From fd99e5fa3b25ba8ad54078722fac92142619d476 Mon Sep 17 00:00:00 2001 From: jranek Date: Tue, 23 Jun 2026 11:32:25 -0700 Subject: [PATCH 3/6] sparse input data parsing --- sketchKH/kh.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/sketchKH/kh.py b/sketchKH/kh.py index a78da32..7282fb3 100644 --- a/sketchKH/kh.py +++ b/sketchKH/kh.py @@ -89,7 +89,7 @@ def kernel_herding(phi: np.ndarray, num_subsamples: int): return kh_indices def _parse_input(adata: anndata.AnnData): - """accesses and parses data from adata object + """Accesses and parses data from adata object Parameters adata: anndata.AnnData @@ -102,16 +102,13 @@ def _parse_input(adata: anndata.AnnData): array of data (dimensions = cells x features) ---------- """ - try: - if isinstance(adata, anndata.AnnData): - X = adata.X.copy() - if isinstance(X, scipy.sparse.csr_matrix): - X = np.asarray(X.todense()) - if is_numeric_dtype(adata.obs_names): - logging.warning('Converting cell IDs to strings.') - adata.obs_names = adata.obs_names.astype('str') - except NameError: - pass + if isinstance(adata, anndata.AnnData): + X = adata.X + if sparse.issparse(X): + X = X.toarray() + if is_numeric_dtype(adata.obs_names): + logging.warning('Converting cell IDs to strings.') + adata.obs_names = adata.obs_names.astype('str') return X From 8ce124f7dcc1114144301f6f3ea1ac51df3fa57b Mon Sep 17 00:00:00 2001 From: jranek Date: Tue, 23 Jun 2026 11:36:48 -0700 Subject: [PATCH 4/6] update parallelization --- sketchKH/kh.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sketchKH/kh.py b/sketchKH/kh.py index 7282fb3..47e5993 100644 --- a/sketchKH/kh.py +++ b/sketchKH/kh.py @@ -102,8 +102,7 @@ def _parse_input(adata: anndata.AnnData): array of data (dimensions = cells x features) ---------- """ - if isinstance(adata, anndata.AnnData): - X = adata.X + X = adata.X if sparse.issparse(X): X = X.toarray() if is_numeric_dtype(adata.obs_names): @@ -137,7 +136,7 @@ def kernel_herding_main(sample_set_ind, indices of subsampled cells within the sample-set ---------- """ - X = X[sample_set_ind, :] + X = X[sample_set_ind, :] #will create a copy phi = random_feats(X, gamma = gamma, frequency_seed = frequency_seed) kh_indices = kernel_herding(phi, num_subsamples) @@ -149,8 +148,9 @@ def sketch(adata, gamma: Union[int, float] = 1, frequency_seed: int = None, num_subsamples: int = 500, - n_jobs: int = -1): - """constructs a sketch using kernel herding and random Fourier frequency features + backend: str = 'threading', + n_jobs: int = 1): + """Constructs a sketch using kernel herding and random Fourier frequency features Parameters adata: anndata.Anndata @@ -168,7 +168,9 @@ def sketch(adata, random state parameter num_samples: int (default = None) number of cells to subsample per sample-set - n_jobs: int (default = -1) + backend: str (default = 'threading') + backend for parallelization + n_jobs: int (default = 1) number of tasks ---------- @@ -202,8 +204,8 @@ def sketch(adata, def process_set(i, inds): return kernel_herding_main(sample_set_ind = inds, X = X, gamma = gamma, frequency_seed = frequency_seed, num_subsamples = num_subsamples) - with tqdm_joblib(tqdm(desc="Performing subsampling", total = n_sample_sets)): - kh_indices = Parallel(n_jobs=n_jobs)(delayed(process_set)(i, inds) for i, inds in enumerate(sample_set_inds)) + with tqdm_joblib(total=n_sample_sets, desc='Performing subsampling'): + kh_indices = Parallel(n_jobs=n_jobs, backend=backend)(delayed(process_set)(i, inds) for i, inds in enumerate(sample_set_inds)) subsampled_cell_indices = [sample_set_inds[i][kh_indices[i]] for i in range(n_sample_sets)] subsampled_cell_indices = np.concatenate(subsampled_cell_indices) From 1de68a8ce82dabb02be4e6c9570e8e3633186790 Mon Sep 17 00:00:00 2001 From: jranek Date: Tue, 23 Jun 2026 12:08:34 -0700 Subject: [PATCH 5/6] update tests --- .github/workflows/build.yml | 28 ++++++++++++++++------------ README.md | 4 ++-- pyproject.toml | 4 ++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8afb81..2514abc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,40 +2,44 @@ name: Build and Test on: push: + branches: ["main", "ci_optimization"] + pull_request: + branches: ["main"] permissions: - contents: read # to fetch code (actions/checkout) + contents: read jobs: - build_wheels: - name: ${{ matrix.os }} Wheels + test: + name: ${{ matrix.os }} - Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} strategy: - # Ensure that a wheel builder finishes even if another fails fail-fast: false matrix: - os: [windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macos-latest] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Checkout ${{ github.repository }} - uses: actions/checkout@v3 - with: - fetch-depth: 0 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Dependencies + - name: Install package run: pip install . - name: Test run: | - python -c "\ + python -c " import anndata from sketchKH import * adata = anndata.read_h5ad('data/nk_cell_preprocessed.h5ad') kh_indices, adata_subsample = sketch(adata, sample_set_key = 'FCS_File', gamma = 1, num_subsamples = 500, frequency_seed = 0, n_jobs = 4) - " + + n_samples = adata.obs['FCS_File'].nunique() + assert len(kh_indices) == n_samples, f'Expected {n_samples} indices, Actual {len(kh_indices)}' + assert adata_subsample.shape == (10000, 43), f'Expected shape (10000, 43), Actual {adata_subsample.shape}' + " \ No newline at end of file diff --git a/README.md b/README.md index 10dced5..2f7b9c4 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ Distribution-Informed Sketching with Kernel Herding ## Overview We provide a set of functions for distribution-aware sketching of multiple profiled single-cell samples via Kernel Herding. Our sketches select a small, representative set of cells from each profiled sample so that all major immune cell-types and their relative frequencies are well-represented. * Please see our paper for more information (ACM-BCB 2022) : https://arxiv.org/abs/2207.00584 -* Updated : March 17, 2026 +* Updated : June 23, 2026 ![Sketching via KH Overview](https://github.com/CompCy-lab/SketchKH/blob/main/sketch_overview.png?raw=True) ## Installation Dependencies -* Python >= 3.6, anndata >= 0.7.6, numpy >= 1.22.4, scipy >= 1.7.1, numba, joblib, tqdm_joblib +* Python >= 3.8, anndata >= 0.7.6, numpy >= 1.22.4, scipy >= 1.7.1, numba, joblib, tqdm_joblib You can install the package with `pip` by, ``` diff --git a/pyproject.toml b/pyproject.toml index f1a16a7..6f11b21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta" [project] name = "sketchKH" -version = "0.1.3" +version = "0.1.4" description = "Distribution-based sketching of single-cell samples" readme = "README.md" -requires-python = ">=3.6" +requires-python = ">=3.8" authors = [ { name = "CompCy Lab", email = "compcylab@gmail.com" } ] From 7e43da21bd0fe0de07c9ecd6a367b234b0770f88 Mon Sep 17 00:00:00 2001 From: jranek Date: Tue, 23 Jun 2026 12:10:38 -0700 Subject: [PATCH 6/6] update parallelization --- sketchKH/kh.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sketchKH/kh.py b/sketchKH/kh.py index 47e5993..01b978b 100644 --- a/sketchKH/kh.py +++ b/sketchKH/kh.py @@ -1,7 +1,6 @@ import numpy as np import anndata from typing import Union -import scipy import logging from pandas.api.types import is_numeric_dtype from numba import njit