Skip to main content

Poisson Arrivals

SimulationBank uses a Poisson process to model customer arrivals, which is the gold standard for modeling random arrivals in queueing theory.

What is a Poisson Process?

A Poisson process describes events that occur:
  • Randomly in time
  • Independently of each other
  • At a constant average rate (λ, lambda)
Examples: Customer arrivals, phone calls to a call center, website visits, radioactive decay.
The Poisson process is memoryless: knowing when the last customer arrived tells you nothing about when the next will arrive.

Key Parameters

Arrival Rate (λ)

From simulation_config.py:14-18:
arrival_config: Dict[str, Any] = field(default_factory=lambda: {
    "arrival_rate": 1.0,  # λ (lambda): average customers per second
    "arrival_dist": "exponential",
    "priority_weights": [0.1, 0.3, 0.6]
})
λ = arrival_rate = average number of customers arriving per time unit (second)
  • λ = 0.5: 0.5 customers/second = 1 customer every 2 seconds = 30 customers/minute
  • λ = 1.0: 1 customer/second = 60 customers/minute
  • λ = 2.0: 2 customers/second = 120 customers/minute

Inter-Arrival Time Distribution

For a Poisson process, the time between arrivals follows an exponential distribution:
P(T ≤ t) = 1 - e^(-λ*t)

Mean inter-arrival time = 1/λ

Implementation

ConfigurableGenerator Class

From poisson_customer_generator.py:11-34:
class ConfigurableGenerator(CustomerGeneratorPort):
    """
    Adaptador (output): genera clientes utilizando distribuciones estadísticas configurables.
    Permite simular llegadas usando procesos de Poisson (distribución exponencial de tiempos entre llegadas)
    o intervalos fijos.
    """
    def __init__(self, config: Dict[str, Any]):
        self.config = config
        
        # Tasa promedio de llegada de clientes por unidad de tiempo (lambda)
        self.arrival_rate = config.get("arrival_rate", 1.0)
        # Tipo de distribución para las llegadas ('exponential' para procesos de Poisson, o 'fixed' para fijos)
        self.arrival_dist = config.get("arrival_dist", "exponential")
        
        # Tiempo promedio requerido para atender a un cliente (mu)
        self.service_mean = config.get("service_mean", 5.0)
        # Tipo de distribución para la duración del servicio ('exponential', 'normal', o 'constant')
        self.service_dist = config.get("service_dist", "exponential")
        # Desviación estándar utilizada únicamente cuando la distribución del servicio es 'normal'
        self.service_stddev = config.get("service_stddev", 1.0)
        
        # Probabilidades de asignar cada nivel de prioridad al generar un cliente (Alta, Media, Baja)
        self.priority_weights = config.get("priority_weights", [0.1, 0.3, 0.6])

Generating Inter-Arrival Times

From poisson_customer_generator.py:36-48:
def get_next_arrival_interval(self) -> float:
    """
    Calcula y devuelve el tiempo que transcurrirá hasta la llegada del próximo cliente.
    Utiliza distribución exponencial si está configurado como Poisson, o tiempos fijos en caso contrario.
    """
    if self.arrival_dist == "exponential":
        # Para llegadas de Poisson, el tiempo entre llegadas sigue una distribución exponencial
        return random.expovariate(self.arrival_rate)
    elif self.arrival_dist == "fixed":
        # Si el tiempo es fijo, es simplemente la inversa de la tasa de llegada
        return 1.0 / self.arrival_rate
    else:
        return random.expovariate(self.arrival_rate)
# For λ = 1.0 customers/second
interval = random.expovariate(1.0)
# Returns random value with mean = 1.0 second
# Examples: 0.3s, 1.8s, 0.05s, 2.7s (highly variable!)
Characteristics:
  • Mean = 1/λ
  • Variance = 1/λ²
  • Memoryless property
  • Models random, unpredictable arrivals

Arrival Event Scheduling

From simulation.py:120-124:
# In handle_arrival() - after processing current arrival
# Schedule NEXT arrival
next_interval = self.generator.get_next_arrival_interval()
next_time = self.clock + next_interval
if next_time <= self.config.max_simulation_time:
    self.schedule_event(SimulationEvent(next_time, EventType.ARRIVAL))
1

Generate random interval

next_interval = random.expovariate(λ) → exponentially distributed
2

