zmip 0.1.0__tar.gz
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.
- zmip-0.1.0/LICENSE +21 -0
- zmip-0.1.0/PKG-INFO +108 -0
- zmip-0.1.0/README.md +85 -0
- zmip-0.1.0/pyproject.toml +33 -0
- zmip-0.1.0/setup.cfg +4 -0
- zmip-0.1.0/zmip/__init__.py +23 -0
- zmip-0.1.0/zmip/__main__.py +151 -0
- zmip-0.1.0/zmip/annotate.py +520 -0
- zmip-0.1.0/zmip/foreign.py +85 -0
- zmip-0.1.0/zmip/merge.py +145 -0
- zmip-0.1.0/zmip/plan.py +243 -0
- zmip-0.1.0/zmip/report.py +237 -0
- zmip-0.1.0/zmip.egg-info/PKG-INFO +108 -0
- zmip-0.1.0/zmip.egg-info/SOURCES.txt +15 -0
- zmip-0.1.0/zmip.egg-info/dependency_links.txt +1 -0
- zmip-0.1.0/zmip.egg-info/requires.txt +8 -0
- zmip-0.1.0/zmip.egg-info/top_level.txt +1 -0
zmip-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chansigit
|
|
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.
|
zmip-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zmip
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zoom-in pipeline: per-lineage re-embedding, foreign-lineage scoring and agent refinement of an msp annotation
|
|
5
|
+
Author-email: chansigit <chansigit@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/chansigit/zmip
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: msp-sc>=0.2.0
|
|
15
|
+
Requires-Dist: claude-agent-sdk
|
|
16
|
+
Requires-Dist: scanpy
|
|
17
|
+
Requires-Dist: anndata
|
|
18
|
+
Requires-Dist: pandas
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: scipy
|
|
21
|
+
Requires-Dist: matplotlib
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# zmip — zoom-in pipeline
|
|
25
|
+
|
|
26
|
+
The round after [msp](https://github.com/chansigit/msp): take msp's
|
|
27
|
+
`annotated.h5ad`, split it into lineages, re-embed each lineage on its own,
|
|
28
|
+
and let a per-lineage agent refine the annotation, clean noise and hand
|
|
29
|
+
misassigned cells to the lineage they belong to. Same pattern as osp/msp —
|
|
30
|
+
fixed computation, narrow agent decisions validated by the host, one
|
|
31
|
+
self-contained report per lineage plus a global one.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
msp annotated.h5ad ──▶ plan ──▶ per lineage: re-embed → foreign scores → agent ──▶ merge
|
|
35
|
+
(agent) (msp.integrate_adata) (agent) annotated_zmip.h5ad
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install msp-sc # msp on PyPI (import name `msp`)
|
|
42
|
+
pip install zmip # needs claude-agent-sdk + Claude Code CLI credentials
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m zmip msp_out/annotated.h5ad --outdir zmip_out --model claude-sonnet-5 [--min-cells 800]
|
|
49
|
+
python -m zmip.report zmip_out # rebuild the global report only
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Re-running resumes: the plan is reused, lineages whose contract files
|
|
53
|
+
(`annotation_proposal.json`, `annotated.h5ad`, `report.html`) exist are
|
|
54
|
+
skipped; `--force` redoes everything. Integration knobs (`--resolutions`,
|
|
55
|
+
`--n-top-genes`, `--n-pcs`, `--n-neighbors`, `--harmony KEY=VALUE`) are the
|
|
56
|
+
same as msp's and apply to every per-lineage re-embedding.
|
|
57
|
+
|
|
58
|
+
## Steps
|
|
59
|
+
|
|
60
|
+
### 1. plan (`zmip.plan`)
|
|
61
|
+
|
|
62
|
+
Host writes the evidence: cells/samples per coarse label, a kNN
|
|
63
|
+
cross-connectivity matrix between coarse labels (share of each label's
|
|
64
|
+
graph edges landing on every other label), PAGA on the same graph, and the
|
|
65
|
+
coarse-label UMAP. The agent **must read the UMAP** and pools coarse labels
|
|
66
|
+
that form one connected island into one lineage — even across cell types
|
|
67
|
+
when data quality fuses them (T/B/myeloid as one immune island) — and keeps
|
|
68
|
+
separate islands separate even when related; states (proliferating,
|
|
69
|
+
stressed) go with the island they sit in. Host rules: every coarse label
|
|
70
|
+
assigned exactly once; zoom only for lineages with at least `--min-cells`
|
|
71
|
+
(default 800 — below that leiden cannot resolve stable substates); archived
|
|
72
|
+
to `zmip_plan.json`. One lineage or none above the threshold → nothing is
|
|
73
|
+
zoomed and the msp labels pass through.
|
|
74
|
+
|
|
75
|
+
### 2. per lineage (`zmip.foreign`, `msp.integrate_adata`, `zmip.annotate`)
|
|
76
|
+
|
|
77
|
+
- Subset → `msp.integrate_adata`: HVG/PCA/harmony/leiden(0.3/1.0/2.0)/UMAP
|
|
78
|
+
recomputed on the lineage alone, with every msp artifact (QC tables,
|
|
79
|
+
cell-level outliers, standissect fragments, DEG at r1.0/r2.0,
|
|
80
|
+
`preannotation_removal.csv`) in `<lineage>/`.
|
|
81
|
+
- **Foreign-lineage scores**: lineage-level markers (wilcoxon on the whole
|
|
82
|
+
dataset at the plan's lineage level, specific genes only) → `sc.tl.score_genes`
|
|
83
|
+
for every other lineage → `obs["foreign_<lineage>"]`, per-cluster summaries
|
|
84
|
+
and UMAPs. Evidence only: close lineages share programs, so the agent
|
|
85
|
+
decides between doublet, ambient, misassignment and genuine biology.
|
|
86
|
+
- Agent on `msp_leiden_r2.0` of the subset, one Claude Code Task per
|
|
87
|
+
cluster, tools `cluster_context` / `check_genes` / `check_deg` /
|
|
88
|
+
`check_stability` / `subcluster` (reclustering allowed). Per cluster:
|
|
89
|
+
distinctness → identity → foreign signal → merge, and one action:
|
|
90
|
+
`keep` (coarse label within the lineage), `remove` (with reason), or
|
|
91
|
+
`reassign` to another lineage's coarse label (relabel only — the cells are
|
|
92
|
+
not re-embedded there this round). Host validation as in msp.annotate plus
|
|
93
|
+
the reassign rules. Removal is real: subset pre-annotation filtering ∪
|
|
94
|
+
agent-removed clusters.
|
|
95
|
+
- Outputs: `annotation_proposal.json`, `annotation_removed.csv`,
|
|
96
|
+
`annotation_reassigned.csv`, `annotated.h5ad`, `report.html` (msp's report
|
|
97
|
+
with the lineage's Cell Type Annotation section).
|
|
98
|
+
|
|
99
|
+
### 3. merge (`zmip.merge`, `zmip.report`)
|
|
100
|
+
|
|
101
|
+
Fold every lineage back into the global object. `annotated_zmip.h5ad` keeps
|
|
102
|
+
the survivors with `zmip_lineage`, `zmip_cluster` (`<lineage>:<id>`),
|
|
103
|
+
`zmip_ann_coarse`, `zmip_ann_fine`, `zmip_reassigned_from`; `msp_ann_*`
|
|
104
|
+
stay for the audit trail. No global re-embedding here (next round's job):
|
|
105
|
+
the global figures use msp's UMAP. Archives `zmip_removed.csv` (every
|
|
106
|
+
removed cell with lineage, cluster, sources) and `zmip_reassigned.csv`.
|
|
107
|
+
`report.html`: plan · lineages (linked per-lineage reports) · final
|
|
108
|
+
annotation · removed & reassigned.
|
zmip-0.1.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# zmip — zoom-in pipeline
|
|
2
|
+
|
|
3
|
+
The round after [msp](https://github.com/chansigit/msp): take msp's
|
|
4
|
+
`annotated.h5ad`, split it into lineages, re-embed each lineage on its own,
|
|
5
|
+
and let a per-lineage agent refine the annotation, clean noise and hand
|
|
6
|
+
misassigned cells to the lineage they belong to. Same pattern as osp/msp —
|
|
7
|
+
fixed computation, narrow agent decisions validated by the host, one
|
|
8
|
+
self-contained report per lineage plus a global one.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
msp annotated.h5ad ──▶ plan ──▶ per lineage: re-embed → foreign scores → agent ──▶ merge
|
|
12
|
+
(agent) (msp.integrate_adata) (agent) annotated_zmip.h5ad
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install msp-sc # msp on PyPI (import name `msp`)
|
|
19
|
+
pip install zmip # needs claude-agent-sdk + Claude Code CLI credentials
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python -m zmip msp_out/annotated.h5ad --outdir zmip_out --model claude-sonnet-5 [--min-cells 800]
|
|
26
|
+
python -m zmip.report zmip_out # rebuild the global report only
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Re-running resumes: the plan is reused, lineages whose contract files
|
|
30
|
+
(`annotation_proposal.json`, `annotated.h5ad`, `report.html`) exist are
|
|
31
|
+
skipped; `--force` redoes everything. Integration knobs (`--resolutions`,
|
|
32
|
+
`--n-top-genes`, `--n-pcs`, `--n-neighbors`, `--harmony KEY=VALUE`) are the
|
|
33
|
+
same as msp's and apply to every per-lineage re-embedding.
|
|
34
|
+
|
|
35
|
+
## Steps
|
|
36
|
+
|
|
37
|
+
### 1. plan (`zmip.plan`)
|
|
38
|
+
|
|
39
|
+
Host writes the evidence: cells/samples per coarse label, a kNN
|
|
40
|
+
cross-connectivity matrix between coarse labels (share of each label's
|
|
41
|
+
graph edges landing on every other label), PAGA on the same graph, and the
|
|
42
|
+
coarse-label UMAP. The agent **must read the UMAP** and pools coarse labels
|
|
43
|
+
that form one connected island into one lineage — even across cell types
|
|
44
|
+
when data quality fuses them (T/B/myeloid as one immune island) — and keeps
|
|
45
|
+
separate islands separate even when related; states (proliferating,
|
|
46
|
+
stressed) go with the island they sit in. Host rules: every coarse label
|
|
47
|
+
assigned exactly once; zoom only for lineages with at least `--min-cells`
|
|
48
|
+
(default 800 — below that leiden cannot resolve stable substates); archived
|
|
49
|
+
to `zmip_plan.json`. One lineage or none above the threshold → nothing is
|
|
50
|
+
zoomed and the msp labels pass through.
|
|
51
|
+
|
|
52
|
+
### 2. per lineage (`zmip.foreign`, `msp.integrate_adata`, `zmip.annotate`)
|
|
53
|
+
|
|
54
|
+
- Subset → `msp.integrate_adata`: HVG/PCA/harmony/leiden(0.3/1.0/2.0)/UMAP
|
|
55
|
+
recomputed on the lineage alone, with every msp artifact (QC tables,
|
|
56
|
+
cell-level outliers, standissect fragments, DEG at r1.0/r2.0,
|
|
57
|
+
`preannotation_removal.csv`) in `<lineage>/`.
|
|
58
|
+
- **Foreign-lineage scores**: lineage-level markers (wilcoxon on the whole
|
|
59
|
+
dataset at the plan's lineage level, specific genes only) → `sc.tl.score_genes`
|
|
60
|
+
for every other lineage → `obs["foreign_<lineage>"]`, per-cluster summaries
|
|
61
|
+
and UMAPs. Evidence only: close lineages share programs, so the agent
|
|
62
|
+
decides between doublet, ambient, misassignment and genuine biology.
|
|
63
|
+
- Agent on `msp_leiden_r2.0` of the subset, one Claude Code Task per
|
|
64
|
+
cluster, tools `cluster_context` / `check_genes` / `check_deg` /
|
|
65
|
+
`check_stability` / `subcluster` (reclustering allowed). Per cluster:
|
|
66
|
+
distinctness → identity → foreign signal → merge, and one action:
|
|
67
|
+
`keep` (coarse label within the lineage), `remove` (with reason), or
|
|
68
|
+
`reassign` to another lineage's coarse label (relabel only — the cells are
|
|
69
|
+
not re-embedded there this round). Host validation as in msp.annotate plus
|
|
70
|
+
the reassign rules. Removal is real: subset pre-annotation filtering ∪
|
|
71
|
+
agent-removed clusters.
|
|
72
|
+
- Outputs: `annotation_proposal.json`, `annotation_removed.csv`,
|
|
73
|
+
`annotation_reassigned.csv`, `annotated.h5ad`, `report.html` (msp's report
|
|
74
|
+
with the lineage's Cell Type Annotation section).
|
|
75
|
+
|
|
76
|
+
### 3. merge (`zmip.merge`, `zmip.report`)
|
|
77
|
+
|
|
78
|
+
Fold every lineage back into the global object. `annotated_zmip.h5ad` keeps
|
|
79
|
+
the survivors with `zmip_lineage`, `zmip_cluster` (`<lineage>:<id>`),
|
|
80
|
+
`zmip_ann_coarse`, `zmip_ann_fine`, `zmip_reassigned_from`; `msp_ann_*`
|
|
81
|
+
stay for the audit trail. No global re-embedding here (next round's job):
|
|
82
|
+
the global figures use msp's UMAP. Archives `zmip_removed.csv` (every
|
|
83
|
+
removed cell with lineage, cluster, sources) and `zmip_reassigned.csv`.
|
|
84
|
+
`report.html`: plan · lineages (linked per-lineage reports) · final
|
|
85
|
+
annotation · removed & reassigned.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zmip"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Zoom-in pipeline: per-lineage re-embedding, foreign-lineage scoring and agent refinement of an msp annotation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "chansigit", email = "chansigit@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"msp-sc>=0.2.0", # PyPI name of msp (import name `msp`)
|
|
20
|
+
"claude-agent-sdk",
|
|
21
|
+
"scanpy",
|
|
22
|
+
"anndata",
|
|
23
|
+
"pandas",
|
|
24
|
+
"numpy",
|
|
25
|
+
"scipy",
|
|
26
|
+
"matplotlib",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/chansigit/zmip"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["zmip"]
|
zmip-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""zmip (zoom-in pipeline): per-lineage refinement of an msp annotation.
|
|
2
|
+
|
|
3
|
+
plan UMAP-connected lineages from the coarse labels (agent + host rules)
|
|
4
|
+
zoom each lineage re-embedded on its own (msp.integrate_adata), scored
|
|
5
|
+
for foreign-lineage signal, annotated by its own agent
|
|
6
|
+
(refine fine labels / remove noise / reassign / recluster)
|
|
7
|
+
merge fold back with real removal → annotated_zmip.h5ad + report.html
|
|
8
|
+
|
|
9
|
+
Command line:
|
|
10
|
+
python -m zmip annotated.h5ad --outdir zmip_out [--min-cells 800] [--model ...]
|
|
11
|
+
python -m zmip.report zmip_out
|
|
12
|
+
|
|
13
|
+
Depends on msp (integration core, plots, report machinery) and needs the
|
|
14
|
+
claude-agent-sdk for both agent steps.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .foreign import lineage_markers, score_foreign
|
|
18
|
+
from .merge import merge_back
|
|
19
|
+
from .plan import DEFAULT_MIN_CELLS, plan_lineages, validate_plan
|
|
20
|
+
from .report import generate_report
|
|
21
|
+
|
|
22
|
+
__all__ = ["DEFAULT_MIN_CELLS", "generate_report", "lineage_markers", "merge_back", "plan_lineages",
|
|
23
|
+
"score_foreign", "validate_plan"]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""python -m zmip: zoom-in pass over an msp annotated.h5ad.
|
|
2
|
+
|
|
3
|
+
plan agent groups coarse labels into UMAP-connected lineages, picks
|
|
4
|
+
which to zoom (>= --min-cells) → zmip_plan.json
|
|
5
|
+
markers lineage-level marker lists for foreign-lineage scores
|
|
6
|
+
→ lineage_markers.csv
|
|
7
|
+
per lineage (sequential): subset → msp.integrate_adata (re-embed) →
|
|
8
|
+
foreign scores → annotation agent (refine / remove / reassign /
|
|
9
|
+
recluster) → <lineage>/{annotation_proposal.json, annotated.h5ad,
|
|
10
|
+
report.html}
|
|
11
|
+
merge fold back, real removal → annotated_zmip.h5ad,
|
|
12
|
+
zmip_removed.csv, zmip_reassigned.csv, report.html
|
|
13
|
+
|
|
14
|
+
Re-running resumes: the plan is reused, lineages whose contract files exist
|
|
15
|
+
are skipped; --force redoes everything. One lineage (or none above the
|
|
16
|
+
threshold) → nothing is zoomed and annotated_zmip.h5ad carries the msp
|
|
17
|
+
labels unchanged.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
import pandas as pd
|
|
25
|
+
import scanpy as sc
|
|
26
|
+
|
|
27
|
+
from msp.integrate import integrate_adata
|
|
28
|
+
from msp.plots import save_single_umap, slug
|
|
29
|
+
|
|
30
|
+
from .annotate import PREV_SUFFIX, PREVIOUS_COLS, annotate_lineage
|
|
31
|
+
from .foreign import lineage_markers, score_foreign
|
|
32
|
+
from .merge import merge_back
|
|
33
|
+
from .plan import DEFAULT_MIN_CELLS, plan_lineages
|
|
34
|
+
from .report import generate_report
|
|
35
|
+
from msp.report import write_report_context
|
|
36
|
+
|
|
37
|
+
parser = argparse.ArgumentParser(prog="zmip", description=__doc__,
|
|
38
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
39
|
+
parser.add_argument("h5ad", help="msp annotated.h5ad (survivors with msp_ann_coarse/msp_ann_fine)")
|
|
40
|
+
parser.add_argument("--outdir", required=True)
|
|
41
|
+
parser.add_argument("--coarse-col", default="msp_ann_coarse")
|
|
42
|
+
parser.add_argument("--fine-col", default="msp_ann_fine")
|
|
43
|
+
parser.add_argument("--batch-col", default=None, help="defaults to uns['msp']['batch_col']")
|
|
44
|
+
parser.add_argument("--species", default=None, help="defaults to uns['msp']['species']")
|
|
45
|
+
parser.add_argument("--min-cells", type=int, default=DEFAULT_MIN_CELLS,
|
|
46
|
+
help=f"smallest lineage that gets zoomed (default {DEFAULT_MIN_CELLS})")
|
|
47
|
+
parser.add_argument("--resolutions", type=float, nargs="+", default=[0.3, 1.0, 2.0])
|
|
48
|
+
parser.add_argument("--n-top-genes", type=int, default=2000)
|
|
49
|
+
parser.add_argument("--n-pcs", type=int, default=50)
|
|
50
|
+
parser.add_argument("--n-neighbors", type=int, default=15)
|
|
51
|
+
parser.add_argument("--harmony", action="append", default=[], metavar="KEY=VALUE",
|
|
52
|
+
help="harmonypy override for the per-lineage re-embedding, repeatable")
|
|
53
|
+
parser.add_argument("--language", default="English")
|
|
54
|
+
parser.add_argument("--model", default=None)
|
|
55
|
+
parser.add_argument("--effort", default=None, choices=["low", "medium", "high", "xhigh", "max"])
|
|
56
|
+
parser.add_argument("--max-turns", type=int, default=200)
|
|
57
|
+
parser.add_argument("--report-context", default=None, metavar="TEXT",
|
|
58
|
+
help='where this run sits, for report titles (e.g. "round 2 · fu2022-meniscus")')
|
|
59
|
+
parser.add_argument("--force", action="store_true")
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _kv(items):
|
|
64
|
+
def conv(v):
|
|
65
|
+
for cast in (int, float):
|
|
66
|
+
try:
|
|
67
|
+
return cast(v)
|
|
68
|
+
except ValueError:
|
|
69
|
+
pass
|
|
70
|
+
return v
|
|
71
|
+
out = {}
|
|
72
|
+
for it in items:
|
|
73
|
+
if "=" not in it:
|
|
74
|
+
sys.exit(f"--harmony expects KEY=VALUE, got {it!r}")
|
|
75
|
+
k, v = it.split("=", 1)
|
|
76
|
+
out[k.strip()] = [conv(x) for x in v.split(",")] if "," in v else conv(v)
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
out = os.path.abspath(args.outdir)
|
|
81
|
+
os.makedirs(out, exist_ok=True)
|
|
82
|
+
write_report_context(out, args.report_context)
|
|
83
|
+
ad = sc.read_h5ad(args.h5ad)
|
|
84
|
+
meta = ad.uns.get("msp", {})
|
|
85
|
+
batch_col = args.batch_col or meta.get("batch_col")
|
|
86
|
+
if not batch_col:
|
|
87
|
+
sys.exit("no --batch-col and uns['msp']['batch_col'] absent")
|
|
88
|
+
species = args.species or (meta.get("species") or None)
|
|
89
|
+
for c in (args.coarse_col, args.fine_col):
|
|
90
|
+
if c not in ad.obs:
|
|
91
|
+
sys.exit(f"obs[{c!r}] missing — input must be msp's annotated.h5ad (or pass --coarse-col/--fine-col)")
|
|
92
|
+
print(f"== {ad.n_obs} cells, batch={batch_col!r}, species={species}", flush=True)
|
|
93
|
+
|
|
94
|
+
plan = plan_lineages(ad, args.coarse_col, batch_col, out, min_cells=args.min_cells, species=species,
|
|
95
|
+
model=args.model, effort=args.effort)
|
|
96
|
+
label_to_lineage = {lab: ln["name"] for ln in plan["lineages"] for lab in ln["coarse_labels"]}
|
|
97
|
+
ad.obs["_zmip_lineage"] = ad.obs[args.coarse_col].astype(str).map(label_to_lineage).astype("category")
|
|
98
|
+
for ln in plan["lineages"]:
|
|
99
|
+
print(f"== lineage {ln['name']}: {ln['coarse_labels']} n={ln['n_cells']} zoom={ln['zoom']}", flush=True)
|
|
100
|
+
|
|
101
|
+
markers_p = os.path.join(out, "lineage_markers.csv")
|
|
102
|
+
if os.path.exists(markers_p) and not args.force:
|
|
103
|
+
mk = pd.read_csv(markers_p)
|
|
104
|
+
markers = {g: mk.loc[mk["lineage"] == g, "gene"].tolist() for g in mk["lineage"].unique()}
|
|
105
|
+
else:
|
|
106
|
+
print("== lineage-level markers (for foreign-lineage scores)", flush=True)
|
|
107
|
+
markers = lineage_markers(ad, "_zmip_lineage", out)
|
|
108
|
+
|
|
109
|
+
all_labels = set(label_to_lineage)
|
|
110
|
+
zoomed = [ln for ln in plan["lineages"] if ln["zoom"]]
|
|
111
|
+
if not zoomed:
|
|
112
|
+
print("== no lineage reaches min_cells — nothing to zoom; passing msp labels through", flush=True)
|
|
113
|
+
results = {}
|
|
114
|
+
harmony_kwargs = _kv(args.harmony)
|
|
115
|
+
keys_for_foreign = [f"msp_leiden_r{r}" for r in args.resolutions if r in (1.0, 2.0)]
|
|
116
|
+
|
|
117
|
+
for ln in zoomed:
|
|
118
|
+
name, labels = ln["name"], ln["coarse_labels"]
|
|
119
|
+
d = os.path.join(out, slug(name))
|
|
120
|
+
contract = [os.path.join(d, f) for f in ("annotation_proposal.json", "annotated.h5ad", "report.html")]
|
|
121
|
+
if all(os.path.exists(p) for p in contract) and not args.force:
|
|
122
|
+
print(f"== [{name}] already done — skipping (resume)", flush=True)
|
|
123
|
+
results[name] = {"dir": d, "removed": pd.read_csv(os.path.join(d, "annotation_removed.csv")),
|
|
124
|
+
"reassigned": pd.read_csv(os.path.join(d, "annotation_reassigned.csv"))}
|
|
125
|
+
continue
|
|
126
|
+
sub = ad[ad.obs[args.coarse_col].astype(str).isin(labels).values].copy()
|
|
127
|
+
for c in PREVIOUS_COLS:
|
|
128
|
+
src = {"msp_ann_coarse": args.coarse_col, "msp_ann_fine": args.fine_col}[c]
|
|
129
|
+
sub.obs[c + PREV_SUFFIX] = sub.obs[src].astype(str).astype("category")
|
|
130
|
+
del sub.obs["_zmip_lineage"]
|
|
131
|
+
print(f"== [{name}] re-embedding {sub.n_obs} cells", flush=True)
|
|
132
|
+
integrate_adata(sub, batch_col, d, species=species, resolutions=tuple(args.resolutions),
|
|
133
|
+
n_top_genes=args.n_top_genes, n_pcs=args.n_pcs, n_neighbors=args.n_neighbors,
|
|
134
|
+
harmony_kwargs=harmony_kwargs, inputs=[args.h5ad],
|
|
135
|
+
meta_extra={"zmip_lineage": name, "zmip_coarse_labels": list(labels)})
|
|
136
|
+
figdir = os.path.join(d, "figures")
|
|
137
|
+
print(f"== [{name}] foreign-lineage scores", flush=True)
|
|
138
|
+
foreign_cols = score_foreign(sub, markers, name, keys_for_foreign, d, figdir)
|
|
139
|
+
for c in PREVIOUS_COLS:
|
|
140
|
+
col = c + PREV_SUFFIX
|
|
141
|
+
n = sub.obs[col].nunique()
|
|
142
|
+
save_single_umap(sub, col, os.path.join(figdir, f"umap_{col}.png"), repel=True,
|
|
143
|
+
repel_fontsize=8 if n > 15 else 11, figsize=(9, 9) if n > 15 else None)
|
|
144
|
+
proposal, rm, ra = annotate_lineage(sub, d, name, labels, sorted(all_labels - set(labels)), foreign_cols,
|
|
145
|
+
species=species, language=args.language, model=args.model,
|
|
146
|
+
effort=args.effort, max_turns=args.max_turns)
|
|
147
|
+
results[name] = {"dir": d, "removed": rm, "reassigned": ra}
|
|
148
|
+
del sub
|
|
149
|
+
|
|
150
|
+
merge_back(ad, plan, results, out, coarse_col=args.coarse_col, fine_col=args.fine_col)
|
|
151
|
+
print(f"== report: {generate_report(out)}", flush=True)
|