Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/skills/perturbNMF-runner/references/02-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Reconstruction error and stability are computed automatically.
| `--guide_annotation_path` | None | TSV with a `targeting` column to identify non-targeting controls (alternative to `--guide_annotation_key`) |
| `--gene_names_key` | `symbol` | Column in data_guide["rna"].var with gene names |
| `--FDR_method` | `StoreyQ` | FDR correction: `StoreyQ` or `BH` |
| `--organism` | `human` | Species for enrichment |
| `--organism` | `human` | Species for enrichment (`human` or `mouse`; mouse genes are mapped to human orthologs) |
| `--ortholog_table` | None | Mouse-human ortholog TSV; default MGI + Ensembl, cached in `Resources/` |
| `--n_top` | `300` | Top genes for enrichment tests |
| `--guide_annotation_key` | `["non-targeting"]` | Non-targeting guide identifiers (accepts multiple values) |
| `--use_cache` | flag | Load enrichr gene set libraries from cached JSON in `Resources/` instead of downloading; falls back to download + cache on miss |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
| `--selected_k` | None | K value to highlight with a red dashed line |
| `--stability_file` | None | Pre-computed stability/error TSV/NPZ (bypasses `cnmf.consensus()`; for torch-cNMF runs) |
| `--run_program_dotplot` | off | Enable per-(K, sel_thresh) program dotplots (default off; requires the inference h5mu artifact per K/threshold) |
| `--no_trait` | off | Skip trait (GWAS) enrichment inputs and plots (non-human data) |
| `--no_perturbation` | off | Skip perturbation inputs and regulator plots (non-perturbed data, no guides) |

Enrichment file patterns use `{k}` placeholder (and `{sample}` for perturbation). See `references/parameter-catalog.md` Section 4 for all optional params.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ other stage (no notebook required). Submit one job per K to cover multiple K val
| `--perturbation_file_name` | `perturbation_association_results` | Perturbation file stem (e.g. `CRT`) |
| `--effect_size` | `log2FC` | Effect-size column (e.g. `approx_log2FC`) |
| `--gene_names_key` | `symbol` | var column with gene symbols |
| `--organism` | `human` | Species of the data; `mouse` annotates genes with their human orthologs' summaries |
| `--non_targeting_key` | `non-targeting` | Negative-control target label(s) |

See `references/parameter-catalog.md` (Section 12) for the full flag list, including
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ Same as sk-cNMF except:
| `--gwas_data_path` | str | None | Path to GWAS data (required only for trait enrichment) |
| `--X_normalized_path` | str | None | Normalized counts h5ad (needed for explained variance). Path pattern: `<out_dir>/<run_name>/Inference/cnmf_tmp/Inference.norm_counts.h5ad` (prefix is always `Inference`, not the run_name) |
| `--guide_annotation_path` | str | None | Guide annotation TSV |
| `--organism` | str | `human` | Species for enrichment |
| `--organism` | str | `human` | Species for enrichment (`human` or `mouse`; mouse genes are mapped to human orthologs) |
| `--ortholog_table` | str | None | Mouse-human ortholog TSV; default MGI + Ensembl, cached in `Resources/` |
| `--FDR_method` | str | `StoreyQ` | FDR method: `StoreyQ` or `BH` |
| `--n_top` | int | `300` | Top genes for enrichment tests |
| `--gene_names_key` | str | `symbol` | Column in data_guide["rna"].var with gene names |
Expand Down Expand Up @@ -271,6 +272,8 @@ Same as sk-cNMF except:
| `--variance_col` | str | `Total` | No | Variance column name |
| `--stability_file` | str | None | No | Pre-computed stability/error file (TSV or NPZ) |
| `--run_program_dotplot` | flag | False | No | Enable per-(K, sel_thresh) program dotplots; requires `{output_dir}/{run_name}/Inference/adata/cNMF_{K}_{thresh}.h5mu` to exist for each pair |
| `--no_trait` | flag | False | No | Skip trait (GWAS) enrichment files and plots (non-human data) |
| `--no_perturbation` | flag | False | No | Skip perturbation files and regulator plots (non-perturbed data, no guides) |

---

