Skip to content

v0.1.0: monorepo merge — unified training + inference, canonical CellTypeAnnotator, archive-free inference, vendored baselines#41

Merged
xuefei-wang merged 289 commits into
vanvalenlab:masterfrom
xuefei-wang:master
Jul 4, 2026
Merged

v0.1.0: monorepo merge — unified training + inference, canonical CellTypeAnnotator, archive-free inference, vendored baselines#41
xuefei-wang merged 289 commits into
vanvalenlab:masterfrom
xuefei-wang:master

Conversation

@xuefei-wang

@xuefei-wang xuefei-wang commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

This is the v0.1.0 release cut. It merges the separate training repository
(deepcelltypes-cell-type-assignment-pytorch) into this repo and replaces the
legacy CellTypeCLIPModel inference path with the current canonical
CellTypeAnnotator model.

Before this PR, vanvalenlab/deepcell-types was inference-only — it shipped
CellTypeCLIPModel, the dct_kit/ helpers, and a top-level __init__ that
exported just predict. After it, a single package covers training and
inference: inference stays a plain pip install deepcell-types, the full
training pipeline lives behind a [train] extra, and the four paper comparison
baselines are vendored behind per-baseline extras.

⚠️ This PR contains breaking changes — see the section at the bottom.

The CHANGELOG.md
0.1.0 entry is the authoritative, line-by-line record; this description
summarizes it.

Canonical model

model.py is rewritten around CellTypeAnnotator; CellTypeCLIPModel /
CellTypeDataEncoder are 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.

  • Residual-MLP cell-type head is now the default (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-stage
    sampler-off recipe). Checkpoints record config["ct_head_arch"] and
    predict() auto-detects the head from the state dict, so the released
    (legacy-MLP) checkpoint still loads unchanged
    . resmlp and mlp
    checkpoints have different head shapes and are not interchangeable.
  • Mean-intensity injection — per-cell mean marker intensity is scattered
    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.
  • DANN domain adaptation via a gradient-reversal head, on by default
    (--domain_weight 0.1; 0 disables it).
  • Adapter-style fine-tuning: --freeze_backbone trains only the
    mean-intensity branches on top of an existing checkpoint; --unfreeze_ct_head
    additionally co-adapts the CT head / CLS token / final norm without unfreezing
    the transformer backbone.
  • Padding-channel positions are explicitly zeroed (out-of-place 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_feat into the transformer.
  • Self-describing checkpoints: scripts/train.py bundles ct2idx,
    n_heads, and compat_marker0_zero, and inference asserts the vocabulary
    ordering matches (a permuted vocabulary previously passed the count-only
    check and silently mislabeled cells). --resume_path validates n_heads /
    n_celltypes / resnet_channels / d_model before restoring optimizer
    state.

Canonical-only inference

  • Archive-free by default: the marker / cell-type registry ships as a small
    packaged vocab.json snapshot, so pip install deepcell-types +
    download_model() is enough to run predict() — the multi-GB TissueNet zarr
    archive is no longer required (pass zarr_path= / set
    DEEPCELL_TYPES_ZARR_PATH only if you need the tissue→cell-type mapping).
    Verified identical predictions with vs. without the archive on the paper
    checkpoint.
  • Post-hoc abstention is opt-in. predict(ct_abstention_k=...) defaults to
    None, returning the raw argmax label for every cell. Set a float and cells
    below an IQR fence on the per-FOV confidence distribution are relabeled to the
    "Unknown" sentinel; ct_abstention_k=0.2 reproduces the paper headline
    operating point. The pre-abstention argmax is preserved in cell_types_raw
    when return_probabilities=True. (Default is None, not on-by-default, so
    the plain list[str] return is never silently rewritten at a benchmark-tuned
    threshold.)
  • Custom preprocessing hook: predict(..., preprocess=...) overrides the
    per-FOV normalization without retraining, backed by a bounded op library
    (apply_config, make_preprocessor, DEFAULT_CONFIG) and a
    composition-guided adaptation loop (skills/preproc-adapt/).
    make_preprocessor(DEFAULT_CONFIG) reproduces the default bit-for-bit.
  • Input-hardening / training-parity fixes: all-zero marker channels are now
    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) raw
    now raises a clear ValueError instead of a poisoned softmax; per-cell
    tensors are sized to the channels actually present rather than the global
    MAX_NUM_CHANNELS. The first three can change predictions for affected FOVs.
  • The bright-spot clip percentile (DCTConfig.PERCENTILE_THRESHOLD) is now
    99.9, matching the recipe the training archive was built with (was 99.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 a PredictionResult dataclass
    with the full per-cell softmax matrix, cell indices, and the pre-abstention
    argmax labels (cell_types_raw).
  • Loading uses weights_only=True where possible; a missing checkpoint raises a
    clear FileNotFoundError pointing at download_model().

New public API

predict, DCTConfig, PredictionResult, preprocess_fov, apply_config,
make_preprocessor, and DEFAULT_CONFIG are importable from deepcell_types
directly. preprocess_fov(raw, mask, native_mpp, channel_names) → PreprocessedFov is the standalone preprocessing entry point.

Monorepo: training pipeline

  • deepcell_types.training ships behind pip 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 under 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).
  • Canonical split manifests committed under splits/, so the published
    train/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).
  • Experiment logging is plain Python logging — no Weights & Biases anywhere
    (--enable_wandb is gone; confusion matrices save locally as PNGs).
  • zarr>=3.1 pulls the Python floor up to 3.11 for the train extra.

Baselines

  • Four paper comparison baselines vendored under deepcell_types/baselines/
    (cellsighter, maps, nimbus, xgb), invoked through the unified runner
    python -m deepcell_types.baselines <name>, each with a self-contained
    install extra (baseline-cellsighter, baseline-maps, baseline-nimbus,
    baseline-xgboost).
  • All baselines unified onto the DCT sampler by default (--class_balance
    defaults to dct/sqrt), so cross-method comparisons isolate the modeling
    choice rather than the balancing scheme. Each baseline's native sampler stays
    available via --class_balance and is reported separately in the appendix.
  • extract_features_from_zarr(missing_value=...) lets each baseline choose its
    absent-marker sentinel: MAPS / CellSighter keep 0.0; XGBoost passes
    np.nan so absent markers route through XGBoost's learned missing
    direction. The feature matrix records a present_markers mask and the cache
    stays missing-value-agnostic.
  • Each baseline ships a README documenting every deviation from its upstream
    source; third-party licenses are tracked in deepcell_types/baselines/NOTICE.

Breaking changes

  • CellTypeCLIPModel removed. No shim — use from deepcell_types import predict, DCTConfig.
  • All predict() arguments after mpp are keyword-only, preventing
    accidental transposition of the adjacent string arguments. device= is the
    preferred spelling (device_num= remains a deprecated alias).
  • predict(num_workers=...) default is now 0 (was 24) — 24 workers
    OOM'd machines with <64 GB RAM.
  • Clip percentile 99.0 → 99.9 shifts ~5% of predicted labels.
  • Baselines default to the DCT sampler (--class_balance dct) instead of
    each method's native class-balancing scheme.
  • Backbone training no longer applies per-class FocalLoss weights — the
    WeightedRandomSampler is the sole rare-class balancer. --no_class_weights
    and compute_class_weights are removed; the no-flags default now reproduces
    the released-checkpoint recipe.
  • from deepcell_types.predict import PredLogger is gone (now the private
    _InferenceResultBuffer).

Packaging / infra

  • Package data now ships vocab.json, channel_mapping.yaml, and
    training/config/*.yaml (incl. combined_celltypes.yaml), which were
    previously outside the package tree and absent after pip install.
  • NumPy 2.0 compatibility in the inference path (np.ptp free function
    removed in NumPy 2.0 → max - min), so a fresh pip install runs.
  • tifffile declared in the [train] extra; openai declared and pinned.
  • CI workflow added (.github/workflows/ci.yml); inference vs. [train] test
    boundary enforced.
  • LICENSE matches the lab's Modified Apache 2.0 template; NOTICE aligned to
    the vanvalenlab convention and attributes vendored third-party code.

Tests

35+ test modules under tests/ (plus tests/baselines/) covering canonical
inference, 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.

xuefei-wang and others added 30 commits May 18, 2026 09:06
…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>
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>
xuefei-wang and others added 13 commits June 29, 2026 10:47
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)
@xuefei-wang xuefei-wang changed the title v0.1.0: monorepo merge — unified training + inference, canonical model, archive-free inference, vendored baselines v0.1.0: monorepo merge — unified training + inference, canonical CellTypeAnnotator, archive-free inference, vendored baselines Jul 3, 2026
xuefei-wang and others added 12 commits July 3, 2026 10:26
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
xuefei-wang marked this pull request as ready for review July 4, 2026 22:39
@xuefei-wang
xuefei-wang merged commit 09a1c48 into vanvalenlab:master Jul 4, 2026
7 checks passed
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.
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