目录
- 一、说明
- 二、什么是 SimPy?
- 三、为什么使用 Python 进行离散事件模拟
- 四、 仿真数据作为输入参数
- 五、通过排序过程将模型构建为函数
- 5.1 事件 1:客户到货
- 5.2 事件 2:耕作活动
- 5.3 事件 3:只点咖啡的例子
- 5.4 活动四:餐饮活动
- 六、设置主模型运行
- 七、修改停止条件
一、说明
在计算机编程领域,仿真在理解复杂系统、进行实验和做出明智决策方面发挥着关键作用。SimPy 是“Simulation Python”的缩写,是一个功能强大且多功能的仿真框架,允许开发人员和研究人员使用 Python 创建和分析离散事件仿真。无论您是新手还是经验丰富的程序员,SimPy 都提供了一种直观有效的方法来建模和模拟各种场景,使其成为从运筹学到物流、从流行病学到制造等广泛领域不可或缺的工具。本文是一个关于如何精通 simpy 的内容广泛的系列。
二、什么是 SimPy?
SimPy 是一个开源 Python 库,专为建模和模拟离散事件系统而设计。它提供了用于创建模拟的构建块,使您能够准确地对系统中的时间和事件流进行建模。与变量随时间连续变化的连续模拟不同,离散事件模拟关注在特定时间点发生的事件。这些事件会触发系统中的状态变化,使 SimPy 特别适合涉及离散且通常不可预测的事件的建模过程。
SimPy 的一些主要功能包括:
- 进程:在 SimPy 中,模拟中的实体表示为进程。这些进程可以是任何事物,例如队列中的人、交通网络中的车辆或计算机系统中的任务。您可以使用 Python 生成器函数定义流程,从而轻松建模复杂的异步行为。
- 事件:事件是 SimPy 建模方法的核心。事件可以安排在特定时间或满足特定条件后发生。您可以创建事件链并通过调度事件来触发系统中的更改来管理模拟中的时间流。
- 资源:在许多模拟中,资源是有限的,需要在不同实体之间共享。SimPy 提供了一个资源类,允许您对资源分配和争用进行建模。这对于数据中心服务器分配或工厂机器分配等建模场景非常有价值。
- 统计和数据收集: SimPy 提供了一系列用于在模拟过程中收集数据的工具。这包括监视事件、跟踪实体在各种状态下花费的时间以及记录其他相关信息。这些统计数据对于分析和理解系统行为至关重要。
- 集成: SimPy 与其他 Python 库和工具高度兼容。您可以将其与 NumPy 和 Pandas 等数据分析库结合使用来分析模拟结果并做出数据驱动的决策。
三、为什么使用 Python 进行离散事件模拟
实际上,这是一个非常有效,合理的问题,因为就其本质而言,Python可以用于很多事情,但不一定是这个。当然,我们已经有了大量不断增长的软件,可以通过拖放毫不费力地开发离散事件仿真 (DES) 模型。
然而,尽管在构建模型方面,使用 Python 在技术上可能很棘手,而且显然不如强大的软件那么简单,但我发现用 Python 构建的模型将为后续工作带来令人兴奋的机会:收集结果、可视化结果、验证结果并优化它。我相信这些工具使仿真和机器学习的集成可能更加兼容。令人兴奋,不是吗!
无论如何,Python 是免费的,不像其他商业的东西!
最重要的是,我们正在使用 DES 最可靠的库之一:SimPy,这可能在一段时间前非常令人担忧,因为他们没有提供任何库更新的迹象,并且似乎被忽视了,直到他们确实在两个月前进行了一些重大更新并进行了一些修复,也许我们可以期待其他一些重大更新,使 SimPy 成为可靠的, DES建模中的综合工具。你可以在这里移动他们的运动
开始吧:模拟咖啡和披萨店 ????????
注意:以下研究案例叙述和代码可以通过我的 GitHub 存储库访问。 对于Python初学者来说,我采用了更直接的方法,没有将代码开发成基于OOP(面向对象编程)的经典“类”。希望这会更容易理解!
插图(图片由Greggs提供)
想象一下,你在城市主干道的拐角处经营着一家咖啡和披萨店。你想观察餐馆的日常运营是如何形成的,因为你被告知,对于某些人来说,等待时间太长,或者一些顾客抱怨为什么桌子总是被占用。
然后,您设法从直接观察中收集了一些数据。一些数据,如处理时间、客户到达间隔时间,直到收集客户偏好为止。现在使用离散事件模拟,您可以尝试在更具定量代表性的模型中实际了解餐馆的运作方式。
映射流程
流程集非常简单,上图描述了流程或事件如何分支为可能的后续事件,反映了客户旅程。要确定哪个事件将成为其先例的下一个事件,它可能会受到各种因素的影响,但在这种情况下,它仅取决于客户的偏好。例如,仅订购咖啡或比萨饼,或两者兼而有之,由客户决定。以及他们是否想在里面用餐。
四、 仿真数据作为输入参数
运行仿真模型时,必须具有定义和影响系统的输入参数。不同的输入将导致不同的输出,理解它们之间的关系至关重要。
在某些情况下,输入参数是环境因素,例如顾客到达餐厅的频率或典型的用餐时间。但是,在某些情况下,输入参数在我们的控制范围内或可观察,使我们能够确定如何设置它们。例如,可以调整工作人员的数量或桌子的数量以实现特定结果。
下面的代码将演示如何将观察到的数据转换为 python 的变量。但是,此代码不会在此单元格中定义所有必需的输入参数。我们稍后会解释原因!
# NOTE: time units interpreted across the notebook are in MINUTES
inter_arrival_time = random.expovariate(1/5) #customer arriving in every 5 minutes
processing_time = {
"till_process": random.uniform(1,3), #till process duration is uniformly distributed from a minute through 3 minutes per customer
"coffee_process": random.gauss(1,0.5), #coffee making process duration is normally distributed averaging one minute with 0.5 min as standard deviation
"pizza_process": random.gauss(5,1),
"dining_in": random.gauss(15,5)
}
五、通过排序过程将模型构建为函数
从字面上看,Simpy 库包模型离散事件系统。因此,最好将系统理解为离散事件的序列,我们将它们描述为人类驱动的活动。
SimPy 的强度取决于它对时间流逝方式进行建模的方式,因为:
- 正在进行的过程(有或没有资源)
- 等待请求的资源
因此,我们将模型构建为不同的事件块,每个事件块由一个 Python 函数表示。这些函数负责模拟相关事件展开时的时间流逝。
5.1 事件 1:客户到货
客户到达是根据前面定义的到达间隔率的分布值建模的。
这里,我们还注入了一个名为 customer_type 的输入参数,以表示到达餐厅的每组顾客中有多少人。
其中40%是单身人士,30%是夫妻,20%是三人一组,10%是四人一组。
重要提示:正如我们之前所说,并非所有输入参数都将像上面一样在指定的单元格中预定义(从 def 函数代码外部)。有时,这种方式更好,因为预定义的输入参数需要我们将它们作为函数输入参数包含在内。问题是一些输入参数只会被特定函数使用,因此将它们定义为关联功能的局部变量会更方便、更整洁。
def customer_arrival(env, inter_arrival_time):
global customer
global customer_served
customer = 0 #represent the customer ID
while True: #while the simulation is still in condition to be run
yield env.timeout(inter_arrival_time)
customer += 1 #customer ID added
customer_type = random.choices([1,2,3,4], [0.4,0.3,0.2,0.1])[0]
print(f"customer {customer} arrives at {env.now:7.4f}")
next_process = till_activity(env, processing_time, customer, customer_type)
env.process(next_process) #next process is integrated within this function
另一个编码重要说明:
直到进程作为客户到达后的下一个进程,在customer_arrival函数中调用。以这种方式构建代码很重要,因为只有当客户到达事件完成时,才会发生直到过程。
这确保了两个事件/过程按顺序进行。这也适用于剩余的后续流程/事件的其余部分
5.2 事件 2:耕作活动
活动结束后,客户将随机进行以下订单:
- 仅限披萨
- 只喝咖啡
- 咖啡和披萨
def till_activity(env, processing_time, customer, customer_type):
with staff.request() as till_request: #requesting staff to service at the till
yield till_request #waiting until the staff available
yield env.timeout(processing_time["till_process"]) #elapsed time of till activity, staff resource is automatically released after it
print(f"till complete at {env.now:7.4f} for customer {customer}")
order_type = random.randint(1,3) #random assignment for customer ordering type
dining_in = random.randint(0,1) #random assignment for whether customer intend to dine in or take away
order_coffee = coffee_activity(env, processing_time, customer, customer_type, dining_in)
order_pizza = pizza_activity(env, processing_time, customer, customer_type, dining_in)
order_all = coffee_pizza_activity(env, processing_time, customer, customer_type, dining_in)
if order_type == 1: # if customer order type is only ordering coffee, then proceed to order coffee process
env.process(order_coffee)
elif order_type == 2: # same logic with above
env.process(order_pizza)
else: env.process(order_all) # if neither only coffee nor only pizza, then they must order both!
5.3 事件 3:只点咖啡的例子
def coffee_activity(env, processing_time, customer, customer_type, dining_in):
global customer_served
with staff.request() as coffee_request:
yield coffee_request
yield env.timeout(processing_time["coffee_process"])
print(f"order complete at {env.now:7.4f} for customer {customer}")
dining_process = dining_activity(env, processing_time, customer, customer_type)
if dining_in == 1:
env.process(dining_process) #if customer intend to dine in, proceed to dine in process
else:
customer_served += 1 #customer is successfully served
print(f"Customer {customer} leaves at {env.now:7.4f}") #if customer intend to take away, they leave
5.4 活动四:餐饮活动
只有当customer_type == 1 或打算堂食时,客户才能进行此活动。
但是,即使他们打算堂食,如果顾客发现没有座位可供他们堂食,他们也会决定改签外卖。通常他们需要大约 10 秒钟来确认是否有可用的表。
def dining_activity(env, processing_time, customer, customer_type):
global customer_served
if customer_type <= 2:
with two_seater.request() as twoseater_request:
decision = yield twoseater_request | env.timeout(10/60) # the decision is whether there is available two seater or not
if twoseater_request in decision:
yield env.timeout(processing_time["dining_in"]) # customer found two seater and dining in
customer_served += 1
print(f"Dining in complete at {env.now:7.4f} for customer {customer}")
print(f"Customer {customer} leaves at {env.now:7.4f}")
else:
print(f"Customer {customer} leaves at {env.now:7.4f}") # after 10 seconds check, customer found no seat available, hence take away
customer_served += 1
else:
with four_seater.request() as fourseater_request:
decision = yield fourseater_request | env.timeout(2) # same exact scenario for group of three or four looking for four seater
if fourseater_request in decision:
yield env.timeout(processing_time["dining_in"])
print(f"Dining in complete at {env.now:7.4f} for customer {customer}")
print(f"Customer {customer} leaves at {env.now:7.4f}")
customer_served += 1
else:
print(f"Customer {customer} leaves at {env.now:7.4f}")
customer_served += 1
六、设置主模型运行
主模型代码需要设置激活上述所有开发功能所需的所有底层命令。
意味着为 Simpy 环境创建变量,设置 resource 的输入参数(后跟其容量),并调用绑定所有其他后续进程的主进程。
在我们的例子中,customer_arrival是主要或初始过程,然后是耕作活动,然后是准备咖啡和/或比萨饼,以及可选的用餐。由于我们已经确保所有这些活动都将按顺序进行,因此仅召唤customer_arrival过程就足够了,通过env.process(customer_arrival(env, inter_arrival_time))
random.seed(100) #random seed to preserve same random number generated
env = simpy.Environment() #create the essential simpy environment
staff = simpy.Resource(env, capacity = 2) #staff
two_seater = simpy.Resource(env, capacity = 4) #two seater for one or couple customer
four_seater = simpy.Resource(env, capacity = 1) #four seater for three or four group of customer
customer = 0 #set the initial customer id starting from 0
customer_served = 0 #number of customer served during the start of simulation is zero
env.process(customer_arrival(env, inter_arrival_time))
env.run(until=60*4) # run the simulation for 3 hours
print('\n')
print(f"TOTAL COMPLETE CUSTOMER:{customer_served}")
print(f"Customer in System:{customer - customer_served}")
七、修改停止条件
现在,您可以运行模拟并观察结果!在这里,我们仍然只展示非常基本的输出,这些输出是完成他们在餐馆的旅程的总客户。我们还在系统中打印客户的输出,这意味着在模拟时间完成时,有多少客户仍未完成。
您可以修改此模拟停止标准,例如,您想观察完全服务100个客户需要多长时间。以下是在 customer_arrival 函数和主模型运行函数中看到修改的代码
stop_criteria = env.event()
def customer_arrival(env, inter_arrival_time):
global customer
global customer_served
customer = 0 #represent the customer ID
customer_served
while True:
if customer_served >= 100:
stop_criteria.succeed()
else:
yield env.timeout(inter_arrival_time)
customer += 1 #customer ID added
customer_type = random.choices([1,2,3,4], [0.4,0.3,0.2,0.1])[0]
print(f"customer {customer} arrives at {env.now:7.4f}")
next_process = till_activity(env, processing_time, customer, customer_type)
env.process(next_process) #next process is integrated within this function
try:
random.seed(1000) #random seed to preserve same random number generated
env = simpy.Environment() #create the essential simpy environment
staff = simpy.Resource(env, capacity = 2) #staff
two_seater = simpy.Resource(env, capacity = 4) #two seater for one or couple customer
four_seater = simpy.Resource(env, capacity = 1) #four seater for three or four group of customer
customer = 0 #set the initial customer id starting from 0
customer_served = 0 #number of customer served during the start of simulation is zero
env.process(customer_arrival(env, inter_arrival_time))
env.run(until=stop_criteria) # now we don't need time as stopping criteria here
except Exception:
pass
print('\n')
print(f"TOTAL COMPLETE CUSTOMER:{customer_served}")
print(f"Customer in System:{customer - customer_served}")
注意:上面的代码集成在下面的 pass 异常代码中,因为我仍然无法顺利终止“env.run()”
try:
except Exception:
pass
但是,这应该不是问题,并且确实可以解决问题。生成的代码按预期工作。