diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff3ad5c..ce3cc23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,3 +15,15 @@ jobs: python-version: '3.12' - name: Run unit tests run: python -m unittest discover -s tests -v + + test-with-highs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install optional HiGHS dependency + run: python -m pip install -r requirements-optional.txt + - name: Run unit tests with SciPy/HiGHS enabled + run: python -m unittest discover -s tests -v diff --git a/README.md b/README.md index 27fb1c9..2c1df49 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Job Shop Scheduling Problem via Ant Colony Optimization. -This repository began as a 2017 research/prototype implementation of Ant Colony Optimization (ACO) for a small Job Shop Scheduling Problem (JSSP). It now includes a cleaned Python 3 solver, tests, and a dependency-free browser visualizer for generated job-shop instances. +This repository began as a 2017 research/prototype implementation of Ant Colony Optimization (ACO) for a small Job Shop Scheduling Problem (JSSP). It now includes a cleaned Python 3 solver, tests, a HiGHS MILP baseline, and a dependency-free browser visualizer for generated job-shop instances. ## Live demo @@ -10,7 +10,7 @@ Try the browser visualizer here: https://addejans.github.io/ACO-JSSP/ -The demo runs entirely in the browser and lets you choose the number of jobs and machines, tune ACO parameters, and inspect the resulting Gantt chart. +The demo runs entirely in the browser and lets you choose the number of jobs and machines, tune ACO parameters, inspect the resulting Gantt chart, and compare the heuristic result against a capped exact browser search for small instances. Updated LaTeX report: [ACO_JSSP_Updated_Report.pdf](paper/ACO_JSSP_Updated_Report.pdf) @@ -29,7 +29,31 @@ python -m src.aco_jssp --jobs 8 --machines 5 --ants 60 --iterations 150 --seed 1 python -m src.aco_jssp --jobs 8 --machines 5 --output docs/sample-schedule.json ``` -The cleaned implementation is dependency-free and uses only the Python standard library. +The cleaned ACO implementation is dependency-free and uses only the Python standard library. + +## ACO vs HiGHS MILP comparison + +The exact MILP baseline uses SciPy's HiGHS backend. Install the optional dependency first: + +```bash +python -m pip install -r requirements-optional.txt +``` + +Then compare ACO against the MILP baseline: + +```bash +python -m src.compare_solvers --jobs 4 --machines 3 --ants 40 --iterations 120 --milp-time-limit 10 +python -m src.compare_solvers --demo --milp-time-limit 10 --output results/demo-comparison.json +``` + +The comparison output reports: + +- ACO makespan and runtime; +- HiGHS MILP objective, runtime, solver status, and MIP gap; +- ACO optimality gap when HiGHS proves an optimal solution; +- both schedules in JSON-friendly form. + +The browser UI also includes a small-instance exact comparison panel. The browser comparison is not SciPy/HiGHS because this is a static GitHub Pages site, but it gives a useful interactive gap check for small instances. ## Scalable generated instances @@ -59,7 +83,8 @@ The visualizer lives in [`docs/index.html`](docs/index.html). It runs entirely i - ant count; - iteration count; - random seed; -- local-search budget. +- local-search budget; +- capped exact-comparison time limit. The GitHub Pages workflow publishes the `docs/` directory: @@ -81,14 +106,18 @@ The modern solver is still intentionally compact, but it is no longer a direct m - iteration-best pheromone reinforcement; - global-best elite reinforcement; - pair-swap local search on constructed job-order sequences; -- tests for precedence feasibility, machine non-overlap, determinism, and generated larger instances. +- tests for precedence feasibility, machine non-overlap, determinism, generated larger instances, and the optional HiGHS MILP baseline. ## Repository structure ```text src/aco_jssp.py # Modern Python 3 ACO solver -tests/test_aco_jssp.py # Unit tests for schedule validity and determinism +src/milp_jssp.py # Exact MILP baseline using SciPy/HiGHS +src/compare_solvers.py # ACO vs HiGHS comparison runner +tests/test_aco_jssp.py # Unit tests for schedule validity, determinism, and MILP smoke tests docs/index.html # Static browser visualizer / Gantt UI +paper/ # LaTeX report source and compiled PDF +legacy/ # Original 2017 prototype scripts ERRORS_AND_FIXES.md # Review notes and corrected issues ``` diff --git a/docs/index.html b/docs/index.html index 97c7f71..e260e6c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -5,19 +5,18 @@ ACO-JSSP Visualizer
Ant Colony Optimization · Job Shop Scheduling

ACO-JSSP visualizer

-

Pick the size of the job-shop instance, tune the ACO parameters, and inspect the resulting Gantt chart. Small settings run fast in the browser; larger instances are intentionally harder.

+

Pick the size of the job-shop instance, tune the ACO parameters, and inspect the resulting Gantt chart. Small instances also get an exact browser comparison so you can see the ACO gap.

+

Solver comparison

+
—ACO time
—Exact time
—Exact makespan
—ACO gap
+
—ACO makespan
—Operations
—Exact status
—Exact nodes

@@ -64,9 +64,10 @@ 

Build and solve a job shop

