[{"data":1,"prerenderedAt":859},["ShallowReactive",2],{"dev-\u002Fdeveloper\u002Fguides\u002Fscheduling":3},{"id":4,"title":5,"body":6,"description":839,"extension":840,"faq":841,"meta":854,"navigation":88,"noindex":88,"path":855,"seo":856,"stem":857,"__hash__":858},"content\u002Fdeveloper\u002Fguides\u002Fscheduling.md","Schedule jobs to minimize makespan in Python",{"type":7,"value":8,"toc":833},"minimark",[9,13,31,38,43,54,303,310,332,335,339,361,740,745,749,763,773,777,813,824,829],[10,11,5],"h1",{"id":12},"schedule-jobs-to-minimize-makespan-in-python",[14,15,16,17,21,22,25,26,30],"p",{},"A recurring scheduling problem: you have a set of jobs with known durations and\nseveral identical machines (or workers, cores, servers), and you want to split\nthe jobs so everything finishes as early as possible. The finish time of the\nbusiest machine is the ",[18,19,20],"strong",{},"makespan",", and minimizing it is the classic\nparallel-machine scheduling problem. This is ",[18,23,24],{},"load balancing across machines","\n— the ",[27,28,29],"code",{},"P || Cmax"," problem — not job-shop or staff rostering.",[14,32,33,34,37],{},"The obvious approach is to try every way of assigning jobs to machines. The\napproach that scales is to model it and hand it to a solver — with ",[18,35,36],{},"quicopt","\nthat is a few lines of Python.",[39,40,42],"h2",{"id":41},"the-brute-force-trap","The brute-force trap",[14,44,45,46,49,50,53],{},"\"Try every assignment\" means looping over all ways to place ",[27,47,48],{},"N"," jobs on ",[27,51,52],{},"M","\nmachines:",[55,56,62],"pre",{"className":57,"code":58,"filename":59,"language":60,"meta":61,"style":61},"language-python shiki shiki-themes github-dark","from itertools import product\n\nproc = [3, 1, 6, 4, 2, 5, 7, 2]   # processing time per job\nM = 3\nN = len(proc)\n\ndef makespan(assign):\n    load = [0] * M\n    for j, m in enumerate(assign):\n        load[m] += proc[j]\n    return max(load)\n\nprint(min(makespan(a) for a in product(range(M), repeat=N)))  # -> 10\n","brute_force.py","python","",[27,63,64,83,90,148,159,173,178,191,213,230,242,254,259],{"__ignoreMap":61},[65,66,69,73,77,80],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"snl16","from",[65,74,76],{"class":75},"s95oV"," itertools ",[65,78,79],{"class":71},"import",[65,81,82],{"class":75}," product\n",[65,84,86],{"class":67,"line":85},2,[65,87,89],{"emptyLinePlaceholder":88},true,"\n",[65,91,93,96,99,102,106,109,112,114,117,119,122,124,127,129,132,134,137,139,141,144],{"class":67,"line":92},3,[65,94,95],{"class":75},"proc ",[65,97,98],{"class":71},"=",[65,100,101],{"class":75}," [",[65,103,105],{"class":104},"sDLfK","3",[65,107,108],{"class":75},", ",[65,110,111],{"class":104},"1",[65,113,108],{"class":75},[65,115,116],{"class":104},"6",[65,118,108],{"class":75},[65,120,121],{"class":104},"4",[65,123,108],{"class":75},[65,125,126],{"class":104},"2",[65,128,108],{"class":75},[65,130,131],{"class":104},"5",[65,133,108],{"class":75},[65,135,136],{"class":104},"7",[65,138,108],{"class":75},[65,140,126],{"class":104},[65,142,143],{"class":75},"]   ",[65,145,147],{"class":146},"sAwPA","# processing time per job\n",[65,149,151,154,156],{"class":67,"line":150},4,[65,152,153],{"class":75},"M ",[65,155,98],{"class":71},[65,157,158],{"class":104}," 3\n",[65,160,162,165,167,170],{"class":67,"line":161},5,[65,163,164],{"class":75},"N ",[65,166,98],{"class":71},[65,168,169],{"class":104}," len",[65,171,172],{"class":75},"(proc)\n",[65,174,176],{"class":67,"line":175},6,[65,177,89],{"emptyLinePlaceholder":88},[65,179,181,184,188],{"class":67,"line":180},7,[65,182,183],{"class":71},"def",[65,185,187],{"class":186},"svObZ"," makespan",[65,189,190],{"class":75},"(assign):\n",[65,192,194,197,199,201,204,207,210],{"class":67,"line":193},8,[65,195,196],{"class":75},"    load ",[65,198,98],{"class":71},[65,200,101],{"class":75},[65,202,203],{"class":104},"0",[65,205,206],{"class":75},"] ",[65,208,209],{"class":71},"*",[65,211,212],{"class":75}," M\n",[65,214,216,219,222,225,228],{"class":67,"line":215},9,[65,217,218],{"class":71},"    for",[65,220,221],{"class":75}," j, m ",[65,223,224],{"class":71},"in",[65,226,227],{"class":104}," enumerate",[65,229,190],{"class":75},[65,231,233,236,239],{"class":67,"line":232},10,[65,234,235],{"class":75},"        load[m] ",[65,237,238],{"class":71},"+=",[65,240,241],{"class":75}," proc[j]\n",[65,243,245,248,251],{"class":67,"line":244},11,[65,246,247],{"class":71},"    return",[65,249,250],{"class":104}," max",[65,252,253],{"class":75},"(load)\n",[65,255,257],{"class":67,"line":256},12,[65,258,89],{"emptyLinePlaceholder":88},[65,260,262,265,268,271,274,277,280,282,285,288,291,295,297,300],{"class":67,"line":261},13,[65,263,264],{"class":104},"print",[65,266,267],{"class":75},"(",[65,269,270],{"class":104},"min",[65,272,273],{"class":75},"(makespan(a) ",[65,275,276],{"class":71},"for",[65,278,279],{"class":75}," a ",[65,281,224],{"class":71},[65,283,284],{"class":75}," product(",[65,286,287],{"class":104},"range",[65,289,290],{"class":75},"(M), ",[65,292,294],{"class":293},"s9osk","repeat",[65,296,98],{"class":71},[65,298,299],{"class":75},"N)))  ",[65,301,302],{"class":146},"# -> 10\n",[14,304,305,306,309],{},"Fine for eight jobs on three machines. But the number of assignments is ",[27,307,308],{},"M^N",",\nwhich explodes:",[311,312,313,320,326],"ul",{},[314,315,316,319],"li",{},[18,317,318],{},"8 jobs, 3 machines"," → 6,561 (instant)",[314,321,322,325],{},[18,323,324],{},"20 jobs, 3 machines"," → 3,486,784,401",[314,327,328,331],{},[18,329,330],{},"20 jobs, 5 machines"," → 95,367,431,640,625",[14,333,334],{},"Enumeration is hopeless the moment the shop is real.",[39,336,338],{"id":337},"model-it-as-a-milp","Model it as a MILP",[14,340,341,342,345,346,349,350,353,354,357,358,360],{},"Makespan minimization is a ",[18,343,344],{},"mixed-integer linear program (MILP)",". A binary\nvariable ",[27,347,348],{},"x[j][m]"," puts job ",[27,351,352],{},"j"," on machine ",[27,355,356],{},"m",". Each job goes on exactly one\nmachine; one continuous variable ",[27,359,20],{}," is forced to be at least the load of\nevery machine; and we minimize it:",[55,362,365],{"className":57,"code":363,"filename":364,"language":60,"meta":61,"style":61},"from ortools.math_opt.python import mathopt\nfrom quicopt import Client\n\n# Distribute N jobs across M identical machines to minimize the makespan\n# (the finish time of the busiest machine).\nproc = [3, 1, 6, 4, 2, 5, 7, 2]   # processing time per job\nM = 3\nN = len(proc)\n\nmodel = mathopt.Model(name=\"scheduling\")\nx = [[model.add_binary_variable(name=f\"x_{j}_{m}\") for m in range(M)] for j in range(N)]\nmakespan = model.add_variable(lb=0.0, name=\"makespan\")\n\nfor j in range(N):                                   # each job on exactly one machine\n    model.add_linear_constraint(sum(x[j][m] for m in range(M)) == 1)\nfor m in range(M):                                   # makespan >= load of every machine\n    model.add_linear_constraint(sum(proc[j] * x[j][m] for j in range(N)) \u003C= makespan)\n\nmodel.minimize(makespan)\n\nclient = Client(\"https:\u002F\u002Ftry.quicoptapi.pgi.fz-juelich.de\")\nresult = client.solve(model)\nprint(result.display)\n","scheduling.py",[27,366,367,379,391,395,400,405,447,455,465,469,491,559,588,592,609,640,657,689,694,700,705,721,732],{"__ignoreMap":61},[65,368,369,371,374,376],{"class":67,"line":68},[65,370,72],{"class":71},[65,372,373],{"class":75}," ortools.math_opt.python ",[65,375,79],{"class":71},[65,377,378],{"class":75}," mathopt\n",[65,380,381,383,386,388],{"class":67,"line":85},[65,382,72],{"class":71},[65,384,385],{"class":75}," quicopt ",[65,387,79],{"class":71},[65,389,390],{"class":75}," Client\n",[65,392,393],{"class":67,"line":92},[65,394,89],{"emptyLinePlaceholder":88},[65,396,397],{"class":67,"line":150},[65,398,399],{"class":146},"# Distribute N jobs across M identical machines to minimize the makespan\n",[65,401,402],{"class":67,"line":161},[65,403,404],{"class":146},"# (the finish time of the busiest machine).\n",[65,406,407,409,411,413,415,417,419,421,423,425,427,429,431,433,435,437,439,441,443,445],{"class":67,"line":175},[65,408,95],{"class":75},[65,410,98],{"class":71},[65,412,101],{"class":75},[65,414,105],{"class":104},[65,416,108],{"class":75},[65,418,111],{"class":104},[65,420,108],{"class":75},[65,422,116],{"class":104},[65,424,108],{"class":75},[65,426,121],{"class":104},[65,428,108],{"class":75},[65,430,126],{"class":104},[65,432,108],{"class":75},[65,434,131],{"class":104},[65,436,108],{"class":75},[65,438,136],{"class":104},[65,440,108],{"class":75},[65,442,126],{"class":104},[65,444,143],{"class":75},[65,446,147],{"class":146},[65,448,449,451,453],{"class":67,"line":180},[65,450,153],{"class":75},[65,452,98],{"class":71},[65,454,158],{"class":104},[65,456,457,459,461,463],{"class":67,"line":193},[65,458,164],{"class":75},[65,460,98],{"class":71},[65,462,169],{"class":104},[65,464,172],{"class":75},[65,466,467],{"class":67,"line":215},[65,468,89],{"emptyLinePlaceholder":88},[65,470,471,474,476,479,482,484,488],{"class":67,"line":232},[65,472,473],{"class":75},"model ",[65,475,98],{"class":71},[65,477,478],{"class":75}," mathopt.Model(",[65,480,481],{"class":293},"name",[65,483,98],{"class":71},[65,485,487],{"class":486},"sU2Wk","\"scheduling\"",[65,489,490],{"class":75},")\n",[65,492,493,496,498,501,503,505,508,511,514,516,519,522,524,526,528,531,534,536,539,541,544,547,549,552,554,556],{"class":67,"line":244},[65,494,495],{"class":75},"x ",[65,497,98],{"class":71},[65,499,500],{"class":75}," [[model.add_binary_variable(",[65,502,481],{"class":293},[65,504,98],{"class":71},[65,506,507],{"class":71},"f",[65,509,510],{"class":486},"\"x_",[65,512,513],{"class":104},"{",[65,515,352],{"class":75},[65,517,518],{"class":104},"}",[65,520,521],{"class":486},"_",[65,523,513],{"class":104},[65,525,356],{"class":75},[65,527,518],{"class":104},[65,529,530],{"class":486},"\"",[65,532,533],{"class":75},") ",[65,535,276],{"class":71},[65,537,538],{"class":75}," m ",[65,540,224],{"class":71},[65,542,543],{"class":104}," range",[65,545,546],{"class":75},"(M)] ",[65,548,276],{"class":71},[65,550,551],{"class":75}," j ",[65,553,224],{"class":71},[65,555,543],{"class":104},[65,557,558],{"class":75},"(N)]\n",[65,560,561,564,566,569,572,574,577,579,581,583,586],{"class":67,"line":256},[65,562,563],{"class":75},"makespan ",[65,565,98],{"class":71},[65,567,568],{"class":75}," model.add_variable(",[65,570,571],{"class":293},"lb",[65,573,98],{"class":71},[65,575,576],{"class":104},"0.0",[65,578,108],{"class":75},[65,580,481],{"class":293},[65,582,98],{"class":71},[65,584,585],{"class":486},"\"makespan\"",[65,587,490],{"class":75},[65,589,590],{"class":67,"line":261},[65,591,89],{"emptyLinePlaceholder":88},[65,593,595,597,599,601,603,606],{"class":67,"line":594},14,[65,596,276],{"class":71},[65,598,551],{"class":75},[65,600,224],{"class":71},[65,602,543],{"class":104},[65,604,605],{"class":75},"(N):                                   ",[65,607,608],{"class":146},"# each job on exactly one machine\n",[65,610,612,615,618,621,623,625,627,629,632,635,638],{"class":67,"line":611},15,[65,613,614],{"class":75},"    model.add_linear_constraint(",[65,616,617],{"class":104},"sum",[65,619,620],{"class":75},"(x[j][m] ",[65,622,276],{"class":71},[65,624,538],{"class":75},[65,626,224],{"class":71},[65,628,543],{"class":104},[65,630,631],{"class":75},"(M)) ",[65,633,634],{"class":71},"==",[65,636,637],{"class":104}," 1",[65,639,490],{"class":75},[65,641,643,645,647,649,651,654],{"class":67,"line":642},16,[65,644,276],{"class":71},[65,646,538],{"class":75},[65,648,224],{"class":71},[65,650,543],{"class":104},[65,652,653],{"class":75},"(M):                                   ",[65,655,656],{"class":146},"# makespan >= load of every machine\n",[65,658,660,662,664,667,669,672,674,676,678,680,683,686],{"class":67,"line":659},17,[65,661,614],{"class":75},[65,663,617],{"class":104},[65,665,666],{"class":75},"(proc[j] ",[65,668,209],{"class":71},[65,670,671],{"class":75}," x[j][m] ",[65,673,276],{"class":71},[65,675,551],{"class":75},[65,677,224],{"class":71},[65,679,543],{"class":104},[65,681,682],{"class":75},"(N)) ",[65,684,685],{"class":71},"\u003C=",[65,687,688],{"class":75}," makespan)\n",[65,690,692],{"class":67,"line":691},18,[65,693,89],{"emptyLinePlaceholder":88},[65,695,697],{"class":67,"line":696},19,[65,698,699],{"class":75},"model.minimize(makespan)\n",[65,701,703],{"class":67,"line":702},20,[65,704,89],{"emptyLinePlaceholder":88},[65,706,708,711,713,716,719],{"class":67,"line":707},21,[65,709,710],{"class":75},"client ",[65,712,98],{"class":71},[65,714,715],{"class":75}," Client(",[65,717,718],{"class":486},"\"https:\u002F\u002Ftry.quicoptapi.pgi.fz-juelich.de\"",[65,720,490],{"class":75},[65,722,724,727,729],{"class":67,"line":723},22,[65,725,726],{"class":75},"result ",[65,728,98],{"class":71},[65,730,731],{"class":75}," client.solve(model)\n",[65,733,735,737],{"class":67,"line":734},23,[65,736,264],{"class":104},[65,738,739],{"class":75},"(result.display)\n",[741,742],"term-result",{":rows":743,"cmd":744},"[\"├── status:     optimal\",\"├── feasible:   true\",\"├── objective:  10.0\",\"├── x:          makespan=10, x_0_0=0, x_0_1=0, x_0_2=1, x_1_0=0, x_1_1=1, …  (25 variables)\",\"└── solve_time: 0.0196 s\"]","$ python scheduling.py",[39,746,748],{"id":747},"what-you-get","What you get",[14,750,751,754,755,758,759,762],{},[27,752,753],{},"status: optimal"," means the solver ",[18,756,757],{},"proved"," the makespan can't go below ",[27,760,761],{},"10",".\nThe total work is 30 units over three machines, so 10 is a perfect balance — and\nthe solver found an assignment that hits it, in a fraction of a second.",[14,764,765,766,769,770,772],{},"The difference is scaling. Brute force is ",[27,767,768],{},"O(M^n)","; a MILP solver prunes the\nsearch with bounds instead of enumerating it. The same model that schedules 8\njobs schedules 800 — you change the data, not the approach. (This is the\n",[27,771,29],{}," problem, NP-hard in general, which is exactly why a solver beats\nhand-rolled search.)",[39,774,776],{"id":775},"next","Next",[311,778,779,787,799,806],{},[314,780,781,782],{},"The problem class behind this: ",[783,784,786],"a",{"href":785},"\u002Fproblems\u002Fmilp","MILP",[314,788,789,790,794,795],{},"Related guides: ",[783,791,793],{"href":792},"\u002Fdeveloper\u002Fguides\u002Fshift-scheduling","Employee shift scheduling"," · ",[783,796,798],{"href":797},"\u002Fdeveloper\u002Fguides\u002Fassignment","Assignment",[314,800,801,802],{},"A runnable model for every supported class: ",[783,803,805],{"href":804},"\u002Fdeveloper\u002Fexamples","Examples",[314,807,808,809],{},"Set up the client and solve your first model: ",[783,810,812],{"href":811},"\u002Fdeveloper\u002Fgetting-started","Getting started",[14,814,815,818,819,823],{},[18,816,817],{},"Reference:"," R. L. Graham, ",[820,821,822],"em",{},"Bounds on Multiprocessing Timing Anomalies",", SIAM\nJournal on Applied Mathematics, 1969.",[825,826],"contact-cta",{"sub":827,"title":828},"Tell us what you're optimizing. We'll help you model it and point you at the right approach.","A bigger schedule — or a different problem?",[830,831,832],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":61,"searchDepth":85,"depth":85,"links":834},[835,836,837,838],{"id":41,"depth":85,"text":42},{"id":337,"depth":85,"text":338},{"id":747,"depth":85,"text":748},{"id":775,"depth":85,"text":776},"Distribute jobs across machines to minimize the makespan in Python — parallel-machine job scheduling modeled as a MILP and solved with quicopt in a few lines, instead of brute-forcing every assignment.","md",[842,845,848,851],{"q":843,"a":844},"Is this job-shop scheduling?","No — this is parallel-machine makespan (P||Cmax). Job-shop, with per-machine operation sequences and precedence, is a different and larger MILP.",{"q":846,"a":847},"Is this the same as employee shift scheduling?","No — that's workforce rostering (covering demand with staff); see the shift-scheduling guide. This one balances jobs across machines to finish early.",{"q":849,"a":850},"How do I add release times or precedence?","As more linear constraints on the same model — a job can't start before its release time, or must finish before another begins.",{"q":852,"a":853},"Is quicopt free to use?","Yes — pip install quicopt and your first call sets up a free key, no license.",{},"\u002Fdeveloper\u002Fguides\u002Fscheduling",{"title":5,"description":839},"developer\u002Fguides\u002Fscheduling","MmtfOBnDp0F-JXNl6jhsE7u4dNVp0vq3VfeZCzTgdMg",1784110685995]