"""Constraint metadata and helper utilities for patch selection UI.""" from __future__ import annotations from dataclasses import dataclass from typing import Dict, Iterable, Mapping @dataclass(frozen=True) class MetricRange: """Slider bounds for a specific metric.""" default: float minimum: float maximum: float step: float label: str | None = None description: str | None = None @dataclass(frozen=True) class ConstraintDefinition: """UI metadata + semantic description for a constraint.""" name: str label: str description: str entropy: MetricRange perplexity: MetricRange guard: bool = False def range_for_metric(self, metric_name: str) -> MetricRange: """Return the metric range for the given metric label.""" key = metric_name.lower() if key not in ("entropy", "perplexity"): raise ValueError(f"Unknown metric '{metric_name}'") return getattr(self, key) # Constraint slider definitions CONSTRAINT_DEFINITIONS: Dict[str, ConstraintDefinition] = { "global": ConstraintDefinition( name="global", label="Global Threshold", description="H(t) > θ", entropy=MetricRange(default=5.0, minimum=0.0, maximum=20.0, step=0.5), perplexity=MetricRange(default=300.0, minimum=0.0, maximum=2000.0, step=5.0), ), "additive": ConstraintDefinition( name="additive", label="Additive Monotonic", description="H(t) - H(t-1) > θ", entropy=MetricRange(default=0.5, minimum=0.0, maximum=10.0, step=0.1), perplexity=MetricRange(default=5.0, minimum=0.0, maximum=100.0, step=1.0), ), "multiplicative": ConstraintDefinition( name="multiplicative", label="Multiplicative Monotonic", description="H(t) / H(t-1) > θ", entropy=MetricRange(default=1.5, minimum=1.0, maximum=5.0, step=0.1), perplexity=MetricRange(default=1.5, minimum=1.0, maximum=5.0, step=0.1), ), "additive_min": ConstraintDefinition( name="additive_min", label="Additive from Min", description="H(t) - min(H) > θ", entropy=MetricRange(default=2.0, minimum=0.0, maximum=10.0, step=0.1), perplexity=MetricRange(default=20.0, minimum=0.0, maximum=100.0, step=1.0), ), "multiplicative_min": ConstraintDefinition( name="multiplicative_min", label="Multiplicative from Min", description="H(t) / min(H) > θ", entropy=MetricRange(default=2.0, minimum=1.0, maximum=10.0, step=0.1), perplexity=MetricRange(default=3.0, minimum=1.0, maximum=10.0, step=0.1), ), "cumulative": ConstraintDefinition( name="cumulative", label="Cumulative Sum (Min)", description="Σ H(t) > θ (boundary after exceeding)", entropy=MetricRange(default=10.0, minimum=0.0, maximum=100.0, step=1.0), perplexity=MetricRange(default=100.0, minimum=0.0, maximum=1000.0, step=10.0), ), "cumulative_max": ConstraintDefinition( name="cumulative_max", label="Cumulative Sum (Max)", description="Σ H(t) ≤ θ (boundary before exceeding)", entropy=MetricRange(default=50.0, minimum=0.0, maximum=500.0, step=5.0), perplexity=MetricRange(default=500.0, minimum=0.0, maximum=10000.0, step=50.0), ), } MIN_GUARD_DEFINITION = ConstraintDefinition( name="min_metric", label="Minimum Metric for Boundary", description="AND guard: boundaries only trigger once metric exceeds this value", entropy=MetricRange( default=0.0, minimum=0.0, maximum=20.0, step=0.1, label="Minimum Entropy for Boundary", description="Guard: boundaries only trigger once entropy exceeds this value", ), perplexity=MetricRange( default=0.0, minimum=0.0, maximum=2000.0, step=5.0, label="Minimum Perplexity for Boundary", description="Guard: boundaries only trigger once perplexity exceeds this value", ), guard=True, ) def iter_constraint_definitions() -> Iterable[ConstraintDefinition]: """Iterate definitions in sorted order by display label.""" return sorted(CONSTRAINT_DEFINITIONS.values(), key=lambda c: c.label) def get_constraint_definition(name: str) -> ConstraintDefinition: """Return definition by name.""" if name == MIN_GUARD_DEFINITION.name: return MIN_GUARD_DEFINITION try: return CONSTRAINT_DEFINITIONS[name] except KeyError as exc: raise KeyError(f"Unknown constraint '{name}'") from exc def default_thresholds_for_metric(metric_name: str) -> Mapping[str, float]: """Return default slider values for each constraint given the metric.""" return { definition.name: definition.range_for_metric(metric_name).default for definition in iter_constraint_definitions() } def guard_range_for_metric(metric_name: str) -> MetricRange: """Return metric range for the guard slider.""" metric_range = MIN_GUARD_DEFINITION.range_for_metric(metric_name) if metric_range.label and metric_range.description: return metric_range return MetricRange( default=metric_range.default, minimum=metric_range.minimum, maximum=metric_range.maximum, step=metric_range.step, label=MIN_GUARD_DEFINITION.label, description=MIN_GUARD_DEFINITION.description, )