v0.1.0: monorepo merge — unified training + inference, canonical CellTypeAnnotator, archive-free inference, vendored baselines#41
Merged
Conversation
…backbone (#8) The existing --freeze_backbone freezes every parameter except the mean-intensity branches (intensity_cls_branch / intensity_per_channel_proj). The CT classifier head, CLS token, and final norm — all CT-task layers — stay frozen. That's a tight definition of "adapter only" but it leaves a known limitation: the CT head can't adapt to whatever the mean-intensity branch adds to the CLS embedding, so the model's improvement is capped by the pretrained head's biases. Add --unfreeze_ct_head (default off). With this flag set alongside --freeze_backbone, the freeze policy additionally re-enables: - ct_head (the CT classifier MLP, ~105K params) - final_norm (LayerNorm before heads, ~512 params) - cls_token (the trainable CLS embedding parameter) The heavy backbone (transformer 3.2M, per-channel encoder 130K, marker embedder LoRA 175K, spatial encoder 57K) stays frozen as before. Use case: train Frozen-CLS variants where you want the new mean-intensity side-input AND the CT head to co-adapt without unleashing the full transformer. Brings the trainable share from ~3% to ~6% of total parameters. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both `scripts/predict.py --ct_abstention_k` and the module-form `deepcell_types.predict()` now default to k=0.5 — the v10 published headline operating point (≈9% of cells abstained, +5pp macro_F1 on kept cells; clears every baseline including XGBoost-tuned on the held-out test split). Module-form predict() additions: - `ct_abstention_k=0.5` parameter, k<=0 / None disables. - Per-FOV IQR fence Q1 - k*IQR on max-softmax (the whole FOV is a single tissue×modality group). compute_iqr_fence already guards n<4. - Abstained cells get the sentinel label "Unknown" in `cell_types`. Original argmax preserved in PredictionResult.cell_types_raw, with a boolean PredictionResult.abstained mask alongside. scripts/predict.py: - --ct_abstention_k default flipped from None to 0.5. Set 0 or a negative value to disable. Help text updated to point at docs/reports/ct_iqr_abstention_test.md instead of the older audit doc. - Guard tightened to `k > 0` so the disable contract is explicit. Tests: - Replaced test_default_no_abstention_column with two new tests: test_default_k_0_5_abstention_is_on (≈10% abstained on synthetic frame) and test_disable_abstention_with_nonpositive_k (k<=0 / None as no-op). - All 24 existing canonical-inference + abstention tests pass; the 1-cell test_predict_* cases trip compute_iqr_fence's n<4 guard, so no abstention fires and assertions hold. Backwards-compat note: callers that don't pass `ct_abstention_k=` will now see "Unknown" labels appear for low-confidence cells. To restore the pre-change behaviour, pass `ct_abstention_k=0` (or None) at the call site, or `--ct_abstention_k 0` on the CLI. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…,score) (#10) benchmark_gold_standard.py evaluates marker-positivity at a single hardcoded threshold (default 0.5). Per-marker threshold tuning needs the raw per-cell scores, which the script wasn't persisting. When the DCT_GOLD_PREDS_CSV env var is set, after the inference pass the script writes a flat CSV of (fov, cell_id, channel, pred_score) for every prediction the model produced. Downstream callers can then apply oracle CV per-marker τ (or any other threshold-tuning protocol) without re-running the model — see analysis/rescore_gold_oracle_cv.py in the research workspace, which consumes this CSV to produce the final_*_gold_metrics_learned.json adaptive-τ tables. No behaviour change when the env var is unset (no file written). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Module docstring previously documented `k=None (off): default`. The production default in both `scripts/predict.py:81` and `deepcell_types/predict.py:242` is `k=0.5`. Updated the Pareto-sweep note to reflect the current operating point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the data-provenance audit, the --min_channels=3 filter is vacuous on the labeled v10 corpus — the 622 archive FOVs excluded from v10 are unlabeled (no standardized_source annotations), not channel-filtered. The filter logic in dataset.py / baseline_features.py is retained and gated on `min_channels > 0`, so callers who pass it explicitly still get the behavior; only the default changes from 3 → 0 across the 4 CLI scripts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntime After dropping the --min_channels default 3 → 0 (the filter is a no-op on the labeled v10 corpus), load_fov_splits was strict-failing on the recorded vs runtime min_channels metadata. Add min_channels to _ADVISORY_SPLIT_METADATA_KEYS so the mismatch logs a warning instead of raising. Restores load-compatibility with all existing fov_split_v10*.json files (which carry min_channels=3 in their metadata). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e #79) (#11) * docs(abstention): fix stale docstring — k=0.5 is the default Module docstring previously documented `k=None (off): default`. The production default in both `scripts/predict.py:81` and `deepcell_types/predict.py:242` is `k=0.5`. Updated the Pareto-sweep note to reflect the current operating point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * scripts: drop --min_channels default 3 → 0 (filter is a no-op on v10) Per the data-provenance audit, the --min_channels=3 filter is vacuous on the labeled v10 corpus — the 622 archive FOVs excluded from v10 are unlabeled (no standardized_source annotations), not channel-filtered. The filter logic in dataset.py / baseline_features.py is retained and gated on `min_channels > 0`, so callers who pass it explicitly still get the behavior; only the default changes from 3 → 0 across the 4 CLI scripts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(splits): tolerate min_channels mismatch between split file and runtime After dropping the --min_channels default 3 → 0 (the filter is a no-op on the labeled v10 corpus), load_fov_splits was strict-failing on the recorded vs runtime min_channels metadata. Add min_channels to _ADVISORY_SPLIT_METADATA_KEYS so the mismatch logs a warning instead of raising. Restores load-compatibility with all existing fov_split_v10*.json files (which carry min_channels=3 in their metadata). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(predict): FOV-grouped train sampler for --learn_mp_thresholds (#79) Root cause: when --learn_mp_thresholds is on, predict.py builds the train loader with use_weighted_sampler=False, which falls back to shuffle=True. Each random batch of 256 hits ~256 unique FOVs, and FullImageDataset's _get_zarr_arrays runs `raw_np = raw_zarr[:]` on the first hit per worker (populating _zero_channel_cache) even when the FOV exceeds the per-worker numpy budget — a full ~1 GB cold zarr load per FOV. With 8 spawn workers × prefetch=4 × random FOVs, batch 0 waits on terabytes of cold zarr reads and effectively never arrives. Training avoids this entirely because FOVGroupedSampler keeps each worker on one FOV at a time. Fix: add SequentialFOVGroupedSampler — uniform-coverage counterpart to FOVGroupedSampler with the same cache-locality guarantee — and a fov_grouped_train flag on create_dataloader to enable it. predict.py passes the flag when --learn_mp_thresholds is set, so the original issue-#79 invocation now runs to completion. Smoke (8 workers, spawn, v10 test split): first 5 batches in 5 min (cold-load), subsequent batches stream from per-worker numpy cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ty tissue buckets
Three cleanups from the 2026-05-19 triple-review low-priority list:
(1) deepcell_types/model.py constructor defaults aligned with CLI:
- resnet_base_channels: 32 → 48 (canonical paper recipe, matches
--resnet_channels default in scripts/train.py + predict.py)
- mean_intensity_mode: "none" → "cls_residual" (canonical paper recipe,
matches --mean_intensity_mode default in scripts/train.py)
Direct callers like `CellTypeAnnotator(...)` without explicit kwargs
were previously silently building the pre-v10 model variant.
(2) deepcell_types/training/config.py:_compute_all_mappings:
Drop tissues whose tissue_celltype_mapping ends up with an empty
allowed-CT set. Previously 4 tissues (esophagus, immune,
musculoskeletal, colon) had keys created on first sighting but never
populated, since their only FOVs lacked standardized_source
annotations. Empty sets are a bug attractor under
--apply_tissue_mask (the mask becomes all-Inf → NaN softmax). Now
the empty entries are filtered out and --apply_tissue_mask just
skips unmapped tissues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pinned commit a21b97f was the head of merged PR #2; main tip b5447d1 is the merge commit. Same effective content, but the pointer now matches main rather than a non-tip commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Found during a paper-vs-code audit: several user-facing strings still
named SVD artifacts that have been superseded, and the GradReverse
docstring described an older training recipe.
- SVD references updated to the current canonical artifact
embeddings/svd_512_v6.npz (278 markers x 328-d, the artifact actually
on disk in the research workspace and used by every analysis script):
- deepcell_types/model.py docstring v5 (269x319) -> v6 (278x328)
- deepcell_types/training/config.py FileNotFoundError + ValueError hints
- scripts/benchmark_gold_standard.py --svd_path / function default
- scripts/generate_openai_embeddings.py default output bumped from
svd_512_v3.npz to svd_512_v7.npz so that a fresh generator run produces
the *next* versioned artifact instead of (a) silently writing a stale
v3 name or (b) clobbering the canonical v6 file. Help text added to
flag that v6 is the canonical artifact and to bump on regenerate.
- GradReverse docstring (deepcell_types/model.py) corrected: it
previously said the DANN head is "dormant in the canonical training
recipe, which sets --domain_weight 0", but the current train.py
default is --domain_weight 0.1, which is the v10 canonical recipe and
matches the paper. Reworded to describe the actual default and to note
the (still-honored) opt-out path.
scripts/generate_openai_embeddings_v8.py was left alone -- it targets a
separate exploratory 283-channel v8 archive, not the current 278-channel
canonical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Acts on the validated audit in audit/VALIDATION-SYNTHESIS.md. Covers mechanical fixes only; items needing legal/judgment input (LICENSE modification scope, submodule LICENSEs, _model_registry MD5 duplicate intent, .gitmodules URL relocation) are left for the owner. Install / packaging: - Fix the inference-install P0: pandas was leaking into the bare install via predict.py -> training/abstention.py. Move `import pandas as pd` into the helper that needs it; module-level type hints stay valid via `from __future__ import annotations`. - Bump requires-python from ~=3.10 to >=3.11 to match zarr>=3.1's minimum, which made the prior pin unsatisfiable on 3.10. - Add License, Python-version, and audience classifiers. - Move plotly + kaleido into [train] (kaleido is required at runtime by fig.write_image in training/utils.py — validation refuted round-1's "kaleido is unused" claim). - Declare openai (used by scripts/generate_openai_embeddings*.py). - Declare nimbus-inference under [baselines] (used by scripts/run_gold_standard_nimbus.py and benchmark_gold_standard.py). - Drop the dangling [analysis] extra (no analysis/ dir existed). - Drop the dct_kit/config/*.json package-data glob (matched no files). - Add NOTICE attributing FocalLoss (MIT, from AdeelH/pytorch-multi-class-focal-loss) and percentile_threshold (modified-Apache, from vanvalenlab/deepcell-toolbox). Lab-internal paths / W&B: - Remove /data2 fallback in 6 scripts (DATA_DIR -> ""). - ingest_gold_to_zarr: drop /data/xwang3 default; --output_zarr is now required. - Tests: drop /data/xwang3 hardcoded fallback candidates. - wandb.login() now only fires when --enable_wandb is set; prevents blocking downstream users who don't have a W&B account. - W&B project name reads from WANDB_PROJECT env (default "deepcell-types", was hardcoded legacy "deepcelltypes"). Docs: - README: add license disclosure, TissueNet zarr provenance section, and Baselines section. - docs/conf.py: bump release from "0.0.1-dev" to "0.1.0". - docs/index.md: marker-embedding source is OpenAI text-embedding-3-large, not DeepSeek. - docs/site/reference.rst: expand to cover PredictionResult, DCTConfig, preprocess_fov, download_model (was predict-only). - tutorial.md: drop unused `rich` install; fix contradictory napari headless comment. - training/config.TissueNetConfig docstring: stop pointing at /data2 — runtime uses DEEPCELL_TYPES_ZARR_PATH. - dct_kit.DCTConfig: add a proper class docstring + drop person-named tissuenet-caitlin-labels.zarr from the archive-candidate list. Code drift / latent bugs: - scripts/train.py:548: HierarchicalLoss(config_path=...) used "config/combined_celltypes.yaml" (path didn't exist); now uses the in-package CONFIG_DIR / "combined_celltypes.yaml". - tests/test_v2.py: gate `import torchmetrics` with `pytest.importorskip` so the test no longer hard-fails in inference-only installs. - Remove vestigial "Key differences from V1:" comment in training/dataset.py (V1 class is gone). - Remove commented `# raw = np.clip(...)` line in dct_kit/image_funcs.py. Public API surface: - Add explicit __all__ to deepcell_types/__init__.py so `from deepcell_types import *` no longer pulls in submodule names. Gitignore: - Add audit/ (local audit reports — do not ship).
Audited license usage across the org's public repos (8+ flagships, including deepcell-tf @ 473 stars). Findings: - The Modified Apache 2.0 LICENSE we already ship is BYTE-IDENTICAL to deepcell-tf and matches the lab's template used in 8/10 primary releases (deepcell-tf, deepcell-label, deepcell-tracking, deepcell-spots, torch-mesmer, kiosk-console, deepcell-applications, tcga-utils). Plain Apache 2.0 is used only for utility/plugin/ paper-companion repos. -> KEEP LICENSE as-is. - The "[yyyy] [name of copyright owner]" placeholder in the APPENDIX is the standard Apache template text for users applying the license to their own derivative work; deepcell-tf has the same placeholder and the same wording. Round-1's "unfilled placeholder" finding was a false positive. -> No LICENSE edit needed. - Lab convention omits a PyPI "License ::" classifier (deepcell-tf and deepcell-tracking both omit). "Other/Proprietary License" misrepresents an academic-use Apache. -> Drop the classifier I added in the previous commit; mirror deepcell-tf's classifier set (Intended Audience / Topic / Programming Language). Added a deepcell-tf-style copyright header to deepcell_types/__init__.py that names the non-commercial restriction at the top of the package.
…licenses, split-meta tighten) Acts on user decisions on the deferred items in audit/VALIDATION-SYNTHESIS.md. #4 _model_registry — restructured (utils/__init__.py): - Drop the byte-identical "2025-06-09_public-data-only" alias. - Drop the preprint-era "specific_ct_v0.1" entry. - Convert the remaining "2025-06-09" hash to None — the verification fast-path skips when the hash is None; fill in at release time. - Add _baseline_registry + download_baseline_checkpoint() for the four baseline checkpoints, all with None placeholders. - Both download_*() helpers now reject unknown keys with a clear ValueError listing the valid options instead of silently downloading. #5 Drop self-referential backfill scripts: - scripts/_combine_v3_and_C.py (464 LoC, no callers outside its test) - scripts/refine_mp_labels_with_intensity_v2.py (1694 LoC, same) - tests/test_combine_v3_and_C.py - tests/test_refine_mp_labels_v2.py The output of both is baked into the canonical TissueNet zarr; the scripts were one-shot data-prep tools, not part of the active pipeline. Test count drops from 264 to 238 (the 26-test delta matches the 2 deleted test files). #6 Tighten split-metadata loading (training/dataset.py): - Remove "min_channels" from _ADVISORY_SPLIT_METADATA_KEYS — only "zarr_path" stays advisory (mount-point portability). - This restores the original tighter contract that the 3 failing tests in test_dataset_splits.py were asserting: a stale or missing min_channels in a split file now raises in strict mode and surfaces as a "split metadata mismatch:" message in non-strict mode. The legacy "ignore --min_channels drift" relaxation is gone. - All 22 tests in test_dataset_splits.py pass. #1 + #3 Submodule LICENSE / NOTICE / provenance — submodule pointer bumps: Each of the four baseline submodules now ships its own LICENSE (modified Apache 2.0, matching the parent's house template) and a NOTICE crediting the upstream paper. The CellSighter README is also updated from "adapted from" to "independent reimplementation" — the implementation does not copy code from KerenLab/CellSighter, so the upstream's unlicensed status does not affect us. The submodule commits live locally on this branch only; user is planning to transfer the four repos to vanvalenlab/ later (#2), at which point these commits get pushed alongside the transfer.
…ta tighten) Companion to a9332fd, which only landed the file deletions due to a git-add path error. This commit lands the remaining staged work. #4 _model_registry restructure (deepcell_types/utils/__init__.py): - Drop the byte-identical "2025-06-09_public-data-only" alias. - Drop the preprint-era "specific_ct_v0.1" entry. - Convert remaining "2025-06-09" hash to None (release-time placeholder). - Add _baseline_registry + download_baseline_checkpoint() for the four baseline checkpoints, all with None placeholders. - Both download_*() helpers reject unknown keys with a ValueError listing valid options. #6 Tighten split-metadata loading (deepcell_types/training/dataset.py): - Drop "min_channels" from _ADVISORY_SPLIT_METADATA_KEYS; only "zarr_path" stays advisory (mount-point portability). - Restores the tighter contract the 3 failing tests in test_dataset_splits.py were asserting. #1 + #3 Submodule LICENSE / NOTICE / provenance pointer bumps: - baselines/cellsighter -> own commit: LICENSE + NOTICE + README "adapted from" -> "independent reimplementation" wording change. - baselines/maps -> own commit: LICENSE + NOTICE. - baselines/nimbus -> own commit: LICENSE + NOTICE. - baselines/xgboost -> own commit: LICENSE + NOTICE. Submodule commits are local-only on this branch. They get pushed alongside the planned vanvalenlab/ org transfer (#2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…registry
Companion to the checkpoint-folder rename (~/dct-final-ckpt → clean
asset names matching the registry filenames). No content changes —
only naming and registry population.
deepcell_types/utils/__init__.py:
- Populate _model_registry and _baseline_registry with the real MD5
hashes of the v10 paper-release ckpts (recomputed against the
renamed files; content unchanged).
- _latest = "2026-05-17" (the cleanup-date label, replacing the
earlier "2025-06-09" placeholder).
- Asset filenames now use the clean ``deepcell-types_{version}.pt``
and ``deepcell-types_baseline-{name}.{pth,pt,json}`` convention.
- ``maps`` / ``xgboost`` baseline entries are 2-tuples — both ship
a companion file (``_stats.npz`` / ``.remap.json``) required at
inference; ``download_baseline_checkpoint`` returns the full list.
Vestigial-suffix sweep (drop ``_0``, ``v10``, ``lora8``, ``meanint_cls``
from CLI defaults and docs that are user-facing):
- scripts/train.py: --model_name default "v2_test_0" → "deepcell-types".
- scripts/predict.py: same.
- scripts/pretrain.py: --model_name default "pretrain_0" → "pretrain";
usage docstring updated.
- deepcell_types/model.py: drop "pre-v10 default" and "v10 training
recipe" qualifiers from docstrings (the recipe is now THE recipe).
- deepcell_types/predict.py + scripts/predict.py: "v10 paper headline"
→ "paper headline" / "canonical".
- deepcell_types/training/losses.py: "canonical v7/v8 recipe" →
"canonical recipe"; "post-v7 archive" qualifier dropped.
- scripts/generate_splits.py: drop v9/v10 historical labels in
docstring usage examples.
- docs/site/API-key.md: switch the version example from the dropped
"2025-06-09_public-data-only" alias to "2026-05-17"; add a
download_baseline_checkpoint() example.
- docs/site/tutorial.md: bump the cited model name to "2026-05-17".
- tests/test_ct_abstention_cli.py, tests/test_stratified_splits.py:
drop v10/v9 historical labels in test docstrings.
Drop scripts/generate_manifest_index.py (-356 LoC):
- Self-declared "throw-away helper" (its own docstring).
- 0 callers anywhere in the tree.
- Targets ``REPO_ROOT/models/`` and ``REPO_ROOT/output/`` directories
that are gitignored runtime artifacts, not source.
- Hardcoded canonical-checkpoint list (v7/v8 ablation filenames)
bears no relation to the v10 paper-release bundle the rest of the
registry now points at.
The remaining v7/v8/v9 references in tracked code are real product
concepts (zarr archive schema versions ``v7 flat`` / ``v8 5-level``
in ``training/{archive,config,dataset}.py`` and the
``tissuenet-v9.zarr`` filename candidate in dct_kit/config.py); those
stay.
Test count unchanged (238); bare-install import still clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mechanical follow-up after the bigger cleanup commits land. No behaviour
changes for any non-error path.
deepcell_types/training/{baseline_features,dataset}.py:
- Convert all 18 ``print()`` calls in library code to ``logger.{info,warning}``
with %-style format args. Both modules already had ``logger = logging.
getLogger(__name__)`` defined; downstream callers that want the old
stdout chatter can ``logging.basicConfig(level=logging.INFO)``.
deepcell_types/dct_kit/image_funcs.py:
- Replace the 2 ``assert`` statements (shape rank and mask-dtype checks)
with explicit ``ValueError`` / ``TypeError``. ``assert`` doesn't fire
under ``python -O`` and isn't appropriate for input-validation in a
public library.
deepcell_types/utils/_auth.py:
- Drop the two stale TODO comments. The "case statement" TODO predates
the Python 3.11 floor we now require; the suffix dict it gates is
clear enough.
docs/conf.py:
- Drop ``templates_path = ['_templates']`` — the referenced directory
has never existed; the line was silently ignored by Sphinx.
Stale ``deepcelltypes/`` (no-underscore) references — these are
pre-monorepo package paths that no longer resolve. Touched 4 files:
- scripts/ingest_gold_to_zarr.py docstring
- tests/test_embeddings_load.py docstring
- tests/test_hierarchy_one_way.py docstring
- tests/test_preprocessing.py docstring
tests/test_losses.py:
- Fix the legacy ``repo_root/config/combined_celltypes.yaml`` path that
silently ``pytest.skip``ed (the YAML moved into the package at
``deepcell_types/training/config/``). Now resolves via ``CONFIG_DIR``
and actually exercises ``HierarchicalLoss`` end-to-end.
tests/test_preprocessing.py:
- Replace ``not Path(PRODUCTION_ARCHIVE).exists()`` skipif (which
evaluated to False when the env var was unset because ``Path("")``
resolves to ``"."``, the cwd) with an explicit ``not (var and
Path(var).exists())`` check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: public-release cleanup pass
The paper headline operating point is now k=0.2 (was k=0.5). This widens the macro_F1 separation against the strongest baseline (XGBoost-tuned) from a within-noise +0.11 pp at k=0.5 to a clean +4.81 pp at k=0.2, and DCT now sweeps every baseline on every has_support metric on the v10 held-out test split (91.72 / 83.84 / 95.34 / 95.40). Updates: - scripts/predict.py: --ct_abstention_k default 0.5 -> 0.2; refreshed help - deepcell_types/predict.py: predict() ct_abstention_k=0.2; docstring - deepcell_types/training/abstention.py: module docstring example Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-safe B2
Resolves two silent-correctness blockers surfaced by the 2026-05-26 deep
review. Both affected the published-baseline numerics path; both produced
no error signal.
* scripts/predict.py CT abstention: the per-(tissue, modality) IQR fence
read tissue/modality metadata via root.group_keys(), which on v8+
nested archives (modality/tissue/cohort/sample/fov) returns modality
directories, not dataset leaves. The downstream merge then filled
tissue/modality with "unknown" for every row, collapsing the
per-group fence into a single global fence. Replaced with
_discover_fov_keys, which handles both flat (v7) and nested (v8+)
layouts. (errors.md F1)
* model.py mean-intensity CLS residual: scatter_ with safe_idx[~valid]=0
aliased every padding write to column 0, and last-write-wins semantics
then zeroed the real mean intensity of whichever marker sits at index 0
of marker2idx on every forward pass with mean_intensity_mode in
("cls_residual","both") — the paper-default operating point. Fixed
structurally with a sink column at index n_markers; padding writes
land there and the sink is sliced off before the projection. To
preserve inference parity with v0.1.0 checkpoints (which were trained
against the buggy zero-marker-0 contract), the forward pass also zeros
column 0 explicitly when compat_marker0_zero=True (default). Pass
compat_marker0_zero=False to a future v0.2.0 retrain to recover the
real marker-0 signal. (numerical-stability.md HIGH-1)
Also tightens the train/inference module boundary:
* compute_iqr_fence + ABSTENTION_LABEL move to deepcell_types/abstention.py
(numpy-only public module). deepcell_types/predict.py no longer imports
from training/abstention; the [train] extra is no longer load-bearing
on the inference path. training/abstention.py keeps a re-export for
backward compatibility. (complexity.md BLOCKER)
* fusion linear's bias was reaching padding token positions despite the
zeroed inputs (W@0 + b = b); padding tokens are masked from attention
so nothing leaks today, but the CHANGELOG-documented "padding produces
zero output" invariant was violated at the tensor level. Added a
post-fusion masked_fill so the invariant holds. (numerical-stability.md
MEDIUM)
* scripts/predict.py abstention sentinel was -1; Python API uses
"Unknown". CLI now also writes "Unknown" so downstream tooling sees a
single contract. (api.md HIGH-3)
* predict.py: stale "Unknown tissue_exclude=" error message named the
deprecated kwarg and gave no hint of valid tissues — now reads
"Unknown tissue_filter=...; Valid tissues: [...]". Deprecation warning
for the tissue_exclude alias switched DeprecationWarning -> FutureWarning
so end-user scripts actually see it. (api.md HIGH-1, MEDIUM-7)
* Tests updated for the new error message and warning class.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p-review pass)
Resolves the long tail of release-readiness items surfaced by the
2026-05-26 deep review. Nothing here changes inference outputs; the
correctness fixes ride in the prior commit.
Legal / packaging:
* LICENSE: fill the Apache appendix placeholder
("Copyright [yyyy] [name of copyright owner]" -> Caltech, 2024-2026).
* pyproject.toml: switch authors email to vanvalenlab@gmail.com (was a
personal institutional address); add License classifiers ("Apache
Software License" + "Other/Proprietary License" — the LICENSE is a
modified Apache 2.0 with non-commercial / academic carve-outs); add a
setuptools>=61 floor so the build-system actually understands
pyproject-driven [tool.setuptools].
* docs/conf.py: replace literal "%Y" copyright string (Sphinx doesn't
strftime that) with "2024-2026"; switch author to "Van Valen Lab".
Public-facing docs:
* README "--zarr_path" claim corrected to "--zarr_dir" (every training
script declares the latter; the env-var pickup is inference-only).
* docs/index.md: same correction; fix [dc_org] link to the actual
users.deepcell.org login URL (was pointing at the Sphinx page); drop
the stale "torchvision" mention in the [train]-extras list (the
dataset code explicitly avoids torchvision).
* docs/site/tutorial.md: zarr instructions unified on >=3 (the inline
sanity check raised against "zarr>2" pip hint); typo "nad" -> "and";
napari.Viewer(show=True) -> show=False so the notebook executes in
headless docs builds.
Archive version (canonical = v10):
* dct_kit/config.py ARCHIVE_CANDIDATE_NAMES now lists v10 first, v9 and
v8 retained for backward compat.
* docs/index.md, docs/site/tutorial.md, training/archive.py docstrings
updated to reflect v10 as the current canonical.
Public API surface:
* deepcell_types.__all__ adds download_model (the mandatory first call
for any user) and PreprocessedFov (the dataclass returned by
preprocess_fov, which was already exported).
* utils.download_training_data dropped the documented-but-ignored
version= kwarg — the asset is pinned to public_data_v1.1.zip.
R&D residue / lab-internal references:
* training/config.py: stale "deepseek-r1-70b" defaults on
get_channel_embedding / get_celltype_embedding / load_marker_embeddings_array
-> "text-embedding-3-large" (matches docs and the actual pipeline).
* training/config.py: "hubmap-to-zarr migrate_archive_v2.py" references
in user-visible warning strings -> generic "archive-ingestion
pipeline" wording.
* training/__init__.py: drop torchvision from the gated-deps list (not
actually in [train]).
* training/abstention.py: drop dead docs/audits/... cross-reference
from a docstring that ships in the wheel.
* scripts/generate_openai_embeddings.py: usage example used a personal
/data/xwang3/... path and pointed at svd_512_v7.npz (canonical is
svd_512_v6.npz) — fixed both.
* scripts/validate_archive_contract.py, scripts/generate_splits.py,
scripts/benchmark_gold_standard.py: drop the
"tissuenet-caitlin-labels.zarr" default from --zarr_dir / --help text
(a researcher-named lab-internal filename).
* scripts/benchmark_gold_standard.py: "deepcelltypes installed" doc
references the long-retired no-hyphen package name -> "deepcell-types".
Security:
* utils/_auth.py: the 403-on-bad-token branch interpolated the user's
DEEPCELL_ACCESS_TOKEN into the ValueError message — any traceback
logger (Sentry, wandb, CI) would have captured it. Now reads "The
provided DEEPCELL_ACCESS_TOKEN is not valid." with no token echo.
* utils/_auth.py: the streaming download left a truncated file on disk
if the request was interrupted (no atomic-write, no cleanup); now
wrapped in try/except that unlinks fpath on any exception.
* utils/_auth.py: when file_hash is provided, the function now verifies
the downloaded bytes against it before returning (previously the hash
was used only for cache-skip, never as a post-download integrity
gate). Catches truncated downloads and silent CDN corruption.
* utils/_auth.py: drop the emoji from the success log line (breaks on
some HPC log parsers; inconsistent with the rest of the codebase).
* scripts/train.py:395, scripts/benchmark_gold_standard.py:526,
scripts/fold_lora_into_proj.py:59: torch.load(weights_only=False)
-> True. The inference path was already weights_only=True; these
three checkpoint loads ship as part of public scripts that take
CLI-supplied paths, so a malicious .pt would execute arbitrary
pickle on the user's machine. Checkpoints only contain tensor /
primitive state, so weights_only=True is sufficient.
Tests:
* tests/test_canonical_inference.py: the two tests whose expectations
shifted with the new error message and FutureWarning class are
updated in the prior commit (kept together with the production
change).
.gitignore: ignore /.claude/ (per-project local CLI settings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the public-release CI that was missing entirely. Pairs with the
test guards needed to make the inference/[train] dep separation
actually work on a bare ``pip install deepcell-types`` checkout (the
prior 2026-05-26 audit found 11 of ~24 test files collection-erroring
on inference-only installs because of bare top-level imports of
zarr / pandas / torchmetrics).
CI (.github/workflows/ci.yml):
* inference-only job: ``pip install -e .`` + pytest. Guards that the
bare install has every dep it needs and that no [train]-only module
has slipped into the predict() import graph
(tests/test_inference_deps.py is the active probe).
* full job: ``pip install -e .[train]`` + pytest. Catches drift
between training-side code and the tests that exercise it.
* lint job: ruff over deepcell_types/, scripts/, tests/.
* Matrix on Python 3.11 / 3.12; concurrency cancels stale runs on push.
tests/conftest.py:
* collect_ignore-based skip for the 13 test files that legitimately
need a [train]-extra at module-import time (zarr, pandas, ...). On
an inference-only install these now skip cleanly instead of
collection-erroring, which means CI's inference-only matrix row
actually runs to green and verifies the bare-install invariant.
When the [train] extra is installed the same files run normally.
tests/test_post_v7_strip.py:
* _help_output() used check=False; on an inference-only install the
``python -m scripts.predict --help`` subprocess crashed with
ImportError (torchinfo/torchmetrics), the traceback was returned as
a string, and the ``assert flag not in out`` assertions trivially
passed. Switched to check=True + ``sys.executable`` and guarded the
whole module with importorskip("torchinfo") / importorskip("torchmetrics").
tests/test_inference_deps.py:
* Added click, plotly, kaleido, openai, tifffile to TRAINING_ONLY_MODULES
so the dep-separation guard catches future module-level imports of
any [train]-only package — not just the original 8.
.gitignore: ignore /.claude/ (per-project local CLI settings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
k=0 yields fence=Q1 (abstains bottom quartile), not a no-op. The "off" behavior is enforced caller-side via a k>0 guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ipts - embeddings/svd_512_v6.npz -> embeddings/svd_512.npz (4 files) - delete scripts/fold_lora_into_proj.py (LoRA adapter removed, no longer needed) - delete scripts/generate_openai_embeddings_v8.py (one-off, superseded by generate_openai_embeddings.py) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pace Moves the Rumberger et al. 2025 Pan-Multiplex Gold-Standard benchmark + Nimbus baseline runner + dataset ingest + download helper to deepcell-types-research-workspace/scripts_local/. These are evaluation/research tools, not part of the core train/predict/pretrain user-facing CLI surface. - scripts/benchmark_gold_standard.py -> workspace/scripts_local/ - scripts/run_gold_standard_nimbus.py -> workspace/scripts_local/ - scripts/ingest_gold_to_zarr.py -> workspace/scripts_local/ - scripts/download_gold_standard.sh -> workspace/scripts_local/ Drop the corresponding bullets from README/docs/index, tidy the pyproject tifffile comment, and refresh the dataset.py code comment that referenced the old path. CHANGELOG.md is left untouched (historical record). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the vestigial one-deep `dct_kit/` subdir (legacy artifact from a pre-monorepo "library" split) by moving its contents up to the package root: - `dct_kit/config.py` → `deepcell_types/config.py` (DCTConfig) - `dct_kit/config/channel_mapping.yaml` → `deepcell_types/channel_mapping.yaml` - `dct_kit/image_funcs.py` → folded into `deepcell_types/preprocessing.py` Only `patch_generator` is imported externally (by `dataset.py`); all helper functions are now `_`-prefixed private. Two unused helpers (`histogram_normalization`, `combine_raw_mask`) had zero call sites and were dropped. The new and legacy preprocessing pipelines are intentionally kept side by side in `preprocessing.py` — `_percentile_threshold` (NaN-percentile) is the canonical ingest path used by `preprocess_fov`, while `_percentile_threshold_nonzero` (nonzero-indexed) is what the published checkpoint was trained against and is wired into `patch_generator`. A comment block at the merge boundary calls this out so a future reader doesn't try to dedupe them without retraining. Inference + training test suites green (58 passed, 3 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The archive is being rebranded from the lab-internal `caitlin-labels` name. This sweep covers tests, all four baseline submodules' README + CLI defaults, and bumps the submodule pointers (cellsighter, nimbus, xgboost) to their renamed-doc commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI lint job (ruff check) was failing across the fork. This pass fixes
all 75 reported errors without changing behavior:
- scripts/{predict,pretrain,train}.py: move ``logger = ...`` below the
import block (was E402 x25 from being sandwiched between imports).
- deepcell_types/training/config.py: drop the duplicate bottom-of-file
``from .archive import (...)`` block — the top-of-file import already
exposes those names as module attributes (F811 x8). Keep the
``from .patch import (...)`` re-export. Add ``# noqa: F401`` on the
archive import for the names not used in-file.
- Remove confirmed-unused imports across deepcell_types/ and tests/
(F401 x20).
- Convert ``assert x == True/False`` on numpy bool entries to direct
truthiness in tests/test_{v2,zero_channel_masking}.py (E712 x14).
- Drop unused local vars (``ct_label``, ``first_size``, ``epoch1_a``,
``scaler``) (F841 x4).
- Drop ``f`` prefix on placeholder-less f-strings (F541 x2).
- Convert one ``skip = lambda k: ...`` to a ``def`` (E731).
- Rename ambiguous loop var ``l`` -> ``logit`` (E741).
Smoke-checked the inference import path; ``ruff check`` is now clean.
``apply_abstention`` writes ``ABSTENTION_LABEL`` (= "Unknown") to the ``predicted_ct`` column of abstained rows, but ``test_k_0_5_aggressive_on_1000_cells`` was still asserting ``== -1`` — the original numeric sentinel from before the column went string-typed. Pre-existing CI failure, surfaced once the ruff job stopped masking it. Import ``ABSTENTION_LABEL`` and compare against it instead.
…default Clarify how to run the canonical inference pipeline relative to the upstream/self-contained flow: - index.md: add a "TissueNet archive (required)" section — the registry is now read from a tissuenet-v*.zarr archive (was bundled YAML), via zarr_path= / DEEPCELL_TYPES_ZARR_PATH, with the FileNotFoundError / ValueError failure modes spelled out. - tutorial.md: note the on-by-default low-confidence abstention (ct_abstention_k=0.2 -> "Unknown"), how to disable it (=0) for raw argmax labels, and the return_probabilities / PredictionResult option. - CHANGELOG.md: record the abstention default as a breaking change. - preprocessing.py: convert preprocess_fov docstring to NumPy style so numpydoc renders it cleanly (clears the sphinx -W warning). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
create_dataloader_from_config (its only functional consumer) was removed earlier in this PR, leaving DataLoaderConfig as an exported-but-inert dataclass with no ergonomic call path. Remove it for a clean release surface; the full keyword API of create_dataloader remains the single way to configure a loader. - dataloader.py: drop the class and its now-orphaned `dataclass`/`typing` imports; update the module docstring. - dataset.py: drop the back-compat re-export and the `__all__` entry. - test_training_import_order.py: drop the DataLoaderConfig import assertions. Full suite: 358 passed, 1 skipped. ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mpler feat(baselines): unify all baselines on the DCT sampler by default
Rebased onto master (which now has #60's --class_balance / DCT-sampler default). Adds an opt-in --val_split_file to MAPS, CellSighter, XGBoost (plain + tuned) so each trains on the FULL --split_file train and selects its checkpoint / early-stop / Optuna-trial on an EXTERNAL validation set — the 'val' FOVs of --val_split_file, capped to 200k cells at seed 42 (mirroring dataloader.py:269-273 max_val_samples), scored with the canonical hierarchical ct_macro_f1 (metrics.py:399-419). The reported set stays --split_file 'val'. Legacy inner-val carve is unchanged when the flag is absent. This matches how the main DCT model selects (val_macro_f1 on the canonical 302-FOV validation, 200k cap), making baseline model-selection consistent with DCT instead of each baseline self-carving a different 10% inner-val. Combined-state fix (needed once #58's eval_set_external path meets #60's sample_weight): thread class_balance through xgb/tuning.py _run_canonical_val_tuning -> run_tuning / train_best_model, and apply compute_sample_weights_dct in the eval_set_external branch — otherwise the tuned-XGB canonical run would tune with DCT weights but ship an unweighted final model. Also adds --features_cache to xgboost-tune for cache reuse, and lists val_split_file in the frozen CLI option snapshots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…selection feat(baselines): --val_split_file canonical external-val selection (consistent with DCT)
chore: release-readiness cleanup (cruft, stale refs, dead API, split docs)
Commit 4cc7377 reverted LICENSE to plain OSI Apache 2.0 but left NOTICE, pyproject.toml, and deepcell_types/__init__.py all still asserting a Modified Apache License with a non-commercial carve-out — the operative license text (LICENSE itself, per Apache 2.0 §4(d)) contradicted every other document describing it. Restoring the carve-out so all five locations agree, per project decision.
… paper The CLI defaulted CT abstention ON at k=0.2 with help text calling it "the published headline operating point" — stale against the current paper, which reports no-abstention, full-coverage macro-F1 (84.4%). The figure pipeline already passes --ct_abstention_k 0 explicitly; this fixes the CLI's own default/docs so a user following --help reproduces the actual headline instead of a silently-reduced-coverage number.
Every other trusted-local-checkpoint load in the repo (retrain_head.py, train.py x2) uses weights_only=False with a comment explaining that pretrain.py's own checkpoints embed numpy scalars that trip PyTorch 2.6's weights_only=True default. pretrain.py's own --resume path was the one outlier still using weights_only=True, which would raise an opaque UnpicklingError on exactly the checkpoints this repo produces.
Every other version-sensitive dep (torch, zarr, kaleido) has an upper bound with a comment explaining why; openai (a fast-breaking SDK, per its v0->v1 client rewrite) was the one unconstrained major dependency. Live check (.venv/bin/python -c "import openai") showed 2.37.0 installed, not the 1.x the plan template anticipated, so the bound is >=2.0,<3 (matching the actually-verified major version) rather than >=1.0,<2.
…_split_file CHANGELOG.md had gone stale, missing the last several commits including a default-changing behavior (baselines now share the DCT sampler by default) and a new CLI flag. Catching this while it's recent rather than after another release cycle passes.
Aligns with hubmap-to-zarr's migrate_archive_v2.py step4_tissue_normalization, which already strips internal spaces. The two functions previously disagreed (consumer-side kept spaces, producer-side stripped them) and only happened to agree because every tissue value currently in the live archive is already single-word; a future multi-word raw ingest would otherwise silently split into a separate tissue category.
…upport=50 floor Console-printed / model-selection macro-F1 uses support>0; the paper headline additionally applies min_support=50 (research-workspace's _score_csv.py). No behavior change — just preventing a reader from mistaking the two for the same number.
fix: address deep-review findings (license, API drift, docs)
All prose/comments, no behavior change — items surfaced by the v0.1.0 release review: - CHANGELOG: resolve the resmlp-vs-mlp "default" contradiction (the released checkpoint is mlp and loads unchanged; fresh training defaults to resmlp) and fix the np.ptp rationale (NumPy 2.0 removed the ndarray.ptp() method, not the free np.ptp function). - abstention.py: label k=0.2 as the eval-CLI / paper operating point, not the predict() default (which is None = abstention off). - README: drop the broken "version stem" model_name claim — a bare stem resolves to a filename download_model() never writes. - reference.rst: add list_model_versions (in __all__ but undocumented). - dataset.py: clarify duplicate-channel de-dup keeps the first channel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ints scripts/train.py bundles the canonical channel / cell-type registry at the checkpoint top level so inference (validate_checkpoint_vocabulary) can assert the ordering the checkpoint was trained with. retrain_head.py — which writes the new *default* resMLP-head checkpoints — did not, so those checkpoints fell through to the packaged-vocab SHA anchor branch meant only for the legacy released checkpoint. That is a silent-mislabel risk if a future retrain's vocab/marker ordering diverges from the packaged vocab.json. Bundle canonical_channels + ct2idx from the in-scope TissueNetConfig, matching train.py. Additive; existing checkpoints are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- predict(): add the `-> list[str] | PredictionResult` return annotation it was missing, so the flag-dependent dual return is visible to type checkers/IDEs before the 0.1.0 public freeze. Non-breaking. Also qualify the Returns docstring so "Unknown" is described as appearing only when ct_abstention_k is set. - preprocessing_ops._min_max: compute the range as max - min instead of np.ptp, matching the other normalization paths (numerically identical). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Release-review Tier-1 findings — cases where predict() could return a confident but wrong/misaligned result with no error: - predict(): reinstate cells lost when the mask is downscaled to the model target MPP (native mpp < 0.5 vanishes small cells under nearest-neighbor resampling). The returned list now covers every input cell in ascending id order, labeling dropped cells "Unknown" (warning + zero-prob row) so a positional zip against np.unique(mask) stays aligned. No-op when nothing drops. Regression test added. - predict(): raise on non-finite model logits instead of letting a poisoned softmax label every cell class 0 unabstained. - dataset: emit ONE aggregate "K of N channels masked" warning instead of one-per-channel, so a panel mostly failing to resolve is visible rather than hidden behind de-duplicated warnings. - config: warn when the ambient $DATA_DIR probe (not an explicit zarr_path / DEEPCELL_TYPES_ZARR_PATH) is what supplies the marker/cell-type registry, since reading the vocab from an ambient archive can silently change predictions. - preprocessing: unify _percentile_threshold's nonzero mask to the != 0 predicate (matching _percentile_threshold_nonzero / DEFAULT_CONFIG and the deepcell-toolbox recipe) so the archive-builder and inference paths agree bit-for-bit on negative-valued inputs too. Also adds an mlp-head checkpoint load test — the released-checkpoint branch had no coverage after resmlp became the create_model default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-freeze API and documentation cleanups from the release review: - Add module-level __all__ to utils / abstention / preprocessing_ops (and, in the prior commit, predict / config / preprocessing) so a 0.1.0 freeze does not lock in undeclared helpers users happen to import. - Add list_baseline_names() (top-level export) as the list-returning counterpart to list_model_versions(), and document the Path vs list[Path] return asymmetry between download_model / download_baseline_checkpoint. - list_model_versions() returns _latest first explicitly, so the "first element is the default" docstring holds regardless of id sort order. - README: document the baseline-comparison fairness contract (shared --class_balance dct + --val_split_file, which are opt-in). - docs/index.md: drop the bare "pip install deepcell-types" (PyPI is not published; the canonical install is git+https). - reference.rst: list list_baseline_names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n self-describing checkpoints Follow-up to the release-review fixes, tightening two items from "warn/guess" to "remove/require" per maintainer direction: - config (#3): fully remove the $DATA_DIR archive auto-discovery and the now- orphaned DCTConfig.ARCHIVE_CANDIDATE_NAMES attribute. DCTConfig resolves an archive only from an explicit zarr_path= or DEEPCELL_TYPES_ZARR_PATH, then falls back to the packaged vocab.json. A generic DATA_DIR can no longer silently switch the vocabulary source. - predict (#11): drop the guess-canonical-defaults-with-warning fallback for checkpoints missing n_heads/compat_marker0_zero. A self-describing checkpoint (bundles its own ct2idx) must now record both or predict() raises — they are not recoverable from weights and a wrong value loads with silently wrong numerics. The legacy released checkpoint (no bundled ct2idx) still loads via the canonical v0.1.0 defaults; scripts/train.py already records both keys, so its outputs are unaffected. Test fixtures now bundle a realistic config (n_heads/compat_marker0_zero), matching train.py, plus a new test that a self-describing checkpoint missing the keys raises. Full suite green (361 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reverts the non-commercial/academic carve-out in LICENSE back to plain OSI Apache-2.0. Per request, only LICENSE is reverted; NOTICE, pyproject.toml, and __init__.py retain their carve-out wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes the prior Frozen-CLS headline entry from _model_registry; the 2026-06-15 resMLP checkpoint is the sole released model. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mlp head support No backward compatibility is kept for the retired 2026-05-17.pt checkpoint. - predict.py: remove the canonical-ct2idx SHA anchor and its helper; a checkpoint that bundles no ct2idx is now rejected outright. Arch keys (n_heads, compat_marker0_zero) are required unconditionally since every supported checkpoint self-describes. - model.py/predict.py/train.py/scripts: remove the legacy 3-layer 'mlp' cell-type head entirely; ResidualMLPHead is the only head. Drop the --ct_head_arch CLI option and the ct_head_arch parameter. - retrain_head.py: load the backbone with ct_head.* keys stripped and strict=False (the head is discarded and retrained), instead of scaffolding an mlp head to strict-load. - tests: reject-without-ct2idx test replaces the canonical-ordering test; drop the legacy-mlp-head load test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pre-guard checkpoints (#63) Current predict.py / scripts/predict.py enforce validate_checkpoint_vocabulary, which requires a checkpoint to bundle its own `ct2idx` so a permuted vocabulary cannot silently mislabel every cell. train.py and retrain_head.py already write `ct2idx` + `canonical_channels` for new checkpoints, but the released 2026-06-15 resMLP asset predates that convention (it holds only {model, config}) and therefore fails the guard on current master — `download_model("2026-06-15")` + `predict(...)` raises for every user. Add a helper that back-fills those keys from DCTConfig (packaged canonical vocab by default, or an explicit `--zarr_path`), with the same n_celltypes guard the count check would apply. The pure `bundle_vocabulary()` is unit tested. Verified end-to-end: the re-packaged 2026-06-15 asset loads via predict() on current master and yields a plausible distribution on the tutorial's HBM994_PDJN_987 placental FOV. Completing the fix is a release step (maintainer): run this script, upload the output to users.deepcell.org, and pin the uploaded file's md5 in _model_registry (torch.save is non-deterministic, so the hash must be the actual uploaded artifact's). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#64) * chore(release): reconcile stale defaults/docs + model_name convenience Public-release audit of the default-facing surface (defaults, docs, flags, fallbacks). The default inference path already resolves to the canonical model; these changes remove internal inconsistencies a public user could hit. Abstention framing — the paper headline is full-coverage / no abstention; k=0.2 is a historical opt-in ablation. Reconcile the stragglers that still called k=0.2 "the paper headline", matching the authoritative CLI help and README: - deepcell_types/abstention.py, deepcell_types/predict.py (docstrings) - scripts/predict.py (inline comment; it also wrongly said abstention is "on by default" — the CLI default is 0.0 / off) - docs/site/tutorial.md (shipping tutorial) - tests/test_ct_abstention_cli.py (rename the test that asserted a k=0.2 "default" the CLI no longer has; it never checked the real default) Env-var consistency — source the archive path from DEEPCELL_TYPES_ZARR_PATH (the canonical var used by config/inference) with DATA_DIR kept as a non-breaking fallback, across the training scripts and baseline runners. predict() model_name convenience — accept a registry version string (e.g. "2026-06-15") or "latest" and auto-download/cache it, so a caller can reach the canonical checkpoint without knowing the on-disk filename. Minor — export download_training_data at top level (was only in utils.__all__ + reference.rst); clarify in the README that the resMLP head is auto-detected at inference (no ct_head_arch flag). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(predict): drop deprecated device_num alias The `device_num=` keyword was a back-compat alias for `device=` that emitted a DeprecationWarning. For the public release there is no prior published API to stay compatible with, so remove the alias, its resolution branch, and its docstring entry. `device=` is now the single device argument (still required). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(archive): drop zarr-3.0-alpha metadata monkeypatch `_patch_zarr_v3_alpha_metadata` globally monkeypatched GroupMetadata.from_dict and ArrayV3Metadata.from_dict so a zarr 3.0.0a* alpha could parse the `consolidated_metadata` / `storage_transformers` keys emitted by newer writers. The pin is now `zarr>=3.1` (stable), which parses these natively — verified by reading the real 292-marker archive (root group + a preprocessed array's v3 metadata) with the shim removed. Remove the monkeypatch and its back-compat re-export from training/config.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
xuefei-wang
marked this pull request as ready for review
July 4, 2026 22:39
xuefei-wang
added a commit
that referenced
this pull request
Jul 14, 2026
…TypeAnnotator, archive-free inference, vendored baselines Squashed release history for v0.1.0. Consolidates the training and inference codebases into a single package (CellTypeAnnotator), moves the cell-type/marker vocabulary into the zarr archive, adds abstention + PredictionResult to predict(), and vendors the baseline implementations (maps, cellsighter, nimbus, xgb). Supersedes the granular development history brought in via PR #41.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the
v0.1.0release cut. It merges the separate training repository(
deepcelltypes-cell-type-assignment-pytorch) into this repo and replaces thelegacy
CellTypeCLIPModelinference path with the current canonicalCellTypeAnnotatormodel.Before this PR,
vanvalenlab/deepcell-typeswas inference-only — it shippedCellTypeCLIPModel, thedct_kit/helpers, and a top-level__init__thatexported just
predict. After it, a single package covers training andinference: inference stays a plain
pip install deepcell-types, the fulltraining pipeline lives behind a
[train]extra, and the four paper comparisonbaselines are vendored behind per-baseline extras.
The
CHANGELOG.md0.1.0entry is the authoritative, line-by-line record; this descriptionsummarizes it.
Canonical model
model.pyis rewritten aroundCellTypeAnnotator;CellTypeCLIPModel/CellTypeDataEncoderare removed (no shim). Canonical training defaults(
scripts/train.py,click-based CLI):--resnet_channels 48,--domain_weight 0.1,--best_metric macro_f1,--lr 1e-3, and the new--ct_head_arch resmlp.ct_head_arch="resmlp",width-512 / depth-4) for fresh training, replacing the legacy 3-layer MLP.
Trained on the frozen backbone via
scripts/retrain_head.py(the two-stagesampler-off recipe). Checkpoints record
config["ct_head_arch"]andpredict()auto-detects the head from the state dict, so the released(legacy-MLP) checkpoint still loads unchanged.
resmlpandmlpcheckpoints have different head shapes and are not interchangeable.
into a marker-position vector and injected as a CLS residual. The output
projection is zero-init, so warm-starting from a checkpoint preserves
predictions at step 0.
(
--domain_weight 0.1;0disables it).--freeze_backbonetrains only themean-intensity branches on top of an existing checkpoint;
--unfreeze_ct_headadditionally co-adapts the CT head / CLS token / final norm without unfreezing
the transformer backbone.
masked_fill,AMP-safe) through the channel encoder, fusion, and mean-intensity paths so
masked tokens contribute exactly zero rather than leaking
proj.bias/spatial_featinto the transformer.scripts/train.pybundlesct2idx,n_heads, andcompat_marker0_zero, and inference asserts the vocabularyordering matches (a permuted vocabulary previously passed the count-only
check and silently mislabeled cells).
--resume_pathvalidatesn_heads/n_celltypes/resnet_channels/d_modelbefore restoring optimizerstate.
Canonical-only inference
packaged
vocab.jsonsnapshot, sopip install deepcell-types+download_model()is enough to runpredict()— the multi-GB TissueNet zarrarchive is no longer required (pass
zarr_path=/ setDEEPCELL_TYPES_ZARR_PATHonly if you need the tissue→cell-type mapping).Verified identical predictions with vs. without the archive on the paper
checkpoint.
predict(ct_abstention_k=...)defaults toNone, returning the raw argmax label for every cell. Set a float and cellsbelow an IQR fence on the per-FOV confidence distribution are relabeled to the
"Unknown"sentinel;ct_abstention_k=0.2reproduces the paper headlineoperating point. The pre-abstention argmax is preserved in
cell_types_rawwhen
return_probabilities=True. (Default isNone, not on-by-default, sothe plain
list[str]return is never silently rewritten at a benchmark-tunedthreshold.)
predict(..., preprocess=...)overrides theper-FOV normalization without retraining, backed by a bounded op library
(
apply_config,make_preprocessor,DEFAULT_CONFIG) and acomposition-guided adaptation loop (
skills/preproc-adapt/).make_preprocessor(DEFAULT_CONFIG)reproduces the default bit-for-bit.masked at inference (matching the training dataloader — an all-zero channel
was previously fed as a present zero token); duplicate channels resolving to
the same canonical marker are de-duplicated; non-finite (
NaN/inf)rawnow raises a clear
ValueErrorinstead of a poisoned softmax; per-celltensors are sized to the channels actually present rather than the global
MAX_NUM_CHANNELS. The first three can change predictions for affected FOVs.DCTConfig.PERCENTILE_THRESHOLD) is now99.9, matching the recipe the training archive was built with (was99.0).Shifts ~5% of predicted labels; reproduces the canonical predictions slightly
better on a held-out test sample (92.5% vs 91.9% argmax agreement).
predict(return_probabilities=True)returns aPredictionResultdataclasswith the full per-cell softmax matrix, cell indices, and the pre-abstention
argmax labels (
cell_types_raw).weights_only=Truewhere possible; a missing checkpoint raises aclear
FileNotFoundErrorpointing atdownload_model().New public API
predict,DCTConfig,PredictionResult,preprocess_fov,apply_config,make_preprocessor, andDEFAULT_CONFIGare importable fromdeepcell_typesdirectly.
preprocess_fov(raw, mask, native_mpp, channel_names) → PreprocessedFovis the standalone preprocessing entry point.Monorepo: training pipeline
deepcell_types.trainingships behindpip install "deepcell-types[train]":config.py,dataset.py,archive.py,annotations.py,baseline_features.py,gold_metadata.py,losses.py,metrics.py,patch.py,utils.py,abstention.py.scripts/:train.py,pretrain.py,predict.py,retrain_head.py,generate_openai_embeddings.py,generate_splits.py,split_val_for_test.py, plus the release-archive gate(
validate_archive_contract.py,check_release_archive.sh).splits/, so the publishedtrain/val/test partition is reproducible from the repo. Baselines and the main
model select checkpoints against one shared canonical val split via
--val_split_file(200k-cell cap, seed 42).(
--enable_wandbis gone; confusion matrices save locally as PNGs).zarr>=3.1pulls the Python floor up to 3.11 for the train extra.Baselines
deepcell_types/baselines/(
cellsighter,maps,nimbus,xgb), invoked through the unified runnerpython -m deepcell_types.baselines <name>, each with a self-containedinstall extra (
baseline-cellsighter,baseline-maps,baseline-nimbus,baseline-xgboost).--class_balancedefaults to
dct/sqrt), so cross-method comparisons isolate the modelingchoice rather than the balancing scheme. Each baseline's native sampler stays
available via
--class_balanceand is reported separately in the appendix.extract_features_from_zarr(missing_value=...)lets each baseline choose itsabsent-marker sentinel: MAPS / CellSighter keep
0.0; XGBoost passesnp.nanso absent markers route through XGBoost's learnedmissingdirection. The feature matrix records a
present_markersmask and the cachestays missing-value-agnostic.
source; third-party licenses are tracked in
deepcell_types/baselines/NOTICE.Breaking changes
CellTypeCLIPModelremoved. No shim — usefrom deepcell_types import predict, DCTConfig.predict()arguments aftermppare keyword-only, preventingaccidental transposition of the adjacent string arguments.
device=is thepreferred spelling (
device_num=remains a deprecated alias).predict(num_workers=...)default is now0(was24) — 24 workersOOM'd machines with <64 GB RAM.
99.0 → 99.9shifts ~5% of predicted labels.--class_balance dct) instead ofeach method's native class-balancing scheme.
FocalLossweights — theWeightedRandomSampleris the sole rare-class balancer.--no_class_weightsand
compute_class_weightsare removed; the no-flags default now reproducesthe released-checkpoint recipe.
from deepcell_types.predict import PredLoggeris gone (now the private_InferenceResultBuffer).Packaging / infra
vocab.json,channel_mapping.yaml, andtraining/config/*.yaml(incl.combined_celltypes.yaml), which werepreviously outside the package tree and absent after
pip install.np.ptpfree functionremoved in NumPy 2.0 →
max - min), so a freshpip installruns.tifffiledeclared in the[train]extra;openaideclared and pinned..github/workflows/ci.yml); inference vs.[train]testboundary enforced.
LICENSEmatches the lab's Modified Apache 2.0 template;NOTICEaligned tothe vanvalenlab convention and attributes vendored third-party code.
Tests
35+ test modules under
tests/(plustests/baselines/) covering canonicalinference, abstention CLI, checkpoint round-trip, dataset/split/sampler
behavior, preprocessing + the preprocess hook, losses, hierarchical eval,
archive-contract validation, baseline feature splits, and vendored-baseline
equivalence against upstream.