Expand Down Expand Up @@ -616,6 +619,8 @@ cover multiple K values.
| `--save_path` | str | `{out_dir}/{run_name}/Interpretation/Summary_table/{K}_{thresh}` | Output dir for the `.xlsx`, TSVs, and specificity/correlation/KD sidecars |
| `--mdata_path` | str | `{out_dir}/{run_name}/Inference/adata/cNMF_{K}_{thresh}.h5mu` | Input MuData |
| `--num_gene` | int | `300` | Top genes per program / per enriched term to keep |
| `--organism` | str | `human` | Species of the data; with `mouse`, each gene gets the MyGene summaries of its human orthologs (listed in a `Human ortholog` column) |
| `--ortholog_table` | str | None | Mouse-human ortholog TSV; default MGI + Ensembl, cached in `Resources/` |
| `--perturbation_file_name` | str | `perturbation_association_results` | Perturbation result file stem (between `{K}_` and `_{Condition}.txt`); e.g. `CRT` |
| `--Conditions` | str (nargs=\*) | all labels found in the data | Condition labels (values of obs[categorical_key]); `--Sample` is a deprecated alias |
| `--effect_size` | str | `log2FC` | Effect-size column in perturbation files (e.g. `approx_log2FC`) |
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,29 @@ Entries marked ⚠️ change pipeline output — re-run affected analyses.
- Annotation runners locate their scripts via `PIPELINE_ROOT` (fixing paths into a
nonexistent `AGeneTic/` folder); `gpe_export.py` reads the GeneProgramExplorer
location from `GPE_DIR`.
- ⚠️ Mouse data (`--organism mouse`): genes are matched to the human gene-set
libraries (GO, Reactome) through curated mouse-human orthologs (MGI + Ensembl)
instead of upper-cased symbols. Mouse genes without a human ortholog are left
out of enrichment. Re-run gene set and GO enrichment for mouse datasets.
- `--organism` accepts only `human` or `mouse` (case-insensitive) in evaluation
and the Excel summary. Trait (GWAS) enrichment is skipped, with a message, for
mouse data.
- `ensure_umap` (program and perturbed-gene plots) reuses embeddings stored as
`obsm['UMAP']` / `obsm['PCA']` (e.g. Seurat conversions) instead of computing
a new UMAP.

### Added
- `tools/check_no_lab_specific_content.py` guard (CI + pre-commit) that blocks
lab/project-specific content in this public repo.
- Support for scRNA-seq data without perturbations (no guide information):
K-selection `--no_perturbation` / `--no_trait`; program plots and the Excel
summary run without `uns['guide_targets']` (the Targets Summary sheet is
skipped); evaluation stops early when `--Perform_perturbation` is requested
without guides. See the README section "Non-perturbed and mouse data".
- Mouse-human ortholog table (`src/Stage2_Evaluation/A_Metrics/src/orthologs.py`),
downloaded once and cached in `src/Stage2_Evaluation/Resources/`, or supplied
with `--ortholog_table`. With `--organism mouse`, the Excel summary annotates
genes with their human orthologs' summaries in a new `Human ortholog` column.

