Skip to content

docs(tutorial): default to precomputed mask + guard against empty segmentation - #54

Closed
xuefei-wang wants to merge 308 commits into
vanvalenlab:masterfrom
xuefei-wang:docs/tutorial-empty-mask-guard
Closed

docs(tutorial): default to precomputed mask + guard against empty segmentation#54
xuefei-wang wants to merge 308 commits into
vanvalenlab:masterfrom
xuefei-wang:docs/tutorial-empty-mask-guard

Conversation

@xuefei-wang

Copy link
Copy Markdown
Collaborator

Problem

A user reported that "the cell type prediction result appears empty" on the tutorial. The currently-published outputs render fine, but walking the tutorial as a fresh user surfaced the real failure mode:

  • The tutorial's primary path builds the mask with cellSAM, and the only post-segmentation check verifies mask shape (mask.shape == img.shape[1:]).
  • An all-background / degenerate mask (easy to hit: CPU-only, OOM, the "arbitrary" membrane_channel, or a cellSAM version mismatch) has the correct shape and passes that check.
  • predict() then returns [] for a mask with no cells, so the cell-type DataFrame renders empty — exactly the reported symptom, with no error to explain it.

Changes

docs/site/tutorial.md:

  1. Default to the archive's pre-computed CellSAM mask (ds["segmentations/cellsam"]) so the pipeline runs end-to-end without a local segmentation setup (no cellSAM/GPU required).
  2. Sanity-check shape and cell count — fail loudly instead of silently producing empty predictions:
    assert mask.shape == img.shape[1:], "mask shape does not match the image H, W"
    assert mask.max() > 0, "segmentation mask contains no cells"
  3. Keep the cellSAM path intact but optional/reference-only — same code, retitled with an "optional, not executed" note and switched from {code-cell} to plain code blocks, so the docs build no longer requires cellSAM and can't overwrite the default mask.

Net: the "empty prediction result" symptom can no longer occur silently, and the tutorial is completable without a GPU/cellSAM.

Verification

  • Docs build succeeds with the new structure (nb_execution_mode=off structure check passes; cellsam_pipeline no longer appears in any executed input cell).
  • The default execution path was run end-to-end on CPU: ds["segmentations/cellsam"]relabel_sequentialpredict(...) returns a populated list of 1646 labels, and the DataFrame renders non-empty. The two new asserts pass trivially (shape matches, max=1646 > 0).

🤖 Generated with Claude Code