Calculate absolute arrival time

next_time = current_clock + interval
3

Schedule event

Add ARRIVAL event at next_time to event queue
4

Repeat on next arrival

Each arrival schedules the next, creating continuous stream

Mathematical Properties

Exponential Distribution

Probability density function (PDF):
f(t) = λ * e^(-λ*t)    for t ≥ 0
Cumulative distribution function (CDF):
F(t) = 1 - e^(-λ*t)
Key properties:
  • Mean: E[T] = 1/λ
  • Variance: Var[T] = 1/λ²
  • Median: ln(2)/λ ≈ 0.693/λ
  • Mode: 0 (most likely inter-arrival time)
The mode being 0 means very short inter-arrival times are most common, but occasional long gaps occur!

Coefficient of Variation (CV)

CV = σ/μ = (1/λ) / (1/λ) = 1
Exponential distribution always has CV = 1, indicating moderate variability.

Example Arrival Patterns

Low Arrival Rate (λ = 0.2)

arrival_config = {"arrival_rate": 0.2}  # 0.2 customers/second

# Mean inter-arrival time = 1/0.2 = 5 seconds
# Sample arrivals:
Time: 0.0  5.3  8.1  15.7  18.2  26.4  29.1  ...
Gap:       5.3  2.8   7.6   2.5   8.2   2.7
Observation: Average gap ≈ 5 seconds, but highly variable (2.5s to 8.2s)

High Arrival Rate (λ = 5.0)

arrival_config = {"arrival_rate": 5.0}  # 5 customers/second

# Mean inter-arrival time = 1/5.0 = 0.2 seconds
# Sample arrivals:
Time: 0.0  0.1  0.4  0.5  0.7  1.0  1.1  1.2  ...
Gap:       0.1  0.3  0.1  0.2  0.3  0.1  0.1
Observation: Rapid arrivals, can overwhelm tellers if service_mean is too high

Customer Attribute Generation

From poisson_customer_generator.py:65-78:
def get_next_customer_attributes(self) -> Tuple[int, str]:
    """
    Selecciona aleatoriamente los atributos de un nuevo cliente que acaba de llegar.
    Elige su prioridad basada en los pesos configurados y asigna un tipo de transacción uniforme.
    """
    # Elige la prioridad del cliente según las ponderaciones (pesos) definidas
    prio = random.choices(
        [Priority.HIGH.value, Priority.MEDIUM.value, Priority.LOW.value],
        weights=self.priority_weights,
        k=1
    )[0]
    # Selecciona aleatoriamente un tipo de transacción bancaria
    txn = random.choice(list(TransactionType)).value
    return prio, txn
Each arriving customer gets:
  1. Priority: Randomly chosen based on priority_weights (e.g., [0.1, 0.3, 0.6])
  2. Transaction type: Uniformly random (DEPOSIT, WITHDRAWAL, etc.)
  3. Service time: Generated separately based on service_dist

Service Time Generation

From poisson_customer_generator.py:50-63:
def get_service_time(self) -> float:
    """
    Calcula y devuelve la duración del servicio que requerirá un nuevo cliente en la ventanilla.
    Soporta distribuciones exponenciales, normales (con límite inferior > 0) y constantes.
    """
    if self.service_dist == "exponential":
        return random.expovariate(1.0 / self.service_mean)
    elif self.service_dist == "normal":
        time = random.gauss(self.service_mean, self.service_stddev)
        return max(1.0, time)  # Asegura que el tiempo de servicio resultante sea siempre positivo
    elif self.service_dist == "constant":
        return self.service_mean
    else:
        return self.service_mean
service_config = {
    "service_mean": 5.0,
    "service_dist": "exponential"
}
# Mean = 5.0 seconds, high variability (CV = 1)
# Some customers take 1s, others 15s

Traffic Intensity (ρ)

The utilization or traffic intensity determines system stability:
ρ = λ / (c * μ)

Where:
  λ = arrival_rate (customers/second)
  μ = service_rate = 1/service_mean (customers/second per teller)
  c = num_tellers

Stability Condition

For a stable system: ρ < 1If ρ ≥ 1, the queue will grow unbounded!
Examples:
# STABLE: ρ = 0.67
config = SimulationConfig(
    num_tellers=3,
    arrival_config={"arrival_rate": 1.0},  # λ = 1.0
    service_config={"service_mean": 5.0}    # μ = 1/5 = 0.2
)
# ρ = 1.0 / (3 * 0.2) = 1.0 / 0.6 = 1.67... WAIT, this is UNSTABLE!

# CORRECTED STABLE: ρ = 0.83
config = SimulationConfig(
    num_tellers=6,                          # Increased tellers
    arrival_config={"arrival_rate": 1.0},
    service_config={"service_mean": 5.0}
)
# ρ = 1.0 / (6 * 0.2) = 1.0 / 1.2 = 0.83 ✓

Interpreting ρ

ρ ValueInterpretationQueue Behavior
ρ < 0.5UnderutilizedTellers often idle, minimal wait
0.5 ≤ ρ < 0.8HealthyGood balance, moderate queues
0.8 ≤ ρ < 1.0High utilizationLong queues, significant wait
ρ ≥ 1.0UNSTABLEQueue grows to infinity

Poisson vs. Fixed Arrivals

arrival_config = {
"arrival_rate": 1.0,
"arrival_dist": "exponential"  # Poisson process
}
Characteristics:
  • Random, unpredictable arrivals
  • Bursty behavior (clusters of arrivals)
  • Memoryless
  • Realistic for most real-world scenarios
Use for: Bank branches, emergency rooms, call centers

Visualization Example

Arrival Timeline

λ = 1.0 customers/second (exponential)

0    1    2    3    4    5    6    7    8    9   10  (seconds)
|•   |••  | •• |    |•   | •  |  • |••• |    |  • |
C1  C2C3 C4C5     C6   C7   C8  C9C10C11    C12

Notice:
- Cluster at t=1 (C2, C3)
- Gap from t=3-4 (no arrivals)
- Burst at t=7 (C9, C10, C11)
- Average ≈ 1 customer/second, but highly variable!

Configuration Examples

Low Traffic (Off-Peak Hours)

config = SimulationConfig(
    num_tellers=2,
    arrival_config={
        "arrival_rate": 0.3,  # 1 customer every ~3.3 seconds
        "arrival_dist": "exponential",
        "priority_weights": [0.1, 0.3, 0.6]
    },
    service_config={
        "service_mean": 8.0,
        "service_dist": "exponential"
    }
)
# ρ = 0.3 / (2 * 1/8) = 0.3 / 0.25 = 1.2 → UNSTABLE!
# Need 3 tellers: ρ = 0.3 / (3 * 0.125) = 0.8 ✓

High Traffic (Peak Hours)

config = SimulationConfig(
    num_tellers=8,
    arrival_config={
        "arrival_rate": 3.0,  # 3 customers/second!
        "arrival_dist": "exponential",
        "priority_weights": [0.15, 0.35, 0.5]
    },
    service_config={
        "service_mean": 10.0,
        "service_dist": "normal",
        "service_stddev": 2.0
    }
)
# ρ = 3.0 / (8 * 1/10) = 3.0 / 0.8 = 3.75 → VERY UNSTABLE!
# Need 30 tellers: ρ = 3.0 / (30 * 0.1) = 1.0 (marginal)
# Recommend 40 tellers: ρ = 3.0 / 4.0 = 0.75 ✓

Common Pitfalls

Problem: ρ ≥ 1 causes unbounded queue growthSolution: Calculate required tellers:
required_tellers = ceil(λ * service_mean / target_utilization)
# Example: λ=2.0, service_mean=5.0, target=0.8
required = ceil(2.0 * 5.0 / 0.8) = ceil(12.5) = 13 tellers
  • arrival_rate = 1.0 means 1 customer per second
  • Mean inter-arrival time = 1/arrival_rate = 1.0 second
  • NOT the same as “arrivals every 1 second” (that’s deterministic)
Poisson arrivals are bursty - clusters and gaps are normal!If you want uniform spacing, use "arrival_dist": "fixed"
Even if mean arrival and service rates suggest stability, high variance can cause issues.Use normal distribution with low stddev for more predictable behavior.

Further Reading

Discrete Event Simulation

How arrival events are processed

Priority Queuing

How arriving customers are queued by priority

Configuration

All SimulationConfig parameters explained

Configuring Parameters

Practical guide to tuning arrival_rate

Build docs developers (and LLMs) love