Spaces:
Sleeping
Sleeping
| """Core Scheduling Optimisation Environment implementing the OpenEnv API contract. | |
| Design principles | |
| ----------------- | |
| * reset() always returns a valid Observation — never raises. | |
| * step() clamps reward to [0.0, 1.0] unconditionally. | |
| * Task-aware instance routing: conflict_classification and schedule_repair | |
| are shown only infeasible instances; feasibility_check sees all 12. | |
| * Per-step contextual feedback: the context string and info['grading_breakdown'] | |
| give the agent actionable signal on every step, enabling sample-efficient | |
| multi-step improvement within a single episode. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import json | |
| from typing import Any | |
| from graders.grader_classification import ConflictGrader | |
| from graders.grader_detection import FeasibilityGrader | |
| from graders.grader_fix import RepairGrader | |
| from models import Action, Observation | |
| # Grader singletons — one per task, reused across episodes. | |
| _GRADERS: dict[str, Any] = { | |
| "feasibility_check": FeasibilityGrader(), | |
| "conflict_classification": ConflictGrader(), | |
| "schedule_repair": RepairGrader(), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Scheduling instance bank — 12 diverse instances. | |
| # | |
| # Each entry: | |
| # instance – dict exposed to the agent (jobs + machines + proposed_schedule) | |
| # is_feasible – bool, ground-truth for Task 1 | |
| # violation_type – str | None, ground-truth for Task 2 | |
| # optimal_schedule – dict, the repaired schedule for Task 3 | |
| # optimal_makespan – int, minimum achievable makespan | |
| # description – one-line human-readable summary | |
| # --------------------------------------------------------------------------- | |
| INSTANCE_BANK: list[dict[str, Any]] = [ | |
| # ------------------------------------------------------------------ # | |
| # 0 — resource_overload # | |
| # J1[0,4) and J2[2,5) overlap on M1 (capacity=1). # | |
| # Fix: sequence J2 after J1. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P01", | |
| "jobs": [ | |
| {"id": "J1", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J3", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 2}, | |
| {"job_id": "J3", "machine_id": "M2", "start_time": 0}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "resource_overload", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 4}, | |
| {"job_id": "J3", "machine_id": "M2", "start_time": 0}, | |
| ] | |
| }, | |
| "optimal_makespan": 7, | |
| "description": "J1[0,4) and J2[2,5) overlap on M1 (capacity=1) → resource_overload.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 1 — deadline_violation # | |
| # J1 starts late (t=5, dur=5), finishes at t=10 > deadline=8. # | |
| # Fix: schedule J1 first so it finishes at t=5 ≤ 8. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P02", | |
| "jobs": [ | |
| {"id": "J1", "duration": 5, "deadline": 8, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 5}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 0}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "deadline_violation", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 5}, | |
| ] | |
| }, | |
| "optimal_makespan": 8, | |
| "description": "J1 starts at t=5 and finishes at t=10, violating deadline=8.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 2 — precedence_violation # | |
| # J2 depends on J1 (J1 finishes t=8) but J2 starts at t=0. # | |
| # Fix: start J1 first, then J2 after J1 completes. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P03", | |
| "jobs": [ | |
| {"id": "J1", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 3, "deadline": 20, "dependencies": ["J1"], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 5}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 0}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "precedence_violation", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 3}, | |
| ] | |
| }, | |
| "optimal_makespan": 6, | |
| "description": "J2 depends on J1; J2 starts at t=0 but J1 does not finish until t=8.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 3 — availability_conflict # | |
| # M1 available [8,18]. J1 starts at t=5, before the window opens. # | |
| # Fix: shift J1 to start at t=8 (first valid slot). # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P04", | |
| "jobs": [ | |
| {"id": "J1", "duration": 4, "deadline": 24, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 3, "deadline": 24, "dependencies": [], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 8, "available_end": 18}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 5}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 9}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "availability_conflict", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 8}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 12}, | |
| ] | |
| }, | |
| "optimal_makespan": 15, | |
| "description": "J1 starts at t=5, before M1's available window [8,18] → availability_conflict.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 4 — capacity_exceeded # | |
| # 3 jobs on M1 simultaneously; capacity=2 → load=3 > 2. # | |
| # Fix: stagger one job to start after the first batch finishes. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P05", | |
| "jobs": [ | |
| {"id": "J1", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J3", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 2, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M1", "start_time": 0}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "capacity_exceeded", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M1", "start_time": 3}, | |
| ] | |
| }, | |
| "optimal_makespan": 6, | |
| "description": "3 jobs start simultaneously on M1 (capacity=2); concurrent load=3 > 2.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 5 — resource_overload (variant) # | |
| # J1[0,5) and J2[1,5) overlap on M1 (capacity=1). # | |
| # Fix: run jobs sequentially. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P06", | |
| "jobs": [ | |
| {"id": "J1", "duration": 5, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J3", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 1}, | |
| {"job_id": "J3", "machine_id": "M1", "start_time": 8}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "resource_overload", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 5}, | |
| {"job_id": "J3", "machine_id": "M1", "start_time": 9}, | |
| ] | |
| }, | |
| "optimal_makespan": 11, | |
| "description": "J1[0,5) and J2[1,5) overlap on M1 (capacity=1) → resource_overload.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 6 — deadline_violation (chain with avoidable idle time) # | |
| # J1→J2→J3 chain. J1 starts at t=3 (wasted idle), making the chain # | |
| # finish at t=15 > deadline=13. Fix: start J1 at t=0 → chain ends at # | |
| # t=12 ≤ 13. NOTE: J3 duration is 3 (not 4) so the chain IS solvable. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P07", | |
| "jobs": [ | |
| {"id": "J1", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 5, "deadline": 20, "dependencies": ["J1"], "resource_req": 1}, | |
| {"id": "J3", "duration": 3, "deadline": 13, "dependencies": ["J2"], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M3", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 3}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 7}, | |
| {"job_id": "J3", "machine_id": "M3", "start_time": 12}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "deadline_violation", | |
| # Optimal: eliminate idle prefix → J1 starts at t=0, chain finishes at t=12 ≤ 13 | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 4}, | |
| {"job_id": "J3", "machine_id": "M3", "start_time": 9}, | |
| ] | |
| }, | |
| "optimal_makespan": 12, | |
| "description": "J1 starts at t=3 (unnecessary idle); J3 finishes at t=15 > deadline=13.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 7 — precedence_violation (fan-in: two predecessors) # | |
| # J3 depends on J1 and J2; J3 starts at t=2 but J2 finishes at t=4. # | |
| # Fix: delay J3 start to t=4 (max predecessor finish time). # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P08", | |
| "jobs": [ | |
| {"id": "J1", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J3", "duration": 2, "deadline": 20, "dependencies": ["J1", "J2"], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M3", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M3", "start_time": 2}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "precedence_violation", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M3", "start_time": 4}, | |
| ] | |
| }, | |
| "optimal_makespan": 6, | |
| "description": "J3 depends on J1 and J2; J3 starts at t=2 but J2 does not finish until t=4.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 8 — availability_conflict (maintenance window) # | |
| # M1 available only [0,10]. J1 starts at t=9, runs [9,12) → exceeds # | |
| # the window. Fix: schedule J1 before the window closes. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P09", | |
| "jobs": [ | |
| {"id": "J1", "duration": 3, "deadline": 24, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 2, "deadline": 24, "dependencies": [], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| { | |
| "id": "M1", | |
| "capacity": 1, | |
| "available_start": 0, | |
| "available_end": 10, | |
| "note": "M1 under maintenance t=[10,15]; use window [0,10] only.", | |
| }, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 9}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 0}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "availability_conflict", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 2}, | |
| ] | |
| }, | |
| "optimal_makespan": 5, | |
| "description": "J1 starts at t=9, extends into maintenance window [10,15] → availability_conflict.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 9 — capacity_exceeded (four jobs on capacity-3 machine) # | |
| # Concurrent load at t=0 is 4 > capacity=3. # | |
| # Fix: stagger the fourth job. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P10", | |
| "jobs": [ | |
| {"id": "J1", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J3", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J4", "duration": 2, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 3, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J4", "machine_id": "M1", "start_time": 0}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": False, | |
| "violation_type": "capacity_exceeded", | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J4", "machine_id": "M1", "start_time": 2}, | |
| ] | |
| }, | |
| "optimal_makespan": 4, | |
| "description": "4 jobs start simultaneously on M1 (capacity=3); concurrent load=4 > 3.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 10 — FEASIBLE: 3-job, 2-machine # | |
| # All constraints satisfied in the proposed schedule. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P11", | |
| "jobs": [ | |
| {"id": "J1", "duration": 4, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 3, "deadline": 20, "dependencies": [], "resource_req": 1}, | |
| {"id": "J3", "duration": 5, "deadline": 20, "dependencies": ["J1"], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M2", "start_time": 4}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": True, | |
| "violation_type": None, | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M2", "start_time": 4}, | |
| ] | |
| }, | |
| "optimal_makespan": 9, | |
| "description": "Fully feasible 3-job, 2-machine schedule — all constraints satisfied.", | |
| }, | |
| # ------------------------------------------------------------------ # | |
| # 11 — FEASIBLE: 5-job, 3-machine with fan-in precedence # | |
| # All constraints satisfied in the proposed schedule. # | |
| # ------------------------------------------------------------------ # | |
| { | |
| "instance": { | |
| "problem_id": "P12", | |
| "jobs": [ | |
| {"id": "J1", "duration": 3, "deadline": 30, "dependencies": [], "resource_req": 1}, | |
| {"id": "J2", "duration": 2, "deadline": 30, "dependencies": [], "resource_req": 1}, | |
| {"id": "J3", "duration": 4, "deadline": 30, "dependencies": [], "resource_req": 1}, | |
| {"id": "J4", "duration": 3, "deadline": 30, "dependencies": ["J1", "J2"], "resource_req": 1}, | |
| {"id": "J5", "duration": 2, "deadline": 30, "dependencies": ["J3"], "resource_req": 1}, | |
| ], | |
| "machines": [ | |
| {"id": "M1", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M2", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| {"id": "M3", "capacity": 1, "available_start": 0, "available_end": 24}, | |
| ], | |
| "proposed_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M3", "start_time": 0}, | |
| {"job_id": "J4", "machine_id": "M1", "start_time": 3}, | |
| {"job_id": "J5", "machine_id": "M3", "start_time": 4}, | |
| ] | |
| }, | |
| }, | |
| "is_feasible": True, | |
| "violation_type": None, | |
| "optimal_schedule": { | |
| "assignments": [ | |
| {"job_id": "J1", "machine_id": "M1", "start_time": 0}, | |
| {"job_id": "J2", "machine_id": "M2", "start_time": 0}, | |
| {"job_id": "J3", "machine_id": "M3", "start_time": 0}, | |
| {"job_id": "J4", "machine_id": "M1", "start_time": 3}, | |
| {"job_id": "J5", "machine_id": "M3", "start_time": 4}, | |
| ] | |
| }, | |
| "optimal_makespan": 6, | |
| "description": "Fully feasible 5-job, 3-machine schedule with fan-in precedence — all constraints satisfied.", | |
| }, | |
| ] | |
| # --------------------------------------------------------------------------- | |
| # Task-specific instance pools (built once after INSTANCE_BANK is defined). | |
| # This ensures task-appropriate instances are shown per task: | |
| # feasibility_check → all 12 (mix of feasible and infeasible) | |
| # conflict_classification → 10 infeasible only (feasible has no violation) | |
| # schedule_repair → 10 infeasible with known optimal repairs | |
| # --------------------------------------------------------------------------- | |
| _TASK_POOLS: dict[str, list[dict[str, Any]]] = { | |
| "feasibility_check": INSTANCE_BANK, | |
| "conflict_classification": [e for e in INSTANCE_BANK if not e["is_feasible"]], | |
| "schedule_repair": [ | |
| e for e in INSTANCE_BANK if not e["is_feasible"] and e.get("optimal_schedule") | |
| ], | |
| } | |
| class SchedulingOptEnv: | |
| """OpenEnv-compatible scheduling optimisation environment. | |
| Public API (OpenEnv contract) | |
| ----------------------------- | |
| reset(task_id: str) → Observation | |
| step(action: Action) → (Observation, float, bool, dict) | |
| state() → dict | |
| """ | |
| def __init__(self) -> None: | |
| self._task_id: str = "feasibility_check" | |
| self._step: int = 0 | |
| self._max_steps: int = 3 | |
| # Per-task episode counters for round-robin cycling within each pool | |
| self._task_counters: dict[str, int] = {} | |
| # The instance used in the current episode (set by reset) | |
| self._current_instance: dict[str, Any] = {} | |
| self._done: bool = True | |
| self._history: list[dict[str, Any]] = [] | |
| self._cumulative_reward: float = 0.0 | |
| # ------------------------------------------------------------------ | |
| # Public API | |
| # ------------------------------------------------------------------ | |
| def reset(self, task_id: str = "feasibility_check") -> Observation: | |
| """Start a new episode. | |
| Selects the next instance from the task-appropriate pool in round-robin | |
| order so that repeated resets present diverse scheduling problems. | |
| Always succeeds — never raises an exception. | |
| """ | |
| self._task_id = task_id | |
| self._step = 0 | |
| self._done = False | |
| self._history = [] | |
| self._cumulative_reward = 0.0 | |
| step_limits: dict[str, int] = { | |
| "feasibility_check": 3, | |
| "conflict_classification": 5, | |
| "schedule_repair": 8, | |
| } | |
| self._max_steps = step_limits.get(task_id, 3) | |
| # Task-aware round-robin instance selection | |
| pool = _TASK_POOLS.get(task_id, INSTANCE_BANK) | |
| idx = self._task_counters.get(task_id, 0) % len(pool) | |
| self._current_instance = pool[idx] | |
| self._task_counters[task_id] = idx + 1 | |
| return Observation( | |
| schedule_instance=json.dumps(self._current_instance["instance"], indent=2), | |
| task_id=task_id, | |
| context=self._build_context(task_id, step=0, last_reward=None), | |
| step_number=0, | |
| ) | |
| def step(self, action: Action) -> tuple[Observation, float, bool, dict[str, Any]]: | |
| """Process one agent action. | |
| Returns (observation, reward, done, info). | |
| Reward is always clamped to [0.0, 1.0]. | |
| """ | |
| if self._done: | |
| return ( | |
| Observation( | |
| schedule_instance="{}", | |
| task_id=self._task_id, | |
| context="Episode is over. Call /reset to start a new episode.", | |
| step_number=self._step, | |
| ), | |
| 0.0, | |
| True, | |
| {"error": "episode_already_done"}, | |
| ) | |
| self._step += 1 | |
| grader = _GRADERS.get(self._task_id, _GRADERS["feasibility_check"]) | |
| reward: float = grader.grade(action, self._current_instance) | |
| reward = max(0.0, min(1.0, float(reward))) # hard clamp — invariant | |
| self._cumulative_reward += reward | |
| # Capture grading breakdown for rich info dict | |
| breakdown: dict[str, Any] = getattr(grader, "last_breakdown", {}) | |
| # Record step history (truncate long responses for storage efficiency) | |
| self._history.append({ | |
| "step": self._step, | |
| "action": action.response[:300], | |
| "reward": round(reward, 4), | |
| }) | |
| # Termination: max steps exhausted or near-perfect reward (≥0.95) | |
| done = self._step >= self._max_steps or reward >= 0.95 | |
| self._done = done | |
| # Build next observation | |
| if done: | |
| best = max(h["reward"] for h in self._history) | |
| ctx = ( | |
| "Episode complete — constraint satisfied." | |
| if reward >= 0.95 | |
| else f"Max steps reached. Best reward this episode: {best:.2f}." | |
| ) | |
| obs = Observation( | |
| schedule_instance="{}", | |
| task_id=self._task_id, | |
| context=ctx, | |
| step_number=self._step, | |
| ) | |
| else: | |
| obs = Observation( | |
| schedule_instance=json.dumps( | |
| self._current_instance["instance"], indent=2 | |
| ), | |
| task_id=self._task_id, | |
| context=self._build_context( | |
| self._task_id, step=self._step, last_reward=reward | |
| ), | |
| step_number=self._step, | |
| ) | |
| info: dict[str, Any] = { | |
| "step_reward": round(reward, 4), | |
| "cumulative_reward": round(self._cumulative_reward, 4), | |
| "steps_remaining": max(0, self._max_steps - self._step), | |
| "instance_description": self._current_instance.get("description", ""), | |
| "grading_breakdown": breakdown, | |
| } | |
| return obs, round(reward, 4), done, info | |
| def state(self) -> dict[str, Any]: | |
| """Return a snapshot of the full internal environment state.""" | |
| return { | |
| "task_id": self._task_id, | |
| "step": self._step, | |
| "max_steps": self._max_steps, | |
| "done": self._done, | |
| "cumulative_reward": round(self._cumulative_reward, 4), | |
| "history": copy.deepcopy(self._history), | |
| "current_instance_id": ( | |
| self._current_instance.get("instance", {}).get("problem_id", "") | |
| ), | |
| "current_instance_feasible": self._current_instance.get("is_feasible"), | |
| "task_counters": dict(self._task_counters), | |
| "instance_pool_sizes": {k: len(v) for k, v in _TASK_POOLS.items()}, | |
| } | |
| # ------------------------------------------------------------------ | |
| # Internal helpers | |
| # ------------------------------------------------------------------ | |
| def _build_context( | |
| task_id: str, step: int, last_reward: float | None | |
| ) -> str: | |
| """Build a context string that adapts to the current step and last reward. | |
| On the first step (step=0) a clear task description is returned. | |
| On retry steps (step>0, last_reward<0.95) an informative hint is appended | |
| to guide the agent toward a better answer. | |
| """ | |
| base_contexts: dict[str, str] = { | |
| "feasibility_check": ( | |
| "Examine the proposed_schedule against all four constraint categories " | |
| "(machine capacity, job deadlines, precedence dependencies, machine " | |
| "availability windows). Reply with exactly 'feasible' if every constraint " | |
| "is satisfied, or 'infeasible' if any constraint is violated." | |
| ), | |
| "conflict_classification": ( | |
| "The proposed_schedule is infeasible. Identify the PRIMARY constraint " | |
| "violation and reply with exactly one of: resource_overload, " | |
| "deadline_violation, precedence_violation, availability_conflict, " | |
| "capacity_exceeded." | |
| ), | |
| "schedule_repair": ( | |
| "The proposed_schedule is infeasible. Return ONLY a JSON object with key " | |
| '"assignments": a list of {"job_id": str, "machine_id": str, ' | |
| '"start_time": int} dicts that resolves ALL violations (capacity, ' | |
| "deadlines, precedence, availability) and minimises total makespan." | |
| ), | |
| } | |
| ctx = base_contexts.get(task_id, "Analyse the scheduling instance.") | |
| # Add retry hint when the agent is wrong but still has steps remaining | |
| if step > 0 and last_reward is not None and last_reward < 0.95: | |
| hints: dict[str, str] = { | |
| "feasibility_check": ( | |
| " ← Previous answer was incorrect. " | |
| "Re-examine all four constraint types carefully." | |
| ), | |
| "conflict_classification": ( | |
| " ← Previous classification was wrong. " | |
| "Check whether jobs share a machine simultaneously (resource/capacity), " | |
| "miss their deadlines, violate ordering, or run outside availability windows." | |
| ), | |
| "schedule_repair": ( | |
| " ← Previous repair had remaining violations. " | |
| "Ensure no two jobs overlap on a capacity-1 machine, every job " | |
| "finishes before its deadline, precedence order is respected, and " | |
| "all jobs run within machine availability windows." | |
| ), | |
| } | |
| ctx += hints.get(task_id, "") | |
| return ctx | |
| def get_instance_bank() -> list[dict[str, Any]]: | |
| """Return the full instance bank (all 12 entries).""" | |
| return INSTANCE_BANK | |