Skip to content

Commit 5eaacf2

Browse files
anwesh2410claude
andauthored
Validate FitConfig parameters and fail fast on invalid ranges (#4)
FitConfig and EmbeddingConfig previously accepted out-of-range values silently (e.g. target_teacher_agreement=1.5, empty frontier_targets, max_fit_labels=0, batch_size=0). The bad value only surfaced — if at all — as a confusing failure deep inside the fit. Add __post_init__ validation that raises ValueError with a clear, specific message at construction time, plus tests. Non-breaking: all existing valid configs still construct fine. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d24a764 commit 5eaacf2

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

‎src/tracer/config.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class EmbeddingConfig:
1414
batch_size: int = 128
1515
normalize: bool = True
1616

17+
def __post_init__(self) -> None:
18+
if self.batch_size <= 0:
19+
raise ValueError(f"batch_size must be > 0, got {self.batch_size}")
20+
1721

1822
@dataclass
1923
class FitConfig:
@@ -35,3 +39,28 @@ class FitConfig:
3539
# on ~3k × 22-class embedding inputs). See `tracer.fit.surrogate._candidates`
3640
# for the full set of names.
3741
skip_candidates: Tuple[str, ...] = ()
42+
43+
def __post_init__(self) -> None:
44+
if not 0.0 < self.target_teacher_agreement <= 1.0:
45+
raise ValueError(
46+
"target_teacher_agreement must be in (0, 1], got "
47+
f"{self.target_teacher_agreement}")
48+
if not self.frontier_targets:
49+
raise ValueError("frontier_targets must not be empty")
50+
for t in self.frontier_targets:
51+
if not 0.0 < t <= 1.0:
52+
raise ValueError(
53+
f"frontier_targets values must be in (0, 1], got {t}")
54+
if not 0.0 <= self.min_deploy_coverage <= 1.0:
55+
raise ValueError(
56+
"min_deploy_coverage must be in [0, 1], got "
57+
f"{self.min_deploy_coverage}")
58+
if self.max_fit_labels <= 0:
59+
raise ValueError(f"max_fit_labels must be > 0, got {self.max_fit_labels}")
60+
if not 0.0 <= self.explore_rate <= 1.0:
61+
raise ValueError(f"explore_rate must be in [0, 1], got {self.explore_rate}")
62+
if self.retrain_every <= 0:
63+
raise ValueError(f"retrain_every must be > 0, got {self.retrain_every}")
64+
if self.min_labels_to_start < 0:
65+
raise ValueError(
66+
f"min_labels_to_start must be >= 0, got {self.min_labels_to_start}")

‎tests/test_fit.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,55 @@ def test_public_exports():
399399
assert hasattr(tracer, "RepresentativeExample")
400400

401401

402+
# ── Config validation ───────────────────────────────────────────────────────
403+
404+
def test_fitconfig_rejects_out_of_range_ta():
405+
"""target_teacher_agreement must be in (0, 1]."""
406+
from tracer.config import FitConfig
407+
with pytest.raises(ValueError):
408+
FitConfig(target_teacher_agreement=1.5)
409+
with pytest.raises(ValueError):
410+
FitConfig(target_teacher_agreement=-0.2)
411+
with pytest.raises(ValueError):
412+
FitConfig(target_teacher_agreement=0.0)
413+
414+
415+
def test_fitconfig_rejects_empty_frontier_targets():
416+
"""frontier_targets must be non-empty and within (0, 1]."""
417+
from tracer.config import FitConfig
418+
with pytest.raises(ValueError):
419+
FitConfig(frontier_targets=())
420+
with pytest.raises(ValueError):
421+
FitConfig(frontier_targets=(0.9, 1.2))
422+
423+
424+
def test_fitconfig_rejects_bad_scalars():
425+
"""min_deploy_coverage, max_fit_labels, explore_rate, retrain_every are guarded."""
426+
from tracer.config import FitConfig
427+
with pytest.raises(ValueError):
428+
FitConfig(min_deploy_coverage=1.5)
429+
with pytest.raises(ValueError):
430+
FitConfig(max_fit_labels=0)
431+
with pytest.raises(ValueError):
432+
FitConfig(explore_rate=2.0)
433+
with pytest.raises(ValueError):
434+
FitConfig(retrain_every=0)
435+
436+
437+
def test_fitconfig_accepts_valid_config():
438+
"""Default config and a typical override construct without error."""
439+
from tracer.config import FitConfig
440+
FitConfig()
441+
FitConfig(target_teacher_agreement=0.95)
442+
443+
444+
def test_embeddingconfig_rejects_nonpositive_batch_size():
445+
from tracer.config import EmbeddingConfig
446+
with pytest.raises(ValueError):
447+
EmbeddingConfig(batch_size=0)
448+
EmbeddingConfig(batch_size=64)
449+
450+
402451
if __name__ == "__main__":
403452
import traceback
404453
tests = [
@@ -423,6 +472,11 @@ def test_public_exports():
423472
test_skip_candidates_removes_models,
424473
test_demo_runs,
425474
test_public_exports,
475+
test_fitconfig_rejects_out_of_range_ta,
476+
test_fitconfig_rejects_empty_frontier_targets,
477+
test_fitconfig_rejects_bad_scalars,
478+
test_fitconfig_accepts_valid_config,
479+
test_embeddingconfig_rejects_nonpositive_batch_size,
426480
]
427481
for t in tests:
428482
try:

0 commit comments

Comments
 (0)