Kaushalraj Puwar commited on
Commit
92b1f5c
·
1 Parent(s): 11e2bfd

feat(graders): implement task graders with metrics and registry integration

Browse files

Introduce a modular grader system for evaluating episode trajectories across tasks 1-4, including a registry for callable graders, metric computation functions, and integration into the inference pipeline for deterministic scoring. Add comprehensive unit tests covering basic functionality, invalid action penalties, and range validation. Update constants with normalization scales and penalties. Include new documentation directory for related specs.

docs/graders.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Grader Implementation Details
2
+
3
+ The thermal plant environment evaluates external LLM agents using deterministic graders implemented in `graders/task*_grader.py`.
4
+
5
+ ## Core Principles
6
+ - **Deterministic**: Graders rely only on mathematical computations over `trajectory.steps[].raw_state` (full precision) and not on rounded observations.
7
+ - **Normalization**: Computed metrics are mapped to a normalized range `[0,1]` using linear clamping. The default metric scale is `0.5`, defined in `utils/constants.py` under the dictionary `METRIC_NORM_SCALES`.
8
+ - **Score constraints**: All final scores are bounded algebraically and strictly clamped to `[0,1]` via `max(0.0, min(1.0, score))`.
9
+
10
+ ## Shared Metrics (`graders/_metrics.py`)
11
+ All graders share deterministic extraction functions:
12
+ - **TE** (Tracking Error): `(1/N) * sum(|P - L|)`
13
+ - **OS** (Overshoot): `max(0, P - L)` over all steps
14
+ - **SV** (Safety Violations): `(1/N) * sum(max(0, T - 1.0) + max(0, Pr - 1.0))`
15
+ - **OC** (Oscillation): `(1/(N-1)) * sum(|U_t - U_{t-1}|)`
16
+ - **SL** (Stress Level): `(1/N) * sum(S)`
17
+ - **Failure Flag (FF)**: `1` if trajectory ended due to a catastrophic state violation of temperature, pressure, or stress, else `0`
18
+ - **Invalid Output Penalty**: Graders subtract `INVALID_ACTION_PENALTY` (default `0.2`) from the computed task score for *each step* where the LLM's payload was unparseable.
19
+
20
+ ## Special Operations
21
+ - Task 4 (Fault Recovery) computes **RT**, the Recovery Time. This defines the step index when the control tracks within bounds again (`|P - L| < 0.1` and `T < 1.0`). If recovery fails completely, it returns `N`.
22
+ RT is normalized directly as `Clamp(RT / N)` instead of using the generic `0.5` scaling value.
23
+
24
+ ## Debugging and Diagnostics
25
+ The `inference.py` runner includes an optional debug mode. Call inference with `DEBUG=1` to print un-rounded raw values, actual metric outputs, and penalty flags to `stderr`.
26
+ ```bash
27
+ DEBUG=1 python inference.py
28
+ ```
29
+ This data is printed only to `stderr` and never `stdout`, protecting the `[START]/[STEP]/[END]` requirement established by the framework specification.
graders/_metrics.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Metric extraction helpers for graders.
2
+
3
+ Functions accept an `EpisodeTrajectory` (utils.schemas.EpisodeTrajectory) and compute
4
+ the canonical raw metrics described in context/06_tasks_and_graders.md.
5
+ """
6
+
7
+ from typing import Dict, List, Any
8
+ import math
9
+
10
+ import utils.constants as C
11
+
12
+
13
+ def _safe_get(raw: Dict[str, Any], key: str) -> float:
14
+ try:
15
+ return float(raw.get(key, 0.0))
16
+ except Exception:
17
+ return 0.0
18
+
19
+
20
+ def _extract(trajectory) -> Dict[str, List[float]]:
21
+ P = []
22
+ L = []
23
+ T = []
24
+ Pr = []
25
+ U = []
26
+ F = []
27
+ S = []
28
+ for step in getattr(trajectory, "steps", []):
29
+ raw = getattr(step, "raw_state", {}) or {}
30
+ P.append(_safe_get(raw, "P"))
31
+ L.append(_safe_get(raw, "L"))
32
+ T.append(_safe_get(raw, "T"))
33
+ Pr.append(_safe_get(raw, "Pr"))
34
+ U.append(_safe_get(raw, "U"))
35
+ F.append(_safe_get(raw, "F"))
36
+ S.append(_safe_get(raw, "S"))
37
+ return {"P": P, "L": L, "T": T, "Pr": Pr, "U": U, "F": F, "S": S, "N": len(P)}
38
+
39
+
40
+ def compute_TE(trajectory) -> float:
41
+ s = _extract(trajectory)
42
+ N = s["N"]
43
+ if N == 0:
44
+ return 0.0
45
+ return float(sum(abs(p - l) for p, l in zip(s["P"], s["L"])) / N)
46
+
47
+
48
+ def compute_OS(trajectory) -> float:
49
+ s = _extract(trajectory)
50
+ if s["N"] == 0:
51
+ return 0.0
52
+ diffs = [p - l for p, l in zip(s["P"], s["L"])]
53
+ return float(max(0.0, max(diffs)))
54
+
55
+
56
+ def compute_SV(trajectory) -> float:
57
+ s = _extract(trajectory)
58
+ N = s["N"]
59
+ if N == 0:
60
+ return 0.0
61
+ total = 0.0
62
+ for t, pr in zip(s["T"], s["Pr"]):
63
+ total += max(0.0, t - 1.0) + max(0.0, pr - 1.0)
64
+ return float(total / N)
65
+
66
+
67
+ def compute_OC(trajectory) -> float:
68
+ s = _extract(trajectory)
69
+ N = s["N"]
70
+ if N <= 1:
71
+ return 0.0
72
+ diffs = [abs(u1 - u0) for u0, u1 in zip(s["U"][:-1], s["U"][1:])]
73
+ return float(sum(diffs) / max(1, (N - 1)))
74
+
75
+
76
+ def compute_SL(trajectory) -> float:
77
+ s = _extract(trajectory)
78
+ N = s["N"]
79
+ if N == 0:
80
+ return 0.0
81
+ return float(sum(s["S"]) / N)
82
+
83
+
84
+ def compute_LP(trajectory) -> float:
85
+ s = _extract(trajectory)
86
+ N = s["N"]
87
+ if N == 0:
88
+ return 0.0
89
+ return float(sum(max(0.0, abs(p - l) - 0.1) for p, l in zip(s["P"], s["L"])) / N)
90
+
91
+
92
+ def compute_LS(trajectory) -> float:
93
+ s = _extract(trajectory)
94
+ N = s["N"]
95
+ if N == 0:
96
+ return 0.0
97
+ return float(sum(max(0.0, st - 0.5) for st in s["S"]) / N)
98
+
99
+
100
+ def compute_EMB(trajectory) -> float:
101
+ s = _extract(trajectory)
102
+ N = s["N"]
103
+ if N == 0:
104
+ return 0.0
105
+ count = sum(1 for st, t in zip(s["S"], s["T"]) if st < 0.5 and t < 1.0)
106
+ return float(count / N)
107
+
108
+
109
+ def compute_RT(trajectory) -> float:
110
+ s = _extract(trajectory)
111
+ N = s["N"]
112
+ if N == 0:
113
+ return 0.0
114
+ for idx, (p, l, t) in enumerate(zip(s["P"], s["L"], s["T"])):
115
+ if abs(p - l) < 0.1 and t < 1.0:
116
+ return float(idx + 1)
117
+ return float(N)
118
+
119
+
120
+ def compute_RR(trajectory) -> float:
121
+ s = _extract(trajectory)
122
+ N = s["N"]
123
+ if N == 0:
124
+ return 0.0
125
+ return float(sum(max(0.0, t - 1.0) for t in s["T"]) / N)
126
+
127
+
128
+ def compute_failure_flag(trajectory) -> int:
129
+ # FF = 1 if any termination due to catastrophic T/Pr/S breach else 0
130
+ for step in getattr(trajectory, "steps", []):
131
+ err = getattr(step, "error", None)
132
+ if err and isinstance(err, str) and "Catastrophic" in err:
133
+ return 1
134
+ raw = getattr(step, "raw_state", {}) or {}
135
+ if getattr(step, "done", False):
136
+ if _safe_get(raw, "T") > C.FAIL_T or _safe_get(raw, "Pr") > C.FAIL_PR or _safe_get(raw, "S") > C.FAIL_S:
137
+ return 1
138
+ return 0
139
+
140
+
141
+ def compute_invalid_count(trajectory) -> int:
142
+ count = 0
143
+ for step in getattr(trajectory, "steps", []):
144
+ if getattr(step, "env_invalid_action", False):
145
+ count += 1
146
+ continue
147
+ parsed = getattr(step, "parsed_action", None)
148
+ if parsed and getattr(parsed, "invalid_output", False):
149
+ count += 1
150
+ return count
151
+
152
+
153
+ def normalize_metrics(metrics: Dict[str, float], scales: Dict[str, float] = None) -> Dict[str, float]:
154
+ if scales is None:
155
+ scales = getattr(C, "METRIC_NORM_SCALES", {}) or {}
156
+ normalized: Dict[str, float] = {}
157
+ for k, v in metrics.items():
158
+ scale = float(scales.get(k, 0.5)) if scales else 0.5
159
+ if scale <= 0:
160
+ normalized[k] = 0.0
161
+ else:
162
+ normalized[k] = float(max(0.0, min(1.0, v / scale)))
163
+ return normalized
graders/registry.py CHANGED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Grader registry mapping task ids to grader callables.
2
+
3
+ Expose `grader_registry()` which returns a dict mapping task id strings
4
+ to functions with signature `grade(trajectory) -> float`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Callable, Dict
10
+
11
+ from graders import task1_grader, task2_grader, task3_grader, task4_grader
12
+
13
+
14
+ def grader_registry() -> Dict[str, Callable]:
15
+ return {
16
+ "task1": task1_grader.grade,
17
+ "task2": task2_grader.grade,
18
+ "task3": task3_grader.grade,
19
+ "task4": task4_grader.grade,
20
+ }
graders/task1_grader.py CHANGED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task 1 grader: Stable baseline operation.
2
+
3
+ Grader implements the formula from context/06_tasks_and_graders.md and
4
+ returns a float in [0,1].
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ._metrics import (
10
+ compute_TE,
11
+ compute_SV,
12
+ compute_OC,
13
+ compute_failure_flag,
14
+ compute_invalid_count,
15
+ normalize_metrics,
16
+ )
17
+ import utils.constants as C
18
+
19
+
20
+ def grade(trajectory) -> float:
21
+ """Compute Task 1 score from trajectory and return a float in [0,1]."""
22
+ te = compute_TE(trajectory)
23
+ sv = compute_SV(trajectory)
24
+ oc = compute_OC(trajectory)
25
+ ff = compute_failure_flag(trajectory)
26
+ invalid_count = compute_invalid_count(trajectory)
27
+
28
+ norm = normalize_metrics({"TE": te, "SV": sv, "OC": oc})
29
+
30
+ score = 1.0
31
+ score -= 0.5 * norm.get("TE", 0.0)
32
+ score -= 0.2 * norm.get("SV", 0.0)
33
+ score -= 0.2 * norm.get("OC", 0.0)
34
+ score -= 0.3 * (1 if ff else 0)
35
+
36
+ # Apply per-invalid-step penalty
37
+ score -= invalid_count * getattr(C, "INVALID_ACTION_PENALTY", 0.2)
38
+
39
+ return float(max(0.0, min(1.0, score)))
graders/task2_grader.py CHANGED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task 2 grader: Load following.
2
+
3
+ Formula from context/06_tasks_and_graders.md. Returns a float in [0,1].
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ._metrics import (
9
+ compute_TE,
10
+ compute_OS,
11
+ compute_LP,
12
+ compute_SV,
13
+ compute_failure_flag,
14
+ compute_invalid_count,
15
+ normalize_metrics,
16
+ )
17
+ import utils.constants as C
18
+
19
+
20
+ def grade(trajectory) -> float:
21
+ te = compute_TE(trajectory)
22
+ os_ = compute_OS(trajectory)
23
+ lp = compute_LP(trajectory)
24
+ sv = compute_SV(trajectory)
25
+ ff = compute_failure_flag(trajectory)
26
+ invalid_count = compute_invalid_count(trajectory)
27
+
28
+ norm = normalize_metrics({"TE": te, "OS": os_, "LP": lp, "SV": sv})
29
+
30
+ score = 1.0
31
+ score -= 0.4 * norm.get("TE", 0.0)
32
+ score -= 0.2 * norm.get("OS", 0.0)
33
+ score -= 0.2 * norm.get("LP", 0.0)
34
+ score -= 0.2 * norm.get("SV", 0.0)
35
+ score -= 0.3 * (1 if ff else 0)
36
+
37
+ score -= invalid_count * getattr(C, "INVALID_ACTION_PENALTY", 0.2)
38
+
39
+ return float(max(0.0, min(1.0, score)))
graders/task3_grader.py CHANGED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task 3 grader: Preemptive constraint management.
2
+
3
+ Implements specification from context/06_tasks_and_graders.md.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ._metrics import (
9
+ compute_TE,
10
+ compute_LS,
11
+ compute_SV,
12
+ compute_EMB,
13
+ compute_failure_flag,
14
+ compute_invalid_count,
15
+ normalize_metrics,
16
+ )
17
+ import utils.constants as C
18
+
19
+
20
+ def grade(trajectory) -> float:
21
+ te = compute_TE(trajectory)
22
+ ls = compute_LS(trajectory)
23
+ sv = compute_SV(trajectory)
24
+ emb = compute_EMB(trajectory)
25
+ ff = compute_failure_flag(trajectory)
26
+ invalid_count = compute_invalid_count(trajectory)
27
+
28
+ norm = normalize_metrics({"TE": te, "LS": ls, "SV": sv})
29
+
30
+ score = 1.0
31
+ score -= 0.3 * norm.get("TE", 0.0)
32
+ score -= 0.3 * norm.get("LS", 0.0)
33
+ score -= 0.2 * norm.get("SV", 0.0)
34
+ score += 0.2 * float(emb)
35
+ score -= 0.3 * (1 if ff else 0)
36
+
37
+ score -= invalid_count * getattr(C, "INVALID_ACTION_PENALTY", 0.2)
38
+
39
+ return float(max(0.0, min(1.0, score)))
graders/task4_grader.py CHANGED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Task 4 grader: Fault recovery with degradation.
2
+
3
+ Implements the recovery-time based grader from context/06_tasks_and_graders.md.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ._metrics import (
9
+ compute_RT,
10
+ compute_RR,
11
+ compute_SV,
12
+ compute_OC,
13
+ compute_failure_flag,
14
+ compute_invalid_count,
15
+ normalize_metrics,
16
+ )
17
+ import utils.constants as C
18
+
19
+
20
+ def grade(trajectory) -> float:
21
+ rt = compute_RT(trajectory)
22
+ rr = compute_RR(trajectory)
23
+ sv = compute_SV(trajectory)
24
+ oc = compute_OC(trajectory)
25
+ ff = compute_failure_flag(trajectory)
26
+ invalid_count = compute_invalid_count(trajectory)
27
+
28
+ N = len(getattr(trajectory, "steps", []))
29
+ norm_rt = max(0.0, min(1.0, rt / N)) if N > 0 else 0.0
30
+
31
+ norm = normalize_metrics({"RR": rr, "SV": sv, "OC": oc})
32
+
33
+ # Note: spec uses Norm_RT = Clamp(RT / N) where RT in 1..N
34
+ score = 1.0
35
+ score -= 0.3 * norm_rt
36
+ score -= 0.2 * norm.get("RR", 0.0)
37
+ score -= 0.2 * norm.get("SV", 0.0)
38
+ score -= 0.2 * norm.get("OC", 0.0)
39
+ score -= 0.3 * (1 if ff else 0)
40
+
41
+ score -= invalid_count * getattr(C, "INVALID_ACTION_PENALTY", 0.2)
42
+
43
+ return float(max(0.0, min(1.0, score)))
inference.py CHANGED
@@ -282,6 +282,32 @@ def main() -> None:
282
  loop_error = str(exc)
283
  finally:
284
  score = compute_normalized_score(rewards, max_steps)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  success = score >= SUCCESS_SCORE_THRESHOLD
286
  termination_reason = determine_termination_reason(loop_error=loop_error, done=done, steps_taken=steps_taken, max_steps=max_steps)
287
  trajectory.summary = TrajectorySummary(
 
282
  loop_error = str(exc)
283
  finally:
284
  score = compute_normalized_score(rewards, max_steps)
285
+ # If grader registry is available, prefer grader-computed score (deterministic, uses raw internals)
286
+ try:
287
+ from graders.registry import grader_registry
288
+ registry = grader_registry()
289
+ grader_fn = registry.get(TASK_NAME)
290
+ if grader_fn is not None:
291
+ # grader functions return float score in [0,1]
292
+ try:
293
+ gscore = float(grader_fn(trajectory))
294
+ # clamp defensively
295
+ score = max(0.0, min(1.0, gscore))
296
+ if DEBUG:
297
+ from graders._metrics import compute_TE, compute_OS, compute_SV, compute_OC, compute_SL, compute_LP, compute_LS, compute_EMB, compute_RT, compute_RR, compute_failure_flag, compute_invalid_count
298
+ print(
299
+ f"[DEBUG] Grader Metrics: TE={compute_TE(trajectory):.3f} OS={compute_OS(trajectory):.3f} "
300
+ f"SV={compute_SV(trajectory):.3f} OC={compute_OC(trajectory):.3f} SL={compute_SL(trajectory):.3f} "
301
+ f"FF={compute_failure_flag(trajectory)} Invalids={compute_invalid_count(trajectory)}",
302
+ file=sys.stderr, flush=True
303
+ )
304
+ except Exception as e:
305
+ # Ignore grader errors and fall back to reward-based score
306
+ if DEBUG:
307
+ print(f"[DEBUG] Grader exception: {e}", file=sys.stderr, flush=True)
308
+ except Exception:
309
+ # missing registry or other import error -> keep reward-based score
310
+ pass
311
  success = score >= SUCCESS_SCORE_THRESHOLD
312
  termination_reason = determine_termination_reason(loop_error=loop_error, done=done, steps_taken=steps_taken, max_steps=max_steps)
313
  trajectory.summary = TrajectorySummary(
tests/unit/test_graders.py CHANGED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ from utils.schemas import EpisodeTrajectory, TrajectoryStep, ParsedAction
4
+
5
+ from graders.task1_grader import grade as grade_t1
6
+ from graders.task2_grader import grade as grade_t2
7
+ from graders.task3_grader import grade as grade_t3
8
+ from graders.task4_grader import grade as grade_t4
9
+
10
+
11
+ def _make_parsed_action(u=0.5, f=0.5, invalid=False):
12
+ return ParsedAction(
13
+ u_target=float(u),
14
+ f_target=float(f),
15
+ source="json",
16
+ used_fallback=False,
17
+ invalid_output=invalid,
18
+ penalty_applied=0.0,
19
+ raw_text="",
20
+ parse_error=None,
21
+ )
22
+
23
+
24
+ def _make_step(idx: int, raw_state: dict, invalid=False, done=False, error=None):
25
+ pa = _make_parsed_action(u=raw_state.get("U", 0.5), f=raw_state.get("F", 0.5), invalid=invalid)
26
+ return TrajectoryStep(
27
+ step=idx,
28
+ raw_llm_text="",
29
+ parsed_action=pa,
30
+ canonical_action={"U_target": pa.u_target, "F_target": pa.f_target},
31
+ observation={k: float(v) for k, v in raw_state.items()},
32
+ raw_state={k: float(v) for k, v in raw_state.items()},
33
+ reward=0.0,
34
+ done=done,
35
+ error=error,
36
+ env_invalid_action=invalid,
37
+ invalid_penalty_applied=0.0,
38
+ )
39
+
40
+
41
+ def test_task1_basic_deterministic_and_range():
42
+ traj = EpisodeTrajectory(task="task1", benchmark="test", model="tester")
43
+ # Two-step simple trajectory
44
+ s1 = {"P": 0.6, "L": 0.5, "T": 0.5, "Pr": 0.5, "U": 0.5, "F": 0.5, "S": 0.1}
45
+ s2 = {"P": 0.5, "L": 0.7, "T": 0.4, "Pr": 0.3, "U": 0.4, "F": 0.4, "S": 0.2}
46
+ traj.steps.append(_make_step(1, s1))
47
+ traj.steps.append(_make_step(2, s2))
48
+
49
+ score1 = grade_t1(traj)
50
+ score2 = grade_t1(traj)
51
+ assert isinstance(score1, float)
52
+ assert 0.0 <= score1 <= 1.0
53
+ assert math.isclose(score1, score2, rel_tol=1e-9)
54
+
55
+
56
+ def test_invalid_penalty_reduces_score():
57
+ traj = EpisodeTrajectory(task="task1", benchmark="test", model="tester")
58
+ s = {"P": 0.6, "L": 0.5, "T": 0.5, "Pr": 0.5, "U": 0.5, "F": 0.5, "S": 0.1}
59
+ traj.steps.append(_make_step(1, s, invalid=False))
60
+ traj.steps.append(_make_step(2, s, invalid=False))
61
+ base = grade_t1(traj)
62
+
63
+ traj2 = EpisodeTrajectory(task="task1", benchmark="test", model="tester")
64
+ traj2.steps.append(_make_step(1, s, invalid=True))
65
+ traj2.steps.append(_make_step(2, s, invalid=False))
66
+ penalized = grade_t1(traj2)
67
+
68
+ assert penalized <= base
69
+
70
+
71
+ def test_task2_and_task3_and_task4_basic_ranges():
72
+ # Build a short trajectory that is safe
73
+ traj = EpisodeTrajectory(task="task2", benchmark="test", model="tester")
74
+ for i in range(4):
75
+ s = {"P": 0.6, "L": 0.6 + 0.0 * i, "T": 0.5, "Pr": 0.5, "U": 0.5, "F": 0.5, "S": 0.05}
76
+ traj.steps.append(_make_step(i + 1, s))
77
+
78
+ assert 0.0 <= grade_t2(traj) <= 1.0
79
+ assert 0.0 <= grade_t3(traj) <= 1.0
80
+ assert 0.0 <= grade_t4(traj) <= 1.0
tests/unit/test_graders_edgecases.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from utils.schemas import EpisodeTrajectory
3
+ from graders.task1_grader import grade as grade_t1
4
+ from graders.task4_grader import grade as grade_t4
5
+ from graders._metrics import compute_RT, compute_TE
6
+
7
+ def test_empty_trajectory():
8
+ traj = EpisodeTrajectory(task="task1", benchmark="test", model="tester")
9
+ score = grade_t1(traj)
10
+ assert isinstance(score, float)
11
+ assert 0.0 <= score <= 1.0
12
+
13
+ def test_task4_never_recovered():
14
+ traj = EpisodeTrajectory(task="task4", benchmark="test", model="tester")
15
+ from tests.unit.test_graders import _make_step
16
+ # P is 0.5, L is 0.7, diff = 0.2 >= 0.1 -> never recovered
17
+ traj.steps.append(_make_step(1, {"P": 0.5, "L": 0.7, "T": 0.5, "Pr": 0.5, "U": 0.5, "F": 0.5, "S": 0}))
18
+ traj.steps.append(_make_step(2, {"P": 0.5, "L": 0.7, "T": 0.5, "Pr": 0.5, "U": 0.5, "F": 0.5, "S": 0}))
19
+
20
+ rt = compute_RT(traj)
21
+ assert math.isclose(rt, 2.0)
22
+ score = grade_t4(traj)
23
+ # Norm_RT should be 1.0 (2 / 2), penalty = 0.3 * 1.0 = 0.3
24
+ # other penalties 0
25
+ assert score <= 0.7
utils/constants.py CHANGED
@@ -264,4 +264,18 @@ PARSER_DEFAULT_U = 0.5
264
  PARSER_DEFAULT_F = 0.5
265
  INCLUDE_PARSE_ERROR_IN_STEP = True
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
 
264
  PARSER_DEFAULT_F = 0.5
265
  INCLUDE_PARSE_ERROR_IN_STEP = True
266
 
267
+ # Grader normalization scales and penalties
268
+ METRIC_NORM_SCALES = {
269
+ "TE": 0.5,
270
+ "OS": 0.5,
271
+ "SV": 0.5,
272
+ "OC": 0.5,
273
+ "SL": 0.5,
274
+ "LP": 0.5,
275
+ "LS": 0.5,
276
+ "RR": 0.5,
277
+ }
278
+
279
+ INVALID_ACTION_PENALTY = 0.2
280
+
281