La dernière fois, j'ai compris le mécanisme de la simulation d'événements discrets et sa méthode de mise en œuvre la plus élémentaire. Plus précisément, il conserve un calendrier d'événements (une liste d'événements organisés dans l'ordre de synchronisation des occurrences), extrait les événements un par un depuis le début et exécute le traitement en fonction du type. Il y avait. Dans l'exemple de code, il existe une boucle while dans la méthode run ()
du modèle, et chaque fois qu'elle fait le tour de la boucle, le premier événement est retiré du calendrier des événements, le type est confirmé par l'instruction if et il correspond au type. J'effectuais l'étape de guidage vers le traitement. Cette méthode d'implémentation peut être considérée comme le style de codage le plus naïf qui encode simplement le mécanisme de simulation d'événement discret tel qu'il est (ci-après, ce style est parfois appelé orienté événement).
En fait, ce style de codage orienté événement présente (au moins) deux problèmes. La première est que la méthode run ()
grossit en fonction du nombre de types d'événements et du nombre de composants du système cible. Deuxièmement, parce que les processus qui changent l'état du système cible sont regroupés par le signal qui provoque le changement, c'est-à-dire l'événement, plutôt que le sujet qui cause le changement ou l'objet qui entreprend le changement, l'un par rapport à l'autre (du point de vue du sujet ou de l'objet). Le fait est qu'une série de changements connexes sera divisée en petites parties et décrite en plusieurs parties différentes. Tous ces éléments obstruent les perspectives de l'ensemble du code, et leurs effets deviennent de plus en plus graves à mesure que l'échelle du système cible augmente.
Une façon de résoudre ces problèmes consiste à adopter un style de codage orienté processus. Avec l'introduction du module SimPy de Python, vous pouvez facilement procéder au développement dans ce style orienté processus. Cette fois, apprenons les bases du codage de la simulation d'événements discrets à l'aide de ce module.
La première chose à faire est d'installer le module Simply. Avec Google Colaboratoty, vous pouvez facilement installer à l'aide de pip comme indiqué ci-dessous (notez que vous n'avez pas besoin du! Au début de la ligne lors de l'installation dans votre propre environnement local).
! pip install simpy
Il semble que les trois principaux composants de SimPy soient la classe Environment dans core.py, la classe Event et ses sous-classes dans events.py et les classes liées aux ressources dans le répertoire resources. Je devrais le faire. En plus de cela, les fonctions de processus et les méthodes implémentées en tant que générateurs jouent un rôle important. Je parlerai des classes liées aux ressources la prochaine fois, et cette fois, je couvrirai les bases du codage orienté processus basé sur les trois autres.
La classe Environment fournit les fonctions les plus élémentaires pour la simulation d'événements discrets, telles que la gestion du temps de simulation et la manipulation du calendrier des événements. Par conséquent, lors du développement d'un modèle de simulation à l'aide de SimPy, un environnement de simulation (= une instance de la classe Environment) doit être créé. D'autre part, la classe Event est une classe pour exprimer des événements, et certaines sous-classes sont également préparées comme nous le verrons plus tard.
L'enregistrement d'un certain événement (= une instance de la classe Event) dans le calendrier des événements est appelé déclenchement. Dans le cas d'un événement de classe Event normal, il se produira en même temps qu'il a été déclenché. D'autre part, nous souhaitons souvent utiliser des événements qui se produisent après un délai prédéterminé après le déclenchement. Dans ce cas, utilisez la classe Timeout, qui est une sous-classe de la classe Event.
Dans SimPy, le processus exécuté lorsqu'un certain événement se produit est appelé le rappel de cet événement, et en donnant à chaque événement «e» un ensemble de rappels «e.callbacks», l'événement «e» se produit. Les processus qui se produisent avec cela sont exécutés ensemble. Regardons un exemple simple.
import random
import simpy
class Skelton:
def __init__(self, env):
self.env = env # pointer to the SimPy environment
self.count = 0 # an example state variable
def update(self, e):
self.count += 1 # increment the event counter
def print_state(self):
print('{} th event occurs at {}'.format(self.count, round(self.env.now)))
def run(self, horizon):
while True:
e = simpy.Timeout(self.env, random.expovariate(1)) # create an Timeout instance
e.callbacks.append(self.update) # register update() method in e's callbacks
if self.env.now > horizon: # if horizen is passed
break # stop simulation
else:
self.print_state()
self.env.step() # process the next event
env = simpy.Environment()
model = Skelton(env)
model.run(200)
Dans cet exemple, presque la même fonction que le modèle squelette précédent est reproduite à l'aide de la classe Environment et de la classe Timeout de SimPy. La classe Event et la classe Calendar que j'ai créées la dernière fois sont inutiles (puisque SimPy fournit les fonctions correspondantes). Regardez les trois dernières lignes. Après avoir créé l'environnement de simulation (= env
), le modèle ( = model
) du système cible est généré en l'utilisant comme argument. Ensuite, la méthode run ()
de ce modèle est exécutée avec horizon = 200
.
Si vous vérifiez le contenu de la classe Skelton, la méthode run ()
a une boucle while, et chaque tour de celle-ci génère un événement (= e
) de la classe Timeout et définit ses rappels ( = e
). Vous pouvez voir que la méthode ʻupdate () est enregistrée dans .callbacks
). La méthode ʻupdate () est un mannequin qui n'incrémente que
count`. De plus, le callback d'un événement doit se présenter sous la forme d'une fonction (à proprement parler, un objet appelable) qui prend l'événement comme seul argument.
Le premier argument lors de la génération d'un événement de la classe Timeout est l'environnement de simulation correspondant ʻenv`, et le second argument est la longueur du délai (dans l'exemple ci-dessus, il est donné par un nombre aléatoire qui suit la distribution exponentielle). Notez que l'événement Timeout est automatiquement déclenché lorsqu'il est généré (bien qu'un événement de classe Event normal doive être déclenché explicitement comme décrit plus loin).
Le temps de simulation est géré par la variable «now» de l'environnement de simulation «env» (elle peut être désignée par «self.env.now» à partir de la méthode «run ()» ci-dessus). Si cette valeur est supérieure ou égale à l'horizon passé comme argument, la boucle while est quittée et la simulation se termine. Sinon, nous appelons la méthode step ()
de l'environnement de simulation, qui prend le premier événement ʻe du calendrier des événements et le provoque (c'est-à-dire qu'il est inclus dans ʻe.callbacks
). Cela correspond au processus d'exécution des callbacks existants dans l'ordre).
La classe Skelton dans l'exemple ci-dessus est considérablement plus simple que la précédente car certaines de ses fonctions sont laissées à l'environnement de simulation. Cependant, avec cela seul, SimPy prend en charge les fonctions de base et communes, il n'y a donc que moins de partie à coder par vous-même. En fait, on peut dire que le mérite essentiel de l'introduction de SimPy se situe au-delà de cela.
Les fonctions et méthodes de processus apportent ce mérite essentiel. Cela permet à SimPy de coder de manière orientée processus. Ensuite, expliquons le mécanisme de base à l'aide d'un exemple. Voir l'exemple ci-dessous.
class Skelton2:
def __init__(self, env):
self.env = env # pointer to the SimPy environment
self.count = 0 # an example state variable
def print_state(self):
print('{} th event occurs at {}'.format(self.count, round(self.env.now)))
def process_method(self): # an example process method
while True:
self.print_state()
yield simpy.Timeout(self.env, random.expovariate(1))
self.count += 1 # corresponding to Skelton's update()
def process_func(env): # an example process function
while True:
env.model.print_state()
yield simpy.Timeout(env, random.expovariate(1))
env.model.count += 1 # corresponding to Skelton's update()
env = simpy.Environment()
env.model = Skelton2(env)
# simpy.Process(env, process_func(env)) # when using process function
simpy.Process(env, env.model.process_method()) # when using process method
env.run(until=200)
Ceci est une réécriture de l'exemple vu ci-dessus en utilisant les fonctions et méthodes de processus. Vous avez peut-être remarqué que la méthode run ()
(et la méthode ʻupdate () ) dans la classe Skelton a disparu, et une nouvelle méthode appelée
process_method () est apparue dans la classe Skelton2. C'est la méthode du processus. Notez que cette méthode de processus ne peut pas être utilisée, et à la place une fonction de processus qui exécute la même fonction (dans l'exemple ci-dessus, la fonction
process_func ()`) peut être utilisée (les deux sont préparées dans cet exemple, mais en pratique. Un seul d'entre eux est requis).
Comme vous pouvez le voir dans les instructions yield de process_method ()
et process_func ()
, ce sont des générateurs Python. Alors que les fonctions et méthodes normales renvoient un résultat et se terminent, le générateur ne s'arrête que lorsqu'il renvoie un résultat avec yield et ne se termine pas. Ensuite, lorsque le signal de la commande de redémarrage est reçu ultérieurement, le traitement est redémarré à partir de l'extrémité de l'instruction yield.
De cette manière, les fonctions et méthodes de processus sont des générateurs définis sous la forme d'instances génératrices de la classe Event, et SimPy l'utilise comme une astuce pour le codage orienté processus. Plus précisément, lorsqu'une fonction / méthode de processus produit un événement «e», une instruction de redémarrage pour cette fonction / méthode de processus est automatiquement ajoutée à «e.callbacks».
Les fonctions / méthodes de processus sont redémarrées lorsqu'un événement généré se produit, donc le changement d'état provoqué par cet événement (dans cet exemple, l'incrément de count
) doit être décrit directement dans la partie après le redémarrage. devenir. Par conséquent, dans cet exemple, il n'est plus nécessaire d'enregistrer la méthode ʻupdate () `dans l'ensemble des rappels. Comme dans cet exemple, il peut être difficile de réaliser les avantages d'un seul événement Timeout et d'un simple changement d'état (incrément "count"), mais le changement d'état progresse de manière compliquée tout en étant affecté par plusieurs événements. Cela permet de décrire intuitivement n'importe quel processus.
Pour que la fonction / méthode de processus créée puisse être exécutée dans la simulation, elle doit être enregistrée dans l'environnement de simulation. Cela se fait dans la deuxième ligne à partir du bas (et la troisième ligne commentée). Plus précisément, vous pouvez voir qu'une instance de la classe Process est créée. À ce stade, le processus de génération d'un événement (événement d'initialisation) qui émet un signal pour démarrer le processus correspondant et le déclencher est automatiquement exécuté en arrière-plan.
De plus, la méthode run ()
de l'environnement de simulation sur la ligne du bas est un wrapper qui répète la méthode step ()
. Comme «run (until = time)» ou «run (until = event)», la simulation peut continuer jusqu'à un certain moment ou jusqu'à ce qu'un certain événement se produise. Dans cet exemple, la simulation se poursuit jusqu'à ce que le temps de simulation atteigne 200.
Vous pouvez définir plusieurs fonctions / méthodes de processus et les exécuter dans la même simulation tout en les associant les unes aux autres. Regardons un exemple ici. Un exemple simple est présenté ci-dessous.
class Skelton3(Skelton):
def __init__(self, env):
super().__init__(env)
def main_process(self):
while True:
self.print_state()
yield self.env.timeout(random.expovariate(1)) # shortcut for simpy.Timeout()
self.count += 1
if self.count %3 == 0:
self.env.signal4A.succeed() # signal for resuming sub process A
def sub_process_A(self):
self.env.signal4A = self.env.event() # create the first signal
while True:
yield self.env.signal4A
print('> sub process A is resumed at {}'.format(round(self.env.now)))
self.env.signal4A = self.env.event() # create the next signal
if self.count %5 == 0:
self.env.process(self.sub_process_B()) # register sub process B
def sub_process_B(self):
print('>> sub process B is started at {}'.format(round(self.env.now)))
yield self.env.timeout(10) # shortcut for simpy.Timeout()
print('>> sub process B is finished at {}'.format(round(self.env.now)))
env = simpy.Environment()
env.model = Skelton3(env)
env.process(env.model.main_process()) # shortcut for simpy.Process()
env.process(env.model.sub_process_A()) # shortcut for simpy.Process()
env.run(until=200)
Trois méthodes de processus, main_process ()
, sub_pricess_A ()
, et sub_process_B ()
, sont définies dans la classe Skelton3. Parmi celles-ci, la méthode main_process ()
est presque la même que la méthode process_method ()
de la classe Skelton2, à l'exception des deux dernières lignes. La méthode timeout ()
dans l'environnement de simulation est un raccourci vers simpy.Timeout ()
et est souvent utilisée car elle ne nécessite qu'un seul argument.
Dans les deux dernières lignes ajoutées, vous pouvez voir qu'un certain processus est en cours d'exécution lorsque la valeur de count
est divisible par 3. Ici, "signal4A" dans l'environnement de simulation est une instance de la classe Event générée dans la 1ère ligne (et la 5ème ligne) de la méthode "sub_process_A ()", c'est-à-dire un événement. Et la méthode reus ()
de l'événement exécute le processus de déclenchement. Par conséquent, cette partie remplit la fonction de déclencher "signal4A" chaque fois que "count" est divisible par 3.
Ensuite, regardez la méthode sub_process_A ()
. Puisque cet événement est généré sur la troisième ligne, cette méthode sera mise en pause à ce stade. Ensuite, signal4A
est déclenché par la méthode main_process ()
, et lorsque l'environnement de simulation provoque cet événement, la méthodesub_process_A ()
est redémarrée. Ce flux est l'une des méthodes typiques d'association de plusieurs fonctions / méthodes de processus.
En regardant les deuxième et troisième lignes du bas du code entier, vous pouvez voir que la méthode main_process ()
et la méthode sub_process_A ()
sont enregistrées dans l'environnement de simulation avant le début de la simulation. La méthode process ()
dans l'environnement de simulation est un raccourci vers simpy.Process ()
, qui est également souvent utilisée car elle ne nécessite qu'un seul argument.
Par conséquent, lorsque la simulation démarre, ces processus démarreront automatiquement et procéderont selon l'interaction définie ci-dessus (en particulier, la méthode main_process ()
démarrera en premier, puis cédera. Après avoir continué et mis en pause, la méthode sub_process_A ()
démarre, continue à céder et s'arrête. Après cela, lorsqu'un événement Timeout se produit, la méthode main_process ()
est redémarrée, dans laquelle le signal4A`` Quand cela se produit (alors la méthode
main_process ()est mise en pause), la méthode
sub_process_A ()` est redémarrée, et ainsi de suite).
Ensuite, regardons la méthode sub_process_B ()
. On peut voir qu'il s'agit d'un processus ponctuel qui n'a pas de boucle while. Comment l'exécution de ce processus est-elle contrôlée? En fait, le mystère est caché dans la méthode sub_process_A ()
. Regardez les deux dernières lignes. Lorsque count
est divisible par 5, vous pouvez voir que la méthode sub_process_B ()
est enregistrée dans l'environnement de simulation. En réponse à cela, ce processus sera exécuté automatiquement. De cette manière, l'enregistrement d'un nouveau processus dans l'environnement de simulation peut être effectué non seulement avant le début de la simulation, mais également à tout moment après le début de la simulation. Ce flux est également l'une des méthodes typiques d'association de plusieurs fonctions / méthodes de processus.
L'événement «e» a une variable appelée «valeur». La valeur par défaut de ʻe.value est
None, mais vous pouvez la définir sur une valeur (autre que "None") et la transmettre à la fonction / méthode de processus. Pour ce faire, lors du déclenchement de l'événement ʻe
e.succeed(La valeur que vous souhaitez définir en valeur)
(Dans le cas d'un événement Timeout, spécifiez comme argument de mot-clé " valeur = la valeur que vous souhaitez définir sur valeur
"lors de la création d'une instance). Ensuite, côté fonction / méthode de process, dans la partie rendement,
v = yied e
Si vous écrivez, la valeur de ʻe.value est entrée dans
v`.
De plus, l'événement ʻe a également une variable appelée ʻok
. Si la méthode success ()
est utilisée lors du déclenchement de l'événement ʻe, ʻe.ok = True
est automatiquement défini. Cela signifie que l'événement s'est déroulé avec succès, comme vous pouvez le voir à partir du nom de la méthode success ()
.
En fait, vous pouvez aussi utiliser les méthodes ʻe.fail (exception) et ʻe.trigger (event)
pour déclencher l'événement ʻe. Dans le premier, ʻe.ok = False
, suggérant que l'occurrence de l'événement a échoué d'une manière ou d'une autre. Lorsque cette méthode est utilisée, l'exception spécifiée dans ʻexception est entrée dans ʻe.value
, et l'exception se produit lorsque l'événement ʻe` est traité (donc le processus en attente fonctionne- Le traitement des exceptions est effectué par une méthode, etc.). De plus, dans ce dernier, les valeurs de «ok» et de «valeur» de l'événement «e» sont définies pour être les mêmes qu'un autre événement «événement» passé en argument.
Il est possible d'attendre la connexion logique de plusieurs événements avec des fonctions et des méthodes de processus. Dans ce cas, utilisez «&» pour et combinaison et «|» pour ou combinaison. Par exemple, s'il y a trois événements ʻe1, ʻe2
, ʻe3`
values = yield (e1 | e2) & e3
Cela signifie que cela peut être fait comme ça. A ce moment, "values" devient le OrderedDict de "value" de chaque événement (bien sûr, si la valeur de "value" de chaque événement n'est pas nécessaire, "values =" "n'a pas besoin d'être écrite).
Inversement, le même événement peut être attendu par plusieurs fonctions / méthodes de processus. Dans ce cas, ces processus seront redémarrés dans l'ordre dans lequel les instructions de redémarrage sont (automatiquement) ajoutées à l'ensemble des rappels pour cet événement.
Lors de l'enregistrement des fonctions et méthodes de processus, une instance de la classe Process a été créée. ce,
p = simpy.Process(env, process_func())
Il peut être pratique de pouvoir s'y référer ultérieurement, par exemple.
En fait, puisque la classe Process hérite de la classe Event, cela peut également être considéré comme un type d'événement. Autrement dit, le «p» ci-dessus peut être traité comme un événement (il est considéré comme déclenché lors du retour, et s'il y a une valeur de retour, il devient la valeur de «value»).
De plus, en appelant la méthode ʻinterrupt () avant que l'événement
pne soit déclenché, la fonction / méthode de processus correspondante peut être interrompue (arrêt anormal). En conséquence, l'instruction de redémarrage correspondante est supprimée de l'ensemble des rappels pour l'événement «e» que la fonction / méthode de processus attend dans yield. De plus, puisque l'exception «simpy.exceptions.Interrupt (cause)» est lancée dans cette fonction / méthode de processus, le comportement au moment de l'arrêt anormal peut être spécifié en le recevant et en le traitant. Cette méthode ʻinterrupt ()
n'affecte pas l'événement e lui-même (vous pouvez donc attendre de nouveau cet événement ʻe` après la gestion des exceptions).
Enfin, pour vous donner une image plus concrète, permettez-moi de vous donner un exemple de gestion d'inventaire simple que j'ai abordé la dernière fois.
class Model:
def __init__(self, env, op, oq, lt, init):
self.env = env
self.op = op # ordering point
self.oq = oq # order quantity
self.lt = lt # replenishment lead time
self.at_hand = init # how many items you have at hand
self.loss = 0 # opportunity loss
self.orders = [] # list of back orders
@property
def total(self):
return sum(self.orders) +self.at_hand
def print_state(self):
print('[{}] current level: {}, back order: {}, lost sales: {} '.format(round(self.env.now), self.at_hand, self.orders, self.loss))
self.env.log.extend()
def seller(self):
while True:
yield self.env.timeout(random.expovariate(1))
if self.at_hand > 0:
self.at_hand -= 1 # sell an item to the customer
self.env.stocktake.succeed() # activate the stocktaker
else:
self.loss += 1 # sorry we are out of stock
self.print_state() # state after dealing with each customer
def stocktaker(self):
self.env.stocktake = self.env.event() # create the first signal
while True:
yield self.env.stocktake
if self.total <= self.op:
self.orders.append(self.oq)
self.env.process(self.deliverer()) # activate deliverer
self.env.stocktake = self.env.event() # create the next signal
def deliverer(self):
self.print_state() # state after an order is placed
yield self.env.timeout(self.lt)
if len(self.orders) > 0:
self.at_hand += self.orders.pop(0)
self.print_state() # state after an order is fulfilled
Par rapport à la classe Model précédente, vous pouvez voir que la méthode run ()
(et quelques autres méthodes) a été supprimée et que trois nouvelles méthodes de processus ont été définies. Ces méthodes de traitement sont destinées au vendeur qui correspond au client qui arrive au hasard, au responsable de l'inventaire qui vérifie le montant de l'inventaire du magasin et passe une commande au besoin, et au livreur qui reçoit la commande et livre le produit. Cela correspond à chaque œuvre. Par rapport à la méthode précédente run ()
, dans laquelle ces fonctions étaient décrites de manière mixte, il semble que la visibilité du code se soit améliorée. Cet effet augmente avec l'échelle du système cible.
Apportons quelques modifications à la classe Log en fonction de l'introduction de SImPy.
import matplotlib.pyplot as plt
class Log:
def __init__(self, env):
self.env = env
self.time = []
self.at_hand = []
self.loss = []
self.total = []
self.extend()
def extend(self):
self.time.append(self.env.now)
self.at_hand.append(self.env.model.at_hand)
self.loss.append(self.env.model.loss)
self.total.append(self.env.model.total)
def plot_log(self):
plt.plot(self.time, self.at_hand, drawstyle = "steps-post")
plt.xlabel("time (minute)")
plt.ylabel("number of items")
plt.show()
Pour exécuter ce modèle de simulation, procédez comme suit:
env = simpy.Environment()
env.model = Model(env, 10, 20, 10, 20) # op, oq, lt, init
env.log = Log(env)
env.process(env.model.seller())
env.process(env.model.stocktaker())
env.run(until=200)
env.log.plot_log()
Réécrivons le modèle de simulation qui exprime l'état de l'heure du déjeuner dans un restaurant créé dans l'exercice précédent en code orienté processus à l'aide de SImPy.
Cette fois, nous avons présenté SimPy et présenté les bases de la création d'un modèle de simulation orienté processus en l'utilisant. La prochaine fois, examinons les classes liées aux ressources et comment les utiliser.