## [0.1.1] - 2026-08-10

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,29 @@ See [`src/Stage2_Evaluation/B_Calibration/README.md`](src/Stage2_Evaluation/B_Ca

See [`src/Stage3_Interpretation/README.md`](src/Stage3_Interpretation/README.md) for detailed parameters and output format.

## Non-perturbed and mouse data

The pipeline also runs on scRNA-seq data without perturbations (no guide information in the
input) and on mouse data. Per stage:

| Stage | Non-perturbed data (no guides) | Mouse data |
|---|---|---|
| 1. Inference | Leave out the `--guide_*` options | `--species mouse` |
| 2a. Metrics | Leave out `--Perform_perturbation` and `--guide_annotation_path` | `--organism mouse`: genes are mapped to human orthologs for gene set and GO enrichment. Trait (GWAS) enrichment is human-only and is skipped with a message if requested |
| 2b. Calibration | Not applicable (needs non-targeting guides); skip | No change |
| 3a. K-selection plots | `--no_perturbation` | `--no_trait` |
| 3a. Program plots | Leave out `--perturb_path_base` and use `--output_format PDF` or `SVG` (HTML requires `--perturb_path_base`); perturbed-gene annotations are left empty | No change |
| 3a. Perturbed-gene plots | Not applicable; skip | No change |
| 3b. Excel summary | The Targets Summary sheet is skipped automatically | `--organism mouse`: genes are annotated with their human orthologs' summaries |
| 3c. Annotation | No change | `--taxid 10090` (NCBI, UniProt, OmniPath search), `--species mouse` (MyGene search), `--organism mouse` (program extraction) |

Mouse genes are matched to human gene-set libraries and gene summaries through curated
orthologs (union of MGI and Ensembl), not by upper-casing symbols. The table is downloaded
on first use and cached in `src/Stage2_Evaluation/Resources/orthologs_mouse_human.tsv`;
pass `--ortholog_table` to use a fixed copy. Mouse genes without a human ortholog are left
out of enrichment, and a gene with several human orthologs contributes to all of them.
Custom gene sets used with mouse data must therefore use human gene symbols.

## Claude Code Skills

This repo ships four Claude Code skills under `.claude/skills/` for guided pipeline execution, validation, and maintenance. Open the linked `SKILL.md` for trigger phrases, detailed usage, and helper script references.
Expand Down
3 changes: 2 additions & 1 deletion src/Stage2_Evaluation/A_Metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ The evaluation pipeline tests cNMF programs against various criteria to assess t
| X_normalized_path | str | None | Path to normalized cell x gene matrix (.h5ad), required for explained variance |
| guide_annotation_path | str | None | Path to tab-separated guide annotation file with "targeting" column |
| gwas_data_path | str | None | Path to GWAS data file for trait enrichment (required when --Perform_trait is set) |
| organism | str | "human" | Organism/species for enrichment analysis |
| organism | str | "human" | Organism/species for enrichment analysis (`human` or `mouse`; mouse genes are mapped to human orthologs to match the human gene-set libraries) |
| ortholog_table | str | None | For mouse: mouse-human ortholog TSV (columns `mouse_symbol`, `mouse_ensembl`, `human_symbol`, `source`). Default: MGI + Ensembl table, downloaded once and cached in `src/Stage2_Evaluation/Resources/` |
| FDR_method | str | "StoreyQ" | FDR correction method for perturbation association |
| n_top | int | 300 | Number of top loaded genes to use for enrichment tests |
| guide_annotation_key | list of str | ["non-targeting"] | Name(s) of non-targeting guide target labels |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ def _read_samples_from_h5mu(h5mu_path, data_key, categorical_key):
return None


def _h5mu_has_uns_key(h5mu_path, mod_key, uns_key):
"""True if mod/<mod_key>/uns/<uns_key> exists in the h5mu; None if unreadable.

Raises ValueError if the modality itself is missing (e.g. a wrong --prog_key).
"""
try:
f = h5py.File(h5mu_path, "r")
except OSError:
return None
with f:
if f"mod/{mod_key}" not in f:
raise ValueError(
f"Modality '{mod_key}' not found in {h5mu_path}; check --prog_key."
)
return f"mod/{mod_key}/uns/{uns_key}" in f


# Make the repo's src/ importable regardless of the current working directory
from pathlib import Path

Expand Down Expand Up @@ -203,10 +220,20 @@ def main():

parser.add_argument(
"--organism",
help="Organism/species for enrichment analysis (default: human)",
type=str,
help="Organism/species for enrichment analysis: human or mouse (default: human)",
type=str.lower,
choices=["human", "mouse"],
default="human",
)
parser.add_argument(
"--ortholog_table",
type=str,
default=None,
help="For --organism mouse: TSV of mouse-human orthologs used to match genes "
"to the human gene-set libraries (columns: mouse_symbol, mouse_ensembl, "
"human_symbol, source). Default: MGI + Ensembl table, downloaded once and "
"cached in src/Stage2_Evaluation/Resources/",
)
parser.add_argument(
"--FDR_method",
help="Method for FDR correction in perturbation association (default: StoreyQ)",
Expand Down Expand Up @@ -235,6 +262,15 @@ def main():

args = parser.parse_args()

# The bundled GWAS resource (OpenTargets L2G) is human-only.
if args.Perform_trait and args.organism != "human":
print(
f"--organism {args.organism}: skipping trait (GWAS) enrichment, which is "
"only available for human data. Pass --no_trait to K-selection plotting.",
flush=True,
)
args.Perform_trait = False

if args.Perform_trait and args.gwas_data_path is None:
parser.error("--gwas_data_path is required when --Perform_trait is set")

Expand Down Expand Up @@ -284,6 +320,25 @@ def main():
else:
reference_targets = args.guide_annotation_key

# Check for guide information before computing any metric.
if args.Perform_perturbation:
for sel_thresh in args.sel_threshs:
for k in args.K:
h5mu_path = (
f"{args.out_dir}/{args.run_name}/Inference/adata/"
f"cNMF_{k}_{str(sel_thresh).replace('.', '_')}.h5mu"
)
if (
_h5mu_has_uns_key(h5mu_path, args.prog_key, args.guide_targets_key)
is False
):
raise ValueError(
f"--Perform_perturbation needs guide information, but uns['"
f"{args.guide_targets_key}'] is missing from '{args.prog_key}' "
f"in {h5mu_path}. For non-perturbed data, drop "
"--Perform_perturbation."
)

for sel_thresh in args.sel_threshs:
for k in args.K:
output_folder = f"{args.out_dir}/{args.run_name}/Evaluation/{k}_{str(sel_thresh).replace('.', '_')}"
Expand Down Expand Up @@ -441,6 +496,7 @@ def main():
data_key=args.data_key,
prog_name=None,
organism=args.organism,
ortholog_table=args.ortholog_table,
library="Reactome_2022",
method="fisher",
database="enrichr",
Expand All @@ -461,6 +517,7 @@ def main():
data_key=args.data_key,
prog_name=None,
organism=args.organism,
ortholog_table=args.ortholog_table,
library="GO_Biological_Process_2023",
method="fisher",
database="enrichr",
Expand Down
40 changes: 38 additions & 2 deletions src/Stage2_Evaluation/A_Metrics/src/enrichment_geneset.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from gseapy import Biomart, Msigdb
from tqdm.auto import tqdm

from .orthologs import collapse_to_human, load_ortholog_table

logging.basicConfig(level=logging.INFO)

_RESOURCES_DIR = os.path.join(
Expand Down Expand Up @@ -78,17 +80,41 @@ def get_idconversion(var_names, organism="human", chunk_size=100):
return gene_names


def _mouse_gene_ids(mdata, prog_key, data_key, gene_names_key):
"""Mouse symbols or Ensembl IDs for the loadings columns, unchanged in case."""
if gene_names_key is not None and gene_names_key in mdata[data_key].var.columns:
genes = mdata[data_key].var[gene_names_key].astype(str).tolist()
elif "var_names" in mdata[prog_key].uns.keys():
genes = [str(v) for v in mdata[prog_key].uns["var_names"]]
else:
genes = [str(v) for v in mdata[data_key].var_names]
# "Symbol:ENSMUSG..." style names: keep the symbol
return [g.split(":", 1)[0] if ":ens" in g.lower() else g for g in genes]


def get_program_gene_loadings(
mdata,
prog_key="prog",
prog_nam=None,
data_key="rna",
organism="human",
gene_names_key=None,
ortholog_table=None,
map_to_human=True,
):
"""Get gene loadings for each program in the mudata object."""
"""Get gene loadings for each program in the mudata object.

if gene_names_key is not None and gene_names_key in mdata[data_key].var.columns:
For mouse data with map_to_human=True (gene sets in human symbols), genes are
mapped to human orthologs (see orthologs.py); ortholog_table is an optional
path to a pre-built ortholog table. With map_to_human=False, mouse symbols are
kept unchanged (gene sets in mouse symbols, e.g. MSigDB mouse).
"""
if organism not in ("human", "mouse"):
raise ValueError(f"Unsupported organism '{organism}'; use 'human' or 'mouse'.")

if organism == "mouse":
gene_names = _mouse_gene_ids(mdata, prog_key, data_key, gene_names_key)
elif gene_names_key is not None and gene_names_key in mdata[data_key].var.columns:
gene_names = (
mdata[data_key].var[gene_names_key].astype(str).str.upper().tolist()
)
Expand All @@ -113,6 +139,9 @@ def get_program_gene_loadings(
loadings["gene_names"] = gene_names
loadings.set_index("gene_names", inplace=True)

if organism == "mouse" and map_to_human:
loadings = collapse_to_human(loadings, load_ortholog_table(ortholog_table))

# with open('var_names.txt', 'w') as fil:
# for nam in mdata[data_key].var_names:
# fil.write(nam+'\n')
Expand Down Expand Up @@ -403,6 +432,7 @@ def compute_geneset_enrichment(
use_loadings_gene=False, # False = use all oncology gene, True = use interact expresseed gene
gene_names_key: Optional[str] = None,
use_cache: bool = False,
ortholog_table: Optional[str] = None,
**kwargs,
) -> Optional[pd.DataFrame]:
"""
Expand Down Expand Up @@ -442,6 +472,9 @@ def compute_geneset_enrichment(
number of threads to run processes on.
inplace : bool (default: True)
whether to insert the results back into the mudata object.
ortholog_table : str (default: None)
For organism='mouse': path to a mouse-human ortholog table. By default the
MGI + Ensembl table is downloaded once and cached in Resources/.

Returns
-------
Expand Down Expand Up @@ -484,6 +517,9 @@ def compute_geneset_enrichment(
data_key=data_key,
organism=organism,
gene_names_key=gene_names_key,
ortholog_table=ortholog_table,
# Enrichr and user gene sets use human symbols; MSigDB mouse sets do not.
map_to_human=not (database == "msigdb" and user_geneset is None),
)

# run enrichment
Expand Down
Loading
Loading