xuefei-wang and others added 30 commits May 26, 2026 23:34
…-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) <[email protected]>
…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 [email protected] (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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
Address PR #41 review items:

- Strip cell-type quality reporting to macro-F1 only. LossesAndMetrics.compute()
  now returns ct_macro_f1 (drops ct_macro_accuracy / ct_weighted_accuracy /
  ct_weighted_f1); marker-positivity (mp_*) and domain_accuracy are separate
  heads and untouched.
- scripts/train.py: remove the --best_metric flag; log/select on macro-F1 only.
  Rename early-stopping state to best_val_macro_f1, with a legacy
  best_val_macro_acc fallback on resume so old checkpoints still load.
- scripts/predict.py: CT abstention eval reports hierarchical macro-F1 pre/post
  instead of macro/weighted accuracy.
- abstention.py: remove the misplaced macro_weighted_accuracy + hierarchical_correct
  helpers; replace with hierarchical_macro_f1, which delegates to the canonical
  adjust_conf_mat_hierarchy + _conf_mat_summary so the number is comparable to
  the model's ct_macro_f1 and the baseline reports.
- Tests: repoint test_v2 CT-metric tests to macro-F1 (incl. assertion that the
  removed accuracy keys are gone); add hierarchical_macro_f1 coverage.
- Restore Dockerfile (was deleted, and stale): install from pyproject.toml with
  a DCT_EXTRAS build arg covering inference / [train] / [baselines] / [all].

baseline_features._conf_mat_summary still computes all four metrics; the
baselines/* submodules depend on those keys, so it is intentionally unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…lltypes

The per-dataset `celltype_mapping` was an identity `{ct: ct}` dict — a
leftover from before cell types were canonicalized at zarr ingestion.
Every consumer used it as `.get(label, label)`, a no-op. Replace it with
`dataset_celltypes: Dict[str, List[str]]` (the annotated cell-type names
per dataset) and drop the dead remapping:

- `FullImageDataset` index build and `_extract_all_dataset_features`
  no longer reference any mapping — a cell is kept iff its label is in
  `ct2idx` (unchanged behavior; the lookup always returned the input).
- `build_tissue_mapping_from_split` — the one consumer that genuinely
  needs per-dataset names — iterates `dataset_celltypes` directly.

Also extract the metadata aggregation out of `_compute_all_mappings`
(welded to a ProcessPoolExecutor reading ~1GB of zarr.json) into the
pure static `_aggregate_metadata`, making it unit-testable without an
archive. New `tests/test_dataset_celltypes.py` pins the aggregation and
`build_tissue_mapping_from_split` behavior.

Behavior-preserving: `dataset_celltypes` retains all annotated names
(including those outside `ct2idx`); the `ct2idx` filter stays in the
consumers and tissue mapping exactly as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
`scripts/predict.py` defaults `--ct_abstention_k=0.2`, but
test_default_k_0_5_abstention_is_on named, documented, and exercised
k=0.5 as "the default". Rename to test_default_k_0_2_abstention_is_on
and pass k=0.2. On the seed=99 1000-cell synthetic frame this abstains
17.8% (vs 10% at k=0.5), still within the existing 0.03–0.30 band.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Behavior-preserving cleanups surfaced while reviewing the v0.1.0 monorepo
merge (PR #41). No change to model numerics or public inference outputs;
ruff clean, full test suite green (224 passed, 10 skipped).

- config.py: drop never-read constants HIST_NORM_KERNEL_SIZE and
  MAX_CHUNK_PER_CT_PER_DATASET
- utils/_auth.py: remove dead extract_archive() and its now-unused
  tarfile/zipfile imports (no caller anywhere in the repo)
- training/dataset.py: remove dead _build_centroid_tree() wrapper and its
  import; drop unused self._idx2marker
- training/utils.py: add shared load_matching_state_dict() helper
- scripts/{train,pretrain}.py: replace 3 copy-pasted checkpoint
  warm-start loops with the shared helper
- training/baseline_features.py: hoist set(dataset_keys) out of a comprehension

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Approved design for folding the four baseline submodules into an in-repo
deepcell_types.baselines package behind a unified click-group runner, with
per-method extras. Vertical slice (xgboost + nimbus) first; verbatim move +
characterization unit tests guarantee no computation change.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
TDD plan: byte-identical (sha256-verified) move of xgb/{run,tuning}.py and
nimbus run.py into deepcell_types.baselines behind a LazyGroup runner, with
per-method extras and a hand-derived characterization test for the nimbus
metric reducer. Removes the two submodules. maps/cellsighter deferred to round 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
DCTConfig (deepcell_types/config.py):
- self.SEED, self.BATCH_SIZE instance attrs
- self._core_celltypes backing field and the core_celltypes property

TissueNetConfig (deepcell_types/training/config.py):
- SEED, BATCH_SIZE class constants
- NUM_TISSUES property
- get_marker_positivity method (marker_positivity_labels property kept; used by dataset.py)
- combined_celltype_mapping property
- color_mapping property
- core_tree property
- lineage_mapping property
- get_channel_embedding method
- get_celltype_embedding method

Each removed member was verified (grep -rlw across deepcell_types / scripts /
tests / baselines) to have no reader outside its own definition. Kept members
with readers: marker_positivity_labels, tissue2idx, load_marker_embeddings_array,
celltype_mapping, domain_mapping, ct2idx, domain2idx, marker2idx. Pure deletions
(128 lines, 0 insertions); ruff clean; 180 tests pass (15 pre-existing zarr-import
collection errors, identical with and without this change).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…guard, py3.12 note)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Extract cohesive symbol groups from the 1875-line training/dataset.py into
focused sibling modules using the extract-and-re-export pattern. Behavior is
preserved byte-for-byte; every public (and private) symbol historically
importable from deepcell_types.training.dataset is re-exported there, so all
existing imports and `python -m scripts.*` entry points keep working unchanged.

New modules and the symbols moved into each:
- transforms.py: _Compose, _RandomHorizontalFlip, _RandomVerticalFlip,
  AugmentedDataset, DropOutChannels
- samplers.py: compute_sample_weights, FOVGroupedSampler,
  SequentialFOVGroupedSampler
- splits.py: CellIndexRecord, _ADVISORY_SPLIT_METADATA_KEYS,
  _find_sole_source_fovs, _build_fov_strata, create_fov_splits,
  _split_metadata_for_dataset, _format_fov_examples, save_fov_splits,
  load_fov_splits
- dataloader.py: create_dataloader, DataLoaderConfig,
  create_dataloader_from_config

dataset.py keeps the core FullImageDataset and _archive_fingerprint, imports
CellIndexRecord from splits (no cycle: splits never imports dataset), and
re-exports the full public surface via __all__.

dataset.py: 1875 -> 884 lines. behavior-preserving; all symbols re-exported
from training.dataset for back-compat; ruff clean, 224 tests pass / 10 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…nner

Byte-identical copy of xgb/{run,tuning}.py (sha256-verified) behind a
LazyGroup click runner. Submodule still present; removed in a later commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Documents the remaining simplification work with a per-task method contract
(behavior-preserving, ruff+pytest gates, Bash-edit to avoid formatter churn,
no force-push):
- TODO C: promote model **kwargs to explicit params (lowest risk; do first)
- TODO B: CellTypeAnnotator.forward() -> NamedTuple (arity-preserving)
- TODO A: split training/config.py god-file (extract-and-re-export)
- TODO D: behavior-changing / public-API items needing sign-off

Pins the verified current model.py signatures/return shapes at a126fac.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Byte-identical copy of nimbus run.py (sha256-verified); registered in the
runner. Adds a hand-derived characterization test for the pure marker-
positivity metric reducer. Submodule removed in the next commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Removes the two folded-in submodules and their .gitmodules stanzas, adds
self-contained baseline-xgboost / baseline-nimbus extras, registers the new
packages, and carries third-party attribution into deepcell_types/baselines/NOTICE.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
xuefei-wang and others added 25 commits July 3, 2026 13:40
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 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
#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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
v0.1.0: monorepo merge — unified training + inference, canonical CellTypeAnnotator, archive-free inference, vendored baselines
…ckerignore

README hardcoded cuda:0 with no CPU-fallback guidance for GPU-less users.
Add list_supported_markers()/list_supported_cell_types() (mirroring
list_model_versions()/list_baseline_names()) so users can pre-flight-check
their marker panel before downloading a checkpoint, and document them in
docs/index.md and the API reference. Add a .dockerignore so `docker build .`
stops baking .git/.venv/.env*/local artifacts into image layers.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01BFEzZSEzzxkFgfMSU1XFHE
Add the v4 preprint URL and bump the year/pages to match the
2026-07-06 posting date.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01NnpBk4LMEG6MiUrEgt8khK
Replace the "and others" truncation with the remaining authors
(Keren, Yue, Barnowski, Van Valen) from the bioRxiv v4 metadata.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01NnpBk4LMEG6MiUrEgt8khK
bioRxiv v3 and v4 inserted Ahamed Raffey Iqbal as the third author
(between Dilip and Bussi); reflect that in the BibTeX author list.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01NnpBk4LMEG6MiUrEgt8khK
…#65)

- PatchDataset.__init__ now rejects raw/mask spatial-shape mismatches,
  matching the check already present in preprocess_fov.
- validate_checkpoint_vocabulary now hard-requires canonical_channels
  on any checkpoint dict, the same way it already hard-requires ct2idx
  — a checkpoint missing it previously skipped the marker-ordering
  guard entirely, letting a permuted marker vocabulary silently
  mislabel predictions. All in-repo checkpoint writers (train.py,
  retrain_head.py, repackage_release_checkpoint.py) already bundle it.
- Remove the now-unreachable legacy state_dict branch in
  scripts/predict.py: validate_checkpoint_vocabulary raises before
  that branch could ever be reached.


Claude-Session: https://claude.ai/code/session_01BFEzZSEzzxkFgfMSU1XFHE

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…ptional patient-level split grouping (#66)

--resume_path with an increased --epochs silently discarded the new
OneCycleLR schedule: LRScheduler.load_state_dict() is a raw
self.__dict__.update(), so loading the checkpoint's scheduler state
overwrote total_steps/_schedule_phases back to the OLD (shorter) run.
resume_onecycle_schedule() (deepcell_types/training/utils.py) now rebuilds
the schedule at the current run's total_steps and fast-forwards it to the
resumed step count instead, applied in both scripts/train.py and
scripts/pretrain.py.

Also add an opt-in group_by_patient mechanism to create_fov_splits /
save_fov_splits (deepcell_types/training/splits.py) and a
--group-by-patient flag on scripts/generate_splits.py: FOVs sharing a
parsed "...Patient<N>..." id are kept entirely on one side of the split,
preventing patient-level leakage (verified in
splits/fov_split_valsubset.json: mccaffrey_tb_mibi Patient2/10/13 appear in
both train and val today). Default is off, so existing split files are
unaffected unless explicitly regenerated with the flag.


Claude-Session: https://claude.ai/code/session_01BFEzZSEzzxkFgfMSU1XFHE

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…#67)

Nimbus's per-dataset load loop silently dropped FOVs on missing
`preprocessed` data or a `load_fov_data` error, biasing the baseline
comparison (abstention.py documents that baselines must be scored at
full coverage). Mirror training/dataset.py's failed_keys/fail-rate
convention: always report "Skipped N of M datasets" before metrics
are finalized, and raise RuntimeError above a 1% failure-rate
threshold. Also narrow the bare `except Exception` around
load_fov_data to the exception types it can actually raise.


Claude-Session: https://claude.ai/code/session_01BFEzZSEzzxkFgfMSU1XFHE

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…/dataloader integration (#69)

Three critical paths had zero or only indirect coverage:
- resolve_gold_metadata (Pan-M Gold-Standard tissue/modality resolution):
  known-key resolution, unknown-key errors, strict-mode refusal of
  non-canonical substitutions, and the dct_config vocab-validation branch.
- scripts/predict.py::main() (the eval CLI generating reported metrics):
  end-to-end run via CliRunner against a synthetic training-shaped zarr
  archive, covering CSV schema, ct_abstention_k on/off, and the
  hierarchical macro-F1 pre/post reporting.
- FullImageDataset.__getitem__ / create_dataloader: real archive-backed
  construction and indexing (tensor shapes/dtypes, FOV-zero-channel
  masking), plus a real DataLoader batch via create_dataloader.


Claude-Session: https://claude.ai/code/session_01BFEzZSEzzxkFgfMSU1XFHE

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
`compute_iqr_fence` was exported in `deepcell_types.abstention.__all__` and
documented in the public API reference, but it is an internal helper: the
supported entry points are `predict()` (via `ct_abstention_k`) and the batched
`apply_abstention`. IQR-fence abstention is also off by default and dropped
from the paper headline, so surfacing the low-level fence math as public API
overstates its role.

Rename to `_compute_iqr_fence`, drop it from `__all__`, and remove it from
`docs/site/reference.rst`. Internal callers (`predict.py`, `apply_abstention`)
and tests are updated to the private name; behavior is unchanged.


Claude-Session: https://claude.ai/code/session_01AVaWu3LRbCmBDBkB64Y3cH

Co-authored-by: Claude Fable 5 <[email protected]>
The two runnable README snippets hardcoded device="cuda:0", forcing GPU 0.
Derive the device from the environment (default GPU if available, else CPU)
and note how to target a specific GPU, so users pick their own device. This
also subsumes the separate "pass device=cpu" note, which is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HfHQX9gC2no4WamCF4H8PD
… 2026-06-23 SSL arm) (#70)

* chore(models): bump 2026-06-15 registry md5 to the vocab-bundled asset

The pinned 704616a1 checkpoint predates the vocab guard (no bundled ct2idx),
so download_model() -> predict() raises "does not bundle a ct2idx" for every
user. Point the registry at the re-packaged asset (adds ct2idx +
canonical_channels; weights byte-identical).

DRAFT: do not merge until the bundled .pt is uploaded at the same asset path
(md5 b819a7e0b177ad5330394eab3c6c7ad8), else download_model hash-check fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01BFEzZSEzzxkFgfMSU1XFHE

* chore(models): default registry to vocab-bundled 2026-06-23 ptft resMLP

Add the 2026-06-23 pretrain->finetune (SSL) resMLP arm as a registry entry
and make it the default (_latest), matching deepcell-auth's default DCT
version. Its md5 tracks the vocab-bundled repackage (402e94c1...) of
deepcell-types_2026-06-23_resmlp_ptft.pt: weights byte-identical to the
original (0 tensors differ), config identical, ct2idx (51) +
canonical_channels (278) added so it passes validate_checkpoint_vocabulary.
The un-bundled original (067f558b...) fails the guard, like the 2026-06-15
asset. 2026-06-15 (paper Fig-3c headline) is retained as a selectable version.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016oVttaRJScBzjoJDB8ZkpA

* chore(models): default registry to the 2026-06-15 from-scratch resMLP

Set _latest back to 2026-06-15 (paper Fig-3c headline, vocab-bundled md5
b819a7e0...) as the default download_model() checkpoint, keeping the
2026-06-23 pretrain->finetune (SSL) resMLP arm (bundled md5 402e94c1...) as
a selectable version. Aligns the deepcell-types default with the deepcell-auth
default so both packages resolve to the same checkpoint.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016oVttaRJScBzjoJDB8ZkpA

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
… re-hosted nimbus (#74)

The baseline download registry pinned the pre-resume50 generation (two
generations stale: d06f8aee/d2d1930d/e3a54e5a/00d110cc/0d94609a), which no
longer matches the released baseline artifacts. Update cellsighter, maps
(+ _stats.npz), and xgboost (+ .remap.json) to the 2026-06-30 DCT-sampler
retrains -- the release-of-record that produced the published baseline
prediction CSVs and headline numbers.

Also drop the "nimbus" entry: Nimbus is inference-only and its pretrained
weights are produced and distributed upstream (angelolab/Nimbus-Inference);
this project should not redistribute them. The nimbus baseline runner is
unaffected (it loads weights via the official nimbus-inference library).
download_baseline_checkpoint("nimbus") now raises with a pointer to the
official source instead of serving a re-hosted copy.

New md5s require re-uploading the 2026-06-30 files to users.deepcell.org
before they are served (see PR checklist).


Claude-Session: https://claude.ai/code/session_016oVttaRJScBzjoJDB8ZkpA

Co-authored-by: Claude Fable 5 <[email protected]>
feat(usability): CPU-fallback note, marker/cell-type listing API, .dockerignore
- tutorial.md: auto-select inference device (cuda if available else cpu),
  matching the README change in d039da0 instead of hardcoding cuda:0
- API-key.md: drop "nimbus" from download_baseline_checkpoint options
  (it now raises; nimbus weights are distributed upstream) and note the
  [baseline-nimbus] install path


Claude-Session: https://claude.ai/code/session_012KP2MDvQ2tiSYssggv7ygK

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
* refactor(api): default ct_abstention_k=0 (opt-in), matching the CLI

predict() now defaults ct_abstention_k to 0 instead of None. Behavior is
unchanged -- the gate was already `k > 0`, so both None and 0 mean "no
abstention" -- but 0 is now the documented off sentinel and the Python API
matches scripts/predict.py, whose --ct_abstention_k already defaults to 0.
Abstention stays fully opt-in: pass a positive k to enable the per-FOV IQR
fence. Updates the docstrings, the signature-default test, and the tutorial
note.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016rLgKLZ4Z2h4fGDwdyy8Gc

* docs: strip stderr from tutorial output + trim index page

- conf.py: nb_output_stderr = "remove" so the executed tutorial no longer
  renders build-machine file paths, the NVML warning, tqdm bars, or the
  masked-channel UserWarning in its cell output.
- index.md: drop the "TissueNet archive (optional)" section (covered on the
  Model and Datasets page), drop the scripts/train.py internals parenthetical,
  and replace the tissuenet-v8/v9/v10 version terms with expanded-tissuenet.zarr.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016rLgKLZ4Z2h4fGDwdyy8Gc

---------

Co-authored-by: Claude Fable 5 <[email protected]>
…mentation

A fresh user following the tutorial's cellSAM path could get an empty or
degenerate segmentation mask (CPU/OOM, an arbitrary membrane_channel, or a
cellSAM install mismatch). The only post-segmentation check verified mask
*shape*, which an all-background mask passes -- predict() then returns [] and
the cell-type DataFrame renders empty ("prediction result appears empty").

- Default to the archive's precomputed CellSAM mask so the pipeline runs
  end-to-end without a local segmentation setup.
- Sanity-check both shape AND cell count (mask.max() > 0), failing loudly
  instead of silently producing empty predictions.
- Keep the cellSAM path intact but optional/reference-only (plain code blocks,
  not executed at build), so the docs build no longer requires cellSAM/GPU.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@rossbar

rossbar commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

We do not want to replace the computation here - the point of the example is to demonstrate a full end-to-end (i.e. raw image -> segmentation -> celltype prediction) pipeline. Note also that this serves as the only real test that the components of this package are working as expected.

@xuefei-wang

Copy link
Copy Markdown
Collaborator Author

@rossbar Agreed — closing this in favor of #55, which keeps the computation in place.

#55 leaves the raw image → cellSAM → cell-type prediction path fully executed and instead:

  1. Restores the two nim.screenshot cells and their <img> embeds, which master had replaced with prose ("Static documentation builds do not execute or embed GUI screenshots") — so the docs build exercises the visualization step again, as it did at 616d4b5.
  2. Turns the post-segmentation shape check into a hard assert and adds assert mask.max() > 0, which is what the original report ("the cell type prediction result appears empty") actually needed: a degenerate all-background mask has the correct shape, passes the old check, and yields [] from predict() with nothing to explain it.

This PR is also unmergeable independent of the review — its merge base is 616d4b5, so GitHub reports it as 144 files / ~47k lines against master rather than a docs change. #55 is branched from current master and touches docs/site/tutorial.md only.

@xuefei-wang

Copy link
Copy Markdown
Collaborator Author

Superseded by #55.

@xuefei-wang xuefei-wang closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants