-
-
Save Shmew/25e1172ee6d0ce0f97ba5762e5be2c0d to your computer and use it in GitHub Desktop.
Example using Google OR CP-SAT solver for job shop problem with F#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"Resources": [ | |
{ | |
"Id": 0, | |
"Max": 2 | |
}, | |
{ | |
"Id": 1, | |
"Max": 2 | |
}, | |
{ | |
"Id": 2, | |
"Max": 1 | |
}, | |
{ | |
"Id": 3, | |
"Max": 100 | |
}, | |
{ | |
"Id": 4, | |
"Max": 100 | |
} | |
], | |
"Jobs": [ | |
{ | |
"FolderId": 0, | |
"FolderName": "Folder0", | |
"JobId": 0, | |
"JobName": "Job0", | |
"Duration": 3, | |
"Resources": [ 0, 3 ], | |
"ExclusiveResources": [], | |
"Successors": [ 3, 6 ], | |
"Predecessors": [] | |
}, | |
{ | |
"FolderId": 0, | |
"FolderName": "Folder0", | |
"JobId": 1, | |
"JobName": "Job1", | |
"Duration": 2, | |
"Resources": [ 1, 3 ], | |
"ExclusiveResources": [], | |
"Successors": [ 4 ], | |
"Predecessors": [] | |
}, | |
{ | |
"FolderId": 0, | |
"FolderName": "Folder0", | |
"JobId": 2, | |
"JobName": "Job2", | |
"Duration": 2, | |
"Resources": [ 2, 3 ], | |
"ExclusiveResources": [], | |
"Successors": [ 5 ], | |
"Predecessors": [] | |
}, | |
{ | |
"FolderId": 0, | |
"FolderName": "Folder0", | |
"JobId": 3, | |
"JobName": "Job3", | |
"Duration": 2, | |
"Resources": [ 0, 3 ], | |
"ExclusiveResources": [], | |
"Successors": [], | |
"Predecessors": [ 0 ] | |
}, | |
{ | |
"FolderId": 0, | |
"FolderName": "Folder0", | |
"JobId": 4, | |
"JobName": "Job4", | |
"Duration": 1, | |
"Resources": [ 2, 3 ], | |
"ExclusiveResources": [], | |
"Successors": [ 7 ], | |
"Predecessors": [ 1 ] | |
}, | |
{ | |
"FolderId": 0, | |
"FolderName": "Folder0", | |
"JobId": 5, | |
"JobName": "Job5", | |
"Duration": 4, | |
"Resources": [ 1, 3 ], | |
"ExclusiveResources": [ 1 ], | |
"Successors": [], | |
"Predecessors": [ 2 ] | |
}, | |
{ | |
"FolderId": 0, | |
"FolderName": "Folder0", | |
"JobId": 6, | |
"JobName": "Job6", | |
"Duration": 4, | |
"Resources": [ 1, 3 ], | |
"ExclusiveResources": [], | |
"Successors": [], | |
"Predecessors": [ 0 ] | |
}, | |
{ | |
"FolderId": 0, | |
"FolderName": "Folder0", | |
"JobId": 7, | |
"JobName": "Job7", | |
"Duration": 3, | |
"Resources": [ 2, 3 ], | |
"ExclusiveResources": [], | |
"Successors": [], | |
"Predecessors": [ 4 ] | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace OR | |
module Data = | |
open Fake.IO | |
open Fake.IO.FileSystemOperators | |
module Json = | |
open FSharp.Json | |
let config = | |
JsonConfig.create | |
(allowUntyped = true, serializeNone = SerializeNone.Omit) | |
let jsonPath = (__SOURCE_DIRECTORY__ @@ "Data/CMData.json") | |
let getJsonPkg() = File.readAsString jsonPath |> Json.deserializeEx<JsonCM> config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace OR | |
module App = | |
[<EntryPoint>] | |
let main _ = | |
ControlM.solve() |> ignore | |
0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace OR | |
open Google.OrTools.Sat | |
open System | |
module ControlM = | |
let model = CpModel() | |
let jobList, resList = | |
let pkg = Data.Json.getJsonPkg() | |
pkg.Jobs,pkg.Resources | |
let horizon = jobList |> List.sumBy (fun t -> t.Duration) |> int64 | |
let makeSpan = model.NewIntVar(0L, horizon, "makespan") | |
let solver = CpSolver() | |
type JobWithVars = | |
{ Job: Job | |
Start: IntVar | |
End: IntVar | |
Interval: IntervalVar } | |
static member Create (job: Job) = | |
let suff = sprintf "_%i_%i" job.FolderId job.JobId | |
let sV = model.NewIntVar(0L, horizon, sprintf "start%s" suff) | |
let eV = model.NewIntVar(0L, horizon, sprintf "end%s" suff) | |
{ Job = job | |
Start = sV | |
End = eV | |
Interval = model.NewIntervalVar(sV, job.Duration, eV, sprintf "interval%s" suff) } | |
let allJobs = | |
jobList | |
|> List.map (JobWithVars.Create) | |
let folders = | |
allJobs | |
|> List.groupBy (fun t -> t.Job.FolderId) | |
|> List.map (fun (folderId,jobs) -> | |
folderId, | |
jobs |> List.sortBy (fun j -> j.Job.JobId)) | |
let allFolders = folders |> List.map snd | |
let allJobsByResource i = | |
allJobs | |
|> List.filter (fun j -> | |
j.Job.Resources | |
|> List.contains i) | |
module Constraints = | |
let private resourceOverlap () = | |
resList | |
|> List.iter (fun res -> | |
let jList = | |
res.Id | |
|> allJobsByResource | |
let demands = | |
jList | |
|> List.map (fun j -> | |
if j.Job.ExclusiveResources |> List.contains res.Id then | |
res.Max |> int64 | |
else 1L) | |
let intervals = jList |> List.map (fun j -> j.Interval) | |
model.AddCumulative(intervals, demands, res.Max |> int64) | |
|> ignore) | |
/// Start of current job should be >= end of predecessors | |
let private jobPrecedence (job: JobWithVars) (pIndex: int) = | |
allFolders.[job.Job.FolderId] | |
|> List.tryFind (fun j -> j.Job.JobId = job.Job.JobId) | |
|> Option.map (fun j -> j.Start) | |
|> Option.iter (fun current -> | |
allFolders.[job.Job.FolderId] | |
|> List.tryFind (fun j -> j.Job.JobId = pIndex) | |
|> Option.map (fun j -> j.End) | |
|> Option.iter (fun previous -> | |
BoundedLinearExpression(0L, current - previous, Int64.MaxValue) | |
|> model.Add | |
|> ignore)) | |
/// End of current job should be <= start of successors | |
let private jobSuccessor (job: JobWithVars) (sIndex: int) = | |
allFolders.[job.Job.FolderId] | |
|> List.tryFind (fun j -> j.Job.JobId = job.Job.JobId) | |
|> Option.map (fun j -> j.End) | |
|> Option.iter (fun current -> | |
allFolders.[job.Job.FolderId] | |
|> List.tryFind (fun j -> j.Job.JobId = sIndex) | |
|> Option.map (fun j -> j.Start) | |
|> Option.iter (fun next -> | |
BoundedLinearExpression(Int64.MinValue, current - next, 0L) | |
|> model.Add | |
|> ignore)) | |
/// Ensure task sequence order within jobs is respected | |
let private jobsPrecedence () = | |
folders | |
|> List.iter (fun (_,jList) -> | |
jList | |
|> List.iteri (fun i job -> | |
if i < jList.Length - 1 then | |
job.Job.Predecessors |> List.iter (jobPrecedence job) | |
job.Job.Successors |> List.iter (jobSuccessor job))) | |
let createConstraints () = | |
resourceOverlap() | |
jobsPrecedence() | |
let createObjectives () = | |
allFolders | |
|> List.concat | |
|> List.map (fun j -> j.End) | |
|> fun allEnds -> model.AddMaxEquality(makeSpan,allEnds) | |
|> ignore | |
model.Minimize(makeSpan) | |
let solve () = | |
use solPrinter = new ObjectiveSolutionPrinter() | |
Constraints.createConstraints() | |
createObjectives() | |
solver.SolveWithSolutionCallback(model, solPrinter) | |
|> fun status -> | |
solver.ResponseStats() | |
|> fun s -> s.Split('\n') |> Array.reduce (fun acc elem -> sprintf "%s\n %s " acc elem) | |
|> printfn "\n%s" | |
status | |
|> function | |
| CpSolverStatus.Optimal -> | |
let results = | |
folders | |
|> List.map (fun (_, jList) -> | |
jList | |
|> List.map (fun job -> | |
{| Start = | |
allFolders.[job.Job.FolderId] | |
|> List.find (fun j -> j.Job.JobId = job.Job.JobId) | |
|> fun j -> j.Start | |
|> solver.Value | |
Job = job.Job |})) | |
|> List.concat | |
results | |
|> List.groupBy (fun j -> j.Job.FolderId) | |
|> List.map (fun (folder,jList) -> folder,jList |> List.sortBy (fun j -> j.Start)) | |
|> List.iter (fun (_, jList) -> | |
jList | |
|> List.iter (fun j -> | |
printfn " %s_%s - [%i,%i]" j.Job.FolderName j.Job.JobName j.Start (j.Start + (j.Job.Duration |> int64)))) | |
printfn "\nOptimal Schedule Length: %f" solver.ObjectiveValue | |
Some results | |
| CpSolverStatus.Feasible -> | |
printfn "Feasible solution found, but not optimal." | |
None | |
| _ -> | |
printfn "No solution found!" | |
None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace OR | |
[<AutoOpen>] | |
module Types = | |
open Google.OrTools.Sat | |
type Resource = | |
{ Id: int | |
Max: int } | |
type Job = | |
{ FolderId: int | |
FolderName: string | |
JobId: int | |
JobName: string | |
Duration: int | |
Resources: int list | |
ExclusiveResources: int list | |
StartRequirement: int option | |
EndRequirement: int option | |
Successors: int list | |
Predecessors: int list } | |
static member Init (i: int) = | |
{ FolderId = 0 | |
FolderName = "" | |
JobId = i | |
JobName = "" | |
Duration = 0 | |
Resources = [] | |
ExclusiveResources = [] | |
StartRequirement = None | |
EndRequirement = None | |
Successors = [] | |
Predecessors = [] } | |
type JsonCM = | |
{ Resources: Resource list | |
Jobs: Job list } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment