Spaces:
Sleeping
Sleeping
there was a bug...
Browse files- .gitignore +1 -2
- README.md +15 -1
- pyproject.toml +10 -6
- run.py +3 -3
- src/jssp_openenv/client.py +2 -4
- src/jssp_openenv/models.py +23 -9
- src/jssp_openenv/policy.py +57 -80
- src/jssp_openenv/server/jssp_environment.py +24 -28
- src/jssp_openenv/solver.py +5 -5
.gitignore
CHANGED
|
@@ -215,5 +215,4 @@ __marimo__/
|
|
| 215 |
# Streamlit
|
| 216 |
.streamlit/secrets.toml
|
| 217 |
|
| 218 |
-
|
| 219 |
-
!charts/gantt_fifo_policy.png
|
|
|
|
| 215 |
# Streamlit
|
| 216 |
.streamlit/secrets.toml
|
| 217 |
|
| 218 |
+
output/
|
|
|
README.md
CHANGED
|
@@ -53,7 +53,7 @@ The project follows a client-server architecture using the OpenEnv framework:
|
|
| 53 |
|
| 54 |
**Models** (`src/jssp_openenv/models.py`):
|
| 55 |
- `JSSPAction`: Represents scheduling actions (list of job IDs to schedule)
|
| 56 |
-
- `JSSPObservation`: Contains the current state (
|
| 57 |
|
| 58 |
**Environment** (`src/jssp_openenv/server/jssp_environment.py`):
|
| 59 |
- `JSSPEnvironment`: The core simulation environment that:
|
|
@@ -144,6 +144,20 @@ Here is an example:
|
|
| 144 |
|
| 145 |

|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
## Run with docker
|
| 148 |
|
| 149 |
Build the Docker image:
|
|
|
|
| 53 |
|
| 54 |
**Models** (`src/jssp_openenv/models.py`):
|
| 55 |
- `JSSPAction`: Represents scheduling actions (list of job IDs to schedule)
|
| 56 |
+
- `JSSPObservation`: Contains the current state (machine status, job status, remaining operations)
|
| 57 |
|
| 58 |
**Environment** (`src/jssp_openenv/server/jssp_environment.py`):
|
| 59 |
- `JSSPEnvironment`: The core simulation environment that:
|
|
|
|
| 144 |
|
| 145 |

|
| 146 |
|
| 147 |
+
## Current Results
|
| 148 |
+
|
| 149 |
+
Results as of Nov. 7, 2024 on FT06 problem instance. *Note: Non-scientific results, only ran 1 episode per policy.*
|
| 150 |
+
|
| 151 |
+
| Policy | Makespan |
|
| 152 |
+
|--------|----------|
|
| 153 |
+
| **Optimal solution** | **55** |
|
| 154 |
+
| `openai/gpt-oss-20b:groq` | 61 |
|
| 155 |
+
| FIFO | 68 |
|
| 156 |
+
| `openai/gpt-oss-120b:cerebras` | 69 |
|
| 157 |
+
| `Qwen/Qwen3-32B:groq` | 69 |
|
| 158 |
+
| Max-Min | 77 |
|
| 159 |
+
|
| 160 |
+
|
| 161 |
## Run with docker
|
| 162 |
|
| 163 |
Build the Docker image:
|
pyproject.toml
CHANGED
|
@@ -26,11 +26,9 @@ dev = [
|
|
| 26 |
"ty",
|
| 27 |
]
|
| 28 |
|
| 29 |
-
[tool.
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
[tool.setuptools.packages.find]
|
| 33 |
-
where = ["src"]
|
| 34 |
|
| 35 |
[tool.ruff]
|
| 36 |
exclude = [".git", ".ruff_cache", ".venv"]
|
|
@@ -38,4 +36,10 @@ line-length = 119
|
|
| 38 |
# Ignored rules:
|
| 39 |
# "E501" -> line length violation
|
| 40 |
lint.ignore = ["E501"]
|
| 41 |
-
lint.select = ["E", "F", "I", "W"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
"ty",
|
| 27 |
]
|
| 28 |
|
| 29 |
+
[tool.mypy]
|
| 30 |
+
disable_error_code = ["import-untyped"]
|
| 31 |
+
ignore_missing_imports = true
|
|
|
|
|
|
|
| 32 |
|
| 33 |
[tool.ruff]
|
| 34 |
exclude = [".git", ".ruff_cache", ".venv"]
|
|
|
|
| 36 |
# Ignored rules:
|
| 37 |
# "E501" -> line length violation
|
| 38 |
lint.ignore = ["E501"]
|
| 39 |
+
lint.select = ["E", "F", "I", "W"]
|
| 40 |
+
|
| 41 |
+
[tool.setuptools]
|
| 42 |
+
package-dir = {"" = "src"}
|
| 43 |
+
|
| 44 |
+
[tool.setuptools.packages.find]
|
| 45 |
+
where = ["src"]
|
run.py
CHANGED
|
@@ -11,8 +11,8 @@ from jssp_openenv.solver import solve_jssp
|
|
| 11 |
|
| 12 |
SERVER_URL = "http://localhost:8000"
|
| 13 |
MAX_STEPS = 1000 # Maximum number of steps per instance
|
| 14 |
-
|
| 15 |
-
os.makedirs(
|
| 16 |
|
| 17 |
cli = typer.Typer()
|
| 18 |
|
|
@@ -68,7 +68,7 @@ def solve(
|
|
| 68 |
|
| 69 |
print(f"Solved in {makespan} steps")
|
| 70 |
|
| 71 |
-
filepath = os.path.join(
|
| 72 |
gantt_chart(scheduled_events, title=title, makespan=makespan, save_to=filepath)
|
| 73 |
print(f"Saved Gantt chart to {filepath}")
|
| 74 |
|
|
|
|
| 11 |
|
| 12 |
SERVER_URL = "http://localhost:8000"
|
| 13 |
MAX_STEPS = 1000 # Maximum number of steps per instance
|
| 14 |
+
OUTPUT_DIR = "output"
|
| 15 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 16 |
|
| 17 |
cli = typer.Typer()
|
| 18 |
|
|
|
|
| 68 |
|
| 69 |
print(f"Solved in {makespan} steps")
|
| 70 |
|
| 71 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
| 72 |
gantt_chart(scheduled_events, title=title, makespan=makespan, save_to=filepath)
|
| 73 |
print(f"Saved Gantt chart to {filepath}")
|
| 74 |
|
src/jssp_openenv/client.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from openenv_core import HTTPEnvClient, StepResult
|
| 2 |
|
| 3 |
-
from .models import JSSPAction, JSSPObservation, MachineObservation
|
| 4 |
|
| 5 |
|
| 6 |
class JSSPEnvClient(HTTPEnvClient[JSSPAction, JSSPObservation]):
|
|
@@ -12,9 +12,7 @@ class JSSPEnvClient(HTTPEnvClient[JSSPAction, JSSPObservation]):
|
|
| 12 |
return StepResult[JSSPObservation](
|
| 13 |
observation=JSSPObservation(
|
| 14 |
machines=[MachineObservation(**machine) for machine in obs_data.pop("machines")],
|
| 15 |
-
|
| 16 |
-
ReadyOperationObservation(**operation) for operation in obs_data.pop("ready_operations")
|
| 17 |
-
],
|
| 18 |
**obs_data,
|
| 19 |
),
|
| 20 |
reward=payload.get("reward"),
|
|
|
|
| 1 |
from openenv_core import HTTPEnvClient, StepResult
|
| 2 |
|
| 3 |
+
from .models import JobObservation, JSSPAction, JSSPObservation, MachineObservation
|
| 4 |
|
| 5 |
|
| 6 |
class JSSPEnvClient(HTTPEnvClient[JSSPAction, JSSPObservation]):
|
|
|
|
| 12 |
return StepResult[JSSPObservation](
|
| 13 |
observation=JSSPObservation(
|
| 14 |
machines=[MachineObservation(**machine) for machine in obs_data.pop("machines")],
|
| 15 |
+
jobs=[JobObservation(**job) for job in obs_data.pop("jobs")],
|
|
|
|
|
|
|
| 16 |
**obs_data,
|
| 17 |
),
|
| 18 |
reward=payload.get("reward"),
|
src/jssp_openenv/models.py
CHANGED
|
@@ -32,11 +32,12 @@ class MachineObservation:
|
|
| 32 |
|
| 33 |
|
| 34 |
@dataclass
|
| 35 |
-
class
|
|
|
|
|
|
|
| 36 |
job_id: int
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
remaining_ops: int
|
| 40 |
|
| 41 |
|
| 42 |
@dataclass(kw_only=True)
|
|
@@ -47,17 +48,30 @@ class JSSPObservation(Observation):
|
|
| 47 |
|
| 48 |
step_count: int
|
| 49 |
machines: list[MachineObservation]
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
def parse_job_ids(job_ids: str) -> list[int]:
|
| 56 |
"""Parse job_ids from string (error out if cannot be parsed)."""
|
| 57 |
try:
|
| 58 |
return [int(job_id) for job_id in job_ids.split(",") if job_id.strip()]
|
| 59 |
-
except ValueError:
|
| 60 |
-
raise ValueError(f"Invalid job_ids: {job_ids}")
|
| 61 |
|
| 62 |
|
| 63 |
@dataclass
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
@dataclass
|
| 35 |
+
class JobObservation:
|
| 36 |
+
"""Observation of a given Job in the JSSP environment."""
|
| 37 |
+
|
| 38 |
job_id: int
|
| 39 |
+
operations: JobT # remaining operations to be scheduled
|
| 40 |
+
busy_until: Optional[int] # time until the current operation is complete (or none if available)
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
@dataclass(kw_only=True)
|
|
|
|
| 48 |
|
| 49 |
step_count: int
|
| 50 |
machines: list[MachineObservation]
|
| 51 |
+
jobs: list[JobObservation]
|
| 52 |
+
|
| 53 |
+
def available_machines(self) -> list[MachineObservation]:
|
| 54 |
+
"""Get available machines from observation."""
|
| 55 |
+
return [m for m in self.machines if m.busy_until is None or m.busy_until <= self.step_count]
|
| 56 |
+
|
| 57 |
+
def available_jobs(self) -> list[JobObservation]:
|
| 58 |
+
"""Get available jobs from observation."""
|
| 59 |
+
available_machine_ids = [m.machine_id for m in self.available_machines()]
|
| 60 |
+
return [
|
| 61 |
+
job
|
| 62 |
+
for job in self.jobs
|
| 63 |
+
if (job.busy_until is None or job.busy_until <= self.step_count)
|
| 64 |
+
and len(job.operations) > 0
|
| 65 |
+
and job.operations[0][0] in available_machine_ids
|
| 66 |
+
]
|
| 67 |
|
| 68 |
|
| 69 |
def parse_job_ids(job_ids: str) -> list[int]:
|
| 70 |
"""Parse job_ids from string (error out if cannot be parsed)."""
|
| 71 |
try:
|
| 72 |
return [int(job_id) for job_id in job_ids.split(",") if job_id.strip()]
|
| 73 |
+
except ValueError as e:
|
| 74 |
+
raise ValueError(f"Invalid job_ids: {job_ids}") from e
|
| 75 |
|
| 76 |
|
| 77 |
@dataclass
|
src/jssp_openenv/policy.py
CHANGED
|
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
|
|
| 3 |
|
| 4 |
from openai import OpenAI
|
| 5 |
|
| 6 |
-
from .models import JSSPAction, JSSPObservation, MachineObservation
|
| 7 |
|
| 8 |
|
| 9 |
class JSSPEnvPolicy(ABC):
|
|
@@ -15,30 +15,24 @@ class JSSPEnvPolicy(ABC):
|
|
| 15 |
class JSSPFifoPolicy(JSSPEnvPolicy):
|
| 16 |
def act(self, observation: JSSPObservation) -> JSSPAction:
|
| 17 |
"""
|
| 18 |
-
FIFO scheduling: schedule
|
| 19 |
|
| 20 |
-
This policy schedules
|
| 21 |
-
|
| 22 |
-
currently available (not busy).
|
| 23 |
"""
|
| 24 |
-
#
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
# Filter to only ready operations with available machines
|
| 28 |
-
available_ops = [op for op in observation.ready_operations if machine_available.get(op.machine_id, False)]
|
| 29 |
-
|
| 30 |
-
# Sort by job_id (FIFO: first job_id first)
|
| 31 |
-
available_ops.sort(key=lambda op: op.job_id)
|
| 32 |
|
| 33 |
# Track which machines we've already scheduled to avoid conflicts
|
| 34 |
scheduled_machines = set()
|
| 35 |
scheduled_job_ids = []
|
| 36 |
|
| 37 |
# Schedule jobs in FIFO order, but skip if machine is already taken
|
| 38 |
-
for
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
| 42 |
|
| 43 |
return JSSPAction(job_ids=scheduled_job_ids)
|
| 44 |
|
|
@@ -48,18 +42,19 @@ class JSSPMaxMinPolicy(JSSPEnvPolicy):
|
|
| 48 |
"""
|
| 49 |
Max-Min scheduling: schedule the operation with the longest duration first.
|
| 50 |
"""
|
| 51 |
-
# Sort
|
| 52 |
-
|
| 53 |
|
| 54 |
# Track which machines we've already scheduled to avoid conflicts
|
| 55 |
scheduled_machines = set()
|
| 56 |
scheduled_job_ids = []
|
| 57 |
|
| 58 |
# Schedule jobs in max-min order, but skip if machine is already taken
|
| 59 |
-
for
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
| 63 |
|
| 64 |
return JSSPAction(job_ids=scheduled_job_ids)
|
| 65 |
|
|
@@ -69,46 +64,33 @@ You are solving a Job Shop Scheduling Problem (JSSP). Your goal is to minimize t
|
|
| 69 |
|
| 70 |
You must optimize for minimal makespan while respecting all constraints. Each job consists of multiple operations that must be completed in sequence, and each operation requires a specific machine for a given duration.
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
### 🕒 Current State
|
| 75 |
-
**Step:** {step_count} | **Completed:** {completed_jobs}/{total_jobs}
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
### ⚙️ Machine Status
|
| 80 |
{machines_status}
|
| 81 |
|
| 82 |
You must check machine availability before scheduling. Machines that are busy cannot start new operations until they finish their current task.
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
### ✅ Ready to Schedule (NOW)
|
| 87 |
-
{ready_operations_list}
|
| 88 |
-
|
| 89 |
-
Each entry shows: **machine**, **duration**, and **remaining ops**.
|
| 90 |
-
You can only schedule operations that are ready at this step. These are operations whose previous steps in the job sequence have been completed.
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
### 🎯 Rules You Must Follow
|
| 95 |
-
1. You must schedule only **ready** operations. Do not attempt to schedule operations that are not ready.
|
| 96 |
2. Each machine can run **one job at a time**. You cannot schedule multiple jobs on the same machine simultaneously.
|
| 97 |
3. You must not schedule jobs on **busy** machines (`busy_until > current step`). Check machine availability before scheduling.
|
| 98 |
4. You may **schedule multiple** jobs on different machines in the same step, or you may choose to wait if no good scheduling opportunity exists.
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
### 🧩 Available Actions
|
| 103 |
-
{legal_actions}
|
| 104 |
|
| 105 |
-
These are the valid job IDs you can schedule at this step. You must choose from this list.
|
| 106 |
|
| 107 |
-
**
|
| 108 |
- To schedule jobs: `"0,2"` or `"1"` (comma-separated job IDs)
|
| 109 |
- To wait: `""` (empty string)
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
"""
|
| 113 |
|
| 114 |
|
|
@@ -132,35 +114,27 @@ class JSSPLLMPolicy(JSSPEnvPolicy):
|
|
| 132 |
"""
|
| 133 |
LLM scheduling: use an LLM to schedule the operations.
|
| 134 |
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
a
|
|
|
|
|
|
|
| 138 |
"""
|
| 139 |
-
|
| 140 |
-
machine_available = {
|
| 141 |
-
m.machine_id: m.busy_until is None or m.busy_until <= observation.step_count for m in observation.machines
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
# Filter ready operations to only include those with available machines
|
| 145 |
-
legal_job_ids = [
|
| 146 |
-
op.job_id for op in observation.ready_operations if machine_available.get(op.machine_id, False)
|
| 147 |
-
]
|
| 148 |
|
| 149 |
# If no legal actions, return empty action (wait)
|
| 150 |
-
if not
|
| 151 |
return JSSPAction(job_ids=[])
|
| 152 |
|
| 153 |
# Format prompt
|
| 154 |
machines_status = self._format_machines_status(observation.machines, observation.step_count)
|
| 155 |
-
|
| 156 |
|
| 157 |
prompt = PROMPT_TEMPLATE.format(
|
| 158 |
step_count=observation.step_count,
|
| 159 |
-
completed_jobs=observation.completed_jobs,
|
| 160 |
-
total_jobs=observation.total_jobs,
|
| 161 |
machines_status=machines_status,
|
| 162 |
-
|
| 163 |
-
|
| 164 |
)
|
| 165 |
print(f"Step {observation.step_count}")
|
| 166 |
|
|
@@ -170,9 +144,7 @@ class JSSPLLMPolicy(JSSPEnvPolicy):
|
|
| 170 |
model=self.model_id, messages=[{"role": "user", "content": prompt}], temperature=0.0
|
| 171 |
)
|
| 172 |
llm_output = response.choices[0].message.content or ""
|
| 173 |
-
|
| 174 |
-
job_ids = self._parse_action(llm_output, legal_job_ids)
|
| 175 |
-
print(f"Job IDs: {job_ids}")
|
| 176 |
|
| 177 |
# Ensure we don't schedule multiple jobs on the same machine
|
| 178 |
# Track which machines we've already scheduled to avoid conflicts
|
|
@@ -180,10 +152,10 @@ class JSSPLLMPolicy(JSSPEnvPolicy):
|
|
| 180 |
filtered_job_ids = []
|
| 181 |
for job_id in job_ids:
|
| 182 |
# Find the operation for this job
|
| 183 |
-
op = next((op for op in
|
| 184 |
-
if op is not None and op.
|
| 185 |
filtered_job_ids.append(job_id)
|
| 186 |
-
scheduled_machines.add(op.
|
| 187 |
|
| 188 |
return JSSPAction(job_ids=filtered_job_ids)
|
| 189 |
|
|
@@ -207,18 +179,23 @@ class JSSPLLMPolicy(JSSPEnvPolicy):
|
|
| 207 |
return "\n".join(lines) if lines else " (No machines)"
|
| 208 |
|
| 209 |
@staticmethod
|
| 210 |
-
def
|
| 211 |
-
"""Format
|
| 212 |
lines = []
|
| 213 |
-
for
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
@staticmethod
|
| 220 |
-
def _parse_action(text: str,
|
| 221 |
"""Parse comma-separated job IDs from model output."""
|
|
|
|
|
|
|
| 222 |
# First, we remove the reasoning section
|
| 223 |
text = text.split("<think>")[-1].split("</think>")[-1].strip()
|
| 224 |
|
|
|
|
| 3 |
|
| 4 |
from openai import OpenAI
|
| 5 |
|
| 6 |
+
from .models import JobObservation, JSSPAction, JSSPObservation, MachineObservation
|
| 7 |
|
| 8 |
|
| 9 |
class JSSPEnvPolicy(ABC):
|
|
|
|
| 15 |
class JSSPFifoPolicy(JSSPEnvPolicy):
|
| 16 |
def act(self, observation: JSSPObservation) -> JSSPAction:
|
| 17 |
"""
|
| 18 |
+
FIFO scheduling: schedule available jobs in order of job_id.
|
| 19 |
|
| 20 |
+
This policy schedules jobs in FIFO order (by job_id), respecting machine availability.
|
| 21 |
+
It only schedules jobs for machines that are currently available (not busy).
|
|
|
|
| 22 |
"""
|
| 23 |
+
# Filter to only available jobs with available machines + sort by job_id
|
| 24 |
+
sorted_jobs = sorted(observation.available_jobs(), key=lambda job: job.job_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
# Track which machines we've already scheduled to avoid conflicts
|
| 27 |
scheduled_machines = set()
|
| 28 |
scheduled_job_ids = []
|
| 29 |
|
| 30 |
# Schedule jobs in FIFO order, but skip if machine is already taken
|
| 31 |
+
for job in sorted_jobs:
|
| 32 |
+
machine_id = job.operations[0][0]
|
| 33 |
+
if machine_id not in scheduled_machines:
|
| 34 |
+
scheduled_job_ids.append(job.job_id)
|
| 35 |
+
scheduled_machines.add(machine_id)
|
| 36 |
|
| 37 |
return JSSPAction(job_ids=scheduled_job_ids)
|
| 38 |
|
|
|
|
| 42 |
"""
|
| 43 |
Max-Min scheduling: schedule the operation with the longest duration first.
|
| 44 |
"""
|
| 45 |
+
# Sort available jobs by duration (max-min)
|
| 46 |
+
sorted_jobs = sorted(observation.available_jobs(), key=lambda job: job.operations[0][1], reverse=True)
|
| 47 |
|
| 48 |
# Track which machines we've already scheduled to avoid conflicts
|
| 49 |
scheduled_machines = set()
|
| 50 |
scheduled_job_ids = []
|
| 51 |
|
| 52 |
# Schedule jobs in max-min order, but skip if machine is already taken
|
| 53 |
+
for job in sorted_jobs:
|
| 54 |
+
machine_id = job.operations[0][0]
|
| 55 |
+
if machine_id not in scheduled_machines:
|
| 56 |
+
scheduled_job_ids.append(job.job_id)
|
| 57 |
+
scheduled_machines.add(machine_id)
|
| 58 |
|
| 59 |
return JSSPAction(job_ids=scheduled_job_ids)
|
| 60 |
|
|
|
|
| 64 |
|
| 65 |
You must optimize for minimal makespan while respecting all constraints. Each job consists of multiple operations that must be completed in sequence, and each operation requires a specific machine for a given duration.
|
| 66 |
|
| 67 |
+
**Current step:** {step_count}
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
**Machine Status:**
|
|
|
|
|
|
|
| 70 |
{machines_status}
|
| 71 |
|
| 72 |
You must check machine availability before scheduling. Machines that are busy cannot start new operations until they finish their current task.
|
| 73 |
|
| 74 |
+
**Jobs:**
|
| 75 |
+
{jobs_list}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
**Rules You Must Follow:**
|
| 78 |
+
1. You must schedule only **available** jobs. Do not attempt to schedule jobs that are not available.
|
|
|
|
|
|
|
| 79 |
2. Each machine can run **one job at a time**. You cannot schedule multiple jobs on the same machine simultaneously.
|
| 80 |
3. You must not schedule jobs on **busy** machines (`busy_until > current step`). Check machine availability before scheduling.
|
| 81 |
4. You may **schedule multiple** jobs on different machines in the same step, or you may choose to wait if no good scheduling opportunity exists.
|
| 82 |
|
| 83 |
+
**Legal actions:**
|
| 84 |
+
{legal_job_ids}
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
These are the valid job IDs you can schedule at this step. You must choose a subset from this list or choose to wait.
|
| 87 |
|
| 88 |
+
**Action format:**
|
| 89 |
- To schedule jobs: `"0,2"` or `"1"` (comma-separated job IDs)
|
| 90 |
- To wait: `""` (empty string)
|
| 91 |
+
Select the best subset of jobs to schedule to minimize the total makespan once all jobs are completed.
|
| 92 |
|
| 93 |
+
Response only with the action format specified above, and nothing else.
|
| 94 |
"""
|
| 95 |
|
| 96 |
|
|
|
|
| 114 |
"""
|
| 115 |
LLM scheduling: use an LLM to schedule the operations.
|
| 116 |
|
| 117 |
+
Process:
|
| 118 |
+
- Determine legal job IDs (available jobs on available machines)
|
| 119 |
+
- Format a prompt
|
| 120 |
+
- Call the LLM
|
| 121 |
+
- Parse the response to return a scheduling action
|
| 122 |
"""
|
| 123 |
+
available_jobs = observation.available_jobs()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
# If no legal actions, return empty action (wait)
|
| 126 |
+
if not available_jobs:
|
| 127 |
return JSSPAction(job_ids=[])
|
| 128 |
|
| 129 |
# Format prompt
|
| 130 |
machines_status = self._format_machines_status(observation.machines, observation.step_count)
|
| 131 |
+
jobs_list = self._format_jobs(observation.jobs)
|
| 132 |
|
| 133 |
prompt = PROMPT_TEMPLATE.format(
|
| 134 |
step_count=observation.step_count,
|
|
|
|
|
|
|
| 135 |
machines_status=machines_status,
|
| 136 |
+
jobs_list=jobs_list,
|
| 137 |
+
legal_job_ids=[job.job_id for job in available_jobs],
|
| 138 |
)
|
| 139 |
print(f"Step {observation.step_count}")
|
| 140 |
|
|
|
|
| 144 |
model=self.model_id, messages=[{"role": "user", "content": prompt}], temperature=0.0
|
| 145 |
)
|
| 146 |
llm_output = response.choices[0].message.content or ""
|
| 147 |
+
job_ids = self._parse_action(llm_output, available_jobs)
|
|
|
|
|
|
|
| 148 |
|
| 149 |
# Ensure we don't schedule multiple jobs on the same machine
|
| 150 |
# Track which machines we've already scheduled to avoid conflicts
|
|
|
|
| 152 |
filtered_job_ids = []
|
| 153 |
for job_id in job_ids:
|
| 154 |
# Find the operation for this job
|
| 155 |
+
op = next((op for op in available_jobs if op.job_id == job_id), None)
|
| 156 |
+
if op is not None and op.operations[0][0] not in scheduled_machines:
|
| 157 |
filtered_job_ids.append(job_id)
|
| 158 |
+
scheduled_machines.add(op.operations[0][0])
|
| 159 |
|
| 160 |
return JSSPAction(job_ids=filtered_job_ids)
|
| 161 |
|
|
|
|
| 179 |
return "\n".join(lines) if lines else " (No machines)"
|
| 180 |
|
| 181 |
@staticmethod
|
| 182 |
+
def _format_jobs(jobs: list[JobObservation]) -> str:
|
| 183 |
+
"""Format jobs for prompt."""
|
| 184 |
lines = []
|
| 185 |
+
for job in jobs:
|
| 186 |
+
available = job.busy_until is None
|
| 187 |
+
operations = ", ".join(f"(Machine {op[0]}, {op[1]} min)" for op in job.operations)
|
| 188 |
+
if available:
|
| 189 |
+
lines.append(f" Job {job.job_id}: Available. Remaining operations: {operations}")
|
| 190 |
+
else:
|
| 191 |
+
lines.append(f" Job {job.job_id}: Busy until t={job.busy_until}. Remaining operations: {operations}")
|
| 192 |
+
return "\n".join(lines) if lines else " (No jobs)"
|
| 193 |
|
| 194 |
@staticmethod
|
| 195 |
+
def _parse_action(text: str, available_jobs: list[JobObservation]) -> list[int]:
|
| 196 |
"""Parse comma-separated job IDs from model output."""
|
| 197 |
+
legal_job_ids = [job.job_id for job in available_jobs]
|
| 198 |
+
|
| 199 |
# First, we remove the reasoning section
|
| 200 |
text = text.split("<think>")[-1].split("</think>")[-1].strip()
|
| 201 |
|
src/jssp_openenv/server/jssp_environment.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Optional
|
|
| 5 |
import simpy
|
| 6 |
from openenv_core.env_server import Environment
|
| 7 |
|
| 8 |
-
from ..models import JobT, JSSPAction, JSSPObservation, MachineObservation
|
| 9 |
|
| 10 |
# Example of JSSP initial jobs
|
| 11 |
# Each tuple is a (machine_index, processing_time)
|
|
@@ -50,35 +50,33 @@ class JSSPEnvironment(Environment):
|
|
| 50 |
|
| 51 |
return self.state
|
| 52 |
|
| 53 |
-
def
|
| 54 |
-
"""Get all
|
| 55 |
-
|
| 56 |
for job_id in range(len(self.jobs)):
|
| 57 |
-
#
|
| 58 |
-
|
| 59 |
-
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
-
busy_until
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
duration=duration,
|
| 73 |
-
remaining_ops=remaining,
|
| 74 |
-
)
|
| 75 |
-
)
|
| 76 |
-
|
| 77 |
-
return ready
|
| 78 |
|
| 79 |
def _at_decision_step(self) -> bool:
|
| 80 |
"""Check if we're at a decision step (at least one job can be scheduled)."""
|
| 81 |
-
return len(self.
|
| 82 |
|
| 83 |
def _validate_action(self, action: JSSPAction) -> bool:
|
| 84 |
"""Validate that an action is legal."""
|
|
@@ -204,15 +202,13 @@ class JSSPEnvironment(Environment):
|
|
| 204 |
for i in range(self.nb_machines)
|
| 205 |
]
|
| 206 |
|
| 207 |
-
|
| 208 |
|
| 209 |
return JSSPObservation(
|
| 210 |
done=self.completed_jobs >= len(self.jobs),
|
| 211 |
episode_id=self.episode_id,
|
| 212 |
step_count=self.step_count,
|
| 213 |
machines=machines,
|
| 214 |
-
|
| 215 |
-
completed_jobs=self.completed_jobs,
|
| 216 |
-
total_jobs=len(self.jobs),
|
| 217 |
reward=0.0, # Default, overwritten in step()
|
| 218 |
)
|
|
|
|
| 5 |
import simpy
|
| 6 |
from openenv_core.env_server import Environment
|
| 7 |
|
| 8 |
+
from ..models import JobObservation, JobT, JSSPAction, JSSPObservation, MachineObservation
|
| 9 |
|
| 10 |
# Example of JSSP initial jobs
|
| 11 |
# Each tuple is a (machine_index, processing_time)
|
|
|
|
| 50 |
|
| 51 |
return self.state
|
| 52 |
|
| 53 |
+
def _get_jobs(self) -> list[JobObservation]:
|
| 54 |
+
"""Get all jobs with their status and remaining operations."""
|
| 55 |
+
jobs: list[JobObservation] = []
|
| 56 |
for job_id in range(len(self.jobs)):
|
| 57 |
+
# @dataclass
|
| 58 |
+
# class JobObservation:
|
| 59 |
+
# """Observation of a given Job in the JSSP environment."""
|
| 60 |
|
| 61 |
+
# job_id: int
|
| 62 |
+
# operations: JobT # remaining operations to be scheduled (not counting the current one)
|
| 63 |
+
# busy_until: Optional[int] # time until the current operation is complete (or none if available)
|
| 64 |
+
job_operations = self.jobs[job_id]
|
| 65 |
+
job_progress = self.job_progress[job_id]
|
| 66 |
+
job_remaining_operations = job_operations[job_progress:]
|
| 67 |
|
| 68 |
+
job_busy_until = None
|
| 69 |
+
for current_job, busy_until in zip(self.machine_current_job, self.machine_busy_until):
|
| 70 |
+
if current_job is not None and current_job == job_id:
|
| 71 |
+
job_busy_until = busy_until
|
| 72 |
+
|
| 73 |
+
jobs.append(JobObservation(job_id=job_id, operations=job_remaining_operations, busy_until=job_busy_until))
|
| 74 |
+
|
| 75 |
+
return jobs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
def _at_decision_step(self) -> bool:
|
| 78 |
"""Check if we're at a decision step (at least one job can be scheduled)."""
|
| 79 |
+
return len(self.state.available_jobs()) > 0
|
| 80 |
|
| 81 |
def _validate_action(self, action: JSSPAction) -> bool:
|
| 82 |
"""Validate that an action is legal."""
|
|
|
|
| 202 |
for i in range(self.nb_machines)
|
| 203 |
]
|
| 204 |
|
| 205 |
+
jobs = self._get_jobs()
|
| 206 |
|
| 207 |
return JSSPObservation(
|
| 208 |
done=self.completed_jobs >= len(self.jobs),
|
| 209 |
episode_id=self.episode_id,
|
| 210 |
step_count=self.step_count,
|
| 211 |
machines=machines,
|
| 212 |
+
jobs=jobs,
|
|
|
|
|
|
|
| 213 |
reward=0.0, # Default, overwritten in step()
|
| 214 |
)
|
src/jssp_openenv/solver.py
CHANGED
|
@@ -13,7 +13,7 @@ def solve_jssp(
|
|
| 13 |
|
| 14 |
while not result.done:
|
| 15 |
if verbose:
|
| 16 |
-
print(f"Step {obs.step_count}: {obs.
|
| 17 |
action = policy.act(obs)
|
| 18 |
if verbose:
|
| 19 |
print(f"Action: {action}")
|
|
@@ -21,13 +21,13 @@ def solve_jssp(
|
|
| 21 |
# Record scheduled events
|
| 22 |
if action.job_ids:
|
| 23 |
for job_id in action.job_ids:
|
| 24 |
-
|
| 25 |
-
assert
|
| 26 |
event = ScheduledEvent(
|
| 27 |
job_id=job_id,
|
| 28 |
-
machine_id=
|
| 29 |
start_time=obs.step_count,
|
| 30 |
-
end_time=obs.step_count +
|
| 31 |
)
|
| 32 |
scheduled_events.append(event)
|
| 33 |
|
|
|
|
| 13 |
|
| 14 |
while not result.done:
|
| 15 |
if verbose:
|
| 16 |
+
print(f"Step {obs.step_count}: {', '.join([str(job.job_id) for job in obs.available_jobs()])}")
|
| 17 |
action = policy.act(obs)
|
| 18 |
if verbose:
|
| 19 |
print(f"Action: {action}")
|
|
|
|
| 21 |
# Record scheduled events
|
| 22 |
if action.job_ids:
|
| 23 |
for job_id in action.job_ids:
|
| 24 |
+
job = next((job for job in obs.available_jobs() if job.job_id == job_id), None)
|
| 25 |
+
assert job is not None
|
| 26 |
event = ScheduledEvent(
|
| 27 |
job_id=job_id,
|
| 28 |
+
machine_id=job.operations[0][0],
|
| 29 |
start_time=obs.step_count,
|
| 30 |
+
end_time=obs.step_count + job.operations[0][1],
|
| 31 |
)
|
| 32 |
scheduled_events.append(event)
|
| 33 |
|