Spaces:
Sleeping
Sleeping
Roshan818 commited on
Commit Β·
251eea3
1
Parent(s): 504deea
fix: grader.py pure stdlib, no external deps, works on Python 3.11+
Browse files
grader.py
CHANGED
|
@@ -1,19 +1,136 @@
|
|
| 1 |
"""
|
| 2 |
Graders for Smart Factory Scheduling tasks.
|
| 3 |
|
| 4 |
-
Each
|
| 5 |
-
|
| 6 |
-
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
score_easy(state) # dict with "completed_jobs", "pending_jobs", "time" β¦
|
| 11 |
"""
|
| 12 |
|
| 13 |
from __future__ import annotations
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
def _compute(completed: int, on_time: int, total: int, late: int) -> float:
|
| 19 |
if total == 0:
|
|
@@ -26,79 +143,63 @@ def _compute(completed: int, on_time: int, total: int, late: int) -> float:
|
|
| 26 |
return round(max(0.001, min(0.999, score)), 4)
|
| 27 |
|
| 28 |
|
| 29 |
-
def
|
| 30 |
-
"""
|
| 31 |
if isinstance(obj, dict):
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
late = obj.get("late_jobs", 0) or 0
|
| 35 |
-
t = obj.get("time", 0) or 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
else:
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
) or
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
1 for j in done_jobs
|
| 48 |
-
if (j.get("deadline", 0) if isinstance(j, dict)
|
| 49 |
-
else getattr(j, "deadline", 0)) >= t
|
| 50 |
-
)
|
| 51 |
return _compute(completed, on_time, total, late)
|
| 52 |
|
| 53 |
|
| 54 |
-
def
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
for m in obs.machines:
|
| 62 |
-
if m.status == "idle":
|
| 63 |
-
return FactoryAction(
|
| 64 |
-
action_type="assign_job", job_id=j.id, machine_id=m.id
|
| 65 |
-
)
|
| 66 |
-
return None # wait
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def _run_episode(task: str, seed: int = 42) -> float:
|
| 70 |
-
"""Run one full heuristic episode and return the graded score."""
|
| 71 |
-
from factory_env.env import FactoryEnv
|
| 72 |
-
from factory_env.models import FactoryAction
|
| 73 |
-
|
| 74 |
-
env = FactoryEnv(task=task, seed=seed)
|
| 75 |
-
obs = env.reset()
|
| 76 |
-
for _ in range(obs.max_steps):
|
| 77 |
-
if obs.done:
|
| 78 |
-
break
|
| 79 |
-
action = _heuristic_action(obs) or FactoryAction(action_type="wait")
|
| 80 |
-
obs = env.step(action)
|
| 81 |
-
return _score_from(env)
|
| 82 |
|
| 83 |
|
| 84 |
-
# ββ
|
| 85 |
|
| 86 |
def score_easy(state_or_env=None) -> float:
|
| 87 |
-
"""Grade an easy-task episode (2 machines, 3 jobs, no failures).
|
|
|
|
| 88 |
if state_or_env is not None:
|
| 89 |
-
return
|
| 90 |
return _run_episode("easy")
|
| 91 |
|
| 92 |
|
| 93 |
def score_medium(state_or_env=None) -> float:
|
| 94 |
-
"""Grade a medium-task episode (4 machines, 7 jobs, 8% failures).
|
|
|
|
| 95 |
if state_or_env is not None:
|
| 96 |
-
return
|
| 97 |
return _run_episode("medium")
|
| 98 |
|
| 99 |
|
| 100 |
def score_hard(state_or_env=None) -> float:
|
| 101 |
-
"""Grade a hard-task episode (6 machines, 12 jobs, 15% failures).
|
|
|
|
| 102 |
if state_or_env is not None:
|
| 103 |
-
return
|
| 104 |
return _run_episode("hard")
|
|
|
|
| 1 |
"""
|
| 2 |
Graders for Smart Factory Scheduling tasks.
|
| 3 |
|
| 4 |
+
Each public function accepts an optional state argument:
|
| 5 |
+
- Called with no argument β runs a deterministic heuristic episode and returns a score.
|
| 6 |
+
- Called with a dict/object β scores that state directly.
|
| 7 |
|
| 8 |
+
All functions return a float strictly in (0.0, 1.0).
|
| 9 |
+
No external dependencies β pure Python stdlib only.
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
+
import random
|
| 15 |
+
from typing import Any, Dict, List, Optional
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ββ Minimal in-process environment βββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
|
| 20 |
+
TASK_CONFIGS = {
|
| 21 |
+
"easy": {"num_machines": 2, "num_jobs": 3, "failure_rate": 0.00, "max_steps": 20,
|
| 22 |
+
"job_time_range": (2, 4), "deadline_slack": (2, 5), "max_priority": 1},
|
| 23 |
+
"medium": {"num_machines": 4, "num_jobs": 7, "failure_rate": 0.08, "max_steps": 30,
|
| 24 |
+
"job_time_range": (2, 5), "deadline_slack": (2, 6), "max_priority": 2},
|
| 25 |
+
"hard": {"num_machines": 6, "num_jobs": 12, "failure_rate": 0.15, "max_steps": 40,
|
| 26 |
+
"job_time_range": (2, 6), "deadline_slack": (1, 5), "max_priority": 3},
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class _Machine:
|
| 31 |
+
__slots__ = ("id", "status", "current_job", "failure_rate")
|
| 32 |
+
|
| 33 |
+
def __init__(self, mid: str, failure_rate: float) -> None:
|
| 34 |
+
self.id = mid
|
| 35 |
+
self.status = "idle"
|
| 36 |
+
self.current_job: Optional[str] = None
|
| 37 |
+
self.failure_rate = failure_rate
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class _Job:
|
| 41 |
+
__slots__ = ("id", "remaining_time", "deadline", "priority", "assigned_machine")
|
| 42 |
+
|
| 43 |
+
def __init__(self, jid: str, remaining_time: int, deadline: int, priority: int) -> None:
|
| 44 |
+
self.id = jid
|
| 45 |
+
self.remaining_time = remaining_time
|
| 46 |
+
self.deadline = deadline
|
| 47 |
+
self.priority = priority
|
| 48 |
+
self.assigned_machine: Optional[str] = None
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class _MiniEnv:
|
| 52 |
+
"""Minimal pure-Python factory simulation β no external dependencies."""
|
| 53 |
+
|
| 54 |
+
def __init__(self, task: str, seed: int = 42) -> None:
|
| 55 |
+
cfg = TASK_CONFIGS[task]
|
| 56 |
+
rng = random.Random(seed)
|
| 57 |
+
self.task = task
|
| 58 |
+
self.time = 0
|
| 59 |
+
self.max_steps = cfg["max_steps"]
|
| 60 |
+
self.late_jobs = 0
|
| 61 |
+
self.machines: List[_Machine] = [
|
| 62 |
+
_Machine(f"M{i+1}", cfg["failure_rate"])
|
| 63 |
+
for i in range(cfg["num_machines"])
|
| 64 |
+
]
|
| 65 |
+
self.jobs: List[_Job] = []
|
| 66 |
+
for i in range(cfg["num_jobs"]):
|
| 67 |
+
pt = rng.randint(*cfg["job_time_range"])
|
| 68 |
+
deadline = pt + rng.randint(*cfg["deadline_slack"])
|
| 69 |
+
priority = rng.randint(1, cfg["max_priority"])
|
| 70 |
+
self.jobs.append(_Job(f"J{i+1}", pt, deadline, priority))
|
| 71 |
+
self.completed_jobs: List[_Job] = []
|
| 72 |
+
self._rng = rng
|
| 73 |
+
|
| 74 |
+
# ββ heuristic action ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 75 |
+
def _heuristic(self):
|
| 76 |
+
for m in self.machines:
|
| 77 |
+
if m.status == "broken":
|
| 78 |
+
return ("repair", m.id, None)
|
| 79 |
+
for j in sorted(self.jobs, key=lambda x: (x.deadline, -x.priority)):
|
| 80 |
+
for m in self.machines:
|
| 81 |
+
if m.status == "idle":
|
| 82 |
+
return ("assign", j.id, m.id)
|
| 83 |
+
return ("wait", None, None)
|
| 84 |
+
|
| 85 |
+
def step(self, action) -> bool:
|
| 86 |
+
kind, arg1, arg2 = action
|
| 87 |
+
if kind == "assign":
|
| 88 |
+
job = next((j for j in self.jobs if j.id == arg1), None)
|
| 89 |
+
machine = next((m for m in self.machines if m.id == arg2), None)
|
| 90 |
+
if job and machine and machine.status == "idle":
|
| 91 |
+
job.assigned_machine = machine.id
|
| 92 |
+
machine.status = "busy"
|
| 93 |
+
machine.current_job = job.id
|
| 94 |
+
elif kind == "repair":
|
| 95 |
+
machine = next((m for m in self.machines if m.id == arg1), None)
|
| 96 |
+
if machine and machine.status == "broken":
|
| 97 |
+
machine.status = "idle"
|
| 98 |
+
|
| 99 |
+
self.time += 1
|
| 100 |
+
|
| 101 |
+
for m in self.machines:
|
| 102 |
+
if m.status == "busy":
|
| 103 |
+
job = next((j for j in self.jobs if j.id == m.current_job), None)
|
| 104 |
+
if job:
|
| 105 |
+
job.remaining_time -= 1
|
| 106 |
+
if job.remaining_time <= 0:
|
| 107 |
+
if self.time > job.deadline:
|
| 108 |
+
self.late_jobs += 1
|
| 109 |
+
self.jobs.remove(job)
|
| 110 |
+
self.completed_jobs.append(job)
|
| 111 |
+
m.status = "idle"
|
| 112 |
+
m.current_job = None
|
| 113 |
+
|
| 114 |
+
if m.status == "busy" and m.failure_rate > 0:
|
| 115 |
+
if self._rng.random() < m.failure_rate:
|
| 116 |
+
stalled = next((j for j in self.jobs if j.id == m.current_job), None)
|
| 117 |
+
if stalled:
|
| 118 |
+
stalled.assigned_machine = None
|
| 119 |
+
m.status = "broken"
|
| 120 |
+
m.current_job = None
|
| 121 |
+
|
| 122 |
+
done = self.time >= self.max_steps or len(self.jobs) == 0
|
| 123 |
+
return done
|
| 124 |
+
|
| 125 |
+
def run_heuristic(self) -> None:
|
| 126 |
+
for _ in range(self.max_steps):
|
| 127 |
+
action = self._heuristic()
|
| 128 |
+
done = self.step(action)
|
| 129 |
+
if done:
|
| 130 |
+
break
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# ββ Score computation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 134 |
|
| 135 |
def _compute(completed: int, on_time: int, total: int, late: int) -> float:
|
| 136 |
if total == 0:
|
|
|
|
| 143 |
return round(max(0.001, min(0.999, score)), 4)
|
| 144 |
|
| 145 |
|
| 146 |
+
def _score_from_obj(obj: Any) -> float:
|
| 147 |
+
"""Score from an env object (has .completed_jobs, .jobs/.pending_jobs, .time, .late_jobs)."""
|
| 148 |
if isinstance(obj, dict):
|
| 149 |
+
done_list = obj.get("completed_jobs", []) or []
|
| 150 |
+
pend_list = obj.get("pending_jobs", []) or []
|
| 151 |
+
late = int(obj.get("late_jobs", 0) or 0)
|
| 152 |
+
t = int(obj.get("time", 0) or 0)
|
| 153 |
+
completed = len(done_list)
|
| 154 |
+
total = completed + len(pend_list)
|
| 155 |
+
on_time = sum(
|
| 156 |
+
1 for j in done_list
|
| 157 |
+
if (j.get("deadline", 0) if isinstance(j, dict) else getattr(j, "deadline", 0)) >= t
|
| 158 |
+
)
|
| 159 |
else:
|
| 160 |
+
done_list = list(getattr(obj, "completed_jobs", []) or [])
|
| 161 |
+
pend_list = list(getattr(obj, "jobs", getattr(obj, "pending_jobs", [])) or [])
|
| 162 |
+
late = int(getattr(obj, "late_jobs", 0) or 0)
|
| 163 |
+
t = int(getattr(obj, "time", 0) or 0)
|
| 164 |
+
completed = len(done_list)
|
| 165 |
+
total = completed + len(pend_list)
|
| 166 |
+
on_time = sum(
|
| 167 |
+
1 for j in done_list
|
| 168 |
+
if getattr(j, "deadline", 0) >= t
|
| 169 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
return _compute(completed, on_time, total, late)
|
| 171 |
|
| 172 |
|
| 173 |
+
def _run_episode(task: str) -> float:
|
| 174 |
+
env = _MiniEnv(task, seed=42)
|
| 175 |
+
env.run_heuristic()
|
| 176 |
+
completed = len(env.completed_jobs)
|
| 177 |
+
total = completed + len(env.jobs)
|
| 178 |
+
on_time = max(0, completed - env.late_jobs) # on-time = completed minus late
|
| 179 |
+
return _compute(completed, on_time, total, env.late_jobs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
|
| 182 |
+
# ββ Public graders βββββββββββββββββββββββββββββοΏ½οΏ½οΏ½ββββββββββββββββββββββββββββββ
|
| 183 |
|
| 184 |
def score_easy(state_or_env=None) -> float:
|
| 185 |
+
"""Grade an easy-task episode (2 machines, 3 jobs, no failures).
|
| 186 |
+
Returns a float in (0.0, 1.0). Called with no argument for baseline scoring."""
|
| 187 |
if state_or_env is not None:
|
| 188 |
+
return _score_from_obj(state_or_env)
|
| 189 |
return _run_episode("easy")
|
| 190 |
|
| 191 |
|
| 192 |
def score_medium(state_or_env=None) -> float:
|
| 193 |
+
"""Grade a medium-task episode (4 machines, 7 jobs, 8% failures).
|
| 194 |
+
Returns a float in (0.0, 1.0). Called with no argument for baseline scoring."""
|
| 195 |
if state_or_env is not None:
|
| 196 |
+
return _score_from_obj(state_or_env)
|
| 197 |
return _run_episode("medium")
|
| 198 |
|
| 199 |
|
| 200 |
def score_hard(state_or_env=None) -> float:
|
| 201 |
+
"""Grade a hard-task episode (6 machines, 12 jobs, 15% failures).
|
| 202 |
+
Returns a float in (0.0, 1.0). Called with no argument for baseline scoring."""
|
| 203 |
if state_or_env is not None:
|
| 204 |
+
return _score_from_obj(state_or_env)
|
| 205 |
return _run_episode("hard")
|