Skip to content

CP-4: no silent rescale or channel repetition by default in the torch adapters (breaking) - #73

Merged
guijacquemet merged 5 commits into
mainfrom
claude/cp-4-torch-defaults
Nov 2, 2026
Merged

guijacquemet merged 5 commits into
mainfrom
claude/cp-4-torch-defaults

Conversation

@guijacquemet

@guijacquemet guijacquemet commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

This PR implements the owner decision of 2026-11-02, the open question from the PR #72 review. The torch adapters stop rescaling and stop repeating channels unless the caller asks. v0.2.0rc1 ships with the old defaults; this change is for the final v0.2.0. Breaking.

Chosen default: scale_policy is required (no default)

Omitting scale_policy now raises a TypeError. This applies to PatchTensorConfig(), PatchTensorConfig(channel_policy=...), patch_array_to_tensor(..., config=None), and PatchTorchDataset / ExportedPatchTorchDataset built without tensor_config. The message names both choices:

  • 'none' keeps the values.
  • 'auto' (deprecated) divides an integer patch by its dtype maximum. It was the default before v0.2.0.

Why required rather than defaulting to "none": the science rules forbid an implicit normalization change. A "none" default would silently hand code written for the old "auto" default values up to 65535 times larger, which is an implicit change in the other direction. A required choice stops that code with an error, and every caller's choice is then written in its code. Contract §15 and docs/plan.md §6.8 record this.

Behaviour changes

  • scale_policy: there is no default; omitting it raises the TypeError above. A rescale still happens only with "auto", which is recorded per item as scale_divisor. ScalePolicyDeprecationWarning is kept and repurposed: it now marks the named, deprecated "auto" option, not a default. It is still shown under Python's default filters.
  • channel_policy: the default is now "keep"; before it was "repeat_to_rgb".
    • A one-channel patch becomes (1, H, W).
    • Patches with 2 or more than 3 channels, which the old default refused, are now kept.
    • "repeat_to_rgb" works only when named. Each item records it as channel_repeat: 3 when repeated, otherwise 1. It is new, sits next to scale_divisor, and is also available as the public cellpatcher.torch_dataset.channel_repeat.
  • Example from the review: a uint16 patch of 1000 used to come back as 0.0153 repeated to 3 channels. With PatchTensorConfig(scale_policy="none") it is now (1, 4, 4) holding 1000.0, with scale_divisor 1.0 and channel_repeat 1.

Migration (also in the CHANGELOG):

  • To keep the values (recommended, with config.normalization): PatchTensorConfig(scale_policy="none").
  • To get the exact old behaviour: PatchTensorConfig(channel_policy="repeat_to_rgb", scale_policy="auto"). It warns when it rescales and is recorded per item.
  • For an RGB model on normalized patches: PatchTensorConfig(channel_policy="repeat_to_rgb", scale_policy="none").

Callers in CellPatcher

  • torch_session and the PyTorch notebook: both already pass channel_policy="keep" and scale_policy="none". No change.
  • notebook_torch goes through torch_session. The other notebooks do not use torch.
  • Docs: every PatchTensorConfig in docs/api_examples.md and docs/quick_start.md now names its scale_policy. The examples that feed RGB/DINO models also name channel_policy="repeat_to_rgb". One PatchTorchDataset example had no tensor_config and now has one.
  • Updated text: contract §3 and §15, docs/plan.md §6.8, the PatchTorchDataset docstring, and AGENTS.md (the delivery bullet and the health snapshot).

Tests that fail on old code

The new tests are in tests/test_deprecations.py. They run without torch through the existing stand-in, so CI runs them:

  • test_an_omitted_scale_policy_is_refused_naming_the_choices
  • test_the_default_channel_policy_keeps_one_channel_and_its_values
  • test_the_torch_datasets_need_a_tensor_config_with_a_scale_policy
  • test_the_old_defaults_are_reachable_by_name_and_recorded_per_item: checks scale_divisor 65535, channel_repeat 3 and the warning.
  • test_the_live_dataset_records_channel_repeat_and_keeps_any_channel_count (added in review): PatchTorchDataset records channel_repeat and keeps 1, 2 and 4 channels with their values.

Against main's torch_dataset.py:

  • Without torch: these 5 fail.
  • With CPU torch: 16 fail across test_deprecations.py, test_torch_dataset.py and test_biological_repeat.py (re-measured in review).

Existing tests now name their policies. Tests of the old behaviour use it explicitly (OLD_DEFAULTS, or repeat_to_rgb with "none"). The subprocess test checks that the default-filter warning is still shown for a named "auto".

Gate

Local uv venvs, Python 3.11, Linux, head 81edaa0 (after review):

  • tools/check.py exits 0 (full run, in the torch venv).
  • tools/check.py --rules exits 0.
Environment Passed Skipped
.[test] 1080 41
.[test,zarr] 1101 20
.[test,zarr,notebook] 1129 11
.[test,zarr,notebook] + CPU torch 2.15.0 1178 0

Each environment has 5 more passes than at main (1075 / 1096 / 1124 / 1173). I inspected every skip, and each one is a missing extra:

  • .[test]: zarr 21, notebook extra 10, torch 10.
  • .[test,zarr]: notebook extra 10, torch 10.
  • .[test,zarr,notebook]: torch 11.

CI is green on 81edaa0 with the same three counts. CI has no torch job, so the torch tests and the PyTorch notebook kernel run were run locally only (the 1177 row).

§9 figures

  • src/cellpatcher/torch_dataset.py: 478 lines (425 on main). It is the largest file touched; tests/test_torch_dataset.py is 592, of which only call sites changed.
  • New functions:
    • channel_repeat: 4 executable lines, complexity 1, 3 parameters.
    • PatchTensorConfig.__post_init__: 15 lines, complexity 7 (was 6), 11 statements.
    • Largest new test: test_the_old_defaults_are_reachable_by_name_and_recorded_per_item, 13 lines, 12 statements.
  • Existing functions touched:
    • patch_array_to_tensor now takes the repetition from channel_repeat (review fix): 35 executable lines, complexity 9, 31 statements, 3 parameters.
    • Each dataset __getitem__ gains one line.
    • PatchTorchDataset.__init__ gains docstring lines only.
  • Ratchet: the baseline only shrinks, by 3 entries: one undocumented-public-class in torch_dataset.py, and two line-too-long entries in tests/test_torch_dataset.py. No # noqa was added.

Downstream calls affected (no other repository edited)

Each repository was fetched and its origin/main searched for PatchTorchDataset, ExportedPatchTorchDataset, PatchTensorConfig, scale_policy, channel_policy, scale_divisor and patch_array_to_tensor.

  • MorphoEmbed a8e88df: src/morphoembed/provider_crops.py:59-63,139-144 passes PatchTensorConfig(**TENSOR_CONFIG) with channel_policy="keep", dtype_policy="float32", scale_policy="none", return_metadata=False. Code is unaffected. ROADMAP.md:126-127 describes the old defaults (repeat_to_rgb / auto) and needs a doc update in its migration.
  • MorphoFeature 4e6970f: documentation only. AGENTS.md:245-247 already recommends scale_policy="none". docs/audits/pytorch-encoder-phenome-audit.md:121,200 describes the old default RGB replication; it is a dated audit, so it is history. No code calls.
  • MorphoRelate ea19bb0, fiberfeatures 362f1ba, MorphoObjects 2080ff5: no uses.

Independent review

  • b61f4ff: patch_array_to_tensor repeated the channel with its own check while items recorded channel_repeat(); it now uses channel_repeat() (as it uses scale_divisor()), so the recorded value is the applied one. Plus the live-dataset regression test (stand-in torch, runs in CI).
  • c01cdf2: CHANGELOG downstream list: MorphoObjects is now 2080ff5; the MorphoFeature audit's PT08 row describes the old default.
  • 81edaa0: AGENTS.md health snapshot.

Not tagged.

https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc


Generated by Claude Code

…ing)

PatchTensorConfig.scale_policy has no default: an omitted one (also through
PatchTorchDataset/ExportedPatchTorchDataset without tensor_config, and
patch_array_to_tensor without config) is a TypeError naming "none" and
"auto". A required choice rather than a "none" default, so code written for
the old "auto" default stops instead of receiving values up to 65535 times
larger without a word (contract §15). channel_policy defaults to "keep";
"repeat_to_rgb" only when named, recorded per item as channel_repeat beside
scale_divisor. ScalePolicyDeprecationWarning stays for the named, deprecated
"auto".

