Wauplin HF Staff commited on
Commit
6e3f176
·
verified ·
1 Parent(s): b1556f3

there was a bug...

Browse files
.gitignore CHANGED
@@ -215,5 +215,4 @@ __marimo__/
215
  # Streamlit
216
  .streamlit/secrets.toml
217
 
218
- charts/
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 (machines, ready operations, progress)
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
  ![FIFO Policy Gantt Chart](assets/gantt_fifo_policy.png)
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
  ![FIFO Policy Gantt Chart](assets/gantt_fifo_policy.png)
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.setuptools]
30
- package-dir = {"" = "src"}
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
- CHART_DIR = "charts"
15
- os.makedirs(CHART_DIR, exist_ok=True)
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(CHART_DIR, filename)
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, ReadyOperationObservation
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
- ready_operations=[
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 ReadyOperationObservation:
 
 
36
  job_id: int
37
- machine_id: int
38
- duration: int
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
- ready_operations: list[ReadyOperationObservation]
51
- completed_jobs: int
52
- total_jobs: int
 
 
 
 
 
 
 
 
 
 
 
 
 
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, ReadyOperationObservation
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 ready operations in order of job_id.
19
 
20
- This policy schedules operations in FIFO order (by job_id), respecting
21
- machine availability. It only schedules operations for machines that are
22
- currently available (not busy).
23
  """
24
- # Create a lookup for machine availability
25
- machine_available = {m.machine_id: m.busy_until is None for m in observation.machines}
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 op in available_ops:
39
- if op.machine_id not in scheduled_machines:
40
- scheduled_job_ids.append(op.job_id)
41
- scheduled_machines.add(op.machine_id)
 
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 operations by duration (max-min)
52
- ops = sorted(observation.ready_operations, key=lambda op: op.duration, reverse=True)
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 op in ops:
60
- if op.machine_id not in scheduled_machines:
61
- scheduled_job_ids.append(op.job_id)
62
- scheduled_machines.add(op.machine_id)
 
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
- **Answer format:**
108
  - To schedule jobs: `"0,2"` or `"1"` (comma-separated job IDs)
109
  - To wait: `""` (empty string)
 
110
 
111
- Respond only with the action format specified above.
112
  """
113
 
114
 
@@ -132,35 +114,27 @@ class JSSPLLMPolicy(JSSPEnvPolicy):
132
  """
133
  LLM scheduling: use an LLM to schedule the operations.
134
 
135
- Determines legal actions (ready operations with available machines),
136
- formats a prompt, calls the LLM, and parses the response to return
137
- a scheduling action.
 
 
138
  """
139
- # Determine machine availability
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 legal_job_ids:
151
  return JSSPAction(job_ids=[])
152
 
153
  # Format prompt
154
  machines_status = self._format_machines_status(observation.machines, observation.step_count)
155
- ready_operations_list = self._format_ready_operations(observation.ready_operations)
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
- ready_operations_list=ready_operations_list,
163
- legal_actions=legal_job_ids,
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
- print(f"LLM Output: {llm_output}")
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 observation.ready_operations if op.job_id == job_id), None)
184
- if op is not None and op.machine_id not in scheduled_machines:
185
  filtered_job_ids.append(job_id)
186
- scheduled_machines.add(op.machine_id)
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 _format_ready_operations(ready_operations: list[ReadyOperationObservation]) -> str:
211
- """Format ready operations for prompt."""
212
  lines = []
213
- for op in ready_operations:
214
- lines.append(
215
- f" Job {op.job_id}: Machine {op.machine_id}, Duration {op.duration} min, {op.remaining_ops} ops remaining"
216
- )
217
- return "\n".join(lines) if lines else " (No ready operations)"
 
 
 
218
 
219
  @staticmethod
220
- def _parse_action(text: str, legal_job_ids: list[int]) -> list[int]:
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, ReadyOperationObservation
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 _get_ready_operations(self) -> list[ReadyOperationObservation]:
54
- """Get all operations that are ready to be scheduled now."""
55
- ready = []
56
  for job_id in range(len(self.jobs)):
57
- # Skip if job is complete
58
- if self.job_progress[job_id] >= len(self.jobs[job_id]):
59
- continue
60
 
61
- # Get next operation for this job
62
- machine_id, duration = self.jobs[job_id][self.job_progress[job_id]]
 
 
 
 
63
 
64
- # Check if machine is available
65
- busy_until = self.machine_busy_until[machine_id]
66
- if busy_until is None or busy_until <= self.env.now:
67
- remaining = len(self.jobs[job_id]) - self.job_progress[job_id]
68
- ready.append(
69
- ReadyOperationObservation(
70
- job_id=job_id,
71
- machine_id=machine_id,
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._get_ready_operations()) > 0
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
- ready_ops = self._get_ready_operations()
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
- ready_operations=ready_ops,
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.ready_operations}")
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
- operation = next((op for op in obs.ready_operations if op.job_id == job_id), None)
25
- assert operation is not None
26
  event = ScheduledEvent(
27
  job_id=job_id,
28
- machine_id=operation.machine_id,
29
  start_time=obs.step_count,
30
- end_time=obs.step_count + operation.duration,
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