Skip to content

Time-varying Arrivals

Level: Intermediate

Change the arrival rate and the staffing at set times, such as a rush at lunch or a second packer for an afternoon, and know exactly which events a change reaches. It builds on Changing Demand, which asks what a rush does to waits, and on Live Parameters, which explains registry paths.


Steps

  1. Find the registry path of each parameter that changes: Shop.arrival.orders.rate for an exponential arrival, Shop.resources.packers.current_cap for the number of packers.
  2. Write one step for each change, with the simulation time it applies at, at, in seconds or as a duration such as 20 min.
  3. Return each parameter to its usual value with a later step; a schedule does not repeat on its own.

Here orders arrive twice as fast from 120 to 180 seconds, and a second packer works for that time:

examples/yaml/changing_demand.yaml:schedule
1
2
3
4
5
6
scenario:
  # The rush: twice the orders, and a second packer, from 120 to 180 seconds
  - {at: 120, path: Shop.arrival.orders.rate, value: 0.8}
  - {at: 120, path: Shop.resources.packers.current_cap, value: 2}
  - {at: 180, path: Shop.arrival.orders.rate, value: 0.4}
  - {at: 180, path: Shop.resources.packers.current_cap, value: 1}
examples/builder/changing_demand_example.py:schedule
1
2
3
4
5
6
7
8
9
# The rush: twice the orders, and a second packer, from 120 to 180 seconds
app.add_scenario(
    [
        {"at": 120, "path": "Shop.arrival.orders.rate", "value": 0.8},
        {"at": 120, "path": "Shop.resources.packers.current_cap", "value": 2},
        {"at": 180, "path": "Shop.arrival.orders.rate", "value": 0.4},
        {"at": 180, "path": "Shop.resources.packers.current_cap", "value": 1},
    ]
)

add_step(at, path=..., value=...) adds one step; a step can also carry a record, a ground-truth label, or an interrupt.

examples/low-level/changing_demand_example.py:schedule
1
2
3
4
5
6
7
# The rush: twice the orders, and a second packer, from 120 to 180 seconds
SCHEDULE = [
    {"at": 120, "path": "Shop.arrival.orders.rate", "value": 0.8},
    {"at": 120, "path": "Shop.resources.packers.current_cap", "value": 2},
    {"at": 180, "path": "Shop.arrival.orders.rate", "value": 0.4},
    {"at": 180, "path": "Shop.resources.packers.current_cap", "value": 1},
]

run_scenario(env, SCHEDULE, until=300) is the SimPy process that applies the steps; start it with env.process, beside the arrivals.

The steps change the registry, so everything that reads the path follows: the next arrival gap is drawn from the new rate, and the packers grant or owe units as on Resources.


Boundaries

A step that applies at the same time as another event has to come first or second, and a run that ends at a step's time has to apply it or not. Both are settled the way SimPy settles its own end time:

  • A step at time t applies before every other event at t. It is scheduled as an urgent event, as SimPy's run(until=t) is, so an arrival at t draws its next gap from the new value.
  • Steps at the same time apply in the order written.
  • A step at or after until never applies, as an event due at exactly until does not run.

This function runs orders every 10 seconds, with a step at 30 seconds that makes the gap 5:

docs/snippets/guides/time_varying_arrivals.py:boundary
def arrivals(until: float) -> tuple:
    """Orders every 10 s, every 5 s from 30 s: the arrival times, and the gap in
    force at the end."""
    app = (
        SimulationContext("Shop", factor=0)
        .add_arrival("orders", dist="constant", value=10)
        .add_step(30, path="Shop.arrival.orders.value", value=5)
    )
    times = []

    @app.arrival_loop("orders")
    def order_loop(context):
        while True:
            yield context.wait_for_arrival("orders")
            times.append(context.env.now)

    app.run(until=until)
    return times, app.env.registry.get("Shop.arrival.orders.value").value

arrivals(50) returns arrivals at 10, 20, 30, 35, 40 and 45 seconds, and a gap of 5 at the end: the order of 30 drew its gap after the step. arrivals(30) returns arrivals at 10 and 20 seconds and a gap of 10: the step at 30 is at until, so it never applies, and neither does the arrival due at 30.


Example

The changing_demand example is the model above: one packer, orders at 0.4 a second, and a rush with a second packer from 120 to 180 seconds. It needs no Docker service and ends on its own after 300 simulation seconds.

Quick Start

desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:

1
2
3
4
5
6
7
8
9
# 1. Install
uv tool install desflow

# 2. Download the example
desflow examples get changing-demand --api yaml
cd changing-demand/yaml

# 3. Run it
desflow run changing_demand.yaml
1
2
3
4
5
6
7
8
9
# 1. Install
pip install desflow

# 2. Download the example
desflow examples get changing-demand --api yaml
cd changing-demand/yaml

# 3. Run it
desflow run changing_demand.yaml

Quick Start

desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:

1
2
3
4
5
6
7
8
9
# 1. Install
uv tool install desflow

# 2. Download the example
desflow examples get changing-demand --api builder
cd changing-demand/builder

# 3. Run it
uv run --no-project --with desflow changing_demand_example.py
1
2
3
4
5
6
7
8
9
# 1. Install
pip install desflow

# 2. Download the example
desflow examples get changing-demand --api builder
cd changing-demand/builder

# 3. Run it
python changing_demand_example.py

Quick Start

desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:

1
2
3
4
5
6
7
8
9
# 1. Install
uv tool install desflow

# 2. Download the example
desflow examples get changing-demand --api low-level
cd changing-demand/low-level

# 3. Run it
uv run --no-project --with desflow changing_demand_example.py
1
2
3
4
5
6
7
8
9
# 1. Install
pip install desflow

# 2. Download the example
desflow examples get changing-demand --api low-level
cd changing-demand/low-level

# 3. Run it
python changing_demand_example.py