ADR-0004 — instance_ids, frozen DenseSample, and BatchedDenseSample
| Number | 0004 |
| Title | Instance identity, DenseSample immutability, batched container |
| Status | Accepted |
| Author | @NoeFontana |
| Created | 2026-04-22 |
| Updated | 2026-04-22 |
| Tag | ADR-0004 |
| Supersedes | ADR-0001 Part (iii) — type-table and DenseSample mutability only |
Context
P1 workstream W1 migrates the augmentation pipeline off DetectionTarget and
onto DenseSample end-to-end (see ADR-0003
for the deletion schedule). Three gaps in ADR-0001 Part (iii) surfaced during
W1 design and need pinning before the migration commit lands:
- Per-instance identity. ADR-0001's
DenseSampleschema carrieslabels: torch.Tensor [N] int64(class ids) but no per-instance id. The panoptic composite (later P1 workstream) needs stable, unique instance ids to satisfy the "fresh instance ids on paste" invariant in ADR-0001 §(ii). Synthesizing ids at composite time would break idempotence and make the paste-union invariant harder to verify. Pinning identity as a first-class field closes the gap before more composites land. - Mutability. ADR-0001 Part (iii) declared
DenseSampleas@dataclass(slots=True). The pipeline is functional in practice (CopyPasteAugmentation.transformreturns a new container; no in-place mutation), so freezing the class has no code cost and givestorch.compilea cleaner aliasing story (W5 relies on this). - Batched output.
CopyPasteCollator.__call__today returns a loosely typeddict[str, Tensor | list[Tensor]]. After W1, the internal pipeline isDenseSample-native; the collator output should be a typed container with the same shape as the per-sample type. An untyped dict prevents pyright from catching field-rename regressions across the surface.
Decision
Per-instance identity
Add instance_ids: torch.Tensor | None to DenseSample, shape [N], dtype
torch.int32. Required when instance_masks is not None (co-optional with
instance_masks); None otherwise. Ids are unique within a sample but not
across a batch. The from_detection_target legacy bridge (removed at the end
of W1) synthesizes torch.arange(N, dtype=torch.int32) because
DetectionTarget carries no identity information.
Update the ADR-0001 Part (iii) field table to include instance_ids between
labels and instance_masks. Pasted instances receive fresh ids in the
range [max_prev + 1, max_prev + 1 + k) where max_prev is the largest
surviving-target id (or -1 when the target was empty). This matches the
"fresh ids" wording already reserved in ADR-0001 §(ii) for the panoptic
composite; extending it to the instance composite keeps the two pathways
aligned.
DenseSample is frozen
DenseSample is @dataclass(frozen=True, slots=True). All modifications
return a new instance. __post_init__ validation continues to run under
@skip_if_compiling. No call site in src/segpaste/ mutates a
DenseSample field after construction (verified during W1 exploration);
the flip is source-compatible.
BatchedDenseSample
A new frozen dataclass, pinned into the public surface alongside
DenseSample, is the canonical return type of CopyPasteCollator.__call__.
| Field | Type | Shape | Stacked/ragged |
|---|---|---|---|
images |
tv_tensors.Image |
[B, C, H, W] |
Stacked — LSJ enforces per-sample (C, H, W) homogeneity |
boxes |
list[tv_tensors.BoundingBoxes] |
B × [N_i, 4] |
Ragged |
labels |
list[torch.Tensor] |
B × [N_i] int64 |
Ragged |
instance_masks |
list[InstanceMask] \| None |
B × [N_i, H, W] bool |
Ragged; co-optional with instance_ids |
instance_ids |
list[torch.Tensor] \| None |
B × [N_i] int32 |
Ragged |
semantic_maps |
SemanticMap \| None |
[B, H, W] int64 |
Stacked |
panoptic_maps |
PanopticMap \| None |
[B, H, W] int64 |
Stacked |
depth |
torch.Tensor \| None |
[B, 1, H, W] float32 |
Stacked |
depth_valid |
torch.Tensor \| None |
[B, 1, H, W] bool |
Stacked |
normals |
torch.Tensor \| None |
[B, 3, H, W] float32 |
Stacked |
padding_mask |
PaddingMask \| None |
[B, 1, H, W] bool |
Stacked |
camera_intrinsics |
list[CameraIntrinsics] \| None |
B elements |
Python-object list |
BatchedDenseSample.from_samples(list[DenseSample]) -> BatchedDenseSample is
the canonical constructor; it raises on inconsistent (H, W) across
samples or modality-active mismatch (e.g. sample 0 sets semantic_map and
sample 1 does not). to_samples() is the inverse. __post_init__ validation
runs under @skip_if_compiling.
An empty batch (B == 0) is valid: stacked fields are zero-sized tensors
with the correct rank; ragged-list fields are empty lists.
Public surface delta
segpaste.__all__ gains exactly one symbol: BatchedDenseSample. The
pinned tuple in tests/test_public_surface.py::_EXPECTED_PUBLIC_API must
grow by the same symbol atomically with the __all__ edit. All other
existing public names are preserved.
Consequences
CopyPasteAugmentation.transformsignature changes to(DenseSample, list[DenseSample]) -> DenseSample.CopyPasteCollator.__call__changes to(list[DenseSample]) -> BatchedDenseSample.CocoDetectionV2.__getitem__changes to returnDenseSample. Consumers pinsegpaste<0.9to retain the old shapes — same migration story as ADR-0003.- Benchmark baseline in
benchmarks/baseline.jsonbecomes apples-to-oranges once the collator input type flips. W1's PR rides theskip-perf-gatelabel; a follow-up refresh PR captures a new baseline under ADR-0002 §Part (iii) rules. - ADR-0001 Part (iii) field table is amended by reference. ADR-0001's
header Status line cites this ADR alongside ADR-0003. Part (ii) invariants,
Part (iv) seeding, and every other Part (iii) clause (
Modality,PanopticSchema,CameraIntrinsics, tv-dispatch shim rationale) remain in force unchanged.
Status and supersession
- Accepted when this file lands on
mainwithStatus: Accepted. - ADR-0001's header is updated in the same commit to cite this ADR in the
Statusline (Part iii type-table andDenseSamplemutability only). - Any later decision that removes
instance_ids, un-freezesDenseSample, or changesBatchedDenseSample's pinned layout requires a new ADR that explicitly supersedes this one.
Verification
uv run mkdocs build --strictpasses with this ADR in the nav.- ADR-0001 renders with the updated
Statuscross-linking this ADR.