Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
743 changes: 45 additions & 698 deletions README.md

Large diffs are not rendered by default.

120 changes: 120 additions & 0 deletions algorithms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Algorithm registry and utility helpers."""
from __future__ import annotations

from typing import Callable, Dict, List

from algorithms.classical.dispatching_rules import DISPATCHING_RULES, DispatchingRule
from algorithms.classical.constructive_heuristics import NEHHeuristic, PalmerHeuristic
from algorithms.classical.exact_methods import BranchAndBound
from algorithms.deep_rl.dqn import DQNOptimizer
from algorithms.deep_rl.ppo import PPOOptimizer
from algorithms.hybrid.adaptive_hybrid import AdaptiveHybridOptimizer
from algorithms.metaheuristics import (
AntColonyOptimization,
DifferentialEvolution,
GeneticAlgorithm,
GuidedLocalSearch,
IteratedLocalSearch,
ParticleSwarmOptimization,
SimulatedAnnealing,
TabuSearch,
VariableNeighborhoodSearch,
)
from algorithms.multi_objective.nsga2 import NSGAII
from core.base_optimizer import BaseOptimizer


def get_algorithm(name: str, **kwargs) -> BaseOptimizer:
"""Instantiate an algorithm by name.

Dispatching rules can be referenced directly by their identifier
(e.g. ``"spt"``). Other algorithms expose canonical names matching the
research roadmap (``"simulated_annealing"``, ``"nsga2"``, ``"dqn"``,
``"adaptive_hybrid"``).
"""

name = name.lower()
if name in DISPATCHING_RULES:
return DISPATCHING_RULES[name](**kwargs)

registry: Dict[str, Callable[..., BaseOptimizer]] = {
"neh": NEHHeuristic,
"palmer": PalmerHeuristic,
"branch_and_bound": BranchAndBound,
"simulated_annealing": SimulatedAnnealing,
"genetic_algorithm": GeneticAlgorithm,
"particle_swarm": ParticleSwarmOptimization,
"ant_colony": AntColonyOptimization,
"tabu_search": TabuSearch,
"variable_neighborhood_search": VariableNeighborhoodSearch,
"iterated_local_search": IteratedLocalSearch,
"guided_local_search": GuidedLocalSearch,
"differential_evolution": DifferentialEvolution,
"nsga2": NSGAII,
"dqn": DQNOptimizer,
"ppo": PPOOptimizer,
"adaptive_hybrid": AdaptiveHybridOptimizer,
}
if name not in registry:
raise KeyError(f"Unknown algorithm '{name}'")
return registry[name](**kwargs)


def list_algorithms(include_dispatching: bool = True) -> List[str]:
"""Return the list of registered optimisation algorithms.

Parameters
----------
include_dispatching:
When *True*, short-horizon dispatching heuristics are included in
addition to advanced optimisation methods. This is particularly
useful for interactive exploration in the visual dashboard where the
researcher may want to benchmark simple baselines alongside
state-of-the-art learners.
"""

names: List[str] = [
"neh",
"palmer",
"branch_and_bound",
"simulated_annealing",
"genetic_algorithm",
"particle_swarm",
"ant_colony",
"tabu_search",
"variable_neighborhood_search",
"iterated_local_search",
"guided_local_search",
"differential_evolution",
"nsga2",
"dqn",
"ppo",
"adaptive_hybrid",
]
if include_dispatching:
names = list(DISPATCHING_RULES.keys()) + names
return sorted(dict.fromkeys(names))


__all__ = [
"get_algorithm",
"DISPATCHING_RULES",
"DispatchingRule",
"list_algorithms",
"NEHHeuristic",
"PalmerHeuristic",
"BranchAndBound",
"SimulatedAnnealing",
"GeneticAlgorithm",
"ParticleSwarmOptimization",
"AntColonyOptimization",
"TabuSearch",
"VariableNeighborhoodSearch",
"IteratedLocalSearch",
"GuidedLocalSearch",
"DifferentialEvolution",
"NSGAII",
"DQNOptimizer",
"PPOOptimizer",
"AdaptiveHybridOptimizer",
]
77 changes: 77 additions & 0 deletions algorithms/classical/constructive_heuristics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Constructive heuristics for flow-shop style problems."""
from __future__ import annotations

from typing import List

from core.base_optimizer import BaseOptimizer
from core.metrics import evaluate_schedule
from core.problem import ManufacturingProblem
from core.solution import ScheduleSolution


class NEHHeuristic(BaseOptimizer):
"""Implementation of the classic Nawaz-Enscore-Ham heuristic."""

def solve(self, problem: ManufacturingProblem) -> ScheduleSolution:
if problem.jobs.empty:
return ScheduleSolution(schedule=problem.jobs)

jobs = problem.jobs.copy()
processing = jobs.get("Processing_Time")
if processing is None:
raise ValueError("Processing_Time column is required for NEH heuristic")

# Sort jobs by decreasing processing time.
ordered_indices = list(processing.sort_values(ascending=False).index)
sequence: List[int] = []

for job in ordered_indices:
best_sequence: List[int] | None = None
best_cost = float("inf")
for position in range(len(sequence) + 1):
candidate = sequence[:position] + [job] + sequence[position:]
schedule = problem.build_schedule(candidate)
cost = evaluate_schedule(schedule)["makespan"]
if cost < best_cost:
best_cost = cost
best_sequence = candidate
assert best_sequence is not None # for mypy / static typing
sequence = best_sequence

final_schedule = problem.build_schedule(sequence)
return ScheduleSolution(schedule=final_schedule, metadata={"sequence": sequence})


class PalmerHeuristic(BaseOptimizer):
"""Palmer's slope index heuristic for flow shop scheduling."""

def solve(self, problem: ManufacturingProblem) -> ScheduleSolution:
if problem.jobs.empty:
return ScheduleSolution(schedule=problem.jobs)

jobs = problem.jobs.copy()
processing = jobs.get("Processing_Time")
if processing is None:
raise ValueError("Processing_Time column is required for Palmer heuristic")

machines = jobs.get("Machine_ID")
slope_index: List[float]
if machines is not None and not machines.empty:
unique_machines = sorted(machines.unique())
if len(unique_machines) == 1:
weight_map = {unique_machines[0]: 0.0}
else:
step = 2.0 / (len(unique_machines) - 1)
weight_map = {machine: -1.0 + idx * step for idx, machine in enumerate(unique_machines)}
slope_index = [weight_map.get(machines.iloc[i], 0.0) for i in range(len(machines))]
else:
if len(jobs) <= 1:
slope_index = [0.0 for _ in range(len(jobs))]
else:
step = 2.0 / (len(jobs) - 1)
slope_index = [-1.0 + i * step for i in range(len(jobs))]

priority = [slope_index[i] * processing.iloc[i] for i in range(len(processing))]
ordered = jobs.assign(_priority=priority).sort_values("_priority", ascending=True)
schedule = problem.build_schedule(ordered.index)
return ScheduleSolution(schedule=schedule)
Loading