function improve(jobs,order,r,steps){let best=schedule(jobs,order), bestOrder=order.slice();for(let s=0;s`${i}:${j}`, ph=new Map();for(let i=-1;io[0]))+1;for(let it=0;itv>0?i:-1).filter(v=>v>=0);let w=feas.map(j=>{let [m,d]=jobs[j][next[j]],finish=Math.max(jr[j],mr[m])+d,eta=1/Math.max(finish+.15*remainingWork(jobs,j,next[j]),1);return Math.pow(ph.get(key(prev,j))||1,1)*Math.pow(eta,2)});let ch=r()<.15?feas[w.indexOf(Math.max(...w))]:choice(r,feas,w);order.push(ch);let [m,d]=jobs[ch][next[ch]],end=Math.max(jr[ch],mr[m])+d;jr[ch]=end;mr[m]=end;next[ch]++;rem[ch]--;prev=ch}let cand=improve(jobs,order,r,local);if(!ib||cand.makespan
';const line=axis.querySelector('.axis-line');let step=Math.max(1,Math.ceil(res.makespan/8));for(let t=0;t<=res.makespan;t+=step){let s=document.createElement('span');s.className='tick';s.style.left=(t/res.makespan*100)+'%';s.textContent=t;line.appendChild(s)}g.appendChild(axis);for(let m=0;mMachine ${m+1}
`;let ml=row.querySelector('.machine-line');res.operations.filter(o=>o.machine===m).forEach(o=>{let b=document.createElement('div');b.className='op';b.style.background=palette[o.job%palette.length];b.style.left=(o.start/res.makespan*100)+'%';b.style.width=Math.max(((o.end-o.start)/res.makespan*100),1.2)+'%';b.title=`${o.label}: ${o.start} → ${o.end}`;b.textContent=o.label;ml.appendChild(b)});g.appendChild(row)}const leg=document.getElementById('legend');leg.innerHTML='';for(let j=0;jJob ${j+1}`;leg.appendChild(s)}} -function run(){let vals={jobs:+jobsEl.value,machines:+machinesEl.value,ants:+ants.value,iterations:+iterations.value,instanceSeed:+instanceSeed.value,algoSeed:+algoSeed.value,minDur:+minDur.value,maxDur:+maxDur.value,local:+local.value};let data=generatedJobs(vals);render(solve(data,vals))} -const jobsEl=document.getElementById('jobs'),machinesEl=document.getElementById('machines'),ants=document.getElementById('ants'),iterations=document.getElementById('iterations'),instanceSeed=document.getElementById('instanceSeed'),algoSeed=document.getElementById('algoSeed'),minDur=document.getElementById('minDur'),maxDur=document.getElementById('maxDur'),local=document.getElementById('local');document.getElementById('run').addEventListener('click',run);run(); +function exactSearch(jobs, initial, limitMs){const start=performance.now(), mCount=Math.max(...jobs.flat().map(o=>o[0]))+1, n=jobs.length, opsPerJob=jobs[0].length, total=n*opsPerJob;if(total>30)return{status:'skipped',reason:'too large',time:0,nodes:0,best:initial.makespan,schedule:initial.operations};let best=initial.makespan,bestOps=initial.operations,nodes=0,timed=false;const suffix=jobs.map(route=>{let s=Array(route.length+1).fill(0);for(let i=route.length-1;i>=0;i--)s[i]=s[i+1]+route[i][1];return s});function bound(next,jr,mr){let b=Math.max(...jr,...mr);for(let j=0;jlimitMs){timed=true;return}nodes++;if(ops.length===total){let ms=Math.max(...mr);if(ms({...o}))}return}if(bound(next,jr,mr)>=best)return;let cand=[];for(let j=0;ja[0]-b[0]);for(const[e,s,j,m,d]of cand){if(e>=best)continue;let ni=next[j], next2=next.slice(), jr2=jr.slice(), mr2=mr.slice();next2[j]++;jr2[j]=e;mr2[m]=e;ops.push({job:j,index:ni,machine:m,duration:d,start:s,end:e,label:`J${j+1}-O${ni+1}`});dfs(next2,jr2,mr2,ops);ops.pop();if(timed)return}}dfs(Array(n).fill(0),Array(n).fill(0),Array(mCount).fill(0),[]);return{status:timed?'timed out':'optimal',time:performance.now()-start,nodes,best,schedule:bestOps}} +function render(res, exact, acoTime){document.getElementById('makespan').textContent=res.makespan;document.getElementById('ops').textContent=res.operations.length;document.getElementById('acoTime').textContent=acoTime.toFixed(1)+' ms';document.getElementById('exactTime').textContent=exact.status==='skipped'?'—':exact.time.toFixed(1)+' ms';document.getElementById('exactBest').textContent=exact.status==='skipped'?'skipped':exact.best;document.getElementById('status').textContent=exact.status;document.getElementById('nodes').textContent=exact.nodes.toLocaleString();document.getElementById('gap').textContent=(exact.status==='optimal'&&exact.best>0)?(((res.makespan-exact.best)/exact.best*100).toFixed(1)+'%'):'—';document.getElementById('json').textContent=JSON.stringify({aco:res,exact_comparison:exact},null,2);const g=document.getElementById('gantt');g.innerHTML='';const axis=document.createElement('div');axis.className='axis';axis.innerHTML='
';const line=axis.querySelector('.axis-line');let step=Math.max(1,Math.ceil(res.makespan/8));for(let t=0;t<=res.makespan;t+=step){let s=document.createElement('span');s.className='tick';s.style.left=(t/res.makespan*100)+'%';s.textContent=t;line.appendChild(s)}g.appendChild(axis);for(let m=0;mMachine ${m+1}
`;let ml=row.querySelector('.machine-line');res.operations.filter(o=>o.machine===m).forEach(o=>{let b=document.createElement('div');b.className='op';b.style.background=palette[o.job%palette.length];b.style.left=(o.start/res.makespan*100)+'%';b.style.width=Math.max(((o.end-o.start)/res.makespan*100),1.2)+'%';b.title=`${o.label}: ${o.start} → ${o.end}`;b.textContent=o.label;ml.appendChild(b)});g.appendChild(row)}const leg=document.getElementById('legend');leg.innerHTML='';for(let j=0;jJob ${j+1}`;leg.appendChild(s)}} +function run(){let vals={jobs:+jobsEl.value,machines:+machinesEl.value,ants:+ants.value,iterations:+iterations.value,instanceSeed:+instanceSeed.value,algoSeed:+algoSeed.value,minDur:+minDur.value,maxDur:+maxDur.value,local:+local.value};let data=generatedJobs(vals);let t0=performance.now();let aco=solve(data,vals);let acoTime=performance.now()-t0;let exact=exactSearch(data,aco,+exactLimit.value);render(aco,exact,acoTime)} +const jobsEl=document.getElementById('jobs'),machinesEl=document.getElementById('machines'),ants=document.getElementById('ants'),iterations=document.getElementById('iterations'),instanceSeed=document.getElementById('instanceSeed'),algoSeed=document.getElementById('algoSeed'),minDur=document.getElementById('minDur'),maxDur=document.getElementById('maxDur'),local=document.getElementById('local'),exactLimit=document.getElementById('exactLimit');document.getElementById('run').addEventListener('click',run);run(); diff --git a/paper/ACO_JSSP_Updated_Report.pdf b/paper/ACO_JSSP_Updated_Report.pdf index f7ccc8d..76ed11c 100644 Binary files a/paper/ACO_JSSP_Updated_Report.pdf and b/paper/ACO_JSSP_Updated_Report.pdf differ diff --git a/paper/ACO_JSSP_Updated_Report.tex b/paper/ACO_JSSP_Updated_Report.tex index c95a9d0..06c0cdb 100644 --- a/paper/ACO_JSSP_Updated_Report.tex +++ b/paper/ACO_JSSP_Updated_Report.tex @@ -1,309 +1,96 @@ \documentclass[11pt]{article} - \usepackage[margin=1in]{geometry} -\usepackage{amsmath, amssymb} -\usepackage{booktabs} -\usepackage{array} -\usepackage{longtable} -\usepackage{xcolor} -\usepackage{hyperref} -\usepackage{enumitem} -\usepackage{fancyhdr} -\usepackage{titlesec} -\usepackage{listings} -\usepackage{caption} -\usepackage{tikz} - -\hypersetup{ - colorlinks=true, - linkcolor=blue!50!black, - urlcolor=blue!50!black, - citecolor=blue!50!black -} - -\pagestyle{fancy} -\fancyhf{} -\lhead{ACO-JSSP Updated Report} -\rhead{Adam DeJans Jr.} -\cfoot{\thepage} - +\usepackage{amsmath, amssymb, booktabs, array, longtable, xcolor, hyperref, enumitem, fancyhdr, titlesec, listings, caption, tikz} +\hypersetup{colorlinks=true,linkcolor=blue!50!black,urlcolor=blue!50!black,citecolor=blue!50!black} +\pagestyle{fancy}\fancyhf{}\lhead{ACO-JSSP Updated Report}\rhead{Adam DeJans Jr.}\cfoot{\thepage} \titleformat{\section}{\normalfont\Large\bfseries}{\thesection}{0.75em}{} \titleformat{\subsection}{\normalfont\large\bfseries}{\thesubsection}{0.75em}{} - -\lstset{ - basicstyle=\ttfamily\small, - breaklines=true, - frame=single, - columns=fullflexible, - backgroundcolor=\color{gray!6}, - rulecolor=\color{gray!35} -} - +\lstset{basicstyle=\ttfamily\small,breaklines=true,frame=single,columns=fullflexible,backgroundcolor=\color{gray!6},rulecolor=\color{gray!35}} \newcommand{\repo}{\href{https://github.com/addejans/ACO-JSSP}{github.com/addejans/ACO-JSSP}} \newcommand{\demo}{\href{https://addejans.github.io/ACO-JSSP/}{addejans.github.io/ACO-JSSP}} - \begin{document} - -\begin{titlepage} -\centering -\vspace*{0.55in} -{\Huge\bfseries Ant Colony Optimization for the Job Shop Scheduling Problem\par} -\vspace{0.25in} -{\Large Updated Technical Report and Implementation Notes\par} -\vspace{0.45in} -{\large Adam DeJans Jr.\par} -\vspace{0.12in} -{\large June 2026\par} -\vfill -\begin{tabular}{rl} -Repository: & \repo \\ -Live demo: & \demo \\ -Implementation: & \texttt{src/aco\_jssp.py} \\ -Visualizer: & \texttt{docs/index.html} -\end{tabular} -\vfill +\begin{titlepage}\centering\vspace*{0.55in} +{\Huge\bfseries Ant Colony Optimization for the Job Shop Scheduling Problem\par}\vspace{0.25in} +{\Large Updated Technical Report: ACO, HiGHS MILP Baseline, and Visual Comparison\par}\vspace{0.45in} +{\large Adam DeJans Jr.\par}\vspace{0.12in}{\large June 2026\par}\vfill +\begin{tabular}{rl}Repository: & \repo \\ Live demo: & \demo \\ ACO implementation: & \texttt{src/aco\_jssp.py} \\ MILP baseline: & \texttt{src/milp\_jssp.py} \\ Comparison runner: & \texttt{src/compare\_solvers.py} \\ Visualizer: & \texttt{docs/index.html}\end{tabular}\vfill \begin{abstract} -This report updates the original ACO-JSSP project documentation to reflect the modern Python 3 solver, generated instances, browser-based Gantt visualizer, test suite, and GitHub Pages deployment. The project demonstrates how Ant Colony Optimization can be used as a constructive metaheuristic for the Job Shop Scheduling Problem. The current implementation separates problem parameters from algorithm parameters, supports arbitrary generated job-shop instances within bounded UI ranges, applies a remaining-work-aware transition heuristic, mixes exploration with exploitation, reinforces both iteration-best and global-best solutions, and applies pair-swap local search to improve constructed schedules. The objective is practical clarity: valid schedules, understandable controls, visible Gantt charts, and a codebase organized for future algorithmic extensions. -\end{abstract} -\vfill -\end{titlepage} - -\tableofcontents -\newpage - +This report updates the original ACO-JSSP project documentation to reflect the modern Python 3 solver, generated instances, browser-based Gantt visualizer, HiGHS mixed-integer linear programming baseline, comparison runner, test suite, and GitHub Pages deployment. The project demonstrates how Ant Colony Optimization can be used as a constructive metaheuristic for the Job Shop Scheduling Problem and how a solver-backed MILP formulation can measure optimality gaps on smaller instances. The current implementation separates problem parameters from algorithm parameters, supports generated instances, applies a remaining-work-aware transition heuristic, mixes exploration with exploitation, reinforces both iteration-best and global-best solutions, applies pair-swap local search, and compares heuristic schedules against exact or bounded exact results. +\end{abstract}\vfill\end{titlepage} +\tableofcontents\newpage \section{Introduction} +The Job Shop Scheduling Problem (JSSP) is a canonical problem in operations research and combinatorial optimization. A set of jobs must be processed on a set of machines. Each job has a fixed route through the machines, and each operation has a processing time. A feasible schedule must respect job precedence and machine capacity. -The Job Shop Scheduling Problem (JSSP) is a canonical problem in operations research and combinatorial optimization. A set of jobs must be processed on a set of machines. Each job has a fixed route through the machines, and each operation has a processing time. A feasible schedule must respect two core restrictions: job precedence and machine capacity. Job precedence means that a job's second operation cannot begin until its first operation is complete. Machine capacity means that a machine can process at most one operation at a time. - -This repository began as a 2017 research prototype applying Ant Colony Optimization (ACO) to a small job-shop instance. The original version was useful as a learning artifact but difficult to maintain: it used Python 2 syntax, relied on an old Plotly API, used heavy global state, and did not include tests or a modern UI. The updated repository keeps the historical scripts for reference, but moves the active implementation into a cleaner Python 3 module with tests and a browser visualizer. - -The live visualizer is available at: - -\begin{center} -\demo -\end{center} - -The visualizer lets a user choose the number of jobs, number of machines, operation duration range, ant count, iteration count, random seeds, and local-search budget. The output is a Gantt chart and a JSON representation of the best schedule found. +This repository began as a 2017 research prototype applying Ant Colony Optimization (ACO) to a small job-shop instance. The original version was useful as a learning artifact but difficult to maintain: it used Python 2 syntax, relied on an old Plotly API, used heavy global state, included hard-coded plotting credentials, and did not include tests or a modern UI. The updated repository keeps the historical scripts in \texttt{legacy/}, but moves active work into a cleaner Python 3 implementation with tests, a static UI, an exact MILP baseline, and report documentation. +The live visualizer is available at: \begin{center}\demo\end{center} \section{Problem Definition} - -Let $J = \{1,\ldots,n\}$ be the set of jobs and $M = \{1,\ldots,m\}$ be the set of machines. Each job $j$ contains an ordered sequence of operations: - -\[ -O_j = (O_{j,1}, O_{j,2}, \ldots, O_{j,m}). -\] - -Each operation $O_{j,k}$ has a processing time $p_{j,k} > 0$ and a required machine $\mu_{j,k} \in M$. In the generated instances used by the visualizer, each job visits every machine exactly once, but in a randomly generated route order. - -The goal is to assign a start time $s_{j,k}$ to every operation. A schedule is feasible if it satisfies: - -\subsection{Job precedence} - -\[ -s_{j,k+1} \geq s_{j,k} + p_{j,k} -\quad \forall j,\; k=1,\ldots,m-1. -\] - -\subsection{Machine capacity} - -For any two operations $O_{j,k}$ and $O_{j',k'}$ requiring the same machine, they cannot overlap: - -\[ -[s_{j,k}, s_{j,k}+p_{j,k}) \cap [s_{j',k'}, s_{j',k'}+p_{j',k'}) = \emptyset. -\] - -\subsection{Objective} - -The repository minimizes makespan: - -\[ -C_{\max} = \max_{j,k} (s_{j,k} + p_{j,k}). -\] - -A smaller makespan means the final operation completes earlier. - -\section{Why Ant Colony Optimization?} - -JSSP becomes difficult quickly because every machine sequence interacts with every job route. Exact methods can be valuable, but this repository focuses on a metaheuristic because ACO provides a clear educational bridge between random construction, learning, and local improvement. - -ACO is inspired by the way ants use pheromones to bias future path choices. In optimization, artificial ants construct solutions. Edges or choices that appear in better solutions receive more pheromone. Over time, the search becomes biased toward solution components that have historically produced good results. - -In this project, an ant constructs a \textit{job-order sequence}. If there are $n$ jobs and $m$ machines, the sequence has length $n \times m$. Each time job $j$ appears in the sequence, the decoder schedules the next unscheduled operation of job $j$ at the earliest feasible time. This representation is compact, easy to validate, and convenient for local search. - -\section{Current Repository Organization} - -The active code is organized around a clean separation between solver, tests, UI, legacy artifacts, and paper documentation. - -\begin{longtable}{p{2.15in}p{4.1in}} -\toprule -\textbf{Path} & \textbf{Purpose} \\ -\midrule -\texttt{src/aco\_jssp.py} & Modern Python 3 solver, generated instance creation, CLI, schedule decoder, local search, and JSON output. \\ -\texttt{tests/test\_aco\_jssp.py} & Unit tests for precedence, machine non-overlap, deterministic seeds, generated instances, and complete schedules. \\ -\texttt{docs/index.html} & Static browser UI and Gantt visualizer deployed through GitHub Pages. \\ +Let $J = \{1,\ldots,n\}$ be the set of jobs and $M = \{1,\ldots,m\}$ be the set of machines. Each job $j$ contains an ordered route of operations, $O_j=(O_{j,1},\ldots,O_{j,m})$. Each operation $O_{j,k}$ has processing time $p_{j,k}>0$ and required machine $\mu_{j,k}\in M$. In generated instances, each job visits each machine exactly once in a random route order. +\subsection{Job precedence} \[s_{j,k+1}\geq s_{j,k}+p_{j,k}\quad \forall j,\;k=1,\ldots,m-1.\] +\subsection{Machine capacity} For any two operations requiring the same machine, processing intervals cannot overlap: \[[s_{j,k},s_{j,k}+p_{j,k})\cap[s_{j',k'},s_{j',k'}+p_{j',k'})=\emptyset.\] +\subsection{Objective} The repository minimizes makespan: \[C_{\max}=\max_{j,k}(s_{j,k}+p_{j,k}).\] +\section{Repository Organization} +\begin{longtable}{p{2.15in}p{4.1in}}\toprule\textbf{Path} & \textbf{Purpose} \\ \midrule +\texttt{src/aco\_jssp.py} & Modern Python 3 ACO solver, generated instances, CLI, schedule decoder, local search, and JSON output. \\ +\texttt{src/milp\_jssp.py} & Exact disjunctive MILP baseline using SciPy and the HiGHS solver backend. \\ +\texttt{src/compare\_solvers.py} & Command-line comparison runner for ACO versus HiGHS MILP. \\ +\texttt{tests/test\_aco\_jssp.py} & Unit tests for schedule validity, determinism, generated instances, and optional HiGHS smoke tests. \\ +\texttt{docs/index.html} & Static browser UI, Gantt visualizer, and small-instance exact comparison panel. \\ \texttt{paper/} & Updated LaTeX report source and compiled PDF. \\ \texttt{legacy/} & Original 2017 prototype scripts retained for historical reference. \\ -\texttt{ERRORS\_AND\_FIXES.md} & Review notes describing issues found in the historical implementation and fixes added in the modernized project. \\ -\bottomrule -\end{longtable} - -This organization makes the repository easier to read: the active implementation is in \texttt{src/}, the UI is in \texttt{docs/}, the report is in \texttt{paper/}, and the old scripts are explicitly labeled as legacy. - -\section{Solver Design} - -\subsection{Generated problem instances} - -The solver can generate reproducible random JSSP instances. The user chooses the number of jobs, number of machines, random seed, and duration range. Each generated job visits each machine exactly once, in a random route order. This keeps the model simple while letting the UI demonstrate larger and harder examples. - -\subsection{Job-order decoding} - -The core decoder turns a job-order sequence into a feasible active schedule. It tracks: - -\begin{itemize}[leftmargin=0.25in] -\item the next operation to schedule for each job; -\item the earliest time each job is ready; -\item the earliest time each machine is available; -\item the start and end time assigned to every operation. -\end{itemize} - -When job $j$ is selected, the decoder schedules the next operation of job $j$ at: - -\[ -s = \max(\text{jobReady}_j, \text{machineReady}_{\mu_{j,k}}). -\] - -This guarantees job precedence and machine capacity by construction. - -\subsection{Transition rule} - -At each construction step, an ant chooses among jobs that still have unscheduled operations. A job's attractiveness depends on pheromone and a heuristic score: - -\[ -P(j) = \frac{\tau_{i,j}^{\alpha}\eta_j^{\beta}}{\sum_{\ell \in F} \tau_{i,\ell}^{\alpha}\eta_{\ell}^{\beta}}, -\] - -where $F$ is the feasible set, $i$ is the previously selected job, $\tau_{i,j}$ is the pheromone value for moving from job $i$ to job $j$, and $\eta_j$ is the heuristic desirability. - -The updated heuristic is remaining-work-aware: - -\[ -\eta_j = \frac{1}{\max(\text{earliestFinish}_j + 0.15 \cdot \text{remainingWork}_j, 1)}. -\] - -This biases construction toward moves that look good immediately while also accounting for downstream work still left in the job. - -\section{Additional Techniques Implemented} - -The updated repository adds several techniques beyond the original prototype. - -\subsection{Separation of problem and algorithm parameters} - -The UI now clearly separates \textit{problem parameters} from \textit{algorithm parameters}. Problem parameters define the generated instance: jobs, machines, duration range, and instance seed. Algorithm parameters define how the solver searches: ants, iterations, local search budget, and algorithm seed. This separation makes the UI easier to understand and aligns with how optimization systems should be configured. - -\subsection{Exploration and exploitation} - -The solver mixes random weighted choice with a small amount of greedy exploitation. With probability $q_0$, an ant chooses the feasible move with the highest weighted score. Otherwise, it samples probabilistically from the weighted distribution. This keeps the algorithm from being purely random while still preserving diversity. - -\subsection{Pheromone evaporation and reinforcement} - -After each iteration, pheromone is evaporated: - -\[ -\tau_{i,j} \leftarrow (1-\rho)\tau_{i,j}. -\] - -A numerical floor prevents pheromone values from disappearing completely. The solver then reinforces the iteration-best order and also applies an elite deposit to the global-best order: - -\[ -\tau_{i,j} \leftarrow \tau_{i,j} + \frac{Q}{C_{\max}} -\] - -for transitions that appeared in a good sequence. - -\subsection{Pair-swap local search} - -After an ant constructs a full job-order sequence, the solver applies a small local search. It randomly swaps two positions in the sequence, decodes the candidate, and keeps the swap if makespan does not worsen. This is a simple neighborhood, but it improves the demonstration because constructed solutions can be refined before they influence pheromone. - -\subsection{Seeded reproducibility} - -The UI now separates instance seed from algorithm seed. Changing the instance seed changes the generated problem. Changing the algorithm seed changes the stochastic search path. This makes experimentation clearer and helps with testing. - -\section{Browser Visualizer} - -The browser visualizer is a static HTML page under \texttt{docs/index.html}. It runs directly on GitHub Pages without a backend. The UI exposes controls for both problem generation and algorithm behavior. - -\begin{longtable}{p{2in}p{4.25in}} -\toprule -\textbf{Control group} & \textbf{Controls} \\ -\midrule +\bottomrule\end{longtable} +\section{ACO Solver Design} +The ACO solver constructs a job-order sequence. If there are $n$ jobs and $m$ machines, the sequence has length $n\times m$. Each time job $j$ appears, the decoder schedules the next unscheduled operation of that job at the earliest feasible time. +\subsection{Schedule decoding} The decoder tracks job readiness, machine readiness, and the next operation index for each job. When job $j$ is selected, the next operation starts at \[s=\max(\text{jobReady}_j,\text{machineReady}_{\mu_{j,k}}).\] +\subsection{Transition rule} At each construction step, an ant chooses among jobs that still have unscheduled operations. A job's attractiveness depends on pheromone and a heuristic score: \[P(j)=\frac{\tau_{i,j}^{\alpha}\eta_j^{\beta}}{\sum_{\ell\in F}\tau_{i,\ell}^{\alpha}\eta_{\ell}^{\beta}}.\] The updated heuristic is remaining-work-aware: \[\eta_j=\frac{1}{\max(\text{earliestFinish}_j+0.15\cdot\text{remainingWork}_j,1)}.\] +\section{Additional ACO Techniques Implemented} +\subsection{Problem versus algorithm parameters} The UI separates problem parameters from algorithm parameters. Problem parameters define the instance: jobs, machines, duration range, and instance seed. Algorithm parameters define the search: ants, iterations, local search budget, and algorithm seed. +\subsection{Exploration and exploitation} The solver mixes probabilistic weighted choice with a small amount of greedy exploitation. With probability $q_0$, an ant chooses the highest-weight feasible move; otherwise it samples from the weighted transition distribution. +\subsection{Pheromone evaporation and elite reinforcement} After each iteration, pheromone evaporates: \[\tau_{i,j}\leftarrow(1-\rho)\tau_{i,j}.\] The solver reinforces the iteration-best order and applies an elite deposit to the global-best order: \[\tau_{i,j}\leftarrow\tau_{i,j}+\frac{Q}{C_{\max}}.\] +\subsection{Pair-swap local search} After an ant constructs a full job-order sequence, the solver applies pair-swap local search. It swaps two positions, decodes the candidate, and keeps the move if makespan does not worsen. +\section{Exact MILP Baseline with HiGHS} +The repository now includes an exact mixed-integer linear programming baseline in \texttt{src/milp\_jssp.py}. The baseline is not meant to replace ACO. It is a measuring stick. For small and moderate instances, HiGHS can prove optimality or return a solver status, objective value, runtime, and MIP gap. +\subsection{MILP variables} The formulation uses start-time variables $s_o$, makespan variable $C_{\max}$, and binary ordering variables $y_{a,b}$ for each pair of operations sharing a machine. +\subsection{Makespan constraints} For every operation $o$ with processing time $p_o$: \[C_{\max}\geq s_o+p_o.\] +\subsection{Disjunctive machine constraints} For two operations $a$ and $b$ on the same machine, let $y_{a,b}=1$ mean operation $a$ precedes operation $b$. With horizon $H$, the disjunction is linearized as: \[s_b-s_a-Hy_{a,b}\geq p_a-H,\] \[s_a-s_b+Hy_{a,b}\geq p_b.\] +\subsection{Comparison runner} After installing SciPy, ACO and HiGHS can be compared from the command line: +\begin{lstlisting}[language=bash] +python -m pip install -r requirements-optional.txt +python -m src.compare_solvers --jobs 4 --machines 3 --ants 40 --iterations 120 --milp-time-limit 10 +\end{lstlisting} +The output includes ACO makespan, ACO runtime, HiGHS objective, HiGHS runtime, solver status, MIP gap, and ACO gap when HiGHS proves optimality. +\section{Browser Visualizer and UI Comparison} +The browser visualizer is a static HTML page under \texttt{docs/index.html}. It runs directly on GitHub Pages. Since SciPy/HiGHS cannot run inside a static browser page, the UI includes a capped exact branch-and-bound comparison for small instances. It shows ACO makespan, exact makespan when proven, runtime, search nodes, and ACO gap. +\begin{longtable}{p{2in}p{4.25in}}\toprule\textbf{Control group} & \textbf{Controls / outputs} \\ \midrule Problem parameters & Jobs, machines, minimum duration, maximum duration, instance seed. \\ -Algorithm parameters & Ants, iterations, local search budget, algorithm seed. \\ -Output & Makespan, number of operations, Gantt chart, job legend, schedule JSON. \\ -\bottomrule -\end{longtable} - -The visualization is deliberately not overbuilt. It is meant to show the relationship between instance size, search effort, and resulting schedule structure. The Gantt chart makes machine conflicts and job sequencing visible immediately. - +Algorithm parameters & Ants, iterations, local search budget, algorithm seed, exact-search time limit. \\ +Comparison outputs & ACO runtime, exact runtime, ACO makespan, exact makespan, exact status, exact nodes, and ACO gap. \\ +Visualization outputs & Gantt chart, job legend, and schedule JSON. \\ +\bottomrule\end{longtable} \section{Testing and Validation} - -The test suite checks correctness properties that should hold regardless of stochastic performance: - -\begin{itemize}[leftmargin=0.25in] -\item every generated schedule contains the required number of operations; -\item each job's operations respect route order; -\item no two operations overlap on the same machine; -\item generated instances are reproducible for a fixed seed; -\item the solver is deterministic for a fixed algorithm seed; -\item the solver can handle larger generated instances as a smoke test. -\end{itemize} - -The tests can be run with: - +The tests focus on correctness properties that should hold regardless of stochastic quality: complete schedules, job route precedence, machine non-overlap, reproducible generated instances, deterministic ACO for fixed seeds, and optional HiGHS MILP smoke tests when SciPy is installed. \begin{lstlisting}[language=bash] python -m unittest discover -s tests -v \end{lstlisting} - -These tests validate feasibility and implementation stability. They do not prove global optimality. - \section{Limitations} - -This project is an educational and demonstrative implementation. It does not yet include standard benchmark comparisons, optimality gaps, parallel ants, advanced critical-path neighborhoods, disjunctive graph visualizations, or exact MILP baselines. The browser UI is intentionally bounded so that it remains responsive. - -The current ACO implementation also learns pheromone on transitions between job selections rather than richer operation-level or machine-level sequencing decisions. This keeps the method understandable, but richer pheromone models may improve solution quality. - +The HiGHS MILP baseline is valuable for small instances and quality comparisons, but exact formulations can become expensive as the number of jobs and machines grows. The browser exact search is deliberately capped to keep GitHub Pages responsive. The current ACO model learns pheromone on transitions between job selections rather than richer operation-level or machine-level sequencing decisions. \section{Future Work} - -Useful extensions include: - \begin{enumerate}[leftmargin=0.25in] -\item Add benchmark instances such as FT06, LA, ORB, and ABZ families. -\item Add a MILP or CP-SAT baseline for small cases. -\item Add critical-path-based local search neighborhoods. +\item Add standard benchmark instances such as FT06, LA, ORB, and ABZ families. +\item Extend the HiGHS comparison runner with benchmark tables and solver diagnostics. +\item Add a CP-SAT baseline for comparison against the MILP formulation. +\item Add critical-path local search neighborhoods. \item Add machine-specific pheromone matrices. -\item Add dispatching-rule hybridization, such as SPT, LPT, slack, or bottleneck-aware priority rules. -\item Export schedules to CSV and JSON from the UI. +\item Add dispatching-rule hybrids such as SPT, LPT, slack, and bottleneck-aware rules. \item Add runtime and convergence charts. -\item Add parallel ant construction for larger instances. +\item Export schedules to CSV and JSON from the browser UI. \end{enumerate} - \section{Conclusion} - -The updated ACO-JSSP repository is now more than a historical script. It is a small but coherent optimization demonstration: a Python solver, a generated instance model, a static browser UI, a Gantt visualizer, unit tests, GitHub Pages deployment, and a LaTeX-style technical report. The core lesson is that metaheuristics become much more useful when paired with clean representations, validation, reproducibility, and visualization. - +The updated ACO-JSSP repository is now a coherent optimization demonstration. It includes a Python ACO solver, generated instances, an exact HiGHS MILP baseline, a solver comparison runner, a static browser UI, Gantt visualization, unit tests, GitHub Pages deployment, and a LaTeX-style technical report. The practical lesson is that metaheuristics become much more useful when paired with clean representations, validation, exact baselines, reproducibility, and visualization. \begin{thebibliography}{9} - -\bibitem{dorigo1996} -M. Dorigo, V. Maniezzo, and A. Colorni. ``Ant System: Optimization by a Colony of Cooperating Agents.'' \textit{IEEE Transactions on Systems, Man, and Cybernetics}, 26(1), 1996. - -\bibitem{dorigo2004} -M. Dorigo and T. Stuetzle. \textit{Ant Colony Optimization}. MIT Press, 2004. - -\bibitem{garey1976} -M. R. Garey, D. S. Johnson, and R. Sethi. ``The Complexity of Flowshop and Jobshop Scheduling.'' \textit{Mathematics of Operations Research}, 1(2), 1976. - -\bibitem{blazewicz1996} -J. Blazewicz, W. Domschke, and E. Pesch. ``The Job Shop Scheduling Problem: Conventional and New Solution Techniques.'' \textit{European Journal of Operational Research}, 93(1), 1996. - +\bibitem{dorigo1996} M. Dorigo, V. Maniezzo, and A. Colorni. ``Ant System: Optimization by a Colony of Cooperating Agents.'' \textit{IEEE Transactions on Systems, Man, and Cybernetics}, 26(1), 1996. +\bibitem{dorigo2004} M. Dorigo and T. Stuetzle. \textit{Ant Colony Optimization}. MIT Press, 2004. +\bibitem{garey1976} M. R. Garey, D. S. Johnson, and R. Sethi. ``The Complexity of Flowshop and Jobshop Scheduling.'' \textit{Mathematics of Operations Research}, 1(2), 1976. +\bibitem{blazewicz1996} J. Blazewicz, W. Domschke, and E. Pesch. ``The Job Shop Scheduling Problem: Conventional and New Solution Techniques.'' \textit{European Journal of Operational Research}, 93(1), 1996. \end{thebibliography} - \end{document} diff --git a/requirements-optional.txt b/requirements-optional.txt new file mode 100644 index 0000000..becf4fa --- /dev/null +++ b/requirements-optional.txt @@ -0,0 +1 @@ +scipy>=1.11 diff --git a/src/compare_solvers.py b/src/compare_solvers.py new file mode 100644 index 0000000..c395672 --- /dev/null +++ b/src/compare_solvers.py @@ -0,0 +1,124 @@ +"""Compare the ACO heuristic against the HiGHS MILP baseline.""" + +from __future__ import annotations + +import argparse +import json +import time +from pathlib import Path +from typing import Sequence + +from src.aco_jssp import demo_problem, problem_to_dict, random_problem, schedule_to_dict, solve_aco +from src.milp_jssp import solve_milp_highs + + +def compare( + *, + demo: bool = False, + jobs: int = 4, + machines: int = 3, + min_duration: int = 1, + max_duration: int = 20, + instance_seed: int = 7, + algorithm_seed: int = 11, + ants: int = 40, + iterations: int = 120, + local_search_steps: int = 80, + milp_time_limit: float = 10.0, +) -> dict: + problem = demo_problem() if demo else random_problem( + num_jobs=jobs, + num_machines=machines, + seed=instance_seed, + min_duration=min_duration, + max_duration=max_duration, + ) + + aco_start = time.perf_counter() + aco_makespan, aco_schedule, aco_order = solve_aco( + problem, + ants=ants, + iterations=iterations, + seed=algorithm_seed, + local_search_steps=local_search_steps, + ) + aco_runtime = time.perf_counter() - aco_start + + milp = solve_milp_highs(problem, time_limit=milp_time_limit) + best_known = milp.objective + aco_gap = None + if best_known and best_known > 0: + aco_gap = (aco_makespan - best_known) / best_known + + return { + "problem": problem_to_dict(problem), + "settings": { + "demo": demo, + "instance_seed": instance_seed, + "algorithm_seed": algorithm_seed, + "ants": ants, + "iterations": iterations, + "local_search_steps": local_search_steps, + "milp_time_limit": milp_time_limit, + }, + "aco": schedule_to_dict(aco_makespan, aco_schedule, aco_order), + "aco_runtime_seconds": aco_runtime, + "milp_highs": { + "status": milp.status, + "message": milp.message, + "objective": milp.objective, + "optimal": milp.optimal, + "mip_gap": milp.mip_gap, + "runtime_seconds": milp.runtime_seconds, + "operations": [op.__dict__ | {"label": op.id} for op in milp.schedule], + }, + "comparison": { + "aco_makespan": aco_makespan, + "milp_makespan_or_best_bound": best_known, + "aco_gap_vs_milp": aco_gap, + "aco_gap_percent": None if aco_gap is None else 100.0 * aco_gap, + "milp_proved_optimal": milp.optimal, + }, + } + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Compare ACO against a HiGHS MILP baseline.") + parser.add_argument("--demo", action="store_true") + parser.add_argument("--jobs", type=int, default=4) + parser.add_argument("--machines", type=int, default=3) + parser.add_argument("--min-duration", type=int, default=1) + parser.add_argument("--max-duration", type=int, default=20) + parser.add_argument("--instance-seed", type=int, default=7) + parser.add_argument("--algorithm-seed", type=int, default=11) + parser.add_argument("--ants", type=int, default=40) + parser.add_argument("--iterations", type=int, default=120) + parser.add_argument("--local-search-steps", type=int, default=80) + parser.add_argument("--milp-time-limit", type=float, default=10.0) + parser.add_argument("--output", type=Path) + args = parser.parse_args(argv) + + payload = compare( + demo=args.demo, + jobs=args.jobs, + machines=args.machines, + min_duration=args.min_duration, + max_duration=args.max_duration, + instance_seed=args.instance_seed, + algorithm_seed=args.algorithm_seed, + ants=args.ants, + iterations=args.iterations, + local_search_steps=args.local_search_steps, + milp_time_limit=args.milp_time_limit, + ) + text = json.dumps(payload, indent=2) + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text + "\n", encoding="utf-8") + else: + print(text) + return 0 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/src/milp_jssp.py b/src/milp_jssp.py new file mode 100644 index 0000000..880c776 --- /dev/null +++ b/src/milp_jssp.py @@ -0,0 +1,155 @@ +"""MILP baseline for the Job Shop Scheduling Problem using SciPy/HiGHS. + +The ACO solver is intentionally heuristic. This module provides an exact mixed- +integer linear programming baseline for small and medium generated instances so +solution quality can be compared against a solver-backed objective value. + +SciPy is an optional dependency. Install it with: + + python -m pip install scipy +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass +from typing import Dict, List, Sequence, Tuple + +from src.aco_jssp import JobShopProblem, ScheduledOperation + + +@dataclass(frozen=True) +class MilpSolveResult: + status: int + message: str + objective: float | None + optimal: bool + mip_gap: float | None + runtime_seconds: float + schedule: List[ScheduledOperation] + + +def _operations(problem: JobShopProblem) -> List[Tuple[int, int, int, int]]: + """Return operations as (job, op_index, machine, duration).""" + + ops: List[Tuple[int, int, int, int]] = [] + for job, route in enumerate(problem.jobs): + for index, (machine, duration) in enumerate(route): + ops.append((job, index, machine, duration)) + return ops + + +def solve_milp_highs(problem: JobShopProblem, time_limit: float | None = 10.0) -> MilpSolveResult: + """Solve a JSSP instance as a disjunctive MILP with SciPy's HiGHS backend. + + Variables: + - start time for every operation; + - one makespan variable; + - one binary ordering variable for every pair of operations sharing a machine. + + The formulation minimizes makespan subject to job precedence and machine + non-overlap constraints. + """ + + try: + import numpy as np + from scipy.optimize import Bounds, LinearConstraint, milp + from scipy.sparse import lil_matrix + except Exception as exc: # pragma: no cover - depends on optional SciPy install + raise RuntimeError("SciPy is required for the HiGHS MILP baseline. Install with `python -m pip install scipy`." ) from exc + + start_clock = time.perf_counter() + ops = _operations(problem) + n_ops = len(ops) + cmax_idx = n_ops + horizon = float(sum(duration for *_prefix, duration in ops)) + + by_job: Dict[Tuple[int, int], int] = {(job, index): i for i, (job, index, _m, _d) in enumerate(ops)} + machine_pairs: List[Tuple[int, int]] = [] + for machine in range(problem.num_machines): + machine_ops = [i for i, (_job, _index, m, _duration) in enumerate(ops) if m == machine] + for pos, first in enumerate(machine_ops): + for second in machine_ops[pos + 1:]: + machine_pairs.append((first, second)) + + first_binary_idx = n_ops + 1 + n_vars = first_binary_idx + len(machine_pairs) + c = np.zeros(n_vars) + c[cmax_idx] = 1.0 + + lb = np.zeros(n_vars) + ub = np.full(n_vars, horizon) + lb[first_binary_idx:] = 0.0 + ub[first_binary_idx:] = 1.0 + integrality = np.zeros(n_vars) + integrality[first_binary_idx:] = 1 + + rows: List[Tuple[Dict[int, float], float, float]] = [] + + # Job precedence: s_{j,k+1} - s_{j,k} >= p_{j,k} + for job in range(problem.num_jobs): + for index in range(problem.operations_per_job - 1): + prev_idx = by_job[(job, index)] + next_idx = by_job[(job, index + 1)] + duration = ops[prev_idx][3] + rows.append(({next_idx: 1.0, prev_idx: -1.0}, float(duration), np.inf)) + + # Makespan: Cmax - s_i >= p_i + for op_idx, (_job, _index, _machine, duration) in enumerate(ops): + rows.append(({cmax_idx: 1.0, op_idx: -1.0}, float(duration), np.inf)) + + # Machine disjunctions with y=1 meaning first before second. + for pair_idx, (first, second) in enumerate(machine_pairs): + y_idx = first_binary_idx + pair_idx + first_duration = ops[first][3] + second_duration = ops[second][3] + rows.append(({second: 1.0, first: -1.0, y_idx: -horizon}, float(first_duration) - horizon, np.inf)) + rows.append(({first: 1.0, second: -1.0, y_idx: horizon}, float(second_duration), np.inf)) + + a = lil_matrix((len(rows), n_vars), dtype=float) + lower = np.empty(len(rows), dtype=float) + upper = np.empty(len(rows), dtype=float) + for row_idx, (coefficients, lo, hi) in enumerate(rows): + for col, value in coefficients.items(): + a[row_idx, col] = value + lower[row_idx] = lo + upper[row_idx] = hi + + options = {"disp": False} + if time_limit is not None: + options["time_limit"] = float(time_limit) + + result = milp( + c=c, + integrality=integrality, + bounds=Bounds(lb, ub), + constraints=LinearConstraint(a.tocsr(), lower, upper), + options=options, + ) + runtime = time.perf_counter() - start_clock + + schedule: List[ScheduledOperation] = [] + if result.x is not None: + for op_idx, (job, index, machine, duration) in enumerate(ops): + start = int(round(float(result.x[op_idx]))) + schedule.append( + ScheduledOperation( + job=job, + index=index, + machine=machine, + duration=duration, + start=start, + end=start + duration, + ) + ) + schedule.sort(key=lambda op: (op.start, op.machine, op.job, op.index)) + + return MilpSolveResult( + status=int(result.status), + message=str(result.message), + objective=None if result.fun is None else float(result.fun), + optimal=bool(result.success), + mip_gap=None if not hasattr(result, "mip_gap") else float(result.mip_gap), + runtime_seconds=runtime, + schedule=schedule, + ) diff --git a/tests/test_aco_jssp.py b/tests/test_aco_jssp.py index af7211b..6d24989 100644 --- a/tests/test_aco_jssp.py +++ b/tests/test_aco_jssp.py @@ -1,8 +1,12 @@ +import importlib.util import unittest from src.aco_jssp import demo_problem, random_problem, schedule_from_job_order, solve_aco +SCIPY_AVAILABLE = importlib.util.find_spec("scipy") is not None + + def assert_valid_schedule(testcase, problem, makespan, schedule): testcase.assertGreater(makespan, 0) testcase.assertEqual(len(schedule), problem.num_operations) @@ -59,6 +63,16 @@ def test_solver_handles_larger_generated_problem(self): self.assertEqual(len(order), problem.num_operations) assert_valid_schedule(self, problem, makespan, schedule) + @unittest.skipUnless(SCIPY_AVAILABLE, "SciPy is optional; skipping HiGHS MILP test") + def test_highs_milp_solves_small_generated_problem(self): + from src.milp_jssp import solve_milp_highs + + problem = random_problem(num_jobs=3, num_machines=3, seed=9, min_duration=1, max_duration=8) + result = solve_milp_highs(problem, time_limit=5.0) + self.assertIsNotNone(result.objective) + self.assertEqual(len(result.schedule), problem.num_operations) + assert_valid_schedule(self, problem, int(round(result.objective)), result.schedule) + if __name__ == "__main__": unittest.main()