Skip to content

Quisim Cookbook ​

This cookbook collects short recipes for modeling tasks that come up again and again in simulation studies. Each recipe answers one question with a small, complete model that you can paste into Quisim and run. It assumes you know the basics from the Quisim Simulation Modeling Handbook: entities, blocks, pipes and wires, and the Statistics block. The Block API Reference lists every attribute and socket. The Examples, the Hospital Case Study and the Extrusion Line Case Study cover larger models.

Every recipe has the same parts:

  • Idea: the modeling trick in two or three sentences.
  • Model: a minimal model with fixed seeds, so your run is reproducible.
  • What to look at: which numbers or plots answer the question, with the values from a test run.
  • Variation: a common change to the recipe.
  • Blocks: links to the API reference.

The numbers quoted come from runs of exactly the model shown. With another Quisim version your numbers may differ slightly. The Statistics block reports mean ± radius of the 95% confidence interval (see Reading the confidence interval). Counts such as "parts produced" are per run, with cumulative: true.

Recipes

  1. Routing by probability
  2. Priorities in a queue
  3. Limited waiting room, balking and reneging
  4. Machine breakdowns
  5. Shift schedules and time-dependent arrivals
  6. Batching
  7. Assembly and synchronization
  8. A resource shared between stations
  9. Several lanes or one line
  10. Counting and throughput
  11. Waiting time, flow time and service level
  12. Choosing a warm-up period
  13. Replications and confidence intervals for a terminating system
  14. Comparing two configurations
  15. Optimizing a parameter

How do I send a fixed share of entities elsewhere? ​

Example: 20% of the parts fail inspection and go to a rework station.

Idea. A Decide block sends each entity to outlet ⌊S⌋\lfloor S \rfloor. Wire a RandomUniform on [p,1+p)[p, 1 + p) into S: the value is ≥1\ge 1 with probability pp, so a share pp goes to outlets[1] and the rest to outlets[0]. Give the routing its own seed, so that it does not disturb the other random streams.

diagramdiagram
Model codeOpen in Quisim
qml
// 20% of the parts need rework at a second station
RandomExponential { id: rndA; mean: 100; seed: 11 }
Steering { id: steering0; duration: 100000; replications: 5; seed: 1 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: queue0.inlet }
Queue { id: queue0 }
Pipe { from: queue0.outlet; to: perform0.inlet }
RandomExponential { id: rndB; mean: 70; seed: 22 }
Wire { from: rndB.measureRandomVariate; to: perform0.parameterDuration }
Perform { id: perform0; label: "Machine" }
Pipe { from: perform0.outlet; to: decide0.inlet }
RandomUniform { id: rndRoute; lower: 0.2; upper: 1.2; seed: 33 }   // P[U >= 1] = 0.2
Wire { from: rndRoute.measureRandomVariate; to: decide0.parameterSelectedOutlet }
Decide { id: decide0; label: "Inspection" }
Pipe { from: decide0.outlets[0]; to: leaveOk.inlet }
Pipe { from: decide0.outlets[1]; to: queueRework.inlet }
Queue { id: queueRework }
Pipe { from: queueRework.outlet; to: performRework.inlet }
Perform { id: performRework; duration: 150; label: "Rework" }
Pipe { from: performRework.outlet; to: leaveRework.inlet }
Leave { id: leaveOk; label: "Good" }
Leave { id: leaveRework; label: "Reworked" }
Statistics { id: stats0; numParameters: 2; cumulative: true }
Wire { from: leaveOk.measureNumPassed; to: stats0.parameters[0] }
Wire { from: leaveRework.measureNumPassed; to: stats0.parameters[1] }

What to look at. The Statistics block counts the parts per run: 812 ± 12 good and 201 ± 12 reworked, a rework share of 19.8%. Check such a share whenever you build a Decide: a Decide with nothing wired to S routes nothing at all (it is a closed gate).

Variation. For more than two routes, set numOutlets and use a Function with cumulative probabilities, e.g. for 50/30/20%: M[0] = P[0] < 0.5 ? 0 : (P[0] < 0.8 ? 1 : 2) with a RandomUniform on [0, 1) wired into P[0]. For a rework loop, pipe decide0.outlets[1] back into queue0.inlet instead of a separate station; pipes may form cycles.

Blocks: Decide, RandomUniform, Leave, Statistics.


How do I serve urgent customers first? ​

Example: 30% of the customers are urgent and jump ahead of the others in the queue.

Idea. A Set block writes a priority attribute onto each entity, and the Queue sorts by it: criterion: "priority" serves the lowest value first (ascending: false reverses this). To measure each class separately, read the attribute back after service with a Get block and split the stream with a Decide.

diagramdiagram
Model codeOpen in Quisim
qml
// Two customer classes share one server; class 0 (urgent, 30%) is served first
RandomExponential { id: rndA; mean: 100; seed: 11 }
Steering { id: steering0; duration: 200000; replications: 10; seed: 1 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: setPrio.inlet }
RandomUniform { id: rndClass; lower: 0.7; upper: 1.7; floor: true; seed: 22 }  // 0 with prob. 0.3, else 1
Wire { from: rndClass.measureRandomVariate; to: setPrio.parameterValue }
Set { id: setPrio; attribute: "priority" }
Pipe { from: setPrio.outlet; to: queue0.inlet }
Queue { id: queue0; criterion: "priority" }        // lowest value first
Pipe { from: queue0.outlet; to: perform0.inlet }
RandomExponential { id: rndB; mean: 80; seed: 33 }
Wire { from: rndB.measureRandomVariate; to: perform0.parameterDuration }
Perform { id: perform0 }
Pipe { from: perform0.outlet; to: getPrio.inlet }
Get { id: getPrio; attribute: "priority" }
Pipe { from: getPrio.outlet; to: decide0.inlet }
Wire { from: getPrio.measureValue; to: decide0.parameterSelectedOutlet }
Decide { id: decide0 }
Pipe { from: decide0.outlets[0]; to: leaveUrgent.inlet }
Pipe { from: decide0.outlets[1]; to: leaveNormal.inlet }
Leave { id: leaveUrgent }
Leave { id: leaveNormal }
Statistics { id: stats0; numParameters: 2; label: "Time in system\nurgent / normal" }
Wire { from: leaveUrgent.measureSojourn; to: stats0.parameters[0] }
Wire { from: leaveNormal.measureSojourn; to: stats0.parameters[1] }

What to look at. The mean time in system is 167 ± 6 for urgent and 503 ± 60 for normal customers. Without the criterion (plain FIFO) both classes get about 400 (407 ± 43 and 399 ± 43), the M/M/1 value for utilization 0.8. Priorities do not make the server faster. They move waiting time from one class to the other.

Variation. Priority can also come from the pipes instead of an attribute: give each class its own Queue and pipe all queues into the same Perform. A free Perform pulls from the pipe defined last first (see Entity Handover Protocol). The Triage example in Quisim's example list works like this. For a rule that depends on the current state, such as "serve the emergency queue whenever it is not empty", use a Select block driven by a Function (see the extended hospital model).

Blocks: Set, Queue, Get, Decide, RandomUniform.


How do I model a full waiting room and impatient customers? ​

Example: a ticket counter with 5 waiting places. Customers who find it full walk away (balking), and those who wait longer than 300 time units give up (reneging).

Idea. capacity limits the Queue. When it is full, the Arrive block offers the customer to its next pipe, a Leave block for lost customers. The pipe defined last is tried first, so define the pipe to the Queue last. renegeDuration sends a customer who has waited too long to the outRenege socket.

diagramdiagram
Model codeOpen in Quisim
qml
// A ticket counter: at most 5 waiting places (balking), customers give up after 300 (reneging)
RandomExponential { id: rndA; mean: 90; seed: 11 }
Steering { id: steering0; duration: 100000; replications: 10; seed: 1 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: leaveBalk.inlet }    // tried second: only when the queue is full
Pipe { from: arrive0.outlet; to: queue0.inlet }       // defined last = tried first
Queue { id: queue0; capacity: 5; renegeDuration: 300 }
Pipe { from: queue0.outlet; to: perform0.inlet }
Pipe { from: queue0.outRenege; to: leaveRenege.inlet }
RandomExponential { id: rndB; mean: 100; seed: 22 }
Wire { from: rndB.measureRandomVariate; to: perform0.parameterDuration }
Perform { id: perform0 }
Pipe { from: perform0.outlet; to: leaveServed.inlet }
Leave { id: leaveServed; label: "Served" }
Leave { id: leaveBalk; label: "Balked" }
Leave { id: leaveRenege; label: "Reneged" }
Statistics { id: stats0; numParameters: 4; cumulative: true; label: "arrived\nserved\nbalked\nreneged" }
Wire { from: arrive0.measureNumPassed; to: stats0.parameters[0]; bend:281; incline:33; debend:304; decline:410 }
Wire { from: leaveServed.measureNumPassed; to: stats0.parameters[1] }
Wire { from: leaveBalk.measureNumPassed; to: stats0.parameters[2] }
Wire { from: leaveRenege.measureNumPassed; to: stats0.parameters[3] }

What to look at. Per run, 1121 ± 17 customers arrive, 845 ± 16 are served, 62 ± 6 balk and 210 ± 11 renege, so about 25% are lost. Arrivals equal served + balked + reneged + those still inside at the end. Use this balance to check the model. Without the balking pipe, a full queue would block the Arrive block and silently delay the arrival process.

Variation. For a loss system with no waiting room at all (Erlang B), pipe Arrive directly to a Perform with capacity: c and to a Leave for rejected customers, as in the Capacity example in Quisim. For a random patience, wire a random block into queue0.parameterRenegeDuration. The value is drawn when the customer enters the queue.

Blocks: Queue, Arrive, Leave, Pipe.


How do I model machine breakdowns? ​

Example: a machine runs for an exponential time with mean 2000, then is repaired for an exponential time with mean 200.

Idea. One extra entity, the failure token, cycles forever between an "Up time" Perform and a "Repair" Perform. A Function turns the repair state into the machine's capacity: 1 − (load of Repair), i.e. 0 while in repair and 1 otherwise. When the capacity drops to 0, the job in service is finished, but no new job starts. Provide → Meter { capacity: 1 } creates the single token.

diagramdiagram
Model codeOpen in Quisim
qml
// A machine fails after Exp(2000) of operation and is repaired in Exp(200)
Steering { id: steering0; duration: 200000; replications: 10; seed: 1 }
RandomExponential { id: rndA; mean: 100; seed: 11 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: queue0.inlet }
Queue { id: queue0 }
Pipe { from: queue0.outlet; to: perform0.inlet }
RandomExponential { id: rndB; mean: 70; seed: 22 }
Wire { from: rndB.measureRandomVariate; to: perform0.parameterDuration }
Perform { id: perform0; label: "Machine" }
Pipe { from: perform0.outlet; to: leave0.inlet }
Leave { id: leave0 }
// failure cycle: one "failure token" alternates between up time and repair
Provide { id: provideF }
Pipe { from: provideF.outlet; to: meterF.inlet }
Meter { id: meterF; capacity: 1 }
Pipe { from: meterF.outlet; to: performUp.inlet }
RandomExponential { id: rndUp; mean: 2000; seed: 44 }
Wire { from: rndUp.measureRandomVariate; to: performUp.parameterDuration }
Perform { id: performUp; label: "Up time" }
Pipe { from: performUp.outlet; to: performRepair.inlet }
RandomExponential { id: rndRepair; mean: 200; seed: 55 }
Wire { from: rndRepair.measureRandomVariate; to: performRepair.parameterDuration }
Perform { id: performRepair; label: "Repair" }
Pipe { from: performRepair.outlet; to: performUp.inlet; bend:-87; incline:25; debend:-15; decline:57 }
Function { id: fnCap; code: "M[0] = 1 - P[0]"; label: "machine capacity" }  // 0 while in repair
Wire { from: performRepair.measureLoad; to: fnCap.parameters[0] }
Wire { from: fnCap.measures[0]; to: perform0.parameterCapacity; bend:34; incline:27; debend:-12; decline:81 }
Statistics { id: stats0; numParameters: 2; timeAverage: true; label: "in repair\nqueue length" }
Wire { from: performRepair.measureLoad; to: stats0.parameters[0] }
Wire { from: queue0.measureLoad; to: stats0.parameters[1] }
Statistics { id: statsW; label: "waiting time" }
Wire { from: queue0.measureWait; to: statsW.parameters[0] }

What to look at. The time-weighted load of "Repair" is the share of time the machine is down: 0.085 ± 0.005, close to 200/(2000+200)=0.091200 / (2000 + 200) = 0.091. The mean waiting time rises from 151 ± 10 without breakdowns (remove the wire into perform0.parameterCapacity) to 250 ± 24. Breakdowns cost more waiting than the 9% capacity loss suggests, because jobs pile up during each repair.

Variation. In this recipe the up time runs on calendar time, even while the machine is idle. If failures depend on operating time, count the processed jobs or the busy time instead, e.g. let the token wait in a Queue that a Decide opens after every 50 jobs (a Meter after the machine gives the count). The same token pattern models planned maintenance (constant durations) and shift breaks (next recipe).

Blocks: Provide, Meter, Perform, Function, Statistics.


How do I model shifts and arrival rates that change over the day? ​

Example: an emergency department (time unit: minutes) with four 6-hour shifts. The mean time between patients is 30, 10, 6 and 15 minutes, and 3, 5, 7 and 4 doctors are on duty.

Idea. The Steering block reports the current time at measureCurrentVirtualTime. A Function maps the time onto the shift number and looks up the shift's values in arrays. Wire them to the mean of the arrival distribution and to the capacity of the Perform block. One run is one day (1440 minutes), and each replication is another day.

diagramdiagram
Model codeOpen in Quisim
qml
// Emergency department, time unit minutes: arrival rate and number of doctors change every 6 h
Steering { id: steering0; duration: 1440; replications: 100; seed: 1 }
Function { id: fnShift; numMeasures: 2; label: "shift plan"
           code: "// 0 night, 1 morning, 2 afternoon, 3 evening
const s = Math.floor(P[0] / 360) % 4
M[0] = [30, 10, 6, 15][s] // mean interarrival
M[1] = [3, 5, 7, 4][s]      // doctors on duty"; offLayout:true; x:109; y:11 }
Wire { from: steering0.measureCurrentVirtualTime; to: fnShift.parameters[0] }
Wire { from: fnShift.measures[0]; to: rndA.parameterMean; bend:-1; incline:6; debend:11; decline:88 }
Wire { from: fnShift.measures[1]; to: perform0.parameterCapacity }
RandomExponential { id: rndA; seed: 11; x:112; y:153 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0; beginAtZero: false; x:211; y:154 }
Pipe { from: arrive0.outlet; to: queue0.inlet }
Queue { id: queue0; x:307; y:154 }
Pipe { from: queue0.outlet; to: perform0.inlet }
RandomExponential { id: rndB; mean: 30; seed: 22; x:22; y:153 }
Wire { from: rndB.measureRandomVariate; to: perform0.parameterDuration; bend:37; incline:-31; debend:16; decline:-32 }
Perform { id: perform0; label: "Doctors"; x:403; y:171 }
Pipe { from: perform0.outlet; to: leave0.inlet }
Leave { id: leave0; x:499; y:189 }
Statistics { id: statsN; cumulative: true; label: "patients per day"; x:22; y:265 }
Wire { from: arrive0.measureNumPassed; to: statsN.parameters[0] }
Statistics { id: statsW; label: "waiting time"; x:341; y:267 }
Wire { from: queue0.measureWait; to: statsW.parameters[0]; bend:7; incline:-29; debend:-15; decline:57 }
EnsemblePlot { id: chart0; numParameters: 2; x:20; y:492; height:192; width:526; timeAverage:true }
Wire { from: queue0.measureLoad; to: chart0.parameters[0] }
Wire { from: perform0.measureLoad; to: chart0.parameters[1]; bend:80; incline:97; debend:73; decline:-120 }

What to look at. The Chart with overlayReplications draws all 100 days on top of each other, so the daily pattern of the queue is easy to see. The model gives 128 ± 2 patients per day and a mean wait of 3.8 ± 0.7 minutes. The rates alone predict 12+36+60+24=13212 + 36 + 60 + 24 = 132 patients. The model gets slightly fewer because each inter-arrival time is drawn with the rate at the time of drawing: a long gap drawn late in a quiet shift reaches into the next, busier one. For hour-long shifts with many arrivals per shift, this effect is small. If it matters, use shorter time slices in the lookup.

Variation. For a closing time, a Function M[0] = P[0] < 480 ? 0 : 1 on the current time drives a Decide that turns away customers after time 480 (used in recipe 12). For a break of the only server, set the capacity to 0 during the break: entities in service are finished, new ones wait. The Hospital Case Study finds the staffing plan used here, one shift at a time.

Blocks: Steering, Function, RandomExponential, Perform, Chart.


How do I process entities in batches? ​

Example: items arrive one by one, and an oven bakes them in trays of 4 (bake time 100).

Idea. A Pack block puts arriving entities into a packet entity, here an empty tray from a Provide block. With capacity: 4, the tray leaves as soon as it holds 4 items. After the oven, an Unpack block releases the items again and sends the empty tray out of outPack.

diagramdiagram
Model codeOpen in Quisim
qml
// An oven bakes trays of 4 items; items arrive one by one
Steering { id: steering0; duration: 100000; replications: 10; seed: 1 }
RandomExponential { id: rndA; mean: 60; seed: 11 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: queueItems.inlet }
Queue { id: queueItems }
Pipe { from: queueItems.outlet; to: pack0.inlet }
Provide { id: provideTray; label: "Empty trays" }
Pipe { from: provideTray.outlet; to: pack0.inPacket }
Pack { id: pack0; capacity: 4 }
Pipe { from: pack0.outlet; to: queueTrays.inlet }
Queue { id: queueTrays }
Pipe { from: queueTrays.outlet; to: oven.inlet }
Perform { id: oven; duration: 100; label: "Oven" }
Pipe { from: oven.outlet; to: unpack0.inlet }
Unpack { id: unpack0 }
Pipe { from: unpack0.outlet; to: leave0.inlet }
Pipe { from: unpack0.outPack; to: leaveTray.inlet }
Leave { id: leave0; label: "Items" }
Leave { id: leaveTray; label: "Trays" }
Statistics { id: statsT; label: "time in system" }
Wire { from: leave0.measureSojourn; to: statsT.parameters[0] }
Statistics { id: statsL; numParameters: 2; timeAverage: true; label: "items in pack\noven busy" }
Wire { from: pack0.measureLoad; to: statsL.parameters[0] }
Wire { from: oven.measureLoad; to: statsL.parameters[1] }
Statistics { id: statsN; numParameters: 2; cumulative: true; label: "items\ntrays" }
Wire { from: leave0.measureNumPassed; to: statsN.parameters[0] }
Wire { from: leaveTray.measureNumPassed; to: statsN.parameters[1] }

What to look at. The time in system is 192 ± 1: on average an item waits 1.5 inter-arrival times (90) until its tray is full, then bakes for 100. The oven is busy only 42% of the time, so most of the delay comes from filling the batch, not from the oven.

Variation. Take what is there: set sufficiency: 1 on the Pack and pipe pack0.outlet directly into the oven (no tray queue). A tray then leaves as soon as it holds one item and the oven is free, and items keep being added while the oven is busy (up to capacity). The time in system drops to 148 ± 1, at the price of 907 instead of 419 oven cycles per run and an oven that is busy 91% of the time. At high load, partial batches waste oven capacity, so compare both rules for your arrival rate. The Pack–Unpack example and the logistics examples show nested packing (items on palettes on trucks).

Blocks: Pack, Unpack, Provide, Perform.


How do I model an assembly that needs one part of each kind? ​

Example: one housing and one circuit board are assembled into one device.

Idea. A Lineup block waits until there is an entity at every inlet and then releases them all at once. Pack the released parts into one new entity, the device: a Pack with capacity: 2 and a Provide block for the device shell. Each part stream needs its own Queue in front of the Lineup.

diagramdiagram
Model codeOpen in Quisim
qml
// Assembly: one housing and one circuit board make one device
Steering { id: steering0; duration: 100000; replications: 10; seed: 1 }
RandomExponential { id: rndH; mean: 100; seed: 11 }
Wire { from: rndH.measureRandomVariate; to: arriveH.parameterDuration }
Arrive { id: arriveH; label: "Housings" }
Pipe { from: arriveH.outlet; to: queueH.inlet }
Queue { id: queueH }
RandomExponential { id: rndB; mean: 100; seed: 22 }
Wire { from: rndB.measureRandomVariate; to: arriveB.parameterDuration }
Arrive { id: arriveB; label: "Boards" }
Pipe { from: arriveB.outlet; to: queueB.inlet }
Queue { id: queueB }
Pipe { from: queueH.outlet; to: lineup0.inlets[0] }
Pipe { from: queueB.outlet; to: lineup0.inlets[1] }
Lineup { id: lineup0 }                        // waits for one of each
Pipe { from: lineup0.outlets[0]; to: pack0.inlet }
Pipe { from: lineup0.outlets[1]; to: pack0.inlet }
Provide { id: provideDevice; label: "Device" }
Pipe { from: provideDevice.outlet; to: pack0.inPacket }
Pack { id: pack0; capacity: 2 }               // the two parts become one device
Pipe { from: pack0.outlet; to: queueA.inlet }
Queue { id: queueA }
Pipe { from: queueA.outlet; to: assembly.inlet }
RandomExponential { id: rndS; mean: 80; seed: 33 }
Wire { from: rndS.measureRandomVariate; to: assembly.parameterDuration }
Perform { id: assembly; label: "Assembly" }
Pipe { from: assembly.outlet; to: leave0.inlet }
Leave { id: leave0 }
Statistics { id: statsQ; numParameters: 3; timeAverage: true; label: "housings\nboards\ndevices waiting" }
Wire { from: queueH.measureLoad; to: statsQ.parameters[0] }
Wire { from: queueB.measureLoad; to: statsQ.parameters[1] }
Wire { from: queueA.measureLoad; to: statsQ.parameters[2] }
Statistics { id: statsN; cumulative: true; label: "devices" }
Wire { from: leave0.measureNumPassed; to: statsN.parameters[0] }

What to look at. 990 ± 13 devices are assembled per run, as expected from one pair per 100 time units. The parts stocks, however, are large and very uncertain: 12.5 ± 5.8 housings and 2.1 ± 1.9 boards waiting on average. Two independent random streams with the same mean drift apart like a random walk, so one of the two stocks keeps growing over the run. This is a typical result for assembly: with independent suppliers, balanced rates are not enough. Couple the supply to the demand, e.g. with a pull release from a Provide block.

Variation. For more parts, raise numConnectors (e.g. 4 for a package, a leaflet and two blisters, see the Packaging Machine). For a part that is always available, feed its inlet from a Provide block. If the parts need not stay together, skip the Pack and pipe the Lineup outlets onward separately: one into the assembly step, the other into a Leave.

Blocks: Lineup, Pack, Provide, Queue.


How do I share one pool of workers between several stations? ​

Example: two stations share 2 operators. A job needs one operator for its whole service time.

Idea. The operators are entities. They wait in a pool Queue, created once by Provide → Meter { capacity: 2 }. At each station, the operator becomes the packet of a Pack block with capacity: 1, so the operator "carries" the job through the Perform. The Perform has unlimited capacity, because the number of operators limits the work. An Unpack releases the job, and the operator goes back to the pool through outPack. A Decide in front of the stations acts as a dispatcher: it sends an idle operator only to a station where a job is waiting.

diagramdiagram
Model codeOpen in Quisim
qml
// Two stations share a pool of 2 operators; a job needs one operator for its whole service
Steering { id: steering0; duration: 100000; replications: 10; seed: 1 }
// operator pool: 2 operator entities, created once at time 0
Provide { id: provideOp; label: "Operators" }
Pipe { from: provideOp.outlet; to: meterOp.inlet }
Meter { id: meterOp; capacity: 2 }
Pipe { from: meterOp.outlet; to: pool.inlet }
Queue { id: pool; label: "Idle operators" }
Pipe { from: pool.outlet; to: dispatch.inlet }
Function { id: fnDispatch; numParameters: 2; code: "M[0] = P[0] > 0 ? 0 : (P[1] > 0 ? 1 : -1)"; label: "where is work?" }
Wire { from: queueA.measureLoad; to: fnDispatch.parameters[0] }
Wire { from: queueB.measureLoad; to: fnDispatch.parameters[1] }
Wire { from: fnDispatch.measures[0]; to: dispatch.parameterSelectedOutlet }
Decide { id: dispatch }                      // -1 keeps idle operators in the pool
Pipe { from: dispatch.outlets[0]; to: packA.inPacket }
Pipe { from: dispatch.outlets[1]; to: packB.inPacket }
// station A
RandomExponential { id: rndA; mean: 100; seed: 11 }
Wire { from: rndA.measureRandomVariate; to: arriveA.parameterDuration }
Arrive { id: arriveA }
Pipe { from: arriveA.outlet; to: queueA.inlet }
Queue { id: queueA }
Pipe { from: queueA.outlet; to: packA.inlet }
Pack { id: packA; capacity: 1 }              // operator "carries" the job
Pipe { from: packA.outlet; to: performA.inlet }
RandomExponential { id: rndSA; mean: 120; seed: 22 }
Wire { from: rndSA.measureRandomVariate; to: performA.parameterDuration }
Perform { id: performA; capacity: Infinity; label: "Station A" }
Pipe { from: performA.outlet; to: unpackA.inlet }
Unpack { id: unpackA }
Pipe { from: unpackA.outlet; to: leaveA.inlet }
Pipe { from: unpackA.outPack; to: pool.inlet }  // operator returns to the pool
Leave { id: leaveA }
// station B
RandomExponential { id: rndB; mean: 200; seed: 33 }
Wire { from: rndB.measureRandomVariate; to: arriveB.parameterDuration }
Arrive { id: arriveB }
Pipe { from: arriveB.outlet; to: queueB.inlet }
Queue { id: queueB }
Pipe { from: queueB.outlet; to: packB.inlet }
Pack { id: packB; capacity: 1 }
Pipe { from: packB.outlet; to: performB.inlet }
RandomExponential { id: rndSB; mean: 80; seed: 44 }
Wire { from: rndSB.measureRandomVariate; to: performB.parameterDuration }
Perform { id: performB; capacity: Infinity; label: "Station B" }
Pipe { from: performB.outlet; to: unpackB.inlet }
Unpack { id: unpackB }
Pipe { from: unpackB.outlet; to: leaveB.inlet }
Pipe { from: unpackB.outPack; to: pool.inlet }
Leave { id: leaveB }
Statistics { id: statsL; numParameters: 3; timeAverage: true; label: "idle operators\nbusy A\nbusy B" }
Wire { from: pool.measureLoad; to: statsL.parameters[0] }
Wire { from: performA.measureLoad; to: statsL.parameters[1] }
Wire { from: performB.measureLoad; to: statsL.parameters[2] }
Statistics { id: statsW; numParameters: 2; label: "waiting A\nwaiting B" }
Wire { from: queueA.measureWait; to: statsW.parameters[0] }
Wire { from: queueB.measureWait; to: statsW.parameters[1] }

What to look at. Idle, busy at A and busy at B add up to the 2 operators: 0.39+1.21+0.400.39 + 1.21 + 0.40. The offered work is 120/100=1.2120/100 = 1.2 at A and 80/200=0.480/200 = 0.4 at B. The dispatcher checks station A first, so A has priority: jobs wait 87 ± 7 at A and 421 ± 86 at B.

Pitfall: Without the dispatcher (pool piped straight into both inPacket sockets), each Pack takes an operator in advance and holds it while it waits for a job. Each station then has its own operator, and nothing is shared. In this example the waiting time at A explodes to over 17 000. Check that idle + busy adds up to the pool size.

Variation. For a fairer rule, send the operator to the longer queue: M[0] = P[0] + P[1] == 0 ? -1 : (P[0] >= P[1] ? 0 : 1). For operators who need a break or have different skills, give them attributes with a Set block before the pool, and sort or route by them. If a job needs a resource only for part of its time at a station, unpack it in between.

Blocks: Pack, Unpack, Decide, Function, Provide, Meter.


How do I model several lanes, joining the shortest line, and switching lanes? ​

Example: an airport security checkpoint with 4 officers. Is it better to have four lanes or one line? And does it matter how passengers pick a lane?

Idea. Each lane is a Queue followed by a Perform. Arrive is piped to all four Queues, and the Queue capacities decide who takes the passenger. A small Function counts the people in each lane (waiting + in service), finds the fewest, mm, and sets each Queue's capacity to m+1−m + 1 - (people in service there). A Queue then accepts a newcomer only while its lane is a shortest one, so passengers join the shortest line. For switching, each Queue also has evictExcess: true and its outlet E piped to the other Queues: when another lane gets shorter by two, the capacity drops below the load and the Queue sends its last person over. This is the line-jockeying trick from the handbook (Line jockeying), extended to four lanes.

diagramdiagram
Model codeOpen in Quisim
qml
// Four lanes, one officer each. Passengers join the shortest lane; the last in a lane switches when another lane is shorter by two
Steering { id: steering0; replications:20; duration:1000000; seed:1 }
NumberInput { id: number0; initValue:200; label:"1/λ" }
Wire { from: number0.measureValue; to: rnd0.parameterMean }
RandomExponential { id:rnd0 }
Wire { from: rnd0.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id:arrive0 }
Pipe { from: arrive0.outlet; to: queue0.inlet }
Pipe { from: arrive0.outlet; to: queue1.inlet }
Pipe { from: arrive0.outlet; to: queue2.inlet }
Pipe { from: arrive0.outlet; to: queue3.inlet }

// n = people in a lane (waiting + in service). A lane accepts only while it is a shortest one: capacity = min n + 1 − (in service)
Function { id: cap; numParameters:8; numMeasures:4; label:"Capacities"; code:"let m = Infinity; // fewest people in a lane\nfor (let i = 0; i < 4; i++) m = Math.min(m, P[i] + P[4 + i]);\nfor (let i = 0; i < 4; i++) M[i] = m + 1 - P[4 + i];" }
Wire { from: queue0.measureLoad; to: cap.parameters[0] }
Wire { from: queue1.measureLoad; to: cap.parameters[1] }
Wire { from: queue2.measureLoad; to: cap.parameters[2] }
Wire { from: queue3.measureLoad; to: cap.parameters[3] }
Wire { from: perform0.measureLoad; to: cap.parameters[4] }
Wire { from: perform1.measureLoad; to: cap.parameters[5] }
Wire { from: perform2.measureLoad; to: cap.parameters[6] }
Wire { from: perform3.measureLoad; to: cap.parameters[7] }
Wire { from: cap.measures[0]; to: queue0.parameterCapacity }
Wire { from: cap.measures[1]; to: queue1.parameterCapacity }
Wire { from: cap.measures[2]; to: queue2.parameterCapacity }
Wire { from: cap.measures[3]; to: queue3.parameterCapacity }

Queue { id:queue0; evictExcess:true; x:456; y:58 }
Queue { id:queue1; evictExcess:true; x:456; y:168 }
Queue { id:queue2; evictExcess:true; x:456; y:280 }
Queue { id:queue3; evictExcess:true; x:456; y:384 }
// switching: a Queue over its capacity sends its last person to a shorter lane
Pipe { from: queue0.outEvict; to: queue1.inlet }
Pipe { from: queue0.outEvict; to: queue2.inlet }
Pipe { from: queue0.outEvict; to: queue3.inlet }
Pipe { from: queue1.outEvict; to: queue0.inlet }
Pipe { from: queue1.outEvict; to: queue2.inlet }
Pipe { from: queue1.outEvict; to: queue3.inlet }
Pipe { from: queue2.outEvict; to: queue0.inlet }
Pipe { from: queue2.outEvict; to: queue1.inlet }
Pipe { from: queue2.outEvict; to: queue3.inlet }
Pipe { from: queue3.outEvict; to: queue0.inlet }
Pipe { from: queue3.outEvict; to: queue1.inlet }
Pipe { from: queue3.outEvict; to: queue2.inlet }

Pipe { from:queue0.outlet; to:perform0.inlet }
Pipe { from:queue1.outlet; to:perform1.inlet }
Pipe { from:queue2.outlet; to:perform2.inlet }
Pipe { from:queue3.outlet; to:perform3.inlet }
NumberInput { id: number1; initValue:500; label:"1/µ" }
Wire { from: number1.measureValue; to: rnd1.parameterMean }
RandomExponential { id:rnd1 }
Wire { from:rnd1.measureRandomVariate; to:perform0.parameterDuration }
Wire { from:rnd1.measureRandomVariate; to:perform1.parameterDuration }
Wire { from:rnd1.measureRandomVariate; to:perform2.parameterDuration }
Wire { from:rnd1.measureRandomVariate; to:perform3.parameterDuration }
Perform { id:perform0 }
Perform { id:perform1 }
Perform { id:perform2 }
Perform { id:perform3 }

Pipe { from:perform0.outlet; to:leave0.inlet }
Pipe { from: perform1.outlet; to: leave0.inlet }
Pipe { from: perform2.outlet; to: leave0.inlet }
Pipe { from: perform3.outlet; to: leave0.inlet }
Leave { id:leave0 }
Statistics { id: statistics0; warmupWidth:50000; label:"Mean" }
Wire { from: leave0.measureSojourn; to: statistics0.parameters[0] }
Statistics { id: statistics1; warmupWidth:50000; quantile:0.95; label:"95th percentile" }
Wire { from: leave0.measureSojourn; to: statistics1.parameters[0] }

Three related models make the comparison:

  • Shortest line, no switching: delete the twelve outEvict pipes and evictExcess.

  • Random lane: replace the four pipes from Arrive and the Function with a Decide:

    Pipe { from: arrive0.outlet; to: decide0.inlet }
    RandomUniform { id: rndLane; upper:4 }
    Wire { from: rndLane.measureRandomVariate; to: decide0.parameterSelectedOutlet }
    Decide { id: decide0; numOutlets:4 }
    Pipe { from: decide0.outlets[0]; to: queue0.inlet }
    // … outlets[1] to queue1, outlets[2] to queue2, outlets[3] to queue3
  • One line: Arrive → one Queue → one Perform with capacity: 4, as in the M/M/k example.

What to look at. Two Statistics blocks show the time in the system (waiting + service): the Mean, and the 95th percentile, the time that 95% of the passengers do not exceed (computed per run, then averaged over the runs). Test runs, 20 runs of 1 000 000 each with the first 50 000 cut, same seeds for all; each cell is mean / 95th percentile:

1/λ (officers busy)Random laneShortest lineShortest line + switchingOne line
200 (63%)1350 / 4026668 / 1944606 / 1733606 / 1702
140 (89%)4140 / 121381583 / 45791412 / 41121412 / 3874

Queueing theory: a random lane makes four M/M/1 queues, mean 1333 at 63% and 4667 at 89%; one line is an M/M/4 queue, mean 607 and 1402. At 89% even these long runs fall short for the random lane: each lane drifts slowly between long and short phases.

  • A random split is much worse: each lane is its own small system, and an officer can be idle while another lane has a line.
  • Picking the shortest line recovers most of the loss, but not all: someone who has already joined a line cannot follow an officer who becomes free. The gap to one line is about 10% in the mean, at both loads.
  • Switching closes the gap in the mean exactly. It lowers the mean only when someone moves to an officer who would otherwise be idle; a move between two busy lanes helps the one who moves exactly as much as it delays the next arrival in that lane, and the officers' work rate, and with it the mean (Little's law), stays the same. With switching no officer is idle while someone waits, so the mean equals one line's.
  • One line is still fairer: its 95th percentile is lower, because a switch moves the last person in a line, not the one who has waited longest. The single line serves strictly first come, first served.
  • At 89% load runs of 100 000 are far too short: they put switching and no switching within 1% of each other in one test, and 4% apart in another. Check how many replications and how long a run you need (see Replications) before you trust a small difference.

Variation. Two lanes with two officers each: keep two lanes, give each Perform capacity: 2, and loop over two lanes in the Function. Or make one officer slower with a separate RandomExponential and see how much switching helps then.

Blocks: Queue, Function, Perform, Decide, RandomUniform.


How do I measure throughput? ​

Example: a two-machine line. How many parts per day does each machine finish, and which machine is the bottleneck?

Idea. A Meter block counts the entities that pass through it. Place it after the machine, so it counts finished parts, not arriving ones. A Statistics block with cumulative: true turns the running count into "parts per run". The utilization, i.e. the time-weighted load of each Perform, shows the bottleneck.

diagramdiagram
Model codeOpen in Quisim
qml
// Two-step production line: count the parts finished by each machine
Steering { id: steering0; duration: 10000; replications: 20; seed: 9211 }
Arrive { id: arrive0; duration: 50 }
Pipe { from: arrive0.outlet; to: queue0.inlet }
Queue { id: queue0 }
Pipe { from: queue0.outlet; to: machine1.inlet }
RandomExponential { id: rnd1; mean: 120; seed: 11 }
Wire { from: rnd1.measureRandomVariate; to: machine1.parameterDuration }
Perform { id: machine1 }
Pipe { from: machine1.outlet; to: meter1.inlet }
Meter { id: meter1 }                          // after the machine, not before
Pipe { from: meter1.outlet; to: queue1.inlet }
Queue { id: queue1 }
Pipe { from: queue1.outlet; to: machine2.inlet }
RandomExponential { id: rnd2; mean: 80; seed: 22 }
Wire { from: rnd2.measureRandomVariate; to: machine2.parameterDuration }
Perform { id: machine2 }
Pipe { from: machine2.outlet; to: meter2.inlet }
Meter { id: meter2 }
Pipe { from: meter2.outlet; to: leave0.inlet }
Leave { id: leave0 }
Statistics { id: statsN; numParameters: 2; cumulative: true; label: "parts from M1 / M2" }
Wire { from: meter1.measureNumPassed; to: statsN.parameters[0] }
Wire { from: meter2.measureNumPassed; to: statsN.parameters[1] }
Statistics { id: statsU; numParameters: 2; timeAverage: true; label: "utilization M1 / M2" }
Wire { from: machine1.measureLoad; to: statsU.parameters[0] }
Wire { from: machine2.measureLoad; to: statsU.parameters[1] }

What to look at. Machine 1 finishes 80.9 ± 3.4 and machine 2 78.9 ± 3.2 parts per day of 10 000 time units, i.e. a throughput of about 0.008 parts per time unit. Machine 1 is busy 99.7% of the time and machine 2 only 67%: machine 1 is the bottleneck. More arrivals would not raise the output, only a faster or second machine 1 would (see the scenarios of the Two-Step Production Line). For the throughput rate, divide the count by the duration. A Leave block's measureNumPassed gives the same count at the end of the line without a Meter.

Variation. With capacity: N, a Meter becomes a counter that closes after NN entities. Behind a Provide block it releases exactly NN jobs, which is the standard way to start a model with a given work load (see the Flow Shop). measureLastTime (LT) gives the time the last entity passed, e.g. the completion time of the last job.

Blocks: Meter, Statistics, Perform, Leave.


How do I measure waiting time, flow time and a service level? ​

Example: patients register (1 clerk) and are then treated (7 doctors), in minutes. How long do they wait, how long do they stay, and what share waits at most 20 minutes for treatment?

Idea. Every Queue reports the waiting time of the entity that just left it (measureWait). The Leave block reports its flow time, or time in system (measureSojourn). For a service level, turn each waiting time into 1 (target met) or 0 with a Function; the mean of these 0/1 values is the share. For the 90% quantile, set quantile: 0.9 on a Statistics block.

diagramdiagram
Model codeOpen in Quisim
qml
// Registration (1 clerk) then treatment (7 doctors); time unit minutes
Steering { id: steering0; duration: 10500; replications: 20; seed: 1 }
RandomExponential { id: rndA; mean: 6; seed: 11 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: queueReg.inlet }
Queue { id: queueReg }
Pipe { from: queueReg.outlet; to: registration.inlet }
RandomExponential { id: rndR; mean: 4; seed: 22 }
Wire { from: rndR.measureRandomVariate; to: registration.parameterDuration }
Perform { id: registration }
Pipe { from: registration.outlet; to: queueTreat.inlet }
Queue { id: queueTreat }
Pipe { from: queueTreat.outlet; to: meterT.inlet }
Meter { id: meterT }
Pipe { from: meterT.outlet; to: treatment.inlet }
RandomExponential { id: rndT; mean: 30; seed: 33 }
Wire { from: rndT.measureRandomVariate; to: treatment.parameterDuration }
Perform { id: treatment; capacity: 7 }
Pipe { from: treatment.outlet; to: leave0.inlet }
Leave { id: leave0 }
Statistics { id: statsT; numParameters: 3; warmupWidth: 500; label: "wait reg.\nwait treat.\ntime in system" }
Wire { from: queueReg.measureWait; to: statsT.parameters[0] }
Wire { from: queueTreat.measureWait; to: statsT.parameters[1] }
Wire { from: leave0.measureSojourn; to: statsT.parameters[2] }
Function { id: fnOk; numRequested: 1; code: "M[0] = P[1] <= 20 ? 1 : 0"; label: "waited ≤ 20?" }
Wire { from: meterT.measureNumPassed; to: fnOk.parameters[0] }   // fires once per patient
Wire { from: queueTreat.measureWait; to: fnOk.parameters[1] }    // read only
Statistics { id: statsSL; warmupWidth: 500; label: "service level" }
Wire { from: fnOk.measures[0]; to: statsSL.parameters[0] }
Statistics { id: statsQ90; quantile: 0.9; warmupWidth: 500; label: "90% of patients\nare done within" }
Wire { from: leave0.measureSojourn; to: statsQ90.parameters[0] }

What to look at. The registration wait is 8.4 ± 0.9 minutes (M/M/1 theory: 8.0), the treatment wait 4.9 ± 0.7 and the time in system 47.2 ± 1.5, which is about the sum of the two waits plus 4+304 + 30 minutes of service. 91.3% ± 1.6% of the patients wait at most 20 minutes for treatment, and 90% of them leave within 92 ± 3 minutes. Waiting and flow times are per-patient observations, so leave timeAverage off for them.

Why the Meter? A Function runs only when one of its first numParameters inputs changes. Two patients in a row who wait 0 minutes give the same input twice, and the second one would not be counted. Since zero waits are common, a Function wired directly to queueTreat.measureWait underestimates the service level badly: 78% instead of 91% in this model. Here the Meter count, which changes for every patient, triggers the Function. The waiting time is a requested parameter (numRequested: 1), which is read but does not trigger.

Variation. For the waiting time per class, split the stream after service as in recipe 2. Several wires into one Statistics input (e.g. measureSojourn of three Leave blocks into parameters[0]) give the overall mean. The Hospital Case Study uses the service level to choose the number of doctors.

Blocks: Queue, Leave, Meter, Function, Statistics.


How long is the warm-up, and how do I remove it? ​

Example: an M/M/1 queue (utilization 0.8) starts the day with a backlog of 30 jobs. We want the steady-state queue length.

Idea. Data from the start-up phase bias steady-state means. An EnsemblePlot (Welch plot) averages the replications over time, and the warm-up ends where the curve flattens. Its dashed line marks the MSER-5 suggestion. With warmupMSER: true, a Statistics block chooses the warm-up itself after the run and writes it into warmupWidth.

diagramdiagram
Model codeOpen in Quisim
qml
// M/M/1 (utilization 0.8) that starts with a backlog of 30 jobs: how long is the warm-up?
Steering { id: steering0; duration: 100000; replications: 10; seed: 1 }
Provide { id: provideBacklog }
Pipe { from: provideBacklog.outlet; to: meterBacklog.inlet }
Meter { id: meterBacklog; capacity: 30 }     // 30 jobs already waiting at time 0
Pipe { from: meterBacklog.outlet; to: queue0.inlet }
RandomExponential { id: rndA; mean: 125; seed: 11 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: queue0.inlet }
Queue { id: queue0 }
Pipe { from: queue0.outlet; to: perform0.inlet }
RandomExponential { id: rndB; mean: 100; seed: 22 }
Wire { from: rndB.measureRandomVariate; to: perform0.parameterDuration }
Perform { id: perform0 }
Pipe { from: perform0.outlet; to: leave0.inlet }
Leave { id: leave0 }
EnsemblePlot { id: welch0; timeAverage: true; batchWidth: 1000; label: "Welch plot of queue length" }
Wire { from: queue0.measureLoad; to: welch0.parameters[0] }
Statistics { id: statsAll; timeAverage: true; label: "queue length\n(no warm-up)" }
Wire { from: queue0.measureLoad; to: statsAll.parameters[0] }
Statistics { id: statsMser; timeAverage: true; warmupMSER: true; label: "queue length\n(MSER-5 warm-up)" }
Wire { from: queue0.measureLoad; to: statsMser.parameters[0] }

What to look at. The Welch plot starts at 30 and falls while the backlog drains, at about μ−λ=0.002\mu - \lambda = 0.002 jobs per time unit, i.e. for roughly the first 15 000 time units. MSER-5 chose a warm-up of about 22 000. The mean queue length is then 2.8 ± 0.6, which covers the theoretical 3.2. Without removing the warm-up, the mean is 6.1 ± 1.4, almost twice as large, and the interval misses 3.2.

Variation. Once you know the warm-up, enter it as a fixed warmupWidth (here e.g. 20 000). A warm-up chosen from the same data gives slightly too narrow intervals. For waiting times, wire queue0.measureWait into an EnsemblePlot without timeAverage: it then batches by observation count (batchWidth = observations per point). Warm-up removal is only for non-terminating systems. A system with a natural end, such as one day in a shop, is analysed from its start (next recipe). More in Output Analysis.

Blocks: EnsemblePlot, Statistics, Provide, Meter.


How many replications do I need for a day-long simulation? ​

Example: a shop with 3 cashiers opens for 480 minutes. Customers who are inside at closing time are still served. What is the mean waiting time, and when does the last customer leave?

Idea. A terminating system starts empty every day, so each replication is one day and nothing is cut off. The Statistics block gives the mean over the replications and its confidence radius. The radius shrinks like 1/R1/\sqrt{R}: four times as many replications halve it. For a value that should be recorded once per day, such as the time the last customer leaves, the per-run maximum of an increasing measure works: quantile: 1 on meterOut.measureLastTime.

diagramdiagram
Model codeOpen in Quisim
qml
// A shop with 3 cashiers opens for 480 min; customers inside at closing are still served
Steering { id: steering0; duration: 1000; replications: 10; seed: 1 }
RandomExponential { id: rndA; mean: 1; seed: 11 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: door.inlet }
Function { id: fnOpen; code: "M[0] = P[0] < 480 ? 0 : 1"; label: "door open?" }
Wire { from: steering0.measureCurrentVirtualTime; to: fnOpen.parameters[0] }
Wire { from: fnOpen.measures[0]; to: door.parameterSelectedOutlet }
Decide { id: door }
Pipe { from: door.outlets[0]; to: queue0.inlet }
Pipe { from: door.outlets[1]; to: leaveClosed.inlet }
Leave { id: leaveClosed; label: "Door closed" }
Queue { id: queue0 }
Pipe { from: queue0.outlet; to: cashiers.inlet }
RandomExponential { id: rndS; mean: 2.5; seed: 22 }
Wire { from: rndS.measureRandomVariate; to: cashiers.parameterDuration }
Perform { id: cashiers; capacity: 3 }
Pipe { from: cashiers.outlet; to: meterOut.inlet }
Meter { id: meterOut }
Pipe { from: meterOut.outlet; to: leave0.inlet }
Leave { id: leave0 }
Statistics { id: statsW; label: "waiting time" }
Wire { from: queue0.measureWait; to: statsW.parameters[0] }
Statistics { id: statsEnd; quantile: 1; label: "last customer leaves at" }
Wire { from: meterOut.measureLastTime; to: statsEnd.parameters[0] }

What to look at.

ReplicationsWaiting timeLast customer leaves at
103.23 ± 0.77484.0 ± 2.1
403.30 ± 0.54486.8 ± 1.7

Four times the replications should halve the radius. Here it shrinks from 0.77 to 0.54, because the radius from only 10 replications is itself a rough estimate. Choose RR so that the radius is small enough for your decision, e.g. "the mean wait is below 4 minutes" is already clear with 10 replications. The steering duration (1000) only has to be longer than the day plus the time needed to serve the last customers. The run time after closing does not enter the statistics, because the measures no longer change.

Variation. To see the effect of RR directly, let the edition number set the replications, as in the Flow Shop (M[0] = 100 + P[0]*100 wired into steering0.parameterReplications). For a non-terminating system, one long run with numBatches (batch means) is an alternative to many replications. Remove the warm-up first (recipe 11).

Blocks: Steering, Statistics, Meter, Decide, Function.


How do I compare two configurations of a system? ​

Example: is one fast server (mean service time 100) better than two slow servers (mean 200 each)? Customers arrive every 125 on average.

Idea. One model, several editions: the Steering block's edition number (measureEdition: 0, 1, …) drives a Function that looks up the settings of each configuration. The Statistics block then shows one row per edition. Each edition starts again with the same seeds, so all configurations see the same arrivals and service draws (common random numbers).

diagramdiagram
Model codeOpen in Quisim
qml
// Edition 0: one fast server (mean 100). Edition 1: two slow servers (mean 200 each)
Steering { id: steering0; duration: 200000; replications: 10; editions: 2; seed: 1 }
Function { id: fnConfig; numMeasures: 2; label: "configuration"
           code: "M[0] = [1, 2][P[0]]       // servers
M[1] = [100, 200][P[0]]   // mean service time" }
Wire { from: steering0.measureEdition; to: fnConfig.parameters[0] }
Wire { from: fnConfig.measures[0]; to: perform0.parameterCapacity }
Wire { from: fnConfig.measures[1]; to: rndB.parameterMean }
RandomExponential { id: rndA; mean: 125; seed: 11 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: queue0.inlet }
Queue { id: queue0 }
Pipe { from: queue0.outlet; to: perform0.inlet }
RandomExponential { id: rndB; seed: 22 }
Wire { from: rndB.measureRandomVariate; to: perform0.parameterDuration }
Perform { id: perform0 }
Pipe { from: perform0.outlet; to: leave0.inlet }
Leave { id: leave0 }
Statistics { id: stats0; numParameters: 2; label: "waiting time\ntime in system" }
Wire { from: queue0.measureWait; to: stats0.parameters[0] }
Wire { from: leave0.measureSojourn; to: stats0.parameters[1] }

What to look at.

EditionConfigurationWaiting time (theory)Time in system (theory)
01 server, mean 100364 ± 46 (400)464 ± 46 (500)
12 servers, mean 200317 ± 46 (356)517 ± 46 (556)

Two slow servers give shorter waits but a longer time in system. Which configuration is better depends on the KPI you choose. The two intervals overlap, so these per-edition intervals alone do not show a significant difference. Because both editions use the same random numbers, you can compute the difference in one model and get a much narrower interval for it (see System Comparison with Common Random Numbers).

Variation. Parameter sweep: with more editions, the lookup becomes a sweep, e.g. M[0] = [5, 6, 7, 8, 9, 10][P[0]] into the capacity with editions: 6. For an M/M/c queue with mean inter-arrival time 10 and mean service time 45 (5 replications of 20 000, warm-up 1000), the mean queue length drops from 4.8 ± 1.0 at 5 servers to 0.99, 0.30, 0.10, 0.03 and 0.01 for 6 to 10 servers. For several factors at two levels each, decode the edition number into bits (see Design of Experiments).

Blocks: Steering, Function, Statistics, Perform.


How do I find the best value of a parameter? ​

Example: a faster machine costs more. With mean service time ss, the machine costs 2000/s2000/s per time unit, and each job in the system costs 10 per time unit. Which ss between 50 and 120 minimizes the total cost? Jobs arrive every 125 on average.

Idea. The BrentOptimize block searches one continuous parameter within [lower, upper]. Its guess G drives the model, and the objective is wired back into V. Every edition is one evaluation: BrentOptimize sets a new guess when a new edition starts. With fixed seeds, each edition sees the same random numbers, so the objective is a smooth function of the guess.

diagramdiagram
Model codeOpen in Quisim
qml
// Choose the machine speed: mean service time s costs 2000/s per time unit, each job in the system costs 10
Steering { id: steering0; duration: 200000; editions: 15; seed: 1 }
BrentOptimize { id: brent0; lower: 50; upper: 120 }
Wire { from: brent0.measureGuess; to: rndB.parameterMean }
RandomExponential { id: rndA; mean: 125; seed: 11 }
Wire { from: rndA.measureRandomVariate; to: arrive0.parameterDuration }
Arrive { id: arrive0 }
Pipe { from: arrive0.outlet; to: queue0.inlet }
Queue { id: queue0 }
Pipe { from: queue0.outlet; to: perform0.inlet }
RandomExponential { id: rndB; seed: 22 }
Wire { from: rndB.measureRandomVariate; to: perform0.parameterDuration }
Perform { id: perform0 }
Pipe { from: perform0.outlet; to: leave0.inlet }
Leave { id: leave0 }
Statistics { id: statsL; numParameters: 2; timeAverage: true; label: "waiting\nin service" }
Wire { from: queue0.measureLoad; to: statsL.parameters[0] }
Wire { from: perform0.measureLoad; to: statsL.parameters[1] }
Function { id: fnCost; numParameters: 3; code: "M[0] = 2000 / P[0] + 10 * (P[1] + P[2])"; label: "cost" }
Wire { from: brent0.measureGuess; to: fnCost.parameters[0] }
Wire { from: statsL.measures[0]; to: fnCost.parameters[1] }
Wire { from: statsL.measures[1]; to: fnCost.parameters[2] }
Wire { from: fnCost.measures[0]; to: brent0.parameterObjective }
Report { id: report0; numParameters: 2; clearBetweenEditions: false; label: "guess / cost" }
Wire { from: brent0.measureGuess; to: report0.parameters[0] }
Wire { from: fnCost.measures[0]; to: report0.parameters[1] }

What to look at. The Report lists the guess and the cost of every edition. The first guess is the golden-section point 50+0.382⋅70≈7750 + 0.382 \cdot 70 \approx 77. After about 10 editions the guess settles at s≈71.4s \approx 71.4, with a cost of 40.6. For this M/M/1 case the exact optimum can be computed: s=125/(1+0.625)≈69.8s = 125 / (1 + \sqrt{0.625}) \approx 69.8 with cost 41.3. The simulation optimum differs a little because one run of 200 000 time units still has noise. With CRN across editions, the location of the optimum is found reliably. For the cost at the optimum, run the best ss again with replications.

Variation. Integer parameters, such as the number of servers, are better swept over editions (recipe 13). Set minimize: false to maximize, e.g. a profit.

The Simulation Optimization case study compares grid search, Kiefer–Wolfowitz and BrentOptimize on an M/M/10 cost problem.

Blocks: BrentOptimize, Function, Statistics, Report, Steering.