1"use strict";(globalThis.webpackChunkscheduleopt=globalThis.webpackChunkscheduleopt||[]).push([[2071],{4651(e,n,s){s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>p,frontMatter:()=>l,metadata:()=>a,toc:()=>h});const a=JSON.parse('{"id":"Tutorial/alternatives","title":"Alternative Constraints","description":"So far, every task in our model has been mandatory","source":"@site/docs/Tutorial/alternatives.md","sourceDirName":"Tutorial","slug":"/Tutorial/alternatives","permalink":"/docs/Tutorial/alternatives","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":6,"frontMatter":{"title":"Alternative Constraints","sidebar_position":6},"sidebar":"documentationSidebar","previous":{"title":"Transition Times","permalink":"/docs/Tutorial/transition-times"},"next":{"title":"Reservoir Constraints","permalink":"/docs/Tutorial/reservoir"}}');var t=s(4848),r=s(8453),i=s(4865),o=s(9365);const l={title:"Alternative Constraints",sidebar_position:6},d="Alternative Constraints",c={},h=[{value:"Sanding Options",id:"sanding-options",level:2},{value:"Optional Intervals",id:"optional-intervals",level:2},{value:"How Alternative Works",id:"how-alternative-works",level:3},{value:"Power Sander as a Shared Resource",id:"power-sander-as-a-shared-resource",level:2},{value:"Constraining the Main Interval",id:"constraining-the-main-interval",level:2},{value:"Complete Model with Alternatives",id:"complete-model-with-alternatives",level:2},{value:"What Will the Solver Choose?",id:"what-will-the-solver-choose",level:3},{value:"Absent Semantics",id:"absent-semantics",level:2},{value:"Other Uses of Optional Intervals",id:"other-uses-of-optional-intervals",level:2},{value:"With Alternatives",id:"with-alternatives",level:3},{value:"Without Alternatives",id:"without-alternatives",level:3},{value:"What We Learned",id:"what-we-learned",level:2},{value:"See Also",id:"see-also",level:2},{value:"Next Steps",id:"next-steps",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.header,{children:(0,t.jsx)(n.h1,{id:"alternative-constraints",children:"Alternative Constraints"})}),"\n",(0,t.jsxs)(n.p,{children:["So far, every task in our model has been ",(0,t.jsx)(n.strong,{children:"mandatory"}),": it must be executed exactly once. But real scheduling problems often involve ",(0,t.jsx)(n.strong,{children:"choices"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"A task can be performed on machine A or machine B"}),"\n",(0,t.jsx)(n.li,{children:"A delivery can go by truck or by air"}),"\n",(0,t.jsx)(n.li,{children:"A manufacturing step can use method 1 (slow, cheap) or method 2 (fast, expensive)"}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["In our furniture workshop, let's introduce a choice for sanding: ",(0,t.jsx)(n.strong,{children:"hand sanding"})," (slow but doesn't require equipment) or ",(0,t.jsx)(n.strong,{children:"power sander"})," (faster but the power sander is a shared resource)."]}),"\n",(0,t.jsx)(n.h2,{id:"sanding-options",children:"Sanding Options"}),"\n",(0,t.jsx)(n.p,{children:"We'll modify our model so that sanding can be done two ways:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Hand sanding"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Desk: 30 minutes (slower than the original 20 minutes)"}),"\n",(0,t.jsx)(n.li,{children:"Chair: 25 minutes (slower than the original 15 minutes)"}),"\n",(0,t.jsx)(n.li,{children:"No special equipment needed"}),"\n"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Power sanding"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Desk: 15 minutes (faster)"}),"\n",(0,t.jsx)(n.li,{children:"Chair: 10 minutes (faster)"}),"\n",(0,t.jsx)(n.li,{children:"Requires the power sander (one available, shared between desk and chair)"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"The solver will choose which method to use for each item to minimize makespan."}),"\n",(0,t.jsx)(n.h2,{id:"optional-intervals",children:"Optional Intervals"}),"\n",(0,t.jsxs)(n.p,{children:["To model choices, we use ",(0,t.jsx)(n.strong,{children:"optional intervals"}),": intervals that may or may not be executed. We create one interval for each option, mark them all as ",(0,t.jsx)(n.code,{children:"optional=True"}),", and then add an ",(0,t.jsx)(n.strong,{children:"alternative constraint"})," to ensure exactly one is chosen."]}),"\n","\n",(0,t.jsxs)(i.A,{groupId:"lang",children:[(0,t.jsx)(o.A,{value:"python",label:"Python",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:'# Sanding the desk parts - two options\nsand_desk_parts = model.interval_var(name="SandDeskParts") # Main task (always present)\nsand_desk_hand = model.interval_var(length=30, optional=True, name="SandDeskHand")\nsand_desk_power = model.interval_var(length=15, optional=True, name="SandDeskPower")\n\n# Alternative: exactly one option is executed\nmodel.alternative(sand_desk_parts, [s
1and_desk_hand, sand_desk_power])\n'})})}),(0,t.jsx)(o.A,{value:"csharp",label:"C#",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'// Sanding the desk parts - two options\nvar sandDeskParts = model.IntervalVar(name: "SandDeskParts"); // Main task (always present)\nvar sandDeskHand = model.IntervalVar(length: 30, optional: true, name: "SandDeskHand");\nvar sandDeskPower = model.IntervalVar(length: 15, optional: true, name: "SandDeskPower");\n\n// Alternative: exactly one option is executed\nmodel.Alternative(sandDeskParts, new[] { sandDeskHand, sandDeskPower });\n'})})}),(0,t.jsx)(o.A,{value:"typescript",label:"TypeScript",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:'// Sanding the desk parts - two options\nconst sandDeskParts = model.intervalVar({ name: "SandDeskParts" }); // Main task (always present)\nconst sandDeskHand = model.intervalVar({ length: 30, optional: true, name: "SandDeskHand" });\nconst sandDeskPower = model.intervalVar({ length: 15, optional: true, name: "SandDeskPower" });\n\n// Alternative: exactly one option is executed\nmodel.alternative(sandDeskParts, [sandDeskHand, sandDeskPower]);\n'})})})]}),"\n",(0,t.jsx)(n.h3,{id:"how-alternative-works",children:"How Alternative Works"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"alternative(main, [option1, option2, ...])"})," constraint ensures:"]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Exactly one option is present"}),": One of the options must be executed, and the others are absent"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Main interval covers the chosen option"}),": The main interval starts when the chosen option starts and ends when it ends"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Presence is synchronized"}),": If main is present, exactly one option is present; if main is absent, all options are absent"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["In our model, ",(0,t.jsx)(n.code,{children:"sand_desk_parts"})," is mandatory (not optional), so exactly one sanding method will always be chosen. But the main interval can also be optional\u2014useful when the entire choice is conditional."]}),"\n",(0,t.jsxs)(n.p,{children:['This pattern lets you model choices while maintaining a single "handle" (',(0,t.jsx)(n.code,{children:"sand_desk_parts"}),") that other constraints can reference."]}),"\n",(0,t.jsx)(n.h2,{id:"power-sander-as-a-shared-resource",children:"Power Sander as a Shared Resource"}),"\n",(0,t.jsx)(n.p,{children:"If both desk and chair are power-sanded, they can't use the power sander simultaneously. We add a no-overlap constraint on the power sanding options:"}),"\n",(0,t.jsxs)(i.A,{groupId:"lang",children:[(0,t.jsx)(o.A,{value:"python",label:"Python",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:"# Power sander is shared - can only be used by one item at a time\nmodel.no_overlap([sand_desk_power, sand_chair_power])\n"})})}),(0,t.jsx)(o.A,{value:"csharp",label:"C#",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Power sander is shared - can only be used by one item at a time\nmodel.NoOverlap(new[] { sandDeskPower, sandChairPower });\n"})})}),(0,t.jsx)(o.A,{value:"typescript",label:"TypeScript",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// Power sander is shared - can only be used by one item at a time\nmodel.noOverlap([sandDeskPower, sandChairPower]);\n"})})})]}),"\n",(0,t.jsxs)(n.p,{children:["If one or both sanding operations choose hand sanding instead, those intervals are ",(0,t.jsx)(n.strong,{children:"absent"}),", and they don't participate in the no-overlap constraint. OptalCP handles this automatically."]}),"\n",(0,t.jsx)(n.h2,{id:"constraining-the-main-interval",children:"Constraining the Main Interval"}),"\n",(0,t.jsx)(n.admonition,{title:"Best Practice: Use Main Intervals in Constraints",type:"tip",children:(0,t.jsxs)(n.p,{children:["Use the ",(0,t.jsx)(n.strong,{children:"main interval"})," in constraints and expressions whenever possible\u2014not just for precedence, but for cumulative constraints, objectives, and any other constraints that apply regardless of which option is chosen."]})}),"\n",(0,t.jsxs)(n.p,{children:["Our precedence constraints reference the ",(0,t.jsx)(n.strong,{children:"main intervals"})," (",(0,t.jsx)(n.code,{children:"sand_desk_parts"}),", ",(0,t.jsx)(n.code,{children:"sand_chair_parts"}),"), not the individual options:"]}),"\n",(0,t.jsxs)(i.A,{groupId:"lang",children:[(0,t.jsx)(o.A,{value:"python",label:"Python",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:"# Precedence uses the main intervals\ncut_desk_wood.end_before_start(sand_desk_parts)\nsand_desk_parts.end_before_start(assemble_desk)\n"})})}),(0,t.jsx)(o.A,{value:"csharp",label:"C#",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Precedence uses the main intervals\ncutDeskWood.EndBeforeStart(sandDeskParts);\nsandDeskParts.EndBeforeStart(assembleDesk);\n"})})}),(0,t.jsx)(o.A,{value:"typescript",label:"TypeScript",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// Precedence uses the main intervals\ncutDeskWood.endBeforeStart(sandDeskParts);\nsandDeskParts.endBeforeStart(assembleDesk);\n"})})})]}),"\n",(0,t.jsx)(n.p,{children:"This approach has two benefits:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Simpler model"}),": You write one constraint instead of duplicating it for each option"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Better performance"}),": The solver can propagate constraints more effectively through the main interval"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Since ",(0,t.jsx)(n.code,{children:"sand_desk_parts"})," covers whichever option is chosen, the precedence works correctly regardless of which sanding method is selected."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"When to constrain option intervals instead"}),": Use the option interval when the constraint is specific to that option. For example, the power sander no-overlap constraint only applies to ",(0,t.jsx)(n.code,{children:"sand_desk_power"})," and ",(0,t.jsx)(n.code,{children:"sand_chair_power"}),"\u2014it wouldn't make sense to put it on the main intervals."]}),"\n",(0,t.jsx)(n.h2,{id:"complete-model-with-alternatives",children:"Complete Model with Alternatives"}),"\n",(0,t.jsx)(n.p,{children:"Let's integrate the sanding alternatives into our full model:"}),"\n",(0,t.jsxs)(i.A,{groupId:"lang",children:[(0,t.jsx)(o.A,{value:"python",label:"Python",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-python",children:'import optalcp as cp\n\nmodel = cp.Model()\n\n# Create interval variables\ncut_desk_wood = model.interval_var(length=30, name="CutDeskWood")\ncut_chair_wood = model.interval_var(length=25, name="CutChairWood")\n\n# highlight-start\n# [
1NEW] Sanding alternatives for desk\nsand_desk_parts = model.interval_var(name="SandDeskParts")\nsand_desk_hand = model.interval_var(length=30, optional=True, name="SandDeskHand")\nsand_desk_power = model.interval_var(length=15, optional=True, name="SandDeskPower")\nmodel.alternative(sand_desk_parts, [sand_desk_hand, sand_desk_power])\n\n# [NEW] Sanding alternatives for chair\nsand_chair_parts = model.interval_var(name="SandChairParts")\nsand_chair_hand = model.interval_var(length=25, optional=True, name="SandChairHand")\nsand_chair_power = model.interval_var(length=10, optional=True, name="SandChairPower")\nmodel.alternative(sand_chair_parts, [sand_chair_hand, sand_chair_power])\n# highlight-end\n\n# Rest of the tasks\nassemble_desk = model.interval_var(length=45, name="AssembleDesk")\nassemble_chair = model.interval_var(length=35, name="AssembleChair")\nstain_desk = model.interval_var(length=20, name="StainDesk")\nstain_chair = model.interval_var(length=15, name="StainChair")\napply_finish = model.interval_var(length=30, name="ApplyFinish")\nfinal_inspect = model.interval_var(length=10, name="FinalInspect")\n\n# Precedence constraints - Desk\ncut_desk_wood.end_before_start(sand_desk_parts)\nsand_desk_parts.end_before_start(assemble_desk)\nassemble_desk.end_before_start(stain_desk)\nstain_desk.end_before_start(apply_finish)\n\n# Precedence constraints - Chair\ncut_chair_wood.end_before_start(sand_chair_parts)\nsand_chair_parts.end_before_start(assemble_chair)\nassemble_chair.end_before_start(stain_chair)\nstain_chair.end_before_start(apply_finish)\n\n# Final inspection\napply_finish.end_before_start(final_inspect)\n\n# Resource constraint: one saw\nmodel.no_overlap([cut_desk_wood, cut_chair_wood])\n\n# highlight-start\n# [NEW] Resource constraint: power sander (only if both choose power sanding)\nmodel.no_overlap([sand_desk_power, sand_chair_power])\n# highlight-end\n\n# Resource constraint: 2 workers\n# Note: Use main intervals for worker counting\nworker_usage = [\n model.pulse(cut_desk_wood, 1),\n model.pulse(cut_chair_wood, 1),\n model.pulse(sand_desk_parts, 1),\n model.pulse(sand_chair_parts, 1),\n model.pulse(assemble_desk, 2),\n model.pulse(assemble_chair, 1),\n model.pulse(stain_desk, 1),\n model.pulse(stain_chair, 1),\n model.pulse(apply_finish, 1),\n model.pulse(final_inspect, 1),\n]\nmodel.enforce(model.sum(worker_usage) <= 2)\n\n# Resource constraint: spray booth with transitions\nspray_booth_seq = model.sequence_var([stain_desk, stain_chair, apply_finish])\ntransitions = [\n [0, 10, 5],\n [10, 0, 5],\n [5, 5, 0],\n]\nmodel.no_overlap(spray_booth_seq, transitions)\n\n# Minimize makespan\nmodel.minimize(final_inspect.end())\n\n# Solve\nresult = model.solve()\n\nif result.solution:\n print(f"Makespan: {result.objective} minutes")\n print(f"Optimal: {result.proof}")\n\n # Check which sanding method was chosen\n if result.solution.is_present(sand_desk_hand):\n print("Desk: hand sanding")\n elif result.solution.is_present(sand_desk_power):\n print("Desk: power sanding")\n\n if result.solution.is_present(sand_chair_hand):\n print("Chair: hand sanding")\n elif result.solution.is_present(sand_chair_power):\n print("Chair: power sanding")\n'})})}),(0,t.jsx)(o.A,{value:"csharp",label:"C#",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'using OptalCP;\n\nvar model = new Model();\n\n// Create interval variables\nvar cutDeskWood = model.IntervalVar(length: 30, name: "CutDeskWood");\nvar cutChairWood = model.IntervalVar(length: 25, name: "CutChairWood");\n\n// highlight-start\n// [NEW] Sanding alternatives for desk\nvar sandDeskParts = model.IntervalVar(name: "SandDeskParts");\nvar sandDeskHand = model.IntervalVar(length: 30, optional: true, name: "SandDeskHand");\nvar sandDeskPower = model.IntervalVar(length: 15, optional: true, name: "SandDeskPower");\nmodel.Alternative(sandDeskParts, new[] { sandDeskHand, sandDeskPower });\n\n// [NEW] Sanding alternatives for chair\nvar sandChairParts = model.IntervalVar(name: "SandChairParts");\nvar sandChairHand = model.IntervalVar(length: 25, optional: true, name: "SandChairHand");\nvar sandChairPower = model.IntervalVar(length: 10, optional: true, name: "SandChairPower");
1\nmodel.Alternative(sandChairParts, new[] { sandChairHand, sandChairPower });\n// highlight-end\n\n// Rest of the tasks\nvar assembleDesk = model.IntervalVar(length: 45, name: "AssembleDesk");\nvar assembleChair = model.IntervalVar(length: 35, name: "AssembleChair");\nvar stainDesk = model.IntervalVar(length: 20, name: "StainDesk");\nvar stainChair = model.IntervalVar(length: 15, name: "StainChair");\nvar applyFinish = model.IntervalVar(length: 30, name: "ApplyFinish");\nvar finalInspect = model.IntervalVar(length: 10, name: "FinalInspect");\n\n// Precedence constraints - Desk\ncutDeskWood.EndBeforeStart(sandDeskParts);\nsandDeskParts.EndBeforeStart(assembleDesk);\nassembleDesk.EndBeforeStart(stainDesk);\nstainDesk.EndBeforeStart(applyFinish);\n\n// Precedence constraints - Chair\ncutChairWood.EndBeforeStart(sandChairParts);\nsandChairParts.EndBeforeStart(assembleChair);\nassembleChair.EndBeforeStart(stainChair);\nstainChair.EndBeforeStart(applyFinish);\n\n// Final inspection\napplyFinish.EndBeforeStart(finalInspect);\n\n// Resource constraint: one saw\nmodel.NoOverlap(new[] { cutDeskWood, cutChairWood });\n\n// highlight-start\n// [NEW] Resource constraint: power sander (only if both choose power sanding)\nmodel.NoOverlap(new[] { sandDeskPower, sandChairPower });\n// highlight-end\n\n// Resource constraint: 2 workers\n// Note: Use main intervals for worker counting\nvar workerUsage = new[] {\n model.Pulse(cutDeskWood, 1),\n model.Pulse(cutChairWood, 1),\n model.Pulse(sandDeskParts, 1),\n model.Pulse(sandChairParts, 1),\n model.Pulse(assembleDesk, 2),\n model.Pulse(assembleChair, 1),\n model.Pulse(stainDesk, 1),\n model.Pulse(stainChair, 1),\n model.Pulse(applyFinish, 1),\n model.Pulse(finalInspect, 1),\n};\nmodel.Enforce(model.Sum(workerUsage) <= 2);\n\n// Resource constraint: spray booth with transitions\nvar sprayBoothSeq = model.SequenceVar(new[] { stainDesk, stainChair, applyFinish });\nvar transitions = new int[][] {\n new[] { 0, 10, 5 },\n new[] { 10, 0, 5 },\n new[] { 5, 5, 0 },\n};\nmodel.NoOverlap(sprayBoothSeq, transitions);\n\n// Minimize makespan\nmodel.Minimize(finalInspect.End());\n\n// Solve\nvar result = model.Solve();\n\nif (result.Solution != null)\n{\n Console.WriteLine($"Makespan: {result.Objective} minutes");\n Console.WriteLine($"Optimal: {result.Proof}");\n\n // Check which sanding method was chosen\n if (result.Solution.IsPresent(sandDeskHand))\n Console.WriteLine("Desk: hand sanding");\n else if (result.Solution.IsPresent(sandDeskPower))\n Console.WriteLine("Desk: power sanding");\n\n if (result.Solution.IsPresent(sandChairHand))\n Console.WriteLine("Chair: hand sanding");\n else if (result.Solution.IsPresent(sandChairPower))\n Console.WriteLine("Chair: power sanding");\n}\n'})})}),(0,t.jsx)(o.A,{value:"typescript",label:"TypeScript",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:'import * as CP from \'@scheduleopt/optalcp\';\n\nconst model = new CP.Model();\n\n// Create interval variables\nconst cutDeskWood = model.intervalVar({ length: 30, name: "CutDeskWood" });\nconst cutChairWood = model.intervalVar({ length: 25, name: "CutChairWood" });\n\n// highlight-start\n// [NEW] Sanding alternatives for desk\nconst sandDeskParts = model.intervalVar({ name: "SandDeskParts" });\nconst sandDeskHand = model.intervalVar({ length: 30, optional: true, name: "SandDeskHand" });\nconst sandDeskPower = model.intervalVar({ length: 15, optional: true, name: "SandDeskPower" });\nmodel.alternative(sandDeskParts, [sandDeskHand, sandDeskPower]);
1\n\n// [NEW] Sanding alternatives for chair\nconst sandChairParts = model.intervalVar({ name: "SandChairParts" });\nconst sandChairHand = model.intervalVar({ length: 25, optional: true, name: "SandChairHand" });\nconst sandChairPower = model.intervalVar({ length: 10, optional: true, name: "SandChairPower" });\nmodel.alternative(sandChairParts, [sandChairHand, sandChairPower]);\n// highlight-end\n\n// Rest of the tasks\nconst assembleDesk = model.intervalVar({ length: 45, name: "AssembleDesk" });\nconst assembleChair = model.intervalVar({ length: 35, name: "AssembleChair" });\nconst stainDesk = model.intervalVar({ length: 20, name: "StainDesk" });\nconst stainChair = model.intervalVar({ length: 15, name: "StainChair" });\nconst applyFinish = model.intervalVar({ length: 30, name: "ApplyFinish" });\nconst finalInspect = model.intervalVar({ length: 10, name: "FinalInspect" });\n\n// Precedence constraints - Desk\ncutDeskWood.endBeforeStart(sandDeskParts);\nsandDeskParts.endBeforeStart(assembleDesk);\nassembleDesk.endBeforeStart(stainDesk);\nstainDesk.endBeforeStart(applyFinish);\n\n// Precedence constraints - Chair\ncutChairWood.endBeforeStart(sandChairParts);\nsandChairParts.endBeforeStart(assembleChair);\nassembleChair.endBeforeStart(stainChair);\nstainChair.endBeforeStart(applyFinish);\n\n// Final inspection\napplyFinish.endBeforeStart(finalInspect);\n\n// Resource constraint: one saw\nmodel.noOverlap([cutDeskWood, cutChairWood]);\n\n// highlight-start\n// [NEW] Resource constraint: power sander (only if both choose power sanding)\nmodel.noOverlap([sandDeskPower, sandChairPower]);\n// highlight-end\n\n// Resource constraint: 2 workers\nconst workerUsage = [\n model.pulse(cutDeskWood, 1),\n model.pulse(cutChairWood, 1),\n model.pulse(sandDeskParts, 1),\n model.pulse(sandChairParts, 1),\n model.pulse(assembleDesk, 2),\n model.pulse(assembleChair, 1),\n model.pulse(stainDesk, 1),\n model.pulse(stainChair, 1),\n model.pulse(applyFinish, 1),\n model.pulse(finalInspect, 1),\n];\nmodel.enforce(model.sum(workerUsage).le(2));\n\n// Resource constraint: spray booth with transitions\nconst sprayBoothSeq = model.sequenceVar([stainDesk, stainChair, applyFinish]);\nconst transitions = [\n [0, 10, 5],\n [10, 0, 5],\n [5, 5, 0],\n];\nmodel.noOverlap(sprayBoothSeq, transitions);\n\n// Minimize makespan\nmodel.minimize(finalInspect.end());\n\n// Solve\nconst result = await model.solve();\n\nif (result.solution) {\n console.log(`Makespan: ${result.objective} minutes`);\n console.log(`Optimal: ${result.proof}`);\n\n // Check which sanding method was chosen\n if (result.solution.isPresent(sandDeskHand)) {\n console.log("Desk: hand sanding");\n } else if (result.solution.isPresent(sandDeskPower)) {\n console.log("Desk: power sanding");\n }\n\n if (result.solution.isPresent(sandChairHand)) {\n console.log("Chair: hand sanding");\n } else if (result.solution.isPresent(sandChairPower)) {\n console.log("Chair: power sanding");\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.h3,{id:"what-will-the-solver-choose",children:"What Will the Solver Choose?"}
1),"\n",(0,t.jsx)(n.p,{children:"The solver will compare scenarios:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Both hand sanding"}),": Slower (30 + 25 = 55 minutes total), but can be done in parallel"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Both power sanding"}),": Faster (15 + 10 = 25 minutes total), but must be sequential (shared resource)"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Mixed"}),": One hand, one power\u2014combines benefits and drawbacks"]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["The optimal choice depends on the rest of the schedule. If there's parallelism to exploit, hand sanding might win. If the critical path benefits from speed, power sanding might be better. Building on our previous model (makespan: 200 minutes with transition times), adding alternatives gives the optimal makespan of ",(0,t.jsx)(n.strong,{children:"195 minutes"}),"\u2014the alternatives provide flexibility that improves the schedule."]}),"\n",(0,t.jsx)(n.h2,{id:"absent-semantics",children:"Absent Semantics"}),"\n",(0,t.jsx)(n.p,{children:"Optional intervals interact naturally with other constraints:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"No-overlap"}),": Absent intervals don't participate (as if they don't exist)"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Cumulative"}),": Pulses on absent intervals contribute 0"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Precedence"}),": If an optional interval is absent, constraints involving it are ignored"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Expressions"}),": Expressions involving absent intervals are also absent (unless guarded)"]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"This makes modeling flexible: you don't need special cases for \"what if this task isn't chosen.\""}),"\n",(0,t.jsx)(n.h2,{id:"other-uses-of-optional-intervals",children:"Other Uses of Optional Intervals"}),"\n",(0,t.jsx)(n.h3,{id:"with-alternatives",children:"With Alternatives"}),"\n",(0,t.jsx)(n.p,{children:"Alternative constraints work well for many scenarios:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Machine selection"}),": An operation can be performed on multiple machines (create one optional interval per machine)"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Method selection"}),": A task can use different methods with different durations and resource requirements"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Alternative routes"}),": A delivery can take route A or route B"]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"without-alternatives",children:"Without Alternatives"}),"\n",(0,t.jsxs)(n.p,{children:["Optional intervals are also useful ",(0,t.jsx)(n.strong,{children:"without"})," the alternative constraint:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Oversubscribed systems"}),": You have more requests than you can fulfill\u2014make each request an optional interval and maximize the number of present intervals"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Soft constraints"}),": A task is preferred but not required\u2014make it optional and add a penalty to the objective if it's absent"]}),"\n"]}),"\n",(0,t.jsxs)(n.admonition,{title:"Modeling Pitfalls",type:"warning",children:[(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Don't fake absence with dummy values."}),' Don\'t model "not chosen" by ',(0,t.jsx)(n.code,{children:"length=0"})," or ",(0,t.jsx)(n.code,{children:"start=999999"}),". This creates unnecessary complexity and hurts solver performance. Use ",(0,t.jsx)(n.code,{children:"optional=True"})," instead\u2014that's exactly what optional intervals are for."]}),(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Don't use alternatives for equivalent resources."})," If you have multiple identical workers or machines, use a cumulative constraint (as we did in the ",(0,t.jsx)(n.a,{href:"/docs/Tutorial/cumulative",children:"cumulative chapter"}),"), not alternatives. You can always assign tasks to specific workers in postprocessing. Use alternatives when the options have ",(0,t.jsx)(n.em,{children:"different characteristics"})," (different durations, costs, or capabilities)."]})]}),"\n",(0,t.jsx)(n.h2,{id:"what-we-learned",children:"What We Learned"}),"\n",(0,t.jsx)(n.p,{children:"In this chapter, we:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Created ",(0,t.jsx)(n.strong,{children:"optional intervals"})," to represent choices"]}),"\n",(0,t.jsxs)(n.li,{children:["Used the ",(0,t.jsx)(n.strong,{children:"alternative constraint"})," to select exactly one option"]}),"\n",(0,t.jsxs)(n.li,{children:["Modeled a ",(0,t.jsx)(n.strong,{children:"shared resource"})," (power sander) that's only used if certain options are chosen"]}),"\n",(0,t.jsxs)(n.li,{children:["Checked which options were selected in the solution using ",(0,t.jsx)(n.code,{children:"is_present()"})]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Alternatives are useful for modeling real-world flexibility and trade-offs."}),"\n",(0,t.jsx)(n.h2,{id:"see-also",children:"See Also"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"/docs/Modeling/alternative",children:"Modeling / Alternative"})," \u2014 Complete reference for alternative constraints"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"/docs/Modeling/intervals",children:"Modeling / Intervals"})," \u2014 Optional intervals and absent semantics"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"/docs/Resources/no-overlap",children:"Resources / No Overlap"})," \u2014 How absent intervals interact with no-overlap constraints"]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"next-steps",children:"Next Steps"}),"\n",(0,t.jsxs)(n.p,{children:["In the final chapter, we'll model ",(0,t.jsx)(n.strong,{children:"reservoir resources"}),": cutting and sanding generate dust that accumulates over time, and the workshop must be cleaned before dust exceeds safety limits."]}),"\n",(0,t.jsxs)(n.p,{children:["Continue to: ",(0,t.jsx)(n.a,{href:"/docs/Tutorial/reservoir",children:"Reservoir Constraints \u2192"})]})]})}function p(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},9365(e,n,s){s.d(n,{A:()=>l});s(6540);var a=s(4164),t=s(7751);const r="tabItem_Ymn6";var i=s(4848);function o({children:e,className:n,hidden:s}){return(0,i.jsx)("div",{role:"tabpanel",className:(0,a.A)(r,n),hidden:s,children:e})}function l({children:e,className:n,value:s}){const{selectedValue:a,lazy:r}=(0,t.uc)(),l=s===a;return!l&&r?null:(0,i.jsx)(o,{className:n,hidden:!l,children:e})}},4865(e,n,s){s.d(n,{A:()=>m});s(6540);var a=s(4164),t=s(7559),r=s(7751),i=s(3104),o=s(2303);const l="tabList__CuJ",d="tabItem_LNqP";var c=s(4848);function h({className:e}){const{selectedValue:n,selectValue:s,tabValues:t,block:o}=(0,r.uc)(),l=[],{blockElementScrollPositionUntilNextRender:h}=(0,i.a_)(),u=e=>{const a=e.currentTarget,r=l.indexOf(a),i=t[r].value;i!==n&&(h(a),s(i))},p=e=>{let n=null;switch(e.key){case"Enter":u(e);break;case"ArrowRight":{const s=l.indexOf(e.currentTarget)+1;n=l[s]??l[0];break}case"ArrowLeft":{const s=l.indexOf(e.currentTarget)-1;n=l[s]??l[l.length-1];break}}n?.focus()};return(0,c.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":o},e),children:t.map((({value:e,label:s,attributes:t})=>(0,c.jsx)("li",{role:"tab",tabIndex:n===e?0:-1,"aria-selected":n===e,ref:e=>{l.push(e)},onKeyDown:p,onClick:u,...t,className:(0,a.A)("tabs__item",d,t?.className,{"tabs__item--active":n===e}),children:s??e},e)))})}function u({children:e}){return(0,c.jsx)("div",{className:"margin-top--md",children:e})}function p({className:e,children:n}){return(0,c.jsxs)("div",{className:(0,a.A)(t.G.tabs.container,"tabs-container",l),children:[(0,c.jsx)(h,{className:e}),(0,c.jsx)(u,{children:n})]})}function m(e){const n=(0,o.A)(),s=(0,r.OC)(e);return(0,c.jsx)(r.O_,{value:s,children:(0,c.jsx)(p,{className:e.className,children:(0,r.vT)(e.children)})},String(n))}}
1,7751(e,n,s){s.d(n,{OC:()=>m,O_:()=>f,uc:()=>g,vT:()=>c});var a=s(6540),t=s(6347),r=s(205),i=s(7485),o=s(679),l=s(1682),d=s(4848);function c(e){return a.Children.toArray(e).filter((e=>"\n"!==e))}function h(e){const{values:n,children:s}=e;return(0,a.useMemo)((()=>{const e=n??function(e){return a.Children.toArray(e).flatMap((e=>{if(!e)return[];if((0,a.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return[e];const n="string"==typeof e.type?e.type:e.type.name;throw new Error(`Docusaurus error: Bad <Tabs> child <${n}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.\nIf you do not want to pass on a "value" prop to the direct children of <Tabs>, you can also pass an explicit <Tabs values={...}> prop.`)})).map((({props:{value:e,label:n,attributes:s,default:a}})=>({value:e,label:n,attributes:s,default:a})))}(s);return function(e){const n=(0,l.XI)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>`'${e.value}'`)).join(", ")}" found in <Tabs>. Every value needs to be unique.`)}(e),e}),[n,s])}function u({value:e,tabValues:n}){return n.some((n=>n.value===e))}function p({queryString:e=!1,groupId:n}){const s=(0,t.W6)(),r=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,i.aZ)(r),(0,a.useCallback)((e=>{if(!r)return;const n=new URLSearchParams(s.location.search);n.set(r,e),s.replace({...s.location,search:n.toString()})}),[r,s])]}function m(e){const{defaultValue:n,queryString:s=!1,groupId:t}=e,i=h(e),[l,d]=(0,a.useState)((()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the <Tabs> component requires at least one <TabItem> children component");if(e){if(!u({value:e,tabValues:n}))throw new Error(`Docusaurus error: The <Tabs> has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const s=n.find((e=>e.default))??n[0];if(!s)throw new Error("Unexpected error: 0 tabValues");return s.value}({defaultValue:n,tabValues:i}))),[c,m]=p({queryString:s,groupId:t}),[v,g]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[s,t]=(0,o.Dv)(n);return[s,(0,a.useCallback)((e=>{n&&t.set(e)}),[n,t])]}({groupId:t}),f=(()=>{const e=c??v;return u({value:e,tabValues:i})?e:null})();(0,r.A)((()=>{f&&d(f)}),[f]);return{selectedValue:l,selectValue:(0,a.useCallback)((e=>{if(!u({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);d(e),m(e),g(e)}),[m,g,i]),tabValues:i,lazy:e.lazy??!1,block:e.block??!1}}const v=(0,a.createContext)(null);function g(){const e=a.useContext(v);if(!e)throw new Error("useTabsContext() must be used within a Tabs component");return e}function f(e){return(0,d.jsx)(v.Provider,{value:e.value,children:e.children})}},8453(e,n,s){s.d(n,{R:()=>i,x:()=>o});var a=s(6540);const t={},r=a.createContext(t);function i(e){const n=a.useContext(r);return a.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),a.createElement(r.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.