Docs (api_examples, quick_start, contract §3/§15, plan §6.8) name their
policies; CHANGELOG has a Breaking entry with migration notes and the
downstream calls. Baseline shrinks by three entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
@guijacquemet
guijacquemet marked this pull request as ready for review November 2, 2026 12:40
…CP-4 review)

patch_array_to_tensor repeated the channel with its own check while each
dataset item's channel_repeat came from channel_repeat(); the tensor now uses
channel_repeat() too, as it uses scale_divisor(), so the recorded value is the
applied one. Adds a stand-in-torch test that PatchTorchDataset records
channel_repeat and keeps 1, 2 and 4 channels with their values (it fails on main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
…ow on the old default (CP-4 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
@guijacquemet
guijacquemet merged commit 7786906 into main Nov 2, 2026
4 checks passed

Copy link
Copy Markdown
Contributor Author

Independent review: merged as 7786906 (merge commit; branch kept). Not tagged.

Verified

  • Every path that builds a tensor states scale_policy or raises the TypeError that names 'none' and 'auto'. The paths checked: PatchTensorConfig(), PatchTensorConfig(channel_policy=...), patch_array_to_tensor(config=None), and PatchTorchDataset/ExportedPatchTorchDataset without tensor_config, which raise when they are built. torch_session and the PyTorch notebook pass keep/none. Every PatchTensorConfig in the docs examples names its policy. Nothing in app/ or the other notebooks builds a tensor.
  • "auto" is reachable only by name. It warns under Python's default filters (the subprocess test ran with CPU torch) and records scale_divisor. Channel "keep" changes no values: uint16 1000 comes out as 1000.0. The float32 cast comes from dtype_policy, as before. 2-, 3- and 4-channel patches are kept, and this is documented. channel_repeat is recorded per item by both datasets.
  • Contract §15 and the CHANGELOG Breaking entry both argue why scale_policy is required rather than defaulting to "none". The exact migration is given as PatchTensorConfig(channel_policy="repeat_to_rgb", scale_policy="auto").
  • Regression tests against main's src: 5 fail without torch (stand-in, so they run in CI) and 16 fail with CPU torch.
  • Downstream re-checked at origin/main. MorphoEmbed a8e88df code is unaffected; its ROADMAP.md:126-127 still describes the old defaults. MorphoFeature 4e6970f has documentation only. MorphoRelate, fiberfeatures and MorphoObjects (now 2080ff5) do not use it. No other repository was edited.

Fixes (small commits, no force-push)

  • b61f4ff: patch_array_to_tensor decided the repetition separately from channel_repeat() (duplicated logic). It now uses channel_repeat(), so the recorded value is the applied one. This commit also adds a regression test that PatchTorchDataset records channel_repeat and keeps 1, 2 and 4 channels.
  • c01cdf2: CHANGELOG downstream commits and the MorphoFeature audit's PT08 row.
  • 81edaa0: AGENTS.md health snapshot.

Gate (uv venvs, Python 3.11, head 81edaa0)

  • tools/check.py exits 0, and so does --rules.
Environment Passed Skipped
.[test] 1080 41 (zarr 21, notebook 10, torch 10)
.[test,zarr] 1101 20
.[test,zarr,notebook] 1129 11 (all torch)
.[test,zarr,notebook] + CPU torch 2.15.0 1178 0

CI is green with the same three counts. CI has no torch job.

§9 figures

  • Largest file touched: tests/test_torch_dataset.py, 592 lines; src/cellpatcher/torch_dataset.py is 478.
  • patch_array_to_tensor: 35 executable lines, complexity 9, 31 statements, 3 parameters.
  • PatchTensorConfig.__post_init__: 12 lines, complexity 7, 11 statements.
  • channel_repeat: 4 lines, complexity 1, 3 parameters.
  • Pre-existing and baselined, with only docstring lines touched here: PatchTorchDataset.__init__, which has complexity 15, 10 parameters and 39 statements; and the module's missing from __future__ import annotations.
  • Ratchet: the baseline only shrinks (3 entries removed). The # noqa count is 17, the same as main.

Generated by Claude Code

@guijacquemet
guijacquemet deleted the claude/cp-4-torch-defaults branch November 14, 2026 08:25
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