Preserve continuous rollout hard scores
Rollout hard scores can be continuous when smoothed rewards are used. Converting the field through int() turned values like 0.75 into 0, which loses signal before scoring and serialization. Constraint: Keep the existing RolloutResult dictionary shape unchanged. Rejected: Clamp hard scores to 0 or 1 | contradicts existing continuous-score support in compute_score and sleep replay types. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat hard as numeric reward data, not only a binary label. Tested: uv run --with pytest pytest tests/test_types.py tests/test_scoring.py -q Tested: uv run --with ruff ruff check skillopt/types.py tests/test_types.py Not-tested: Full benchmark rollouts.
This commit is contained in:
+5
-5
@@ -11,12 +11,12 @@ BatchSpec — from skillopt.datasets.base
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field, fields as dc_fields
|
from dataclasses import dataclass, field
|
||||||
|
from dataclasses import fields as dc_fields
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from skillopt.evaluation.gate import GateAction, GateResult # noqa: F401
|
|
||||||
from skillopt.datasets.base import BatchSpec # noqa: F401
|
from skillopt.datasets.base import BatchSpec # noqa: F401
|
||||||
|
from skillopt.evaluation.gate import GateAction, GateResult # noqa: F401
|
||||||
|
|
||||||
# ── Atomic types ─────────────────────────────────────────────────────────
|
# ── Atomic types ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ class RolloutResult:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
hard: int
|
hard: float
|
||||||
soft: float
|
soft: float
|
||||||
n_turns: int = 0
|
n_turns: int = 0
|
||||||
fail_reason: str = ""
|
fail_reason: str = ""
|
||||||
@@ -142,7 +142,7 @@ class RolloutResult:
|
|||||||
extras = {k: v for k, v in d.items() if k not in known}
|
extras = {k: v for k, v in d.items() if k not in known}
|
||||||
return cls(
|
return cls(
|
||||||
id=str(d.get("id", "")),
|
id=str(d.get("id", "")),
|
||||||
hard=int(d.get("hard", 0)),
|
hard=float(d.get("hard", 0)),
|
||||||
soft=float(d.get("soft", 0.0)),
|
soft=float(d.get("soft", 0.0)),
|
||||||
n_turns=int(d.get("n_turns", 0)),
|
n_turns=int(d.get("n_turns", 0)),
|
||||||
fail_reason=str(d.get("fail_reason", "")),
|
fail_reason=str(d.get("fail_reason", "")),
|
||||||
|
|||||||
+15
-2
@@ -3,8 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from skillopt.types import Edit, Patch
|
from skillopt.types import Edit, Patch, RolloutResult
|
||||||
|
|
||||||
|
|
||||||
# ── Edit ────────────────────────────────────────────────────────────────────
|
# ── Edit ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -247,3 +246,17 @@ class TestPatchEdgeCases:
|
|||||||
assert isinstance(p.edits[0], Edit)
|
assert isinstance(p.edits[0], Edit)
|
||||||
assert p.edits[0].op == "append"
|
assert p.edits[0].op == "append"
|
||||||
assert p.edits[0].content == "hello"
|
assert p.edits[0].content == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
# ── RolloutResult ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestRolloutResultRoundTrip:
|
||||||
|
"""RolloutResult.to_dict() / RolloutResult.from_dict() round-trip."""
|
||||||
|
|
||||||
|
def test_preserves_fractional_hard_score(self) -> None:
|
||||||
|
"""Hard can be a continuous reward and must not be truncated."""
|
||||||
|
result = RolloutResult.from_dict({"id": "episode-1", "hard": 0.75, "soft": 0.5})
|
||||||
|
|
||||||
|
assert result.hard == pytest.approx(0.75)
|
||||||
|
assert result.to_dict()["hard"] == pytest.approx(0.75)
|
||||||
|
|||||||
Reference in New Issue
Block a user