The shop's one packer packs an order in 1.5 seconds, and orders usually arrive at 0.4 a second. From 20 to 40 minutes a promotion brings 0.9 a second, more than one packer can pack. This page asks: what does a 20-minute rush do to waits, and does a second packer for the rush keep them short? It runs the hour with one packer through the rush and with two.
What You Add
A schedule: changes to the arrival rate and the number of packers at set times. The shop is the one from First Simulation, with packing at a constant 1.5 seconds.
Write the Schedule
A schedule is a list of steps, each a registry path, a value and the simulation time it applies at. The rush is four steps: the rate and the packers change at 20 minutes and change back at 40:
# The rush: 0.9 orders a second from 20 to 40 minutes, and RUSH_PACKERS for itscenario:-{at:20 min, path:Shop.arrival.orders.rate, value:0.9}-{at:20 min, path:Shop.resources.packers.current_cap, value:"${RUSH_PACKERS:-1}"}-{at:40 min, path:Shop.arrival.orders.rate, value:0.4}-{at:40 min, path:Shop.resources.packers.current_cap, value:1}
${RUSH_PACKERS:-1} reads the environment variable RUSH_PACKERS, so RUSH_PACKERS=2 desflow run changing_demand.yaml runs the second plan.
# The rush: 0.9 orders a second from 20 to 40 minutes, and rush_packers for it.add_scenario([{"at":"20 min","path":"Shop.arrival.orders.rate","value":0.9},{"at":"20 min","path":"Shop.resources.packers.current_cap","value":rush_packers,},{"at":"40 min","path":"Shop.arrival.orders.rate","value":0.4},{"at":"40 min","path":"Shop.resources.packers.current_cap","value":1,},])
defschedule(rush_packers):"""The rush: 0.9 orders a second from 20 to 40 minutes, and rush_packers for it."""return[{"at":1200,"path":"Shop.arrival.orders.rate","value":0.9},{"at":1200,"path":"Shop.resources.packers.current_cap","value":rush_packers,},{"at":2400,"path":"Shop.arrival.orders.rate","value":0.4},{"at":2400,"path":"Shop.resources.packers.current_cap","value":1},]
run_scenario is the SimPy process that applies the steps:
# Each step applies before every other event at its timeenv.process(run_scenario(env,schedule(rush_packers),until=3600))
The steps follow the simulation clock, so the schedule repeats exactly on every run and at any factor. A step at a time applies before every other event at that time: the order that arrives at 20 minutes draws its next gap from the rush rate. Time-varying Arrivals sets out these boundaries.
Result
What does a 20-minute rush do to waits? With one packer, it builds a queue for the whole rush: the mean wait over the hour is 147.74 seconds and the longest 416.75 seconds, more than 6 minutes. A second packer for the rush keeps the mean at 0.74 seconds and the longest at 6.06. Both pack the same 1,978 orders:
The Builder and Low-level scripts print both plans:
1 in the rush: 1978 packed, mean wait 147.74 s, longest 416.75 s
2 in the rush: 1978 packed, mean wait 0.74 s, longest 6.06 s
Why It Works
During the rush one packer would need 0.9 × 1.5 = 1.35 seconds of packing every second, more than it has: 0.9 orders arrive a second and the packer finishes about 0.67, so the queue grows by about 0.23 orders a second for as long as the rush lasts. That is a finite overload: when the rush ends the load falls to 0.6, and the packer works the queue off, here by about 54 minutes. A load that stays at 1 or above is different: its queue grows for as long as the run lasts, and no run length gives a stable mean wait, as on Queues and Capacity. A finite overload has an answer, but it depends on the schedule: a mean over the hour mixes the quiet minutes with the rush, so report the longest wait, or the waits during the rush, beside it. With two packers the rush load is 0.68 a packer, and waits stay short.
# The Changing Demand tutorial: a 20-minute rush, with one packer or two.# Run it with: desflow run changing_demand.yaml# With a second packer for the rush: RUSH_PACKERS=2 desflow run changing_demand.yamlsimulation:sim_id:Shopfactor:0random_seed:42resources:packers:{current_cap:1, max_cap:2}services:packing:{dist:constant, value:1.5 s}arrivals:orders:{dist:exponential, rate:0.4, spawn:pack_order}tasks:pack_order:service:packingresource:packerspayload:{event_type:order_packed}# The rush: 0.9 orders a second from 20 to 40 minutes, and RUSH_PACKERS for itscenario:-{at:20 min, path:Shop.arrival.orders.rate, value:0.9}-{at:20 min, path:Shop.resources.packers.current_cap, value:"${RUSH_PACKERS:-1}"}-{at:40 min, path:Shop.arrival.orders.rate, value:0.4}-{at:40 min, path:Shop.resources.packers.current_cap, value:1}# Orders packed, and the mean and longest wait, at the endstats:{level:typical, interval:2 h}egress:-type:Consolerun:until:1 h
"""The Changing Demand tutorial: a 20-minute rush, with one packer or two.One packer and 1.5 seconds of packing; orders arrive at 0.4 a second, and at 0.9from 20 to 40 minutes. Every record is printed to the terminal, and the scriptprints the waits with one packer through the rush and with a second packer for it:python changing_demand.py 2>/dev/null shows only those lines."""importloggingfromdesflowimportConsoleEgress,SimulationContextlogging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")defshop(rush_packers:int)->SimulationContext:"""The shop of First Simulation, with a rush from 20 to 40 minutes."""app=(SimulationContext(sim_id="Shop",factor=0,random_seed=42).add_resource("packers",current_cap=1,max_cap=2).add_service("packing",dist="constant",value=1.5).add_arrival("orders",dist="exponential",rate=0.4)# The rush: 0.9 orders a second from 20 to 40 minutes, and rush_packers for it.add_scenario([{"at":"20 min","path":"Shop.arrival.orders.rate","value":0.9},{"at":"20 min","path":"Shop.resources.packers.current_cap","value":rush_packers,},{"at":"40 min","path":"Shop.arrival.orders.rate","value":0.4},{"at":"40 min","path":"Shop.resources.packers.current_cap","value":1,},])# Orders packed, and the mean and longest wait, at the end.with_stats("typical",interval=7200).add_egress(ConsoleEgress()))@app.task(service_id="packing",resource_id="packers")defpack_order(task_id:int):return{"event_type":"order_packed"}@app.arrival_loop("orders")deforder_generator(context:SimulationContext):task_id=0whileTrue:# The gap is drawn from the rate in force noworder=yieldcontext.wait_for_arrival("orders")context.spawn(pack_order(task_id,entity=order))task_id+=1returnappdefoutcomes(rush_packers:int)->dict:"""Runs the hour and returns the orders packed and their waits."""app=shop(rush_packers)app.run(until="1 h")r=app.env.stats.readings()name="Shop.stats.tasks.pack_order.{}".formatreturn{"finished":r[name("finished")],"wait_mean":r[name("wait_mean")],"wait_max":r[name("wait_max")],}if__name__=="__main__":forpackersin(1,2):r=outcomes(packers)print(f"{packers} in the rush: {r['finished']} packed, "f"mean wait {r['wait_mean']:5.2f} s, longest {r['wait_max']:6.2f} s")
"""The Changing Demand tutorial: a 20-minute rush, with one packer or two.One packer and 1.5 seconds of packing; orders arrive at 0.4 a second, and at 0.9from 20 to 40 minutes. Every record is printed to the terminal, and the scriptprints the waits with one packer through the rush and with a second packer for it:python changing_demand_low_level.py 2>/dev/null shows only those lines."""importitertoolsimportloggingfromdesflowimport(CapacityConfig,ConsoleEgress,DistributionConfig,DynamicRealtimeEnvironment,DynamicResource,Sampler,SimParameter,run_scenario,)logging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")PARAMS=SimParameter(sim_id="Shop",arrival={"orders":DistributionConfig(dist="exponential",rate=0.4)},service={"packing":DistributionConfig(dist="constant",value=1.5)},resources={"packers":CapacityConfig(current_cap=1,max_cap=2)},)defschedule(rush_packers):"""The rush: 0.9 orders a second from 20 to 40 minutes, and rush_packers for it."""return[{"at":1200,"path":"Shop.arrival.orders.rate","value":0.9},{"at":1200,"path":"Shop.resources.packers.current_cap","value":rush_packers,},{"at":2400,"path":"Shop.arrival.orders.rate","value":0.4},{"at":2400,"path":"Shop.resources.packers.current_cap","value":1},]defpack_order(env,sampler,packers,key,task_id):path_id="Shop.service.packing"defpublish(value):env.publish_event(key,value,task="pack_order",instance=task_id)publish({"path_id":path_id,"status":"queued"})env.stats.task_queued("pack_order",task_id)withpackers.request()asrequest:yieldrequestpublish({"path_id":path_id,"status":"started"})env.stats.task_started("pack_order",task_id)yieldenv.timeout(sampler.sample(env.registry.get_config(path_id)))publish({"event_type":"order_packed"})env.stats.task_finished("pack_order",task_id)deforder_generator(env,sampler,packers):arrival=env.registry.get_config("Shop.arrival.orders")fortask_idinitertools.count():# The gap is drawn from the rate in force nowyieldenv.timeout(sampler.sample(arrival))key=f"orders-{task_id+1}"env.process(pack_order(env,sampler,packers,key,task_id))defrun(rush_packers=1,factor=0.0):env=DynamicRealtimeEnvironment(factor=factor)env.registry.register_sim_parameter(PARAMS)env.setup_egress([ConsoleEgress()])# Orders packed, and the mean and longest wait, at the endenv.setup_stats("Shop","typical",interval=7200)packers=DynamicResource(env,"Shop","packers")# Each step applies before every other event at its timeenv.process(run_scenario(env,schedule(rush_packers),until=3600))env.process(order_generator(env,Sampler(seed=42),packers))try:env.run(until=3600)finally:env.teardown()returnenvdefoutcomes(rush_packers:int)->dict:"""Runs the hour and returns the orders packed and their waits."""r=run(rush_packers).stats.readings()name="Shop.stats.tasks.pack_order.{}".formatreturn{"finished":r[name("finished")],"wait_mean":r[name("wait_mean")],"wait_max":r[name("wait_max")],}if__name__=="__main__":forpackersin(1,2):r=outcomes(packers)print(f"{packers} in the rush: {r['finished']} packed, "f"mean wait {r['wait_mean']:5.2f} s, longest {r['wait_max']:6.2f} s")
Next, Local Replay asks what the shop does with the orders of a real morning.