Coverage for cafl4ds/filters/composite.py: 88%
20 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-17 14:39 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-17 14:39 +0000
1"""The composite selector — combine admission filters and a replay buffer (the ``A`` factor).
3The cheap Phase-0 knobs split into two composable roles (see :mod:`cafl4ds.filters.base`):
4zero or more **admission** :class:`~cafl4ds.filters.base.Filter` s that narrow the incoming
5batch, and at most one **replay** :class:`~cafl4ds.filters.base.ReplayBuffer` that stores and
6replays. :class:`CompositeSelector` wires them behind the single ``Filter.select`` interface
7the loop calls, so *what* combination of knobs is active is purely a config choice:
9====================== ========================= ====================
10Baseline admission buffer
11====================== ========================= ====================
12B-floor — —
13B1 (reservoir) — ReservoirReplay
14dedup SemDeDup —
15loss-gate (B3) LossGate —
16B1.5 (dedup+replay) SemDeDup ReservoirReplay
17====================== ========================= ====================
19Admission filters run in listed order (each narrows what the next sees); the buffer always
20runs last, on the admitted frames. This ordering is structural: dedup/loss-gate decide what is
21*worth admitting*, and the buffer then decides what of the admitted history to *replay* — the
22reverse (replaying, then deduping the replay) would be meaningless.
23"""
25from __future__ import annotations
27from dataclasses import replace
29import torch
30from loguru import logger
32from cafl4ds.data.streams import StreamBatch
33from cafl4ds.filters.base import Filter, FilterContext, ReplayBuffer
36class CompositeSelector(Filter):
37 """Compose an admission-filter pipeline with an optional replay buffer."""
39 def __init__(self, admission: list[Filter] | None = None, buffer: ReplayBuffer | None = None) -> None:
40 """Build the composite.
42 Args:
43 admission: Admission filters applied in order (each narrows the batch). ``None`` or
44 an empty list means accept everything (no narrowing) — the B-floor admission.
45 buffer: An optional replay buffer run after admission. ``None`` trains on the
46 admitted frames directly (no replay).
47 """
48 self.admission = list(admission or [])
49 self.buffer = buffer
51 def select(self, batch: StreamBatch, ctx: FilterContext) -> torch.Tensor:
52 """Run the admission pipeline, then the buffer, and return the training batch.
54 Args:
55 batch: The incoming label-free stream batch.
56 ctx: The live filter context (model, step), forwarded to every stage.
58 Returns:
59 The images the SSL update trains on this step. May be *smaller* than the incoming
60 batch (admission narrowed it, possibly to empty) or *larger* (the buffer replayed
61 past frames).
62 """
63 images = batch.images
64 for stage in self.admission:
65 images = stage.select(replace(batch, images=images), ctx)
66 if images.shape[0] == 0: 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true
67 logger.debug(f"step {ctx.step}: admission emptied the batch at {type(stage).__name__}.")
68 break
69 if self.buffer is not None:
70 images = self.buffer.observe(images, ctx)
71 return images