Roshan818 commited on
Commit
251eea3
Β·
1 Parent(s): 504deea

fix: grader.py pure stdlib, no external deps, works on Python 3.11+

Browse files
Files changed (1) hide show
  1. grader.py +164 -63
grader.py CHANGED
@@ -1,19 +1,136 @@
1
  """
2
  Graders for Smart Factory Scheduling tasks.
3
 
4
- Each grader is self-contained: when called with no arguments it creates a
5
- FactoryEnv, runs a deterministic heuristic episode, and returns a score
6
- strictly in (0, 1).
7
 
8
- Alternatively, pass an env object or state dict from an already-run episode:
9
- score_easy(env) # env object with .completed_jobs, .jobs, .time …
10
- score_easy(state) # dict with "completed_jobs", "pending_jobs", "time" …
11
  """
12
 
13
  from __future__ import annotations
14
 
15
-
16
- # ── internal helpers ──────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 _score_from(obj) -> float:
30
- """Accept env object or state dict and return a score."""
31
  if isinstance(obj, dict):
32
- done_jobs = obj.get("completed_jobs", []) or []
33
- pending = obj.get("pending_jobs", []) or []
34
- late = obj.get("late_jobs", 0) or 0
35
- t = obj.get("time", 0) or 0
 
 
 
 
 
 
36
  else:
37
- done_jobs = list(getattr(obj, "completed_jobs", []) or [])
38
- pending = list(
39
- getattr(obj, "jobs", getattr(obj, "pending_jobs", []))
40
- ) or []
41
- late = getattr(obj, "late_jobs", 0) or 0
42
- t = getattr(obj, "time", 0) or 0
43
-
44
- completed = len(done_jobs)
45
- total = completed + len(pending)
46
- on_time = sum(
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 _heuristic_action(obs):
55
- """Simple earliest-deadline-first heuristic."""
56
- from factory_env.models import FactoryAction
57
- for m in obs.machines:
58
- if m.status == "broken":
59
- return FactoryAction(action_type="repair", machine_id=m.id)
60
- for j in sorted(obs.pending_jobs, key=lambda x: (x.deadline, -x.priority)):
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
- # ── public graders ────────────────────────────────────────────────────────────
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 _score_from(state_or_env)
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 _score_from(state_or_env)
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 _score_from(state_or_env)
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")