Skip to content

Instantly share code, notes, and snippets.

@Shmew
Last active July 14, 2020 09:36
Show Gist options
  • Save Shmew/25e1172ee6d0ce0f97ba5762e5be2c0d to your computer and use it in GitHub Desktop.
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#
{
"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 ]
}
]
}
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
namespace OR
module App =
[<EntryPoint>]
let main _ =
ControlM.solve() |> ignore
0
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
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