campmc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- campmc/__init__.py +13 -0
- campmc/_logging.py +13 -0
- campmc/coreset.py +54 -0
- campmc/io.py +31 -0
- campmc/metrics/__init__.py +5 -0
- campmc/metrics/quality.py +105 -0
- campmc/pl/__init__.py +5 -0
- campmc/pl/plots.py +45 -0
- campmc/pp/__init__.py +5 -0
- campmc/pp/preprocess.py +49 -0
- campmc/rare/__init__.py +5 -0
- campmc/rare/recovery.py +72 -0
- campmc/robustness/__init__.py +9 -0
- campmc/robustness/sweeps.py +93 -0
- campmc/tl/__init__.py +5 -0
- campmc/tl/assign/__init__.py +18 -0
- campmc/tl/assign/camp1.py +24 -0
- campmc/tl/assign/camp2.py +43 -0
- campmc/tl/assign/camp3.py +37 -0
- campmc/tl/assign/camp4.py +40 -0
- campmc/tl/partition.py +126 -0
- campmc-0.1.0.dist-info/METADATA +93 -0
- campmc-0.1.0.dist-info/RECORD +25 -0
- campmc-0.1.0.dist-info/WHEEL +4 -0
- campmc-0.1.0.dist-info/licenses/LICENSE +21 -0
campmc/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""CAMP metacell partitioning for AnnData (third-party portable implementation).
|
|
2
|
+
|
|
3
|
+
This package is not maintained by the authors of the CAMP paper. See the
|
|
4
|
+
upstream repository at https://github.com/danrongLi/CAMP for the reference code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from campmc import metrics, pl, pp, rare, robustness, tl
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__", "metrics", "pl", "pp", "rare", "robustness", "tl"]
|
campmc/_logging.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_logger(name: str) -> logging.Logger:
|
|
7
|
+
logger = logging.getLogger(name)
|
|
8
|
+
if not logger.handlers:
|
|
9
|
+
handler = logging.StreamHandler()
|
|
10
|
+
handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
|
11
|
+
logger.addHandler(handler)
|
|
12
|
+
logger.setLevel(logging.INFO)
|
|
13
|
+
return logger
|
campmc/coreset.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from anndata import AnnData
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
|
|
9
|
+
from campmc._logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_q(adata: AnnData, *, rep_key: str = "X_pca") -> np.ndarray:
|
|
15
|
+
"""Importance weights q(x) from distance to mean in representation space."""
|
|
16
|
+
X = adata.obsm[rep_key]
|
|
17
|
+
if sparse.issparse(X):
|
|
18
|
+
x_csr = X.tocsr(copy=False)
|
|
19
|
+
mu = np.asarray(x_csr.mean(axis=0)).ravel()
|
|
20
|
+
x_norm_sq = np.asarray(x_csr.power(2).sum(axis=1)).ravel()
|
|
21
|
+
mu_norm_sq = float(mu @ mu)
|
|
22
|
+
prod = x_csr @ mu
|
|
23
|
+
d_sq = x_norm_sq + mu_norm_sq - 2.0 * prod
|
|
24
|
+
else:
|
|
25
|
+
x_dense = np.asarray(X, dtype=np.float64)
|
|
26
|
+
mu = x_dense.mean(axis=0)
|
|
27
|
+
d_sq = np.sum((x_dense - mu) ** 2, axis=1)
|
|
28
|
+
|
|
29
|
+
n = adata.n_obs
|
|
30
|
+
denom = float(d_sq.sum())
|
|
31
|
+
if denom == 0.0:
|
|
32
|
+
logger.warning("All points coincide; returning uniform q.")
|
|
33
|
+
return np.full(n, 1.0 / n)
|
|
34
|
+
|
|
35
|
+
q = 0.5 * (1.0 / n) + 0.5 * (d_sq / denom)
|
|
36
|
+
q /= q.sum()
|
|
37
|
+
return q
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def gammas_to_m(n_obs: int, gammas: Sequence[int]) -> list[int]:
|
|
41
|
+
return [max(1, int(n_obs / g)) for g in gammas]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sample_coreset(
|
|
45
|
+
n_obs: int,
|
|
46
|
+
m: int,
|
|
47
|
+
q: np.ndarray,
|
|
48
|
+
*,
|
|
49
|
+
random_state: int = 0,
|
|
50
|
+
) -> np.ndarray:
|
|
51
|
+
"""Return `m` unique cell indices sampled with probabilities `q`."""
|
|
52
|
+
rng = np.random.default_rng(random_state)
|
|
53
|
+
m = min(m, n_obs)
|
|
54
|
+
return rng.choice(n_obs, size=m, replace=False, p=q)
|
campmc/io.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from anndata import AnnData
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def write_membership(adata: AnnData, path: str | Path, *, key_added: str = "camp") -> None:
|
|
10
|
+
"""Write partition labels and seed flags to CSV (CAMP membership schema)."""
|
|
11
|
+
path = Path(path)
|
|
12
|
+
label_col = key_added
|
|
13
|
+
seed_col = f"{key_added}_is_seed"
|
|
14
|
+
if label_col not in adata.obs:
|
|
15
|
+
raise KeyError(f"obs missing '{label_col}'")
|
|
16
|
+
if seed_col not in adata.obs:
|
|
17
|
+
raise KeyError(f"obs missing '{seed_col}'")
|
|
18
|
+
|
|
19
|
+
df = pd.DataFrame(
|
|
20
|
+
{
|
|
21
|
+
label_col: adata.obs[label_col].astype(str),
|
|
22
|
+
seed_col: adata.obs[seed_col].astype(bool),
|
|
23
|
+
},
|
|
24
|
+
index=adata.obs_names,
|
|
25
|
+
)
|
|
26
|
+
df.to_csv(path)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def read_membership(path: str | Path) -> pd.DataFrame:
|
|
30
|
+
path = Path(path)
|
|
31
|
+
return pd.read_csv(path, index_col=0)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import scanpy as sc
|
|
6
|
+
from anndata import AnnData
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
from scipy.stats import entropy as scipy_entropy
|
|
9
|
+
from sklearn.metrics import pairwise_distances
|
|
10
|
+
|
|
11
|
+
from campmc._logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _purity(cell_labels: pd.Series) -> float:
|
|
17
|
+
counts = cell_labels.value_counts(dropna=False)
|
|
18
|
+
if len(counts) == 0:
|
|
19
|
+
return np.nan
|
|
20
|
+
return float(counts.max()) / float(counts.sum())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _label_entropy(cell_labels: pd.Series) -> float:
|
|
24
|
+
counts = cell_labels.value_counts(dropna=False).values.astype(float)
|
|
25
|
+
if counts.sum() == 0:
|
|
26
|
+
return np.nan
|
|
27
|
+
p = counts / counts.sum()
|
|
28
|
+
return float(scipy_entropy(p, base=2))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _compactness(diff_coords: np.ndarray) -> float:
|
|
32
|
+
return float(-np.var(diff_coords, axis=0).mean())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _inv(expr_block) -> float:
|
|
36
|
+
x = expr_block.toarray() if sparse.issparse(expr_block) else np.asarray(expr_block)
|
|
37
|
+
gene_var = np.var(x, axis=0)
|
|
38
|
+
gene_mean = np.mean(x, axis=0)
|
|
39
|
+
return float(np.percentile(gene_var / (gene_mean + 1e-8), 95))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ensure_diffmap(adata: AnnData, use_rep: str) -> None:
|
|
43
|
+
if "X_diffmap" in adata.obsm:
|
|
44
|
+
return
|
|
45
|
+
if use_rep not in adata.obsm:
|
|
46
|
+
raise KeyError(f"Missing obsm['{use_rep}'] for diffusion map computation.")
|
|
47
|
+
sc.pp.neighbors(adata, use_rep=use_rep)
|
|
48
|
+
sc.tl.diffmap(adata)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def quality(
|
|
52
|
+
adata: AnnData,
|
|
53
|
+
*,
|
|
54
|
+
partition_key: str = "camp",
|
|
55
|
+
label_key: str | None = None,
|
|
56
|
+
use_rep: str = "X_pca",
|
|
57
|
+
) -> pd.DataFrame:
|
|
58
|
+
"""Per-metacell quality metrics (compactness, separation, INV, optional purity)."""
|
|
59
|
+
if partition_key not in adata.obs:
|
|
60
|
+
raise KeyError(f"obs missing '{partition_key}'")
|
|
61
|
+
|
|
62
|
+
work = adata.copy()
|
|
63
|
+
_ensure_diffmap(work, use_rep)
|
|
64
|
+
x_diff = work.obsm["X_diffmap"]
|
|
65
|
+
x_diff_dense = x_diff if not sparse.issparse(x_diff) else x_diff.toarray()
|
|
66
|
+
work.obs["metacell"] = work.obs[partition_key].astype(str)
|
|
67
|
+
grouped = work.obs.groupby("metacell", observed=True)
|
|
68
|
+
|
|
69
|
+
centroids: dict[str, np.ndarray] = {}
|
|
70
|
+
for metacell_id, group in grouped:
|
|
71
|
+
ix = work.obs.index.get_indexer(group.index)
|
|
72
|
+
centroids[metacell_id] = x_diff_dense[ix].mean(axis=0)
|
|
73
|
+
|
|
74
|
+
mc_ids = list(centroids.keys())
|
|
75
|
+
if not mc_ids:
|
|
76
|
+
return pd.DataFrame()
|
|
77
|
+
|
|
78
|
+
c_mat = np.vstack([centroids[mid] for mid in mc_ids])
|
|
79
|
+
dist = pairwise_distances(c_mat, c_mat)
|
|
80
|
+
np.fill_diagonal(dist, np.inf)
|
|
81
|
+
nearest_sep = np.min(dist, axis=1)
|
|
82
|
+
sep_map = {mid: float(nearest_sep[j]) for j, mid in enumerate(mc_ids)}
|
|
83
|
+
|
|
84
|
+
rows: list[dict] = []
|
|
85
|
+
for metacell_id, group in grouped:
|
|
86
|
+
ix = work.obs.index.get_indexer(group.index)
|
|
87
|
+
diff_coords = x_diff_dense[ix]
|
|
88
|
+
compact = _compactness(diff_coords)
|
|
89
|
+
separation = sep_map[metacell_id]
|
|
90
|
+
row = {
|
|
91
|
+
"metacell_id": metacell_id,
|
|
92
|
+
"size": len(group),
|
|
93
|
+
"compactness": compact,
|
|
94
|
+
"separation": separation,
|
|
95
|
+
"sc_ratio": separation / np.sqrt(max(1e-12, -compact)),
|
|
96
|
+
"INV": _inv(work.X[ix, :]),
|
|
97
|
+
}
|
|
98
|
+
if label_key is not None and label_key in work.obs:
|
|
99
|
+
labels = work.obs.loc[group.index, label_key]
|
|
100
|
+
row["purity"] = _purity(labels.astype(str))
|
|
101
|
+
row["label_entropy"] = _label_entropy(labels.astype(str))
|
|
102
|
+
rows.append(row)
|
|
103
|
+
|
|
104
|
+
logger.info("quality metrics for %d metacells", len(rows))
|
|
105
|
+
return pd.DataFrame(rows)
|
campmc/pl/__init__.py
ADDED
campmc/pl/plots.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import seaborn as sns
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def metric_ecdf(values: np.ndarray, out_png: str | Path, *, title: str = "Metric ECDF") -> None:
|
|
12
|
+
vals = np.asarray(values, dtype=float)
|
|
13
|
+
vals = vals[np.isfinite(vals)]
|
|
14
|
+
if vals.size == 0:
|
|
15
|
+
raise ValueError("No finite values to plot.")
|
|
16
|
+
x = np.sort(vals)
|
|
17
|
+
y = np.arange(1, x.size + 1) / x.size
|
|
18
|
+
fig, ax = plt.subplots(figsize=(5, 4))
|
|
19
|
+
ax.plot(x, y, linewidth=2)
|
|
20
|
+
ax.set_xlabel("value")
|
|
21
|
+
ax.set_ylabel("ECDF")
|
|
22
|
+
ax.set_title(title)
|
|
23
|
+
fig.tight_layout()
|
|
24
|
+
fig.savefig(out_png, dpi=150)
|
|
25
|
+
plt.close(fig)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def metric_lines(
|
|
29
|
+
df: pd.DataFrame,
|
|
30
|
+
out_png: str | Path,
|
|
31
|
+
*,
|
|
32
|
+
x_col: str,
|
|
33
|
+
y_col: str,
|
|
34
|
+
hue_col: str | None = None,
|
|
35
|
+
title: str = "Metric",
|
|
36
|
+
) -> None:
|
|
37
|
+
fig, ax = plt.subplots(figsize=(6, 4))
|
|
38
|
+
if hue_col:
|
|
39
|
+
sns.lineplot(data=df, x=x_col, y=y_col, hue=hue_col, ax=ax)
|
|
40
|
+
else:
|
|
41
|
+
sns.lineplot(data=df, x=x_col, y=y_col, ax=ax)
|
|
42
|
+
ax.set_title(title)
|
|
43
|
+
fig.tight_layout()
|
|
44
|
+
fig.savefig(out_png, dpi=150)
|
|
45
|
+
plt.close(fig)
|
campmc/pp/__init__.py
ADDED
campmc/pp/preprocess.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import scanpy as sc
|
|
5
|
+
from anndata import AnnData
|
|
6
|
+
from scipy import sparse
|
|
7
|
+
from scipy.sparse import csr_matrix
|
|
8
|
+
|
|
9
|
+
from campmc._logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def preprocess(
|
|
15
|
+
adata: AnnData,
|
|
16
|
+
*,
|
|
17
|
+
min_genes: int = 200,
|
|
18
|
+
min_cells: int = 3,
|
|
19
|
+
n_hvg: int = 2000,
|
|
20
|
+
n_pcs: int = 50,
|
|
21
|
+
target_sum: float = 1e4,
|
|
22
|
+
copy: bool = False,
|
|
23
|
+
) -> AnnData | None:
|
|
24
|
+
if copy:
|
|
25
|
+
adata = adata.copy()
|
|
26
|
+
|
|
27
|
+
sc.pp.filter_cells(adata, min_genes=min_genes)
|
|
28
|
+
sc.pp.filter_genes(adata, min_cells=min_cells)
|
|
29
|
+
|
|
30
|
+
x_dense = adata.X.A if sparse.issparse(adata.X) else adata.X
|
|
31
|
+
if np.any(x_dense < 0):
|
|
32
|
+
if sparse.issparse(adata.X):
|
|
33
|
+
adata.X = csr_matrix(np.maximum(adata.X.toarray(), 0))
|
|
34
|
+
else:
|
|
35
|
+
adata.X = np.maximum(adata.X, 0)
|
|
36
|
+
|
|
37
|
+
sc.pp.normalize_total(adata, target_sum=target_sum)
|
|
38
|
+
sc.pp.log1p(adata)
|
|
39
|
+
if not sparse.issparse(adata.X):
|
|
40
|
+
adata.X = csr_matrix(adata.X)
|
|
41
|
+
|
|
42
|
+
sc.pp.highly_variable_genes(adata, n_top_genes=min(n_hvg, adata.n_vars - 1), flavor="seurat")
|
|
43
|
+
keep = np.asarray(adata.var["highly_variable"]).ravel()
|
|
44
|
+
adata._inplace_subset_var(keep)
|
|
45
|
+
|
|
46
|
+
sc.pp.scale(adata, max_value=10)
|
|
47
|
+
sc.tl.pca(adata, n_comps=min(n_pcs, adata.n_vars - 1, adata.n_obs - 1), svd_solver="arpack")
|
|
48
|
+
logger.info("preprocess done: shape=%s", adata.shape)
|
|
49
|
+
return adata if copy else None
|
campmc/rare/__init__.py
ADDED
campmc/rare/recovery.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from anndata import AnnData
|
|
5
|
+
from sklearn.metrics import adjusted_rand_score, normalized_mutual_info_score
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def identify_rare_types(labels: pd.Series, *, rare_threshold: float = 0.01) -> pd.DataFrame:
|
|
9
|
+
counts = labels.value_counts(dropna=False)
|
|
10
|
+
freq = counts / counts.sum()
|
|
11
|
+
rare = freq[freq <= rare_threshold].sort_values()
|
|
12
|
+
return pd.DataFrame(
|
|
13
|
+
{
|
|
14
|
+
"cell_type": rare.index.astype(str),
|
|
15
|
+
"n_cells": counts.loc[rare.index].values.astype(int),
|
|
16
|
+
"fraction": rare.values.astype(float),
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _balanced_accuracy(true: pd.Series, pred: pd.Series, rare_types: list[str]) -> float:
|
|
22
|
+
recalls: list[float] = []
|
|
23
|
+
for ct in rare_types:
|
|
24
|
+
pos_mask = true == ct
|
|
25
|
+
n_pos = int(pos_mask.sum())
|
|
26
|
+
if n_pos == 0:
|
|
27
|
+
continue
|
|
28
|
+
tp = int(((pred == ct) & pos_mask).sum())
|
|
29
|
+
recalls.append(tp / n_pos)
|
|
30
|
+
if not recalls:
|
|
31
|
+
return float("nan")
|
|
32
|
+
return float(sum(recalls) / len(recalls))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def recovery_scores(
|
|
36
|
+
adata: AnnData,
|
|
37
|
+
*,
|
|
38
|
+
partition_key: str,
|
|
39
|
+
label_key: str,
|
|
40
|
+
rare_threshold: float = 0.01,
|
|
41
|
+
) -> pd.DataFrame:
|
|
42
|
+
if partition_key not in adata.obs:
|
|
43
|
+
raise KeyError(partition_key)
|
|
44
|
+
if label_key not in adata.obs:
|
|
45
|
+
raise KeyError(label_key)
|
|
46
|
+
|
|
47
|
+
labels = adata.obs[label_key].astype(str)
|
|
48
|
+
assign = adata.obs[partition_key].astype(str)
|
|
49
|
+
rare_df = identify_rare_types(labels, rare_threshold=rare_threshold)
|
|
50
|
+
rare_types = rare_df["cell_type"].tolist()
|
|
51
|
+
|
|
52
|
+
contingency = pd.crosstab(assign, labels)
|
|
53
|
+
mc_majority = contingency.idxmax(axis=1)
|
|
54
|
+
pred = assign.map(mc_majority).astype(str)
|
|
55
|
+
|
|
56
|
+
mask = labels.isin(rare_types)
|
|
57
|
+
ari = float("nan")
|
|
58
|
+
nmi = float("nan")
|
|
59
|
+
if mask.sum() > 1:
|
|
60
|
+
ari = float(adjusted_rand_score(labels.loc[mask], pred.loc[mask]))
|
|
61
|
+
nmi = float(normalized_mutual_info_score(labels.loc[mask], pred.loc[mask]))
|
|
62
|
+
|
|
63
|
+
return pd.DataFrame(
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
"balanced_accuracy": _balanced_accuracy(labels, pred, rare_types),
|
|
67
|
+
"ari": ari,
|
|
68
|
+
"nmi": nmi,
|
|
69
|
+
"n_rare_types": len(rare_types),
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import scanpy as sc
|
|
8
|
+
from anndata import AnnData
|
|
9
|
+
from scipy import sparse
|
|
10
|
+
|
|
11
|
+
from campmc._logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _mkdir(path: Path) -> None:
|
|
17
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_or_load_base_cache(
|
|
21
|
+
input_file: str | Path,
|
|
22
|
+
dataset_name: str,
|
|
23
|
+
cache_dir: str | Path,
|
|
24
|
+
*,
|
|
25
|
+
min_genes: int = 200,
|
|
26
|
+
min_cells: int = 3,
|
|
27
|
+
normalize_target_sum: float = 1e4,
|
|
28
|
+
) -> Path:
|
|
29
|
+
cache_dir = Path(cache_dir)
|
|
30
|
+
_mkdir(cache_dir)
|
|
31
|
+
out = cache_dir / f"{dataset_name}_BASE_log1p.h5ad"
|
|
32
|
+
if out.exists():
|
|
33
|
+
logger.info("Using base cache %s", out)
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
adata = sc.read_h5ad(input_file)
|
|
37
|
+
sc.pp.filter_cells(adata, min_genes=min_genes)
|
|
38
|
+
sc.pp.filter_genes(adata, min_cells=min_cells)
|
|
39
|
+
if sparse.issparse(adata.X):
|
|
40
|
+
adata.X.data = np.maximum(adata.X.data, 0)
|
|
41
|
+
sc.pp.normalize_total(adata, target_sum=normalize_target_sum)
|
|
42
|
+
sc.pp.log1p(adata)
|
|
43
|
+
adata.write_h5ad(out, compression="gzip")
|
|
44
|
+
meta = cache_dir / f"{dataset_name}_BASE_log1p.meta.json"
|
|
45
|
+
meta.write_text(
|
|
46
|
+
json.dumps({"dataset_name": dataset_name, "source": str(input_file)}, indent=2),
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
)
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_or_load_hvg_pca_cache(
|
|
53
|
+
base_h5ad: str | Path,
|
|
54
|
+
dataset_name: str,
|
|
55
|
+
cache_dir: str | Path,
|
|
56
|
+
*,
|
|
57
|
+
n_hvg: int = 2000,
|
|
58
|
+
max_pcs: int = 50,
|
|
59
|
+
random_state: int = 0,
|
|
60
|
+
) -> Path:
|
|
61
|
+
cache_dir = Path(cache_dir)
|
|
62
|
+
_mkdir(cache_dir)
|
|
63
|
+
out = cache_dir / f"{dataset_name}_HVG{n_hvg}_PCA{max_pcs}.h5ad"
|
|
64
|
+
if out.exists():
|
|
65
|
+
logger.info("Using HVG/PCA cache %s", out)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
adata = sc.read_h5ad(base_h5ad)
|
|
69
|
+
n_hvg_use = min(n_hvg, adata.n_vars)
|
|
70
|
+
sc.pp.highly_variable_genes(adata, n_top_genes=n_hvg_use, flavor="seurat")
|
|
71
|
+
adata = adata[:, adata.var["highly_variable"]].copy()
|
|
72
|
+
sc.pp.scale(adata, max_value=10)
|
|
73
|
+
n_pcs = min(max_pcs, max(2, adata.n_vars - 1), adata.n_obs - 1)
|
|
74
|
+
sc.tl.pca(adata, n_comps=n_pcs, svd_solver="arpack", random_state=random_state)
|
|
75
|
+
adata.write_h5ad(out, compression="gzip")
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_hvg_pca_sweep(
|
|
80
|
+
input_file: str | Path,
|
|
81
|
+
dataset_name: str,
|
|
82
|
+
cache_dir: str | Path,
|
|
83
|
+
hvg_values: list[int],
|
|
84
|
+
pca_values: list[int],
|
|
85
|
+
) -> list[AnnData]:
|
|
86
|
+
"""Build cached AnnData objects for each HVG/PCA combination."""
|
|
87
|
+
base = build_or_load_base_cache(input_file, dataset_name, cache_dir)
|
|
88
|
+
results: list[AnnData] = []
|
|
89
|
+
for n_hvg in hvg_values:
|
|
90
|
+
for n_pcs in pca_values:
|
|
91
|
+
path = build_or_load_hvg_pca_cache(base, dataset_name, cache_dir, n_hvg=n_hvg, max_pcs=n_pcs)
|
|
92
|
+
results.append(sc.read_h5ad(path))
|
|
93
|
+
return results
|
campmc/tl/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from campmc.tl.assign import camp1, camp2, camp3, camp4
|
|
6
|
+
|
|
7
|
+
Method = Literal["camp1", "camp2", "camp3", "camp4"]
|
|
8
|
+
|
|
9
|
+
_DISPATCH = {
|
|
10
|
+
"camp1": camp1,
|
|
11
|
+
"camp2": camp2,
|
|
12
|
+
"camp3": camp3,
|
|
13
|
+
"camp4": camp4,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_assigner(method: Method):
|
|
18
|
+
return _DISPATCH[method]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from sklearn.cluster import KMeans
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def assign_labels(
|
|
8
|
+
X: np.ndarray,
|
|
9
|
+
seed_idx: np.ndarray,
|
|
10
|
+
*,
|
|
11
|
+
random_state: int = 0,
|
|
12
|
+
) -> np.ndarray:
|
|
13
|
+
seed_emb = X[seed_idx]
|
|
14
|
+
m = seed_emb.shape[0]
|
|
15
|
+
km = KMeans(
|
|
16
|
+
n_clusters=m,
|
|
17
|
+
init=seed_emb,
|
|
18
|
+
n_init=1,
|
|
19
|
+
max_iter=10,
|
|
20
|
+
algorithm="lloyd",
|
|
21
|
+
random_state=random_state,
|
|
22
|
+
)
|
|
23
|
+
km.fit(X)
|
|
24
|
+
return km.labels_.astype(np.int64)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _unit_rows(X: np.ndarray) -> np.ndarray:
|
|
7
|
+
norms = np.linalg.norm(X, axis=1, keepdims=True)
|
|
8
|
+
norms = np.maximum(norms, 1e-12)
|
|
9
|
+
return X / norms
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def assign_labels(
|
|
13
|
+
X: np.ndarray,
|
|
14
|
+
seed_idx: np.ndarray,
|
|
15
|
+
*,
|
|
16
|
+
random_state: int = 0,
|
|
17
|
+
) -> np.ndarray:
|
|
18
|
+
del random_state
|
|
19
|
+
x_unit = _unit_rows(np.asarray(X, dtype=np.float64))
|
|
20
|
+
seeds = _unit_rows(x_unit[seed_idx])
|
|
21
|
+
m = seeds.shape[0]
|
|
22
|
+
bs = 100_000
|
|
23
|
+
parts: list[np.ndarray] = []
|
|
24
|
+
for s in range(0, x_unit.shape[0], bs):
|
|
25
|
+
block = x_unit[s : s + bs] @ seeds.T
|
|
26
|
+
parts.append(np.argmax(block, axis=1))
|
|
27
|
+
labels_idx = np.concatenate(parts, axis=0)
|
|
28
|
+
|
|
29
|
+
new_seeds = np.zeros_like(seeds)
|
|
30
|
+
for k in range(m):
|
|
31
|
+
members = labels_idx == k
|
|
32
|
+
if np.any(members):
|
|
33
|
+
c = x_unit[members].mean(axis=0)
|
|
34
|
+
nrm = np.linalg.norm(c)
|
|
35
|
+
new_seeds[k] = c / nrm if nrm > 0 else seeds[k]
|
|
36
|
+
else:
|
|
37
|
+
new_seeds[k] = seeds[k]
|
|
38
|
+
|
|
39
|
+
parts = []
|
|
40
|
+
for s in range(0, x_unit.shape[0], bs):
|
|
41
|
+
block = x_unit[s : s + bs] @ new_seeds.T
|
|
42
|
+
parts.append(np.argmax(block, axis=1))
|
|
43
|
+
return np.concatenate(parts, axis=0).astype(np.int64)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from sklearn.metrics import pairwise_distances
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def assign_labels(
|
|
8
|
+
X: np.ndarray,
|
|
9
|
+
seed_idx: np.ndarray,
|
|
10
|
+
*,
|
|
11
|
+
random_state: int = 0,
|
|
12
|
+
k_scale: int = 15,
|
|
13
|
+
tau: float = 1.0,
|
|
14
|
+
batch_size: int = 20_000,
|
|
15
|
+
eps: float = 1e-8,
|
|
16
|
+
) -> np.ndarray:
|
|
17
|
+
del random_state
|
|
18
|
+
x_pca = np.asarray(X, dtype=np.float32)
|
|
19
|
+
xs = x_pca[seed_idx]
|
|
20
|
+
m = xs.shape[0]
|
|
21
|
+
k = max(1, min(k_scale, m - 1))
|
|
22
|
+
|
|
23
|
+
d_ss = pairwise_distances(xs, xs, metric="euclidean")
|
|
24
|
+
sigma_seed = np.partition(d_ss, kth=k, axis=1)[:, k].astype(np.float32)
|
|
25
|
+
sigma_seed = np.maximum(sigma_seed, eps)
|
|
26
|
+
|
|
27
|
+
parts: list[np.ndarray] = []
|
|
28
|
+
for s in range(0, x_pca.shape[0], batch_size):
|
|
29
|
+
xu = x_pca[s : s + batch_size]
|
|
30
|
+
d_us = pairwise_distances(xu, xs, metric="euclidean")
|
|
31
|
+
sigma_cell = np.partition(d_us, kth=k, axis=1)[:, k].astype(np.float32)
|
|
32
|
+
sigma_cell = np.maximum(sigma_cell, eps)
|
|
33
|
+
denom = tau * (sigma_cell[:, None] * sigma_seed[None, :]) + eps
|
|
34
|
+
w = np.exp(-(d_us**2) / denom, dtype=np.float32)
|
|
35
|
+
parts.append(np.argmax(w, axis=1))
|
|
36
|
+
|
|
37
|
+
return np.concatenate(parts, axis=0).astype(np.int64)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import SEACells.build_graph as seacell_build_graph
|
|
5
|
+
from anndata import AnnData
|
|
6
|
+
from scipy.sparse import csr_matrix
|
|
7
|
+
from tqdm import std as tqdm_std
|
|
8
|
+
|
|
9
|
+
# SEACells uses notebook tqdm, which breaks outside Jupyter.
|
|
10
|
+
seacell_build_graph.tqdm = tqdm_std.tqdm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def assign_labels(
|
|
14
|
+
adata: AnnData,
|
|
15
|
+
seed_names: list[str],
|
|
16
|
+
*,
|
|
17
|
+
reduction_key: str = "X_pca",
|
|
18
|
+
n_neighbors: int = 30,
|
|
19
|
+
graph_construction: str = "union",
|
|
20
|
+
chunk_size: int = 20_000,
|
|
21
|
+
) -> np.ndarray:
|
|
22
|
+
g = seacell_build_graph.SEACellGraph(adata, reduction_key, verbose=False)
|
|
23
|
+
k_mat = g.rbf(k=n_neighbors, graph_construction=graph_construction)
|
|
24
|
+
|
|
25
|
+
k_mat = k_mat.tocsr()
|
|
26
|
+
n_sub = adata.n_obs
|
|
27
|
+
m_seeds = len(seed_names)
|
|
28
|
+
seed_idx = [adata.obs_names.get_loc(s) for s in seed_names]
|
|
29
|
+
|
|
30
|
+
data = np.ones(m_seeds, dtype=np.float32)
|
|
31
|
+
rows = np.array(seed_idx, dtype=int)
|
|
32
|
+
cols = np.arange(m_seeds, dtype=int)
|
|
33
|
+
b_mat = csr_matrix((data, (rows, cols)), shape=(n_sub, m_seeds))
|
|
34
|
+
|
|
35
|
+
labels = np.empty(n_sub, dtype=int)
|
|
36
|
+
for start in range(0, n_sub, chunk_size):
|
|
37
|
+
end = min(start + chunk_size, n_sub)
|
|
38
|
+
block = k_mat[start:end, :].dot(b_mat).toarray()
|
|
39
|
+
labels[start:end] = block.argmax(axis=1)
|
|
40
|
+
return labels.astype(np.int64)
|
campmc/tl/partition.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from anndata import AnnData
|
|
8
|
+
|
|
9
|
+
from campmc._logging import get_logger
|
|
10
|
+
from campmc.coreset import compute_q, gammas_to_m, sample_coreset
|
|
11
|
+
from campmc.tl.assign import camp1, camp2, camp3, camp4
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
Method = Literal["camp1", "camp2", "camp3", "camp4"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _partition_one_batch(
|
|
19
|
+
adata: AnnData,
|
|
20
|
+
*,
|
|
21
|
+
method: Method,
|
|
22
|
+
gamma: int,
|
|
23
|
+
m: int,
|
|
24
|
+
batch_key: str | None,
|
|
25
|
+
reduction_key: str,
|
|
26
|
+
random_state: int,
|
|
27
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
28
|
+
labels = np.empty(adata.n_obs, dtype=object)
|
|
29
|
+
is_seed = np.zeros(adata.n_obs, dtype=bool)
|
|
30
|
+
|
|
31
|
+
if batch_key is None or batch_key not in adata.obs:
|
|
32
|
+
batches = [("__all__", np.arange(adata.n_obs))]
|
|
33
|
+
else:
|
|
34
|
+
batches = []
|
|
35
|
+
for anno in adata.obs[batch_key].astype(str).unique():
|
|
36
|
+
mask = (adata.obs[batch_key].astype(str) == anno).to_numpy()
|
|
37
|
+
batches.append((anno, np.where(mask)[0]))
|
|
38
|
+
|
|
39
|
+
q_global = compute_q(adata, rep_key=reduction_key)
|
|
40
|
+
|
|
41
|
+
for anno, idx in batches:
|
|
42
|
+
if idx.size <= 1:
|
|
43
|
+
labels[idx] = f"{anno}-metacell-1"
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
q_sub = q_global[idx]
|
|
47
|
+
q_sub = q_sub / q_sub.sum()
|
|
48
|
+
m_here = min(m, idx.size)
|
|
49
|
+
local_seed = sample_coreset(idx.size, m_here, q_sub, random_state=random_state)
|
|
50
|
+
global_seed = idx[local_seed]
|
|
51
|
+
seeds = adata.obs_names[global_seed].tolist()
|
|
52
|
+
|
|
53
|
+
x_emb = np.asarray(adata.obsm[reduction_key][idx])
|
|
54
|
+
if method == "camp1":
|
|
55
|
+
lab_local = camp1.assign_labels(x_emb, local_seed, random_state=random_state)
|
|
56
|
+
elif method == "camp2":
|
|
57
|
+
lab_local = camp2.assign_labels(x_emb, local_seed, random_state=random_state)
|
|
58
|
+
elif method == "camp3":
|
|
59
|
+
lab_local = camp3.assign_labels(x_emb, local_seed, random_state=random_state)
|
|
60
|
+
else:
|
|
61
|
+
sub = adata[idx].copy()
|
|
62
|
+
lab_local = camp4.assign_labels(sub, seeds, reduction_key=reduction_key)
|
|
63
|
+
|
|
64
|
+
label_str = [f"seed{lab}-{gamma}-{anno}" for lab in lab_local]
|
|
65
|
+
labels[idx] = label_str
|
|
66
|
+
is_seed[global_seed] = True
|
|
67
|
+
|
|
68
|
+
return labels, is_seed
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def partition(
|
|
72
|
+
adata: AnnData,
|
|
73
|
+
*,
|
|
74
|
+
method: Method = "camp1",
|
|
75
|
+
gamma: int | None = None,
|
|
76
|
+
gammas: Sequence[int] | None = None,
|
|
77
|
+
key_added: str = "camp",
|
|
78
|
+
batch_key: str | None = None,
|
|
79
|
+
reduction_key: str = "X_pca",
|
|
80
|
+
random_state: int = 0,
|
|
81
|
+
copy: bool = False,
|
|
82
|
+
) -> AnnData | None:
|
|
83
|
+
if reduction_key not in adata.obsm:
|
|
84
|
+
msg = f"Missing obsm['{reduction_key}']; run campmc.pp.preprocess or add PCA first."
|
|
85
|
+
raise KeyError(msg)
|
|
86
|
+
if (gamma is None) == (gammas is None):
|
|
87
|
+
raise ValueError("Provide exactly one of gamma or gammas.")
|
|
88
|
+
|
|
89
|
+
target = adata.copy() if copy else adata
|
|
90
|
+
meta: dict = {"method": method, "reduction_key": reduction_key, "random_state": random_state}
|
|
91
|
+
|
|
92
|
+
if gamma is not None:
|
|
93
|
+
m = gammas_to_m(target.n_obs, [gamma])[0]
|
|
94
|
+
labels, is_seed = _partition_one_batch(
|
|
95
|
+
target,
|
|
96
|
+
method=method,
|
|
97
|
+
gamma=gamma,
|
|
98
|
+
m=m,
|
|
99
|
+
batch_key=batch_key,
|
|
100
|
+
reduction_key=reduction_key,
|
|
101
|
+
random_state=random_state,
|
|
102
|
+
)
|
|
103
|
+
target.obs[key_added] = labels
|
|
104
|
+
target.obs[f"{key_added}_is_seed"] = is_seed
|
|
105
|
+
meta["gamma"] = gamma
|
|
106
|
+
meta["m"] = m
|
|
107
|
+
else:
|
|
108
|
+
assert gammas is not None
|
|
109
|
+
meta["gammas"] = list(gammas)
|
|
110
|
+
for g in gammas:
|
|
111
|
+
m = gammas_to_m(target.n_obs, [g])[0]
|
|
112
|
+
labels, is_seed = _partition_one_batch(
|
|
113
|
+
target,
|
|
114
|
+
method=method,
|
|
115
|
+
gamma=g,
|
|
116
|
+
m=m,
|
|
117
|
+
batch_key=batch_key,
|
|
118
|
+
reduction_key=reduction_key,
|
|
119
|
+
random_state=random_state,
|
|
120
|
+
)
|
|
121
|
+
target.obs[f"{key_added}_{g}"] = labels
|
|
122
|
+
target.obs[f"{key_added}_{g}_is_seed"] = is_seed
|
|
123
|
+
|
|
124
|
+
target.uns[key_added] = meta
|
|
125
|
+
logger.info("partition complete: method=%s key=%s", method, key_added)
|
|
126
|
+
return target if copy else None
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: campmc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Third-party portable Python package for CAMP metacell partitioning (not the official paper repo)
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: anndata>=0.11
|
|
9
|
+
Requires-Dist: celltypist
|
|
10
|
+
Requires-Dist: matplotlib
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: scanpy>=1.10
|
|
14
|
+
Requires-Dist: scikit-learn
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: seaborn
|
|
17
|
+
Requires-Dist: seacells
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# CAMP (Coreset Accelerated Metacell Partitioning)
|
|
21
|
+
|
|
22
|
+
Third-party, installable Python reimplementation of the CAMP metacell methods for use with [AnnData](https://anndata.readthedocs.io/) and [Scanpy](https://scanpy.readthedocs.io/).
|
|
23
|
+
|
|
24
|
+
## About this project
|
|
25
|
+
|
|
26
|
+
**This repository is not maintained by the authors of the CAMP paper.** It is an independent packaging effort that ports the research code from the upstream [CAMP](https://github.com/danrongLi/CAMP) repository into a reusable library (`import campmc as cp`, PyPI name **campmc**).
|
|
27
|
+
|
|
28
|
+
Goals of this project:
|
|
29
|
+
|
|
30
|
+
- Make CAMP1–4 and related helpers **portable** (no hardcoded cluster paths, scanpy-style API).
|
|
31
|
+
- Provide tests, documentation, and a normal Python package layout.
|
|
32
|
+
|
|
33
|
+
For the **official reference implementation**, paper benchmarks, and experiment scripts, use the upstream CAMP repository. Behavior here should match the published algorithms in spirit, but this package is maintained separately.
|
|
34
|
+
|
|
35
|
+
If you use the CAMP method, please cite the [bioRxiv preprint](https://www.biorxiv.org/content/10.64898/2025.12.11.693725v1) (Li, Ko & Canzar, 2025; [doi:10.64898/2025.12.11.693725](https://doi.org/10.64898/2025.12.11.693725)) — see [Citation](#citation) below.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd mycamp
|
|
41
|
+
uv sync --group dev
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage (scanpy-style)
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import campmc as cp
|
|
48
|
+
import scanpy as sc
|
|
49
|
+
|
|
50
|
+
adata = sc.datasets.pbmc68k_reduced()
|
|
51
|
+
cp.tl.partition(adata, method="camp3", gamma=25, random_state=0)
|
|
52
|
+
cp.metrics.quality(adata, partition_key="camp", label_key="louvain")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Documentation
|
|
56
|
+
|
|
57
|
+
Build HTML docs locally (same stack as [Scanpy](https://scanpy.readthedocs.io/): Sphinx, MyST, `scanpydoc` theme):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv sync --group doc
|
|
61
|
+
uv run sphinx-build -M html docs docs/_build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Citation
|
|
65
|
+
|
|
66
|
+
If you use the CAMP method, please cite the preprint:
|
|
67
|
+
|
|
68
|
+
> Li, D., Ko, Y. K., & Canzar, S. (2025). CAMP: Coreset Accelerated Metacell Partitioning enables scalable analysis of single-cell data. *bioRxiv*. [https://doi.org/10.64898/2025.12.11.693725](https://doi.org/10.64898/2025.12.11.693725)
|
|
69
|
+
|
|
70
|
+
Preprint: [bioRxiv](https://www.biorxiv.org/content/10.64898/2025.12.11.693725v1)
|
|
71
|
+
|
|
72
|
+
```bibtex
|
|
73
|
+
@article{li2025camp,
|
|
74
|
+
title = {CAMP: Coreset Accelerated Metacell Partitioning enables scalable analysis of single-cell data},
|
|
75
|
+
author = {Li, Danrong and Ko, Young Kun and Canzar, Stefan},
|
|
76
|
+
journal = {bioRxiv},
|
|
77
|
+
year = {2025},
|
|
78
|
+
doi = {10.64898/2025.12.11.693725},
|
|
79
|
+
url = {https://www.biorxiv.org/content/10.64898/2025.12.11.693725v1}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This **campmc** package is a third-party implementation; citing the preprint refers to the CAMP method, not this repository.
|
|
84
|
+
|
|
85
|
+
## Tests
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv run pytest -q
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT — see [LICENSE](LICENSE). Algorithm code is adapted from the upstream [CAMP](https://github.com/danrongLi/CAMP) repository (MIT, © danrongLi). This **campmc** package is a separate third-party distribution; it is not affiliated with or endorsed by the original CAMP authors unless they state otherwise.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
campmc/__init__.py,sha256=ni5zlyMT2jNEjrCqBSRWLeDWWHLC9JjLs1RR-o_6Yzg,435
|
|
2
|
+
campmc/_logging.py,sha256=qt8qwtCqsgrnRjDfaXVeFKkWXCt4m5Vjk1tiuqQs4aQ,400
|
|
3
|
+
campmc/coreset.py,sha256=aPAAkf3kgjAOzuhhbOF1IqwW7pduvwOStC038T5cU0E,1530
|
|
4
|
+
campmc/io.py,sha256=efcHvQ9qAIVtbYlLhIw72Gd-dieoXqYo0WXfp4cbqA4,881
|
|
5
|
+
campmc/metrics/__init__.py,sha256=0JArPO_fvx38oBp4uIbUXK3B8YDiHuPjWPTQzS7iX2Y,102
|
|
6
|
+
campmc/metrics/quality.py,sha256=SIZiFaZEghlkxPxV4HmPAeMEB0Z62lNzjTouI4YgGa0,3484
|
|
7
|
+
campmc/pl/__init__.py,sha256=yc_YTdvzJkkDvBQShnlcu-YX1-49_1qK81lcyJQ6lkY,133
|
|
8
|
+
campmc/pl/plots.py,sha256=8vkPXWEzXsoqh4At0dCwtTBuevt5IKo87fvGtdmQPUs,1168
|
|
9
|
+
campmc/pp/__init__.py,sha256=Jk4W9hlfE5AAp0bbEFVCSqpg52kXDVlBOGlaQCC-d5Y,106
|
|
10
|
+
campmc/pp/preprocess.py,sha256=1jniPi4miC3Z0NuWMhHHf5Q7nbjPKOws3UycBynbe0c,1428
|
|
11
|
+
campmc/rare/__init__.py,sha256=3f9kMUVMsJvV-fFhcBQx0-v7278Xg9b9cHmAaKQ_RVg,160
|
|
12
|
+
campmc/rare/recovery.py,sha256=7NE9Q2WJnyteLUvahqaq2LbRsKz5qDCLYH2dyGxSomo,2226
|
|
13
|
+
campmc/robustness/__init__.py,sha256=kQyC00qIQiy_U7ba9rkBohSDP9zBKLk6qLLKiKRAXEA,255
|
|
14
|
+
campmc/robustness/sweeps.py,sha256=6mYOBIYmi9K3WJy9bpstmdVVcbw_S6T9qot0AL5ogPQ,2775
|
|
15
|
+
campmc/tl/__init__.py,sha256=SDkbHdHobQgr6CWEipKMQcbMt8WDXTGNfbxTocu3bpA,103
|
|
16
|
+
campmc/tl/partition.py,sha256=Lj9Y0A19CTnLw0tDOOlDj22eqFHRRFtfN71hozXHUBc,4147
|
|
17
|
+
campmc/tl/assign/__init__.py,sha256=YuVyX_SjQ5PZM8Cxi7SPjuzljSs-qidqc-JaU15t3-E,336
|
|
18
|
+
campmc/tl/assign/camp1.py,sha256=S_QOJbND51g67E451byXrPA_VSLgjR53m27_IRFKzcQ,482
|
|
19
|
+
campmc/tl/assign/camp2.py,sha256=9xRqq6-2pFBZ33cv58-eDx968VNCeW_hiS9_6hVdtBE,1233
|
|
20
|
+
campmc/tl/assign/camp3.py,sha256=Uy_mij7YonLCxuHvVuVSFJ8PksDii53e-6bvWgrrXC4,1189
|
|
21
|
+
campmc/tl/assign/camp4.py,sha256=F73gFJsdoA-_ihW4QzKCkI4VNklXOue176zx9AvgxbA,1294
|
|
22
|
+
campmc-0.1.0.dist-info/METADATA,sha256=Q5JyRj4fiYYcvumGBXUTuL6CjmPk6a_FJlnDocYXtwE,3496
|
|
23
|
+
campmc-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
24
|
+
campmc-0.1.0.dist-info/licenses/LICENSE,sha256=rtg4nzu6kvO55VOPUn0GpuaKmS2zmkEEcLt7ZZcpYUk,1066
|
|
25
|
+
campmc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 danrongLi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|