Sumário
- See me, Fell me, README
- Apresentação
- Instalando o SimPy
- Começando pelo Python
-
Primeiros passos em SimPy: criando entidades
- Chamada das bibliotecas random e simpy
- Criando um evironment de simulação
- Criando um gerador de chegadas dentro do environment
- Criando intervalos de tempo de espera com env.timeout(tempo_de_espera)
- Executando o modelo por um tempo determinado com env.run(until=tempo_de_simulacao)
- Conceitos desta seção
- Desafios
- Solução dos desafios 2 e 3
- Teste seus conhecimentos
- Criando, ocupando e desocupando recursos
- Juntando tudo em um exemplo: a fila M/M/1
- Atributos e variáveis: diferenças em SimPy
- Environments: controlando a simulação
- Outros tipos de recursos: com prioridade e preemptivos
- Interrupções de processos: simpy.Interrupt
- Armazenagem e seleção de objetos específicos com Store, FilterStore e PriorityStore
- Enchendo ou esvaziando caixas, tanques, estoques ou objetos com Container()
- Criando lotes (ou agrupando) entidades durante a simulação
- Criando, manipulando e disparando eventos com event()
- Aguardando múltiplos eventos ao mesmo tempo com AnyOf e AllOf
- Propriedades úteis dos eventos
- Adicionando callbacks aos eventos
- Interrupções de eventos
- O que são funções geradoras? (ou como funciona o SimPy) - Parte I
- O que são funções geradoras? (ou como funciona o SimPy?) - Parte II
- Um exemplo de simulação e otimização
- Simulação de Agentes em SimPy!
- Entrada e saída de dados por planilha eletrônica
See me, Fell me, README
Este livro foi concebido como uma introdução à biblioteca Python SimPy (Simulation in Python) para construção de modelos de simulação de eventos discretos em Python. Ele é produto do trabalho de alguém que pesquisa, ensina e utiliza profissionalmente ferramentas de simulação no seu dia-a-dia.
Propositadamente, o livro é dividido em seções curtas, enfatizando um conceito por vez, facilitando o processo de aprendizado e a busca rápida por material de referência.
Antes de prosseguir, um pequeno aviso:
A obra pressupõe que o leitor possua conhecimentos de Modelagem e Simulação de Eventos Discretos, esteja fazendo algum curso sobre o assunto ou mesmo lendo o prestigioso livro Modelagem e Simulação de Eventos Discretos (Chwif & Medina).
Este livro, portanto, não se pretende como fonte de consulta sobre metodologias de modelagem de sistemas, mas sim, um livro sobre uma das linguagens mais fascinantes disponíveis para se construir modelos de simulação.
Ao caro leitor que inicia aqui sua navegação, uma sugestão: mentalize e reflita sobre um projeto de simulação que gostaria de desenvolver em SimPy e, a cada seção vencida do livro, tente desenvolver em paralelo o seu próprio projeto em harmonia com o conteúdo aprendido.
Un bon voyage!
Afonso C. Medina
Apresentação
Este livro foi elaborado a partir de seções compactadas em textos curtos, para que o leitor, a cada seção, tenha uma visão clara do conteúdo apresentado, construindo o conhecimento de maneira sólida e respeitando a sua curva de aprendizado pessoal.
SimPy (Simulation in Python) é um framework1 para a construção de modelos de simulação de eventos discretos em Python e distribuído segundo a licença MIT. Ele se diferencia dos pacotes comerciais usualmente utilizados na simulação de eventos discretos, pois não se trata de uma aplicação com objetos prontos, facilmente conectáveis entre si por simples cliques do mouse. Com o SimPy, cabe ao usuário construir um programa de computador em Python que represente seu modelo. Essencialmente, SimPy é uma biblioteca de comandos que conferem ao Python o poder de construir modelos a eventos discretos.
Com o SimPy pode-se construir, além de modelos de simulação discreta, modelos de simulação em “Real Time”, modelos de agentes e até mesmo modelos de simulação contínua. De fato, e como o leitor notará ao longo deste texto, essas possibilidades estão mais associadas ao Python do que propriamente aos recursos fornecidos pelo SimPy.
Por que utilizar o SimPy?
Talvez a pergunta correta seja: “por que utilizar o Python?”
Python é hoje, talvez, a linguagem mais utilizada no meio científico e uma breve pesquisa pela Internet vai sugerir artigos, posts e intermináveis discussões sobre os porquês desse sucesso todo. Eu resumiria o sucesso do Python em 3 grandes razões:
- Facilidade de codificação. Engenheiros, matemáticos e pesquisadores em geral querem pensar no problema, nem tanto na linguagem e Python cumpre o que promete quando se fala em facilidade. Se ela é fácil de codificar, mais fácil ainda é ler e interpretar um código feito em Python;
- Bibliotecas! Bibliotecas! Um número inacreditável de bibliotecas (particularmente para a área científica) está disponível para o programador (e pesquisador).
Além disso, o SimPy, quando comparado com pacotes comerciais, é gratuito - o que por si só é uma grande vantagem num mercado em que os softwares são precificados a partir de milhares de dólares - e bastante flexível, no sentido de que não é engessado apenas pelos módulos existentes.
Sob o aspecto funcional, SimPy se apresenta como uma biblioteca em Python e isso significa que um modelo de simulação desenvolvido com ele, terá à disposição tudo que existe de bom para quem programa em Python: o código fica fácil de ler (e desenvolver), o modelo pode ser distribuído como um pacote (sem a necessidade do usuário final instalar o Python para executá-lo), além das diversas bibliotecas de estatística e otimização disponíveis em Python, que ampliam, em muito, o horizonte de aplicação dos modelos.
Esta disponibilidade de bibliotecas, bem como ser um software livre, torna o SimPy particularmente interessante para quem está desenvolvendo sua pesquisa acadêmica na área de simulação. O seu modelo provavelmente ficará melhor documentado e portanto mais fácil de ser compreendido, potencializando a divulgação dos resultados de sua pesquisa em dissertações, congressos e artigos científicos.
Prós e contras
Prós:
- Código aberto e livre (licença MIT);
- Diversas funções de bibliotecas de otimização, matemática e estatística podem ser incorporadas ao modelo;
- Permite a programação de lógicas sofisticadas, apoiando-se no Python (e suas bibliotecas);
- Comunidade ativa de desenvolvedores e usuários que mantém a biblioteca atualizada.
Contras:
- Desempenho: embora seja notável a melhora de desempenho do Python a cada nova versão, SimPy é uma biblioteca de simulação, isto é: ela pertence a uma classe de aplicações bastante exigente em termos computacionais. Assim, não conte com tempos de processamento comparáveis àqueles de pacotes comerciais, tais como o AnyLogic ou o Simul8, pois não será;
- Ausência de ferramentas para animação;
- Necessidade de se programar cada processo do modelo;
- Exige conhecimento prévio em Python;
- Não inclui um ambiente visual de desenvolvimento.
Um breve histórico do SimPy
O SimPy surgiu em 2002 baseado na combinação das ideias do Simscript e da família de linguagens Simula. Contextualizando, ele combina o SiPy, pacote desenvolvido por Klaus Muller, baseado no Simula, com o SimPy, desenvolvido por Tony Vignaux and Chang Chui e baseado em outra linguagem, o Simscript. Talvez o que melhor simbolize a genênse do SimPy seja a decisão do Prof. Klaus Muller em utilizar geradores ou funções geradoras na construção do pacote. Quando os dois projetos originais foram unidos no SimPy como conhecemos hoje, o trio decidiu por seguir pelo caminho dos geradores, até então uma grata novidade no Python 2.2 (estamos ainda em 2002, pessoal!).
Começou sua infância sendo utilizado em cursos de introdução à simulação e, aos poucos foi despertando o interesse de pesquisadores e profissionais. A partir da versão 3.0, o SimPy, já contando com um número maior de desenvolvedores, foi totalmente reescrito e sua importância no mundo da simulação pode ser medida pela infinidade de trabalhos publicados em conferências e revistas especializadas, bem como na sua reimplementação em outras linguagem, tais como linguagens como a C# (Sim#), a Julia (ConcurrentSim) e o R (simmer).
Onde procurar ajuda sobre o SimPy
Existe uma variedade de vídeos e tutoriais disponíveis na Internet, mas, quando procuro ajuda nos meus projetos, são três as fontes de consulta que mais utilizo:
- O próprio site do projeto http://simpy.readthedocs.io, com exemplos e uma detalhada descrição da da Interface de Programação de Aplicações (Application Programming Interface - API);
- A lista de discussão de usuários é bastante ativa, com respostas bem elaboradoras;
- O Stack Overflow tem um número razoável de questões e exemplos, mas cuidado pois boa parte do material ainda refere-se à antiga versão 2 do SimPy, bastante diferente da versão 3, base deste livro.
Desenvolvimento deste livro
Este texto foi planejado em formato de seções compactas, de modo que sejam curtas e didáticas – tendo por meta que cada seção não ultrapasse muito além de 500 palavras.
Esta é a primeira edição de um livro sobre uma linguagem que vem apresentando interesse crescente. Naturalmente, o aumento de usuários - e espera-se que este livro contribua para isso - provocará também o surgimento de mais conhecimento, mais soluções criativas e que deverão ser incorporadas neste livro em futuras revisões. De fato, este texto já é a evolução do material que disponibizei por alguns anos como tutorial no gitBook.
Se o leitor encontrar algum erro ou quiser enviar alguma sugestão, por favor, encaminhe para o e-mail: livrosimulacao@gmail.com.
O plano proposto por este texto é caminhar pela seguinte sequência:
- Introdução ao SimPy
- Instalação do pacote
- Conceitos básicos: entidades, recursos, filas etc.
- Conceitos avançados: prioridade de recursos, compartilhamento de recursos, controle de filas, Store de recursos etc.
- Experimentação (replicações, tempo de warm-up, intervalos de confiança etc.)
- Simulação de agentes e otimização
- Aplicações
De tempos em tempos, se não chover e não fizer muito Sol, o autor se compromete a atualizar para novas versões do SimPy e do Python.
Em português, ainda é comum o estrangeirismo “framework” entre profissionais da Ciência da Computação.↩︎
Script é uma sequência de comandos executados no interior de algum aquivo por meio de um interpretador. É chamado de “script”, pois eles são lidos e interpretados pelo Python, linha a linha.↩︎
Instalando o SimPy
Nossa jornada começa por um breve tutorial de instalação de alguns programas e bibliotecas úteis para o SimPy. Selecionamos, para começar o tutorial, os seguintes pacotes:
- Python 3.4
- Pip
- SimPy 3.0.10
- NumPy
Um breve preâmbulo das nossas escolhas: no momento da elaboração deste tutorial, o Python estava na versão 3.5.0, mas o Anaconda (explicado adiante) ainda fornecia a versão 3.4. Se você já possui uma instalação com o Python 3.5 ou adelante, pode instalar o SimPy sem problemas, pois ele é compatível com as versões mais recentes do Python.
Pip é um instalador de bibliotecas e facilita muito a vida do programador.
O SimPy 3.0.10 é a versão mais atual no momento em que este tutorial é escrito e traz grandes modificações em relação à versão 2.0.
Existe vasto material disponível na Internet para o SimPy. Contudo, um cuidado especial deve ser tomado: grande parte deste material refere-se a versão 2.0, que possui diferenças críticas em relação à versão mais atual. Este texto é relativo à versão 3 em diante. |
Quanto ao NumPy, vamos aproveitar o embalo para instalá-lo, pois será muito útil nos nossos modelos de simulação. Basicamente, NumPy acrescenta um tipo de dados (n-dimensional array) que facilita a codificação de modelos de simulação, particularmente na análise de dados de saída do modelo.
Passo 1: Anaconda, the easy way
|
Se esta é a sua primeira vez, sugestão: não perca tempo e instale a distribuição gratuita do Anaconda.
Por meio do Anaconda, tudo é mais fácil, limpo e o processo já instala mais de 200 pacotes verificados por toda sorte de compatibilidade, para que você não tenha trabalho algum. (Entre os pacotes instalados está o NumPy que, como explicado, será muito útil no desenvolvimento dos seus modelos).
No momento de elaboração deste livro, eles disponibilizavam as versões 2.7, 3.4 e 3.5 do Python (em 32 e 64 bit) na página de downloads.
Baixe o arquivo com a versão desejada (mais uma vez: SimPy roda nas duas versões) e siga as instruções do instalador.
Passo 2: Instalando o Pip (para quem não instalou o Anaconda)
Se a versão instalada do Python for +3.4 ou você fez o passo anterior, pode pular este passo, pois o pip já foi instalado no seu computador. |
- Baixe o pacote
get-pip.py
por meio deste link, salvando-o em uma pasta de trabalho conveniente. - Execute
python get-pip.py
na pasta de trabalho escolhida (note a mensagem ao final, confirmando que o pip foi instalado com sucesso).
Passo 3: Instalando o SimPy
Instalar o SimPy é fácil!
Digite em uma janela cmd:
A mensagem “Sucessfully installed simpy-3.0.10” indica que você já está pronto para o SimPy. Mas, antes disso, tenho uma sugestão para você:
Passo 4: Instalando algum Ambiente Integrado de Desenvolvimento (IDE)
Os IDEs, para quem não conhece, são verdadeiras interfaces de edição de código que facilitam a vida do programador. Geralmente possuem um editor de textos avançado, recursos de verificação de erros, monitores para os estados das variáveis do código, comandos de processamento passo-a-passo etc.
Se você instalou o Anaconda, então já ganhou um dos bons: o Spyder, que já está configurado e pronto para o uso. Geralmente (a depender da sua versão do Sistema Operacional) ele aparece como um ícone na área de trabalho. Se não localizar o ícone, procure por Spyder no seu computador (repare na pegadinha do “y”) ou digite em uma janela de cmd o comando spyder.
Aberto, o Spyder fica como a figura a seguir:
Outro IDE muito bom é o Wing IDE 101 que é gratuito (e também possui uma versão profissional paga):
Se você instalou o Anaconda e pretende utilizar algum IDE que não o Spyder, siga as instruções de configurações específicas deste link: Using IDEs. |
Se você chegou até aqui e tem tudo instalado, o próximo passo é começar para valer com o SimPy!
Na próxima seção, é claro. Precisamos de uma pausa para um chazinho depois de tantos bytes instalados.
Começando pelo Python
Antes de começarmos com o SimPy, precisamos garantir que você tenha algum conhecimento mínimo de Python. Se você julga que seus conhecimentos na linguagem são razoáveis, a recomendação é que você pule para a seção seguinte, “Teste seus conhecimentos em Python”.
Se você nunca teve contato com a linguagem, aviso que não pretendo construir uma “introdução” ou “tutorial” para o Python, simplesmente porque isso é o que mais existe à disposição na internet.
Procure um tutorial rápido (existem tutoriais de até 10 minutos!) e mãos à obra! Nada mais fácil do que aprender o básico de Python.
Sugestões:
- Sololearn: um dos modos mais rápidos e bacanas de se aprender Python (e outras linguagens). São diversos cursos, divididos por níveis e que podem ser seguidos até pelo celular (sim, aprendi Java viajando pela linha azul do metrô de SP).
- Introdução à Programação Interativa em Python (Parte 1): o Coursera tem um curso que vai ensiná-lo a criar programas que interagem com o usuário, isto é: joguinhos 😂. Eu fiz, é bom!
- Summerfield, Mak. Programming in Python 3: a complete introduction. Addison-Wesley Professional. 2012: eu tenho sempre na minha mesa este ótimo livro de Python. Ele já foi traduzido para o Português e pode ser encontrado na amazon.
Feito o tutorial, curso ou aprendido mesmo tudo sozinho, teste seus conhecimentos para verificar se você sabe o básico necessário de Python para começar com o SimPy.
Teste seus conhecimentos em Python: o problema da ruina do apostador
O problema da ruína do apostador é um problema clássico proposto por Pascal em uma carta para Fermat em 1656. A versão aqui apresentada é uma simplificação, visando avaliar seus conhecimentos em Python.
DesafiosDesafio 1: dois apostadores iniciam um jogo de cara ou coroa em que cada um deles aposta $1 sempre em um mesmo lado da moeda. O vencedor leva a aposta total ($2). Cada jogador tem inicialmente $5 disponíveis para apostar. O jogo termina quando um dos jogadores atinge a ruína e não tem mais dinheiro para apostar. Construa três funções:
Teste o programa com os parâmetros sugeridos (você pode utilizar o código a seguir como uma máscara para o seu programa): |
Agora é com você: complete o código anterior e descubra se você está pronto para inciar com o SimPy!
(A próxima seção apresenta uma possível resposta para o desafio e, na sequência, tudo enfim, começa.)
Solução do desafio 1
O código a seguir é uma possível solução para o desafio 1 da seção anterior. Naturalmente é possível deixá-lo mais claro, eficiente, obscuro, maligno, elegante, rápido ou lento, como todo código de programação. O importante é que se você fez alguma que coisa que funcione, acredito que é o suficiente para começar com o SimPy.
No meu computador, o problema anterior fornece o seguinte resultado:
Note que o resultado fornecido deve ser diferente em seu computador, assim como ele se modifica a cada nova rodada no programa. Para que os resultados entre o meu computador e o seu sejam semelhantes, precisamos garantir que a sequência de números aleatórios nos dois computadores sejam as mesmas. Isso é possível com o comando random.seed(semente)
, que estabelece um valor inicial fixo para a semente da sequência de números aleatórios gerada (veja o item 1 na seção “Teste seus conhecimentos”, na seção a seguir“).
Teste seus conhecimentos
Cada vez que você executa o programa, a função random.uniform(0,1)
sorteia um novo número aleatório ente 0 e 1, tornando imprevisível o resultado do programa. Utilize a função random.seed(semente)
para fazer com que a sequência gerada de números aleatórios seja sempre a mesma.
Acrescente um laço no programa principal de modo que o jogo possa ser repetido até um número pré definido de vezes. Simule 100 partidas e verifique em quantas cada um dos jogadores venceu.
Primeiros passos em SimPy: criando entidades
Algo elementar em qualquer pacote de simulação é uma função para criar entidades dentro do modelo. É o “Alô mundo!” dos pacotes de simulação. Sua primeira missão, caso decida aceitá-la, será construir uma função que gere entidades com intervalos entre chegadas sucessivas exponencialmente distribuídos, com média de 2 min. Simule o sistema por 10 minutos apenas.
Chamada das bibliotecas random e simpy
Inicialmente, serão necessárias duas bibliotecas do Python: a random
– biblioteca de geração de números aleatórios – e a simpy
, que é o próprio SimPy.
Nosso primeiro modelo de simulação em SimPy começa com as chamadas das respectivas bibliotecas de interesse:
Note a linha final random.seed(1000)
. Ela garante que a geração de números aleatórios sempre começará pela mesma semente, de modo que a sequência de números aleatórios gerados a cada execução programa será sempre a mesma, facilitando o processo de verificação do programa.
Criando um evironment de simulação
Tudo no SimPy gira em torno de eventos gerados por funções e todos os eventos devem ocorrer em um environment, ou um “ambiente” de simulação criado a partir da função simpy.Environment()
.
Assim, nosso programa deve conter ao menos uma chamada à função simpy.Environment()
, criando um environment “env”:
Se você executar o programa anterior, nada acontece. No momento, você apenas criou um environment, mas não criou nenhum processo, portanto, não existe ainda nenhum evento a ser simulado pelo SimPy.
Criando um gerador de chegadas dentro do environment
Vamos escrever uma função geraChegadas()
que cria entidades no sistema enquanto durar a simulação. Nosso primeiro gerador de entidades terá três parâmetros de entrada: o environment, um atributo que representará o nome da entidade e a taxa desejada de chegadas de entidades por unidade de tempo. Para o SimPy, equivale dizer que você vai construir uma função geradora de eventos dentro do environment criado. No caso, os eventos gerados serão as chegadas de entidades no sistema.
Assim, nosso código começa a ganhar corpo:
Precisamos informar ao SimPy que a função geraChegadas()
é, de fato, um processo que deve ser executado ao longo de toda a simulação. Um processo é criado dentro do environment
, pelo comando:
A chamada ao processo é sempre feita após a criação do env
, então basta acrescentar uma nova linha ao nosso código:
Criando intervalos de tempo de espera com env.timeout(tempo_de_espera)
Inicialmente, precisamos gerar intervalos de tempos aleatórios, exponencialmente distribuídos, para representar os tempos entre chegadas sucessivas das entidades. Para gerar chegadas com intervalos exponenciais, utilizaremos a biblioteca random
, bem detalhada na sua documentação, e que possui a função:
Onde lambd
é a taxa de ocorrência dos eventos ou, matematicamente, o inverso do tempo médio entre eventos sucessivos. No caso, se queremos que as chegadas ocorram entre intervalos médios de 2 min, a função ficaria:
A linha anterior é basicamente nosso gerador de números aleatórios exponencialmente distribuídos. O passo seguinte será informar ao SimPy que queremos nossas entidades surgindo no sistema segundo a distribuição definida. Isso é feito pela chamada da palavra reservada yield
com a função do SimPy env.timeout(intervalo)
, que nada mais é do que uma função que causa um atraso de tempo, um delay no tempo dentro do enviroment env
criado:
Na linha de código anterior estamos executando yield env.timeout(0.5)
para que o modelo retarde o processo num tempo aleatório gerado pela função random.expovariate(0.5)
.
Oportunamente, discutiremos mais a fundo qual o papel do palavra yield
(spoiler: ela não é do SimPy, mas originalmente do próprio Python). Por hora, considere que ela é apenas uma maneira de criar eventos dentro do env
e que, caso uma função represente um processo, obrigatoriamente ela precisará conter o comando yield *alguma coisa*
, bem como o respectivo environment
do processo.
Uma função criada no Python (com o comando |
Colocando tudo junto na função geraChegadas()
, temos:
O código deve ser autoexplicativo: o laço while
é infinito enquanto dure a simulação; um contador, contaChegada
, armazena o total de entidades geradas e a função print
, imprime na tela o instante de chegada de cada cliente. Note que, dentro do print
, existe uma chamada para a hora atual de simulação env.now
.
Por fim, uma chamada a função random.seed()
garante que os números aleatórios a cada execução do programa serão os mesmos.
Executando o modelo por um tempo determinado com env.run(until=tempo_de_simulacao)
Se você executar o código anterior, nada acontece novamente, pois ainda falta informarmos ao SimPy qual o tempo de duração da simulação. Isto é feito pelo comando:
No exemplo proposto, o tempo de simulação deve ser de 10 min, como representado na linha 15 do código a seguir:
Ao executar o programa, temos a saída:
Agora sim!
Note que env.process(geraChegadas(env))
é um comando que torna a função geraChegadas()
um processo ou um gerador de eventos dentro do Environment env
. Esse processo só começa a ser executado na linha seguinte, quando env.run(until=10)
informa ao SimPy que todo processo pertencente ao env
deve ser executado por um tempo de simulação igual a 10 minutos.
Conceitos desta seção
Conteúdo | Descrição |
---|---|
env = simpy.Environment() |
cria um Environment de simulação |
random.expovariate(lambd) |
gera números aleatórios exponencialmente distribuídos, com taxa de ocorrência (eventos/unidade de tempo) igual a lambd
|
yield env.timeout(time) |
gera um atraso dado por time
|
random.seed(seed) |
define o gerador de sementes aleatórias para um mesmo valor a cada nova simulação |
env.process(geraChegadas(env)) |
inicia a função geraChegadas como um processo em env
|
env.run(until=tempoSim) |
executa a simulação (executa todos os processos criados em env ) pelo tempo tempoSim
|
env.now |
retorna o instante atual da simulação |
DesafiosDesafio 2: é comum que os comandos de criação de entidades nos softwares proprietários tenham a opção de limitar o número máximo de entidades geradas durante a simulação. Desafio 3: modifique a função |
Solução dos desafios 2 e 3
Desafio 2: é comum que os comandos de criação de entidades nos softwares proprietários tenham a opção de limitar o número máximo de entidades geradas durante a simulação.
Modifique a funçãogeraChegadas
de modo que ela receba como parâmetro onumeroMaxChegadas
e limite a criação de entidades a este número.
Neste caso, o script em Python é autoexplicativo, apenas note que limitei o número de chegadas em 5 e fiz isso antes da chamada do processo gerado pela função geraChegadas()
:
Desafio 3: modifique a função
geraChegadas
de modo que as chegadas entre entidades sejam distribuídas segundo uma distribuição triangular de moda 1, menor valor 0,1 e maior valor 1,1.
Neste caso, precisamos verificar na documentação da biblioteca random
, quais são nossas opções. A tabela a seguir, resume as distribuições disponíveis:
Função | Distribuição |
---|---|
random.random() |
gera números aleatórios no intervalo [0.0, 1.0) |
random.uniform(a, b) |
uniforme no intervalo [a, b] |
random.triangular(low, high, mode) |
triangular com menor valor low, maior valor high e moda mode |
random.betavariate(alpha, beta) |
beta com parâmetros alpha e beta |
random.expovariate(lambd) |
exponencial com média 1/lambd |
random.gammavariate(alpha, beta) |
gamma com parâmetros alpha e beta |
random.gauss(mu, sigma) |
normal com média mu e desvio padrão sigma |
random.lognormvariate(mu, sigma) |
lognormal com média mu e desvio padrão sigma |
random.normalvariate(mu, sigma) |
equivalente à random.gauss, mas um pouco mais lenta |
random.vonmisesvariate(mu, kappa) |
distribuição de von Mises com parâmetros mu e kappa |
random.paretovariate(alpha) |
pareto com parâmetro alpha |
random.weibullvariate(alpha, beta) |
weibull com parâmetros alpha e beta |
A biblioteca NumPy, que veremos oportunamente, possui mais opções para distribuições estatísticas. Por enquanto, o desafio 3 pode ser solucionado de maneira literal:
DicaOs modelos de simulação com muitos processos de chegadas e atendimento, tendem a utilizar diversas funções diferentes de distribuição de probabilidades, deixando as coisas meio confusas para o programador. Uma dica bacana é criar uma função que armazene todas as distribuições do modelo em um único lugar, como uma prateleira de distribuições. |
Por exemplo, imagine um modelo em SimPy que possui 3 processos: um exponencial com média 10 min, um triangular com parâmetros (10, 20, 30) min e um normal com média 0 e desvio 1 minuto. A função distribution()
a seguir, armazena todos os geradores de números aleatórios em um único local:
O próximo exemplo testa como chamar a função:
O qual produz a saída:
Essa foi a nossa dica do dia!
Fique a vontade para implementar funções de geração de números aleatórios ao seu gosto. Note, e isso é importante, que praticamente todos os seus modelos de simulação em SimPy precisarão deste tipo de função!
Teste seus conhecimentos
- Acrescente ao programa inicial, uma função
distribution
como a proposta na Dica do Dia e faça o tempo entre chegadas sucessivas de entidades chamar a função para obter o valor correto. - Considere que 50% das entidades geradas durante a simulação são do sexo feminino e 50% do sexo masculino. Modifique o programa para que ele sorteie o gênero dos clientes. Faça esse sorteio dentro da função
distribution
já criada.
Criando, ocupando e desocupando recursos
Criando
Em simulação, é usual representarmos processos que consomem recursos com alguma limitação de capacidade, tais como: máquinas de usinagem, operários em uma fábrica, empilhadeiras em um depósito etc. Quando um recurso é requisitado e não está disponível, há formação de uma fila de espera de entidades pela liberação do recurso.
Por exemplo, considere a simulação de uma fábrica, onde são necessários recursos “máquinas” que serão utilizados nos processos de fabricação.
No SimPy, a sintaxe para criar um recurso é:
Se o parâmetro capacity
não for fornecido, a função assume capacity=1
. Note que
maquinas
foi criando dentro do Environment env.
Ocupando
Como comentado ao início da seção, muitos processos em simulação ocupam recursos. Assim, ocupar um recurso em um processo exige a codificação de uma função específica em que um dos argumentos deve ser o próprio recurso a ser utilizado. O trecho de código a seguir, exemplifica para o caso das máquinas já criadas:
É interessante notar que ocupar um recurso no SimPy é feito em duas etapas:
- Requisitar o recurso desejado com um
req = recurso.request()
(o que é equivalente a entrar na fila para de acesso ao recurso); - Ocupar o recurso com um
yield req
.
Assim, uma chamada ao recurso maquinas
ficaria:
Enquanto a entidade estiver em fila aguardando a liberação do recurso, ela permanece na linha do comando
yield req
. Quando ela finalmente ocupa o recurso, a execução passa para a linha seguinte (comando
Se pode parecer estranho que a ocupação de um recurso envolva duas linhas de código, o bom observador deve notar que isso pode dar flexibilidade em situação de lógica intrincada.
Desocupando
Recurso criado e ocupado é liberado com a função release(req)
. Considerando, por exemplo, que o processamento de peça leva 5 minutos em uma máquina, nossa função processo
ficaria:
Para testarmos nossa função, vamos executar o processo para apenas 4 peças e analisar o resultado. O código a seguir possui um laço for
que chama a função processo
4 vezes no mesmo instante:
Quando executado, o programa retorna:
A saída da simulação permite concluir que as quatro peças chegaram no instante 0, mas como a capacidade do recurso era para apenas 2 peças simultâneas, as peças 3 e 4 tiveram que aguardar em fila até a liberação das máquinas no instante 5.
Status do recurso
O SimPy fornece alguns parâmetros para você acompanhar o status do recursos criados no modelo. Para um recurso res
definido no programa, podem ser extraídos os seguintes parâmetros durante a simulação:
res.capacity
: capacidade do recurso;res.count
: quantas unidades de capacidade do recurso estão ocupadas no momento;res.queue
: lista de objetos (no caso, requisições) que estão em fila no momento. Como res.queue é uma lista, o número de entidades em fila do recurso é obtido diretamente com o comandolen(res.queue)
;res.users
: lista de objetos (no caso, requisições) que estão ocupando o recurso no momento. Como res.users é uma lista, o número de entidades em processo no recurso é obtido diretamente com o comandolen(res.users)
.
Ao exemplo anterior acrescentamos uma pequena função, printStatus
, que imprime na tela todos os parâmetros anteriores de um recurso:
Quando executado, o programa anterior fornece como saída:
Esta seção para por aqui. Na continuação, construiremos um exemplo completo com geração de entidades e ocupação de recursos, de modo a cruzar tudo o que vimos até agora sobre o SimPy.
Conceitos desta seção
Conteúdo | Descrição |
---|---|
meuRecurso = simpy.Resource(env, capacity=1) |
cria um recurso em env com capacidade = 1 |
meuRequest = meuRecurso.request() |
solicita o recurso meuRecurso (note que o comando ainda não ocupa o recurso) |
yield meuRequest |
aguarda em fila a liberação do recurso |
meuRecurso.release(meuResquest) |
libera meuRecurso a partir do meuResquest realizado |
env.process(função_geradora) |
inicia o processo implementado na função_geradora
|
Juntando tudo em um exemplo: a fila M/M/1
A fila M/M/1 (ver, por exemplo, Chwif e Medina, 2015 ) representa um sistema simples em que clientes chegam para atendimento em um servidor de fila única, com intervalos entre chegadas sucessivas exponencialmente distribuídos e tempos de atendimentos também exponencialmente distribuídos.
Para este exemplo, vamos considerar que o tempo médio entre chegadas sucessivas é de 1 min (ou seja, uma taxa de chegadas de 1 cliente/min) e o tempo médio de atendimento no servidor é de 0,5 min (ou seja, uma taxa de atendimento de 2 clientes/min). Como um experimento inicial, o modelo deve ser simulado por 5 minutos apenas.
Geração de chegadas de entidades
Partindo da função geraChegadas,
codificada na seção “Primeiro passo em SimPy: criando entidades”, precisamos criar uma função ou processo para ocupar, utilizar e desocupar o servidor. Criaremos uma função atendimentoServidor
responsável por manter os clientes em fila e realizar o atendimento.
Inicialmente, vamos acrescentar as constantes TEMPO_MEDIO_CHEGADAS
e TEMPO_MEDIO_ATENDIMENTO
, para armazenar os parâmetros das distribuições dos processos de chegada e atendimento da fila. Adicionalmente, vamos criar o recurso servidorRes
com capacidade de atender 1 cliente por vez.
Realizando o atendimento no servidor
Se você executar o script anterior, o recurso é criado, mas nada acontece, afinal, não existe ainda nenhum processo requisitando o recurso.
Precisamos, portanto, construir uma nova função que realize o processo de atendimento. Usualmente, um processo qualquer tem ao menos as 4 etapas a seguir:
- Solicitar o servidor;
- Ocupar o servidor;
- Executar o atendimento por um tempo com distribuição conhecida;
- Liberar o servidor para o próximo cliente.
A função atendimentoServidor
, a seguir, recebe como parâmetros o env
atual, o nome
do cliente e a recurso servidorRes
para executar todo o processo de atendimento.
Neste momento, nosso script possui uma função geradora de clientes e uma função de atendimento dos clientes, mas o bom observador deve notar que não existe conexão entre elas. Em SimPy, e vamos repetir isso a exaustão, tudo é processado dentro de um environment
. Assim, o atendimento é um processo que deve ser iniciado por cada cliente gerado pela função criaChegadas.
Isto é feito por uma chamada a funçãoenv.process(atendimentoServidor(...)).
A função geraChegadas
deve ser alterada, portanto, para receber como parâmetro o recurso servidorRes,
criado no corpo do programa e para iniciar o processo de atendimento por meio da chamada à função env.process
, como representado a seguir:
Agora execute o script e voilá!
Uma representação alternativa para a ocupação e desocupação de recursos
A sequência de ocupação e desocupação do recurso pode ser representada de maneira mais compacta com o laço with
:
No script anterior a ocupação e desocupação é garantida dentro do with
, deixando o código mais compacto e legível. Contudo, a aplicação é limitada a problemas de ocupação e desocupação simples de servidores (veja um contra-exemplo no Desafio 6).
Existem muitos conceitos a serem discutidos sobre os scripts anteriores e, garanto, que eles serão destrinchados nas seções seguintes.
Por hora, e para não esticar demais a atividade, analise atentamente os resultados da execução do script e avance para cima dos nossos desafios.
Conceitos desta seção
Conteúdo | Descrição |
---|---|
with servidorRes.request() as req: |
forma compacta de representar a sequência de ocupação e desocupação do recurso: request(), yield e release(). Tudo que está dentro do with é realizado com o recurso ocupado |
DesafiosDesafio 4: para melhor compreensão do funcionamento do programa, imprima na tela o tempo de simulação e o números de clientes em fila. Quantos clientes existem em fila no instante 4.5? Desafio 5: calcule o tempo de permanência em fila de cada cliente e imprima o resultado na tela. Para isso, armazene o instante de chegada do cliente na fila em uma variável Desafio 6: um problema clássico de simulação envolve ocupar e desocupar recursos na seqüência correta. Considere uma lavanderia com 4 lavadoras, 3 secadoras e 5 cestos de roupas. Quando um cliente chega, ele coloca as roupas em uma máquina de lavar (ou aguarda em fila). A lavagem consome 20 minutos (constante). Ao terminar a lavagem, o cliente retira as roupas da máquina e coloca em um cesto e leva o cesto com suas roupas até a secadora, num processo que leva de 1 a 4 minutos distribuídos uniformemente. O cliente então descarrega as roupas do cesto diretamente para a secadora, espera a secagem e vai embora. Esse processo leva entre 9 e 12 minutos, uniformemente distribuídos. Construa um modelo de simulação que represente o processo anterior. |
Solução dos desafios 4, 5 e 6
Desafio 4: imprima na tela o tempo de simulação e o números de clientes em fila. Quantos clientes existem em fila no instante 4.5?
Para solução do desafio, basta lembrarmos que a qualquer momento, o conjunto de entidades em fila pelo recurso é dado por servidorRes.queue
e, portanto, o número de entidade em fila é facilmente obtido pela expressão:
Foi acrescentada uma chamadas à função print,
de modo a imprimir na tela o número de clientes em fila ao término do atendimento de cada cliente:
Executado o código, descobrimos que no instante 5,5 min, temos 2 clientes em fila:
Portanto, existem 0 cliente em fila no instante 4,5 minutos, nas condições simuladas (note a semente de geração de números aleatórios igual a 2).
Desafio 5: calcule o tempo de permanência em fila de cada cliente e imprima o resultado na tela. Para isso, armazene o instante de chegada do cliente na fila em uma variável
chegada.
Ao final do atendimento, armazene o tempo de fila, numa variáveltempoFila
e apresente o resultado na tela.
A ideia deste desafio é que você se acostume com esse cálculo tão trivial quanto importante dentro da simulação: o tempo de permanência de uma entidade em algum local. Neste caso, o local é uma fila por ocupação de um recurso.
A lógica aqui é a de um cronometrista que deve disparar o cronômetro na chegada do cliente e pará-lo ao início do atendimento.
Assim, ao chegar, criamos uma variável chegada
que armazena o instante atual fornecido pelo comando env.now
do SimPy:
Agora, inciado o atendimento (logo após o yield
que ocupa o recurso), a variável tempoFila
armazena o tempo de permanência em fila. Como num cronômetro, o tempo em fila é calculado pelo instante atual do cronômetro menos o instante de disparo dele já armazenado na variável chegada
:
Para imprimir o resultado, basta simplesmente alterar a chamada à função print
na linha seguinte, de modo que o código final da função atendimentoServidor
fica:
Agora, a execução do programa mostra na tela o tempo de espera de cada cliente:
Desafio 6: um problema clássico de simulação envolve ocupar e desocupar recursos na seqüência correta. Considere uma lavanderia com 4 lavadoras, 3 secadoras e 5 cestos de roupas. Quando um cliente chega, ele coloca as roupas em uma máquina de lavar (ou aguarda em fila). A lavagem consome 20 minutos (constante). Ao terminar a lavagem, o cliente retira as roupas da máquina e coloca em um cesto e leva o cesto com suas roupas até a secadora, num processo que leva de 1 a 4 minutos distribuídos uniformemente. O cliente então descarrega as roupas do cesto diretamente para a secadora, espera a secagem e vai embora. Esse processo leva entre 9 e 12 minutos, uniformemente distribuídos. Construa um modelo que represente o sistema descrito.
A dificuldade do desafio da lavanderia é representar corretamente a sequência de ocupação e desocupação dos recursos necessários de cada cliente. Se você ocupá-los/desocupá-los na ordem errada, fatalmente seu programa apresentará resultados inesperados.
Como se trata de um modelo com vários processos e distribuições, vamos seguir a Dica da seção “Solução dos desafios 2 e 3” e construir uma função para armazenar as distribuições do problema, organizando nosso código:
Como já destacado, a dificuldade é representar a sequência correta de processos do cliente: ele chega, ocupa uma lavadora, lava, ocupa um cesto, libera uma lavadora, ocupa uma secadora, libera o cesto, seca e libera a secadora. Se a sequência foi bem compreendida, a máscara a seguir será de fácil preenchimento:
O programa a seguir apresenta uma possível solução para o desafio, já com diversos comandos de impressão:
A execução do programa anterior fornece como saída:
Teste seus conhecimentos
A fila M/M/1 possui expressões analíticas conhecidas. Por exemplo, o tempo médio de permanência no sistema é dado pela expressão: . Valide seu modelo, ou seja, calcule o resultado esperado para a expressão e compare com o resultado obtido pelo seu programa.
Utilizando a função
plot
da bilbiotecamatplotlib,
construa um gráfico que represente a evolução do número de entidades em fila (dica: você precisará armazenar o tempo de espera em uma lista e plotar a lista em um gráfico ao final da simulação).No problema da lavanderia, crie uma situação de desistência, isto é: caso a fila de espera por lavadoras seja de 5 clientes, o próximo cliente a chegar no sistema desiste imediatamente de entrar na lavanderia.
Atributos e variáveis: diferenças em SimPy
Qual a diferença entre atributo e variável para um modelo de simulação? O atributo pertence à entidade, enquanto a variável pertence ao modelo. De outro modo, se um cliente chega a uma loja e compra 1, 2 ou 3 produtos, esse cliente possui um atributo imediato: o número de produtos comprados. Note que o atributo “número de produtos” é um valor diferente para cada cliente, ou seja: é um valor exclusivo do cliente.
Por outo lado, um parâmetro de saída importante seria o número total de produtos vendidos nesta loja ao longo da duração da simulação. O total de produtos é a soma dos atributos “número de produtos” de cada cliente que comprou algo na loja. Assim, o total vendido é uma variável do modelo, que se acumula a cada nova compra, independentemente de quem é o cliente.
Em SimPy a coisa é trivial: toda variável local funciona como atributo da entidade gerada e toda variável global é naturalmente uma variável do modelo. Não se trata de uma regra absoluta, nem tampouco foi imaginada pelos desenvolvedores da biblioteca, é decorrente da necessidade de se representar os processos do modelo de simulação por meio de funções que, por sua vez representam entidades executando alguma coisa.
Usuários de pacotes comerciais (Simul8, Anylogic, GPSS, Arena etc.) estão acostumados a informar explicitamente ao modelo o que é atributo e o que é variável. Em SimPy, basta lembrar que as variáveis globais serão variáveis de todo o modelo e que os atributos de interesse devem ser transferidos de um processo ao outro por transferência de argumentos no cabeçalho das funções.
Voltemos ao exemplo de chegadas de clientes numa loja. Queremos que cada cliente tenha como atributo o número de produtos desejados:
A execução do programa por apenas 5 minutos, apresenta como resposta:
É importante destacar no exemplo, que o cliente (ou entidade) gerado(a) pela função geraChegadas
é enviado(a) para a função compra
com seu atributo produtos,
como se nota na linha em que o cliente chama o processo de compra:
Agora raciocine de modo inverso: seria possível representar o número total de produtos vendidos como uma variável local? Intuitivamente, somos levados a refletir na possibilidade de transferir o número total de produtos como uma parâmetro de chamada da função. Mas, reflita mais um tiquinho… É possível passar o total vendido como um parâmetro de chamada da função?
Do modo como o problema foi modelado, isso não é possível, pois cada chegada gera um novo processo compra
independente para cada cliente e não há como transferir tal valor de uma chamada do processo para outra. A seção a seguir, apresenta uma alternativa interessante que evita o uso de variáveis globais num modelo de simulação.
Atributos em modelos orientados ao objeto
Para aqueles que seguem o paradigma da programação orientada ao objeto, um atributo de uma entidade é naturalmente o próprio atributo do objeto gerado pela classe. Uma facilidade que a programação voltada ao objeto possui é que podemos criar atributos para recursos também. Neste caso, basta que o recurso seja criado dentro de uma classe.
Por exemplo, a fila M/M/1 poderia ser modelada por uma classe Servidor
, em que um dos seus atributos é o próprio Resource
do SimPy, como mostra o código a seguir:
Quando processado por apenas 5 minutos, o modelo anterior fornece:
No caso da programação voltada ao objeto, uma variável do modelo pode pertencer a uma classe, sem a necessidade de que a variável seja global. Por exemplo, o atributo clientesAtendidos
da classe Servidor
é uma variável que representa o total de cliente atendidos ao longo da simulação. Caso a representação utilizada não fosse voltada ao objeto, o número de clientes atendidos seria forçosamente uma variável global.
Conceitos desta seção
Conteúdo | Descrição |
---|---|
representação de atributos | os atributos devem ser representados localmente e transferidos entre funções (ou processos) como parâmetros das funções (ou processos) |
representação de variáveis | as variáveis do modelo são naturalmente representadas como variáveis globais ou, no caso da programação voltada ao objeto, como atributos de classes. |
DesafiosDesafio 7: retome o problema da lavanderia (Desafio 6). Estime o tempo médio que os clientes atendidos aguardaram pela lavadora. Dica: você precisará de uma variável global para o cálculo do tempo de espera e um atributo para marcar a hora de chegada no sistema. Desafior 8: no desafio anterior, caso você simule por 10 ou mais horas, deve notar como o tempo de espera pela lavadora fica muito alto. Para identificar o gargalo do sistema, acrescente a impressão do número de clientes que ficaram em fila ao final da simulação. Você consegue otimizar o sistema a partir do modelo construído? |
Solução dos desafios 7 e 8
Desafio 7: retome o problema da lavanderia (Desafio 6). Estime o tempo médio que os clientes atendidos aguardaram pela lavadora.
DicaVocê precisará de uma variável global para o cálculo do tempo de espera e um atributo para marcar a hora de chegada do cliente na lavadora. |
O tempo médio de espera por fila de um recurso - no caso lavadoras - é estimado pelo soma do tempo que todos os clientes aguardaram pelo recurso, dividido pelo número de clientes que ocuparam o recurso ao longo da simulação.
Assim, vamos criar duas variáveis globais para armazenar a soma do tempo de espera por lavadora de todos os clientes que ocuparam as lavadoras, bem como o número de clientes que ocuparam as mesmas lavadoras:
A seguir, precisamos alterar a função lavaSeca
para calcular corretamente o tempo de espera por lavadora de cada cliente, somar este valor à variável global tempoEsperaLavadora
e incrementar o número de clientes que ocuparam lavadoras na variável global contaLavadora
(representação apenas da parte do código que é alterada):
Ao final do programa, basta acrescentar uma linha para imprimir o tempo médio em fila de espera por lavadoras e o número de vezes que uma lavadora foi ocupada ao longo da simulação:
Quando executado por 40 minutos, o modelo completo com as alterações anteriores fornece como saída:
Desafio 8: no desafio anterior, caso você simule por 10 ou mais horas, deve notar como o tempo de espera pela lavadora fica muito alto. Para identificar o gargalo do sistema, acrescente a impressão do número de clientes que ficaram em fila ao final da simulação. Você consegue otimizar o sistema a partir do modelo construído?
Quando simulamos o sistema por 10 horas (=10*60 minutos), obtemos como resposta:
Para a solução do desafio, basta acrescentar uma linha ao final do programa principal que imprime as filas de por recursos (lavadoras, cestos e secadoras) ao final da simulação:
Quando simulado por 600 minutos (ou 10 horas), a saída do modelo fornece:
Portanto, ao final da simulação, existem 56 clientes aguardando uma lavadora livre, enquanto nenhum cliente aguarda por cestos ou secadoras. Temos um caso clássico de fila infinita, isto é: a taxa de horária de atendimento das lavadoras é menor que a taxa horária com que os clientes chegam à lavanderia. Assim, se 1 cliente ocupa em média 20 minutos uma lavadora, a taxa de atendimento em cada lavadora é de , enquanto a taxa de chegadas de clientes na lavandeira é de .
Como a taxa de atendimento é menor que a taxa de chegadas, a fila cresce indefinidamente. Para termos um sistema equilibrado, precisaríamos de um número de lavadoras tal que se garanta que a taxa de atendimento da soma das lavadoras seja maior que a taxa de chegadas de clientes no sistema ou:
Portanto, com 5 (ou mais) lavadoras eliminaríamos o gargalo na lavagem.
O bom da simulação é que podemos testar se a calculera anterior faz sentido. Quando simulado para 5 lavadoras, o modelo fornece como saída:
Com 5 lavadoras, portanto, já não existe fila residual.
Teste seus conhecimentos
- Elabore uma pequena rotina capaz de simular o sistema para números diferentes de recursos (será que o número de cestos e secadoras não está exagerado também?). Manipule o modelo para encontrar o número mínimo de recursos necessário, de modo a não haver gargalos no sistema.
Environments: controlando a simulação
No SimPy, o Environment
é quem coordena a execução do seu programa. Ele avança o relógio de simulação, planeja a ordem de execução dos eventos e executa cada evento planejado pelo programa no instante correto.
Controle de execução com env.run(until=fim_da_simulação)
A maneira mais usual de controle de execução de um modelo de simulação é fornecendo a duração do experimento de simulação. O SimPy, como veremos, vai além e permite alguns outros modos de controle.
Incialmente, vamos trabalhar com um modelo simples, que gera chegadas de eventos em intervalos constantes entre si:
Quando executado, o modelo anterior fornece:
No modelo anterior, a última linha informa ao SimPy que a simulação deve ser executada até o instante 5 (implicitamente o SimPy assume que o instante inicial é 0). Esta é a maneira mais usual: o instante final de simulação é um parâmetro de entrada.
O interessante, no modelo anterior, é que se quisermos continuar a execução do instante atual (5, no caso) até o instante 10, por exemplo, podemos simplesmente acrescentar mais uma linha env.run(until=10)
informando que a execução continua de onde está (instante 5) e termina em 10.
Isto pode ser útil em situações em que precisamos modificar algum parâmetro de entrada do modelo ao longo da própria simulação.
Vamos modificar o modelo anterior de modo que nos primeiros 5 minutos o intervalo entre geração de chegadas seja de 1 minuto e, depois, até o instante 10, o intervalo seja de 2 minutos. Para isso, criamos uma variável global intervalo
que armazenará o intervalo entre chegadas, como mostra o código a seguir:
Depois de executado, o modelo anterior fornece:
A segunda chamada do run
, env.run(until=10)
, executou do instante atual (no caso, 5) até o instante 10. Assim, a opção until
não representa a duração da simulação, mas até que instante queremos executá-la. Isto implica também, que uma nova chamada para env.run
não reinicializa o tempo de simulação, isto é, não retorna o relógio do simulador para o instante 0.
Para reinicializar a relógio de um modelo em execução, o que seria equivalente a reinicializar a simulação, uma alternativa possível é acrescentar uma nova linha de criação do environment
(e, nas linhas seguintes, realizar as chamadas de processo e do env.run)
. Por exemplo:
Agora, o modelo reinicializa o relógio, como pode-se verificar pela sua saída:
Parada por execução de todos os processos programados
Quando não se fornece o tempo de simulação (ou ele não é conhecido a priori), podemos interromper a simulação pela própria extição do processo. No programa anterior, por exemplo, podemos substituir o comando while True
por um laço for
e executar a simulação com um número fixo de entidades pré estabelecido:
Quando executado, o modelo anterior fornce como saída:
Um modelo de simulação pode possuir diversos processos ocorrendo ao mesmo tempo, de modo que o término da simulação só é garantido quando todos os processos programados terminarem.
O próximo modelo amplia o exemplo anterior, de modo que dois processos são executados ao mesmo tempo, um com 3 entidades e outro com 5 entidades. Note que os processos também podem se armazendos em uma lista:
Quando executado, o modelos anterior fornece como saída:
Neste último exemplo, a simulação terminou apenas quando o processo “p1”, de 5 entidades, foi exaurido.
Parada por fim de execução de processo específico por env.run(until=processo)
Uma outra alternativa de controle de execução é pelo término do próprio processo de execução. Partindo do exemplo anterior, podemos parar a simulação quando o processo que gera 3 entidades termina. Isto é possível com a opção env.run(until=processo)
:
Quando executado, o programa anterior fornece:
No programa anterior, a linha env.run(until=chegadas[1])
determina que o programa seja executado até que o processo chegadas[1]
esteja concluído. Note que na lista:
chegadas[1]
é o processo env.process(geraChegadas(env, "p2", 3))
que deve terminar após 3 entidades criadas. Verifique na saída do programa que, neste caso, de fato o programa parou após 3 entidades do tipo “p2” geradas.
Simulação passo a passo: peek & step
O SimPy permite a simulação passo a passo por meio de dois comandos:
peek()
: retorna o instante de execução do próximo evento programado. Caso não existam mais eventos programados, retorna infinito(float('inf'));
step()
: processa o próximo evento. Caso não existam mais eventos, ele retorna um exceção internaEmptySchedule.
Um uso interessante da simulação passo a passo é na representação de barras de progresso. O exemplo a seguir faz uso da biblioteca pyprind para gerar uma barra de progresso simples (talvez você tenha de instalar a biblioteca pyprind - veja no link como proceder):
Existem outras possibilidades de uso do peek()
& step()
. Por exemplo, o Spyder (IDE sugerida para desenvolvimento dos programas deste livro) possui opções de controle de execução passo-a-passo para debugging no menu “Debug”. Assim, podemos colocar um breakpoint na linha env.step()
do programa e acompanhar melhor sua execução - coisa boa quando o modelo está com algum bug.
Conceitos desta seção
Conteúdo | Descrição |
---|---|
env.run(until=tempo_de_simulação) |
solicita que o modelo seja executado pelo tempo definido em ‘tempo_de_simulação’. Note que a unidade é instrinseca ao problema simulado. |
env.run(until=processo) |
solicita que o modelo seja simulado até que o processo termine sua execução. (Caso processo tenha um laço infinito, o programa será simulado indefinidamente ou até que Ctrl+C seja acionado no teclado). |
peek() |
retorna o instante de execução do próximo evento programado. Caso não existam mais eventos programados, retorna infinito (float('inf'));
|
step() |
processa o próximo evento. Caso não existam mais eventos, ele retorna um exceção interna EmptySchedule.
|
DesafiosDesafio 9: Considere que cada entidade gerada no primeiro exemplo desta seção tem um peso em gramas dado por uma distribuição normal de média 10 e desvio padrão igual a 3. Crie um critério de parada para quando a média dos pesos das entidades geradas esteja no intervalo entre 9,5 e 10,5. Desafio 10: Modifique o critério anterior para que a parada ocorra quando a média for 10 com um intervalo de confiança de amplitude 0,5 e nível de significância igual a 95%. Dica: utilize a biblioteca |
Solução dos desafios 9 e 10
Desafio 9: Considere que cada entidade gerada no primeiro exemplo desta seção tem um peso em gramas dado por uma distribuição normal de média 10 e desvio padrão igual a 3. Crie um critério de parada para quando a média dos pesos das entidades geradas esteja no intervalo entre 9,5 e 10,5.
Este primeiro desafio envolve poucas modificações no programa original. Acrescentamos três variáveis novas: media,
contador
e pesoTotal;
o laço while
foi substituído pelo critério de parada e algumas linhas foram acrescentadas para o cálculo da média de peso até a última entidade gerada. O peso de cada entidade é sorteado pela função random.normalvariate(mu, sigma)
da biblioteca random.
Quando executado, o modelo anterior apresenta como resultado:
Desafio 10: Modifique o critério anterior para que a parada ocorra quando a média for 10 kg, com um intervalo de confiança de amplitude 0,5 e nível de significância igual a 95%. Dica: utilize a biblioteca
numpy
para isso.
Esta situação exige um pouco mais no processo de codificação, contudo é algo muito utilizado em modelos de simulação de eventos discretos.
Como agora queremos o Intervalo de Confiança de uma dada amostra, os valores dos pesos serão armazenados em uma lista (pesosList
, no caso do desafio).
A biblioteca numpy fornece um meio fácil de se estimar a média e o desvio padrão de uma amostra de valores armazenada numa lista:
numpy.mean(pesosList)
: estima a média da listapesosList
;numpy.std(pesosList)
: estima o desvio-padrão da listapesosList
Para o cálculo do intervalo de confiança, devemos lembrar que, para amostras pequenas, a sua expressão é dada por:
A biblioteca scipy.stats possui diversas funções estatísticas, dentre elas, a distribuição t de student, necessária para o cálculo do intervalo de confiança. Como está será uma operação rotineira nos nossos modelos de simulação, o ideal é encapsular o código em uma função específica:
A função anterior calcula a média e amplitude de um intervalo de confiança, a partir da lista de valores e do nível de confiança desejado.
O novo programa então ficaria:
O programa anterior leva 411 amostras para atingir o intervalo desejado:
Existem diversas maneiras de se estimar o intervalo de confiança utilizando-se as bibliotecas do Python. A maneira aqui proposta se baseia no numpy
e no scipy.stats
. Eventualmente, se tais bibliotecas não estão instaladas no seu ambiente Python, eu antecipo: isso pode ser um baita problema para você.
A questão central é que, usualmente, os modelos de simulação possuem grande demanda por processamento computacional na armazenagem de valores e estimativa de estatísticas durante ou ao final da simulação. A biblioteca numpy
facilita bastante esta tarefa, principalmente quando se considera o suporte dado pelos usuários do Stack Overflow .
Como sugestão, habitue-se a construir funções padronizadas para monitoramento e cálculos estatísticos, de modo que você poderá reaproveitá-las em novos programas. Em algum momento, inclusive, você pode criar sua própria biblioteca de funções para análise de saída de modelos de simulação e compartilhar com a comunidade de software livre.
Outros tipos de recursos: com prioridade e preemptivos
Além do recurso como definido nas seções anteriores, o SimPy possui dois tipos específicos de recursos: com prioridade e “peemptivos”.
Recursos com prioridade: PriorityResource
Um recurso pode ter uma fila de entidades desejando ocupá-lo para executar determinado processo. Existindo a fila, o recurso será ocupado respeitando a ordem de chegada das entidades (ou a regra FIFO).
Contudo, existem situações em que algumas entidades possuem prioridades sobre as outras, de modo que elas desrespeitam a regra do primeiro a chegar é o primeiro a ser atendido.
Por exemplo, considere um consultório de pronto atendimento de um hospital em que 70% do pacientes são de prioridade baixa (pulseira verde), 20% de prioridade intermediária (pulseira amarela) e 10% de prioridade alta (pulseira vermelha). Existem 2 médicos que realizam o atendimento e que sempre verificam inicialmente a ordem de prioridade dos pacientes na fila. Os pacientes chegam entre si em intervalos exponencialmente distribuídos, com média de 5 minutos e o atendimento é também exponencialmente distribuído, com média de 9 minutos por paciente.
No exemplo, os médicos são recursos, mas também respeitam uma regra específica de prioridade. Um médico ou recurso deste tipo, é criado pelo comando:
Para a solução do exemplo, o modelo aqui proposto terá 3 funções: uma para sorteio do tipo de pulseira, uma para geração de chegadas de pacientes e outra para atendimento dos pacientes.
Como uma máscara inicial do modelo, teríamos:
O preenchimento da máscara pode ser feito de diversas maneiras, um possibilidade seria:
O importante a ser destacado é que a prioridade é informada ao request
do recurso medicos
pelo argumento priority:
Para o SimPy, quando menor o valor fornecido para o parâmetro priority,
maior a prioridade daquela entidade na fila. Assim, a função sorteiaPulseira
retorna 3 para a pulseira verde (de menor prioridade) e 1 para a vermelha (de maior prioridade).
Quando o modelo anterior é executado, fornece como saída:
Percebemos que o paciente 5 chegou no instante 11,7 minutos, depois do pacientes 3 e 4, mas iniciou seu atendimento assim que um médico ficou livre no instante 11,8 minutos (exatamente aquele que atendia ao Paciente 1).
Recursos que podem ser interrompidos: PreemptiveResource
Considere, no exemplo anterior, que o paciente de pulseira vermelha tem uma prioridade tal que ele interrompe o atendimento atual do médico e imediatamente é atendido. Os recursos com preemptividade são recursos que aceitam a interrupção da tarefa em execução para iniciar outra de maior prioridade.
Um recurso capaz de ser interrompido é criado pelo comando:
Assim, o modelo anterior precisa ser modificado de modo a criar os médicos corretamente:
Agora, devemos modificar a função atendimento
para garantir que quando um recurso for requisitado por um processo de menor prioridade, ele causará uma interrupção no Python, o que obriga a utilização de bloco de controle de interrupção try:...except
.
Quando um recurso deve ser interrompido, o SimPy retorna um interrupção do tipo simpy.Interrupt,
como mostrado no código a seguir (noteo bloco try...except
dentro da função atendimento):
Quando simulado por apenas 20 minutos, o modelo acrescido das correções apresentadas fornece a seguinte saída:
Note que o Paciente 5 interrompe o atendimento do Paciente 1, como desejado.
Contudo, a implementação anterior está cheia de limitações: pacientes com pulseira amarela não deveriam interromper o atendimento, mas na implementação proposta eles devem interromper o atendimento de pacientes de pulseira verde. Para estas situações, o request
possui um argumento preempt
que permite ligar ou desligar a opção de preemptividade:
O modelo alterado para interromper apenas no caso de pulseiras vermelhas, ficaria (note que o argumento preempt
é agora fornecido diretamente a partir da função sorteiaPulseira):
O modelo anterior, quando executado por apenas 20 minutos, fornece como saída:
Conceitos desta seção
Conteúdo | Descrição |
---|---|
meuRecurso = simpy.PriorityResource(env, capacity=1) |
Cria um recurso com prioridade e capacidade = 1 |
meuRequest = meuRecurso.request(env, priority=prio) |
Solicita o recurso meuRecurso (note que ele ainda não ocupa o recurso) respeitando a ordem de prioridade primeiro e a regra FIFO a seguir |
meuRecursoPreempt = simpy.PreemptiveResource(env, capacity=1) |
Cria um recurso em env que pode ser interrompido por entidades de prioridade maior |
meuRequest = meuRecursoPreempt.request(env, priority=prio, preempt=preempt) |
Solicita o recurso meuRecurso (note que ele ainda não ocupa o recurso) respeitando a ordem de prioridade primeiro e a regra FIFO a seguir. Caso preempt seja False o o recurso não é interrompido |
try:...except simpy.Interrupt: |
Chamada de interrupção utilizada na lógica try:…except: |
DesafiosDesafio 11: acrescente ao último programa proposto o cálculo do tempo de atendimento que ainda falta de atendimento para o paciente que foi interrompido por outro e imprima o resultado na tela. Desafio 12: quando um paciente é interrompido, ele deseja retornar ao atendimento de onde parou. Altere o programa para que um paciente de pulseira verde interrompido possa retornar para ser atendido no tempo restante do seu atendimento. Dica: altere a numeração de prioridades de modo que um paciente verde interrompido tenha prioridade superior ao de um paciente verde que acabou de chegar. |
Solução dos desafios 11 e 12
Desafio 11: acrescente ao último programa proposto o cálculo do tempo de atendimento que ainda falta para o paciente que foi interrompido por outro e imprima o resultado na tela.
Neste caso, precisamos acrescentar o cálculo do tempo faltante para o paciente na função atendimento:
Quando o modelo é executado por apenas 20 minutos, com a alteração apresentada da função atendimento,
temos como saída:
Desafio 12: quando um paciente é interrompido, ele deseja retornar ao atendimento de onde parou. Altere o programa para que um paciente de pulseira verde interrompido possa retornar para ser atendido no tempo restante do seu atendimento. Dica: altere a numeração de prioridades de modo que um paciente verde interrompido tenha prioridade superior ao de um paciente verde que acabou de chegar.
Novamente, as alterações no modelo anterior resumem-se à função atendimento
: precisamos aumentar a prioridade de um paciente interrompido em relação aos pacientes que acabam de chegar com a mesma pulseira, afinal, ele tem prioridade em relação a um paciente recém chegado de gravidade equivalente. Além disso, tal paciente, deve ser atendido pelo tempo restante de atendimento, de modo que a função deve receber como parâmetro esse tempo.
O artifício utilizado neste segundo caso foi acrescentar um parâmetro opcional à função, tempoAtendimento
, de modo que se ele não é fornecido (caso de um paciente novo), a função sorteia um tempo exponecialmente distribuído, com média de 9 minutos. De outro modo, se o parâmetro é fornecido, isso significa que ele é um parceiro interrompido e, portanto, já tem um tempo restante de atendimento calculado.
O código a seguir, representa uma possível solução para a nova função atendimento
do desafio:
Quando executado por apenas 20 minutos, o modelo completo - acrescido da nova função atendimento
, fornece como saída:
Note que agora, o Paciente 1, diferentemente do que ocorre na saída do desafio 11, é atendido antes do Paciente 3, representando o fato de que, mesmo interrompido, ele voltou para o início da fila.
Interrupções de processos: simpy.Interrupt
Você está todo feliz e contente atravessando a galáxia no seu X-Wing quando… PIMBA! Seu dróide astromecânico pifa e só lhe resta interromper a viagem para consertá-lo, antes que apareça um maldito caça TIE das forças imperiais.
Nesta seção iremos interromper processos já em execução e depois retomar a operação inicial. A aplicação mais óbvia é para a quebra de equipamentos durante a operação, como no caso do R2D2.
A interrupção de um processo em SimPy é realizada por meio de um comando Interrupt
aplicado ao processo já iniciado. O cuidado aqui é que quando um recurso é interrompido por outro processo ele causa uma interrupção, de fato, no Python, o que nos permite utilizar o bloco de controle de interrupção try:...except
, o que não deixa de ser uma boa coisa, dada a sua facilidade.
Criando quebras de equipamento
Voltando ao exemplo do X-Wing, considere que a cada 10 horas o R2D2 interrompe a viagem para uma manutenção de 5 horas e que a viagem toda levaria (sem não houvessem paralisações) 50 horas.
Inicialmente, vamos criar duas variáveis globais: uma para representar se o X-Wing está operando - afinal, não queremos interrompê-lo quando ele já estiver em manutenção - e outra para armazenar o tempo ainda restante para a viagem.
O próximo passo, é criar uma função que represente a viagem do x-wing, garantindo não só que ela dure o tempo correto, mas também que lide com o processo de interrupção:
O importante no programa anterior é notar o bloco try:...except:,
interno ao laço while duracaoViagem > 0,
que mantém o nosso X-Wing em processo enquanto a variável duracaoViagem
for maior que zero. O except
aguarda um novo comando, o simpy.Interrupt
, que nada mais é do que uma interrupção causada por algum outro processo dentro do Environment.
Quando executado, o programa fornece uma viagem tranquila:
A viagem é tranquila, pois não criamos ainda um “gerador de interrupções”, que nada mais é do que um processo em SimPy que cria a interrupção da viagem.
Note, na penúltima linha do código anterior, que o processo em execução foi armazenado na variável viagem
e oque devemos fazer é interrompê-lo de 10 em 10 horas. Para tanto, a função paradaTecnica
a seguir, verifica se o processo de viagem está em andamento e paralisa a operação depois de 10 horas:
A função paradaTecnica,
portanto, recebe como parâmetro o próprio objeto que representa o processo viagem
e, por meio do comando:
Provoca uma interrupção no processo, a ser reconhecida pela função viagem
na linha:
Adicionalmente, o processo parada técnica também deve ser inciado ao início da simulação, de modo que a parte final do modelo fica:
Quando executado, o modelo completo fornece como saída:
Alguns aspectos importantes do código anterior:
A utilização de variáveis globais foi fundamental para informar ao processo de parada o status do processo de viagem. É por meio de variáveis globais que um processo “sabe” o que está ocorrendo no outro;
-
Como a execução
env.run()
não tem um tempo final pré-estabelecido, a execução dos processos é terminada quando o laçowhile
resulta em um Falso:Note que esse
while
deve existir nos dois processos em execução, caso contrário, o programa seria executado indefinidamente; Dentro da função
paradaTecnica
a variável globalviajando
impede que ocorram duas quebras ao mesmo tempo. Naturalmente o leitor atento sabe que isso jamais ocorreria, afinal, o tempo de duração da quebra é inferior ao intervalo entre quebras. Mas fica o exercício: execute o mesmo programa, agora para uma duração de quebra de 15 horas e veja o que acontece.-
Se um modelo possui uma lógica de interrupção
.interrupt()
e não possui um comandoexcept simpy.Interrupt
para lidar com a paralização do processo, o SimPy finalizará a simulação retornando o erro:Na próxima seção é apresentada uma alternativa para manter o processamento da simulação.
Interrompendo um proccesso sem captura por try…except
Caso o modelo possua um comando de interrupção de processo, mas não exista nenhuma lógica de captura com o bloco try... except
, a simulação será finalizada com a mensagem de erro:
O SimPy cria, para cada processo, uma propriedade chamada defused
que permite contornar a paralisação. Assim, pode-se interromper um processo, sem que essa interrupção provoque estrago algum ao processamento do modelo. Para desativar a paralisação do modelo (e manter apenas a paralisação do processo), basta tornar a propriedade defused = True
:
Um exemplo bem prático: você está na sua rotina de exercícios matinais, quando… PIMBA:
O modelo anterior deve ser autoexplicativo: o processo forca
é interrompido pelo processo ladoNegro
depois de 3 unidades de tempo. Note a linha proc.defused = True
que, ao “desarmar” o processo, impede que o Python interrompa o programa todo.
Quando executado, o modelo fornece como resultado:
Conceitos desta seção
Conteúdo | Descrição |
---|---|
processVar = env.process(função_processo(env)) |
armazena o processo da função_processo na variável processVar
|
processVar.interrupt() |
interrompe o processo armazenado na variável processVar
|
try:...except simpy.Interrupt |
lógica try...except necessária para interrupção do processo |
DesafiosDesafio 13 Considere que existam dois tipos de paradas: uma do R2D2 e outra do canhão de combate. A parada do canhão de combate ocorre sempre depois de 25 horas de viagem (em quebra ou não) e seu reparo dura 2 horas. Contudo, para não perder tempo, a manutenção do canhão só é realizada quando o R2D2 quebra. Desafio 14 Você não acha que pode viajar pelo espaço infinito sem encontrar alguns TIEs das forças imperiais, não é mesmo? Considere que a cada 25 horas, você se depara com um TIE imperial. O ataque dura 30 minutos e, se nesse tempo você não estiver com o canhão funcionando, a sua próxima viagem é para o encontro do mestre Yoda. Dica: construa uma função |
Solução dos desafios 13 e 14
Desafio 13 Considere que existam dois tipos de paradas: uma do R2D2 e outra do canhão de combate. A parada do canhão de combate ocorre sempre depois de 25 horas de viagem (em quebra ou não) e seu reparo dura 2 horas. Contudo, para não perder tempo, a manutenção do canhão só é realizada quando o R2D2 quebra.
Inicialmente, precisamos de uma variável global para verificar a situação do canhão:
A função viagem, agora deve lidar com um tempo de parada do canhão. Contudo, não existe uma interrupção para o canhão, pois nosso indomável piloto jedi apenas verifica a situação do canhão ao término do concerto do R2D2:
Além da função de parada técnica (já pertencente ao nosso modelo desde a seção anterior), precisamos de uma função que tire de operação o canhão, ou seja, apenas desligue a variável global canhao:
Por fim, o processo de quebra do canhão deve ser inciado juntamente com o resto do modelo de simulação:
Quando o modelo completo é executado, ele fornece como saída:
Desafio 14 Você não acha que pode viajar pelo espaço infinito sem encontrar alguns TIEs das forças imperiais, não é mesmo? Considere que a cada 25 horas, você se depara com um TIE imperial. O ataque dura 30 minutos e, se nesse tempo você não estiver com o canhão funcionando, a sua próxima viagem é para o encontro do mestre Yoda. Dica: construa uma função
executaCombate
que controla todo o processo de combate. Você vai precisar também de uma variável global que informa se o X-Wing está ou não em combate.
Incialmente, precisamos de uma variável global para verificar a situação do combate:
A função executaCombate
a seguir, incia o processo de combate e verifica quem foi o vitorioso:
A quebra de canhão agora deve verificar se a nave está em combate, pois, neste caso, o X-Wing será esmagado pelo TIE:
Finalmente, a incialização do modelo deve contar com uma chamada para o processo executaCombate:
A última linha do código anterior tem algo importante: optei por executar a simulação enquanto durar o processo de combate, afinal, ele mesmo só é executado caso a X-Wing esteja ainda em viagem.
Por fim, quando executado, o modelo completo fornece como saída:
Teste seus conhecimentos
- Não colocamos distribuições aleatórias nos processos. Acrescente distribuições nos diversos processos e verifique se o modelo precisa de alterações.
- Acrescente a tentativa da destruição da Estrela da Morte ao final da viagem: com 50% de chances, nosso intrépido jedi consegue acertar um tiro na entrada do reator e explodir a Estrela da Morte (se ele erra, volta ao combate). Replique algumas vezes o modelo e estime a probabilidade de sucesso da operação.
Armazenagem e seleção de objetos específicos com Store, FilterStore e PriorityStore
O SimPy possui uma ferramenta para armazenamento de objetos - como valores, recursos etc. - chamada Store;
um comando de acesso a objetos específicos dentro do Store
por meio de filtro, o FilterStore
e um comando de acesso de objetos por ordem de prioridade, o PriorityStore.
O programador experiente vai notar certa similaridade da família Store
com o dicionário do Python.
Vamos desvendar o funcionamento do Store
a partir de um exemplo bem simples: simulando o processo de atendimento em uma barbearia com três barbeiros. Quando você chega a uma barbearia e tem uma ordem de preferência entre os barbeiros, isto é: barbeiro 1 vem antes do 2, que vem antes do 3, precisará selecionar o recurso barbeiro na ordem certa de sua preferência, mas lembre-se: cada cliente tem seu gosto e gosto não se discute, simula-se!
Construindo um conjunto de objetos com Store
Inicialmente, considere que os clientes são atribuídos ao primeiro barbeiro que encontrar-se livre, indistintamente. Se todos os barbeiros estiverem ocupados, o cliente aguarda em fila.
O comando que constrói um armazém de objetos é o simpy.Store():
meuStore = simpy.Store(env, capacity=capacidade)
Para manipular o Store
criado, temos três comandos à disposição:
meuStore.items:
adiciona objetos aomeuStore;
yield meuStore.get():
retira o primeiro objeto disponível demeuStore
ou, caso omeuStore
esteja vazio, aguarda até que algum objeto sela colocado noStore;
-
yield meuStore.put(umObjeto):
coloca um objeto nomeuStore
ou, caso omeuStore
esteja cheio, aguarda até que surja um espaço vazio para colocar o objeto.Observação: se a capacidade não for fornecida, o SimPy assumirá que a capacidade do Store é ilimitada.
Para a barbearia, criaremos um Store
que armazenará o nome dos barbeiros, aqui denominados de 0, 1 e 2:
No código anterior, criamos uma lista com três recursos que representarão os barbeiros. A seguir, criamos uma Store,
chamada barbeariaStore,
de capacidade 3 e adicionamos, na linha seguinte, um lista com os três números que representam os próprios barbeiros.
Em resumo, nosso Store
contém apenas os números 0, 1 e 2.
Considere que o intervalo entre chegadas sucessivas de clientes é exponencialmente distribuído com média de 5 minutos e que cada barbeiro leva um tempo normalmente distribuído com média 10 e desvio padrão de 2 minutos para cortar o cabelo.
Uma possível máscara para o modelo de simulação seria:
A função para gerar clientes é semelhante a tantas outras que já fizemos neste livro:
A função de atendimento, traz a novidade de que primeiro devemos retirar um objeto/barbeiro do Store
e, ao final do atendimento, devolvê-lo ao Store:
Quando estamos retirando um objeto do barbeariaStore
, estamos apenas retirando o nome (ou identificador) do barbeiro disponível. Algo como retirar um cartão com o nome do barbeiro de uma pilha de barbeiros disponíveis. Se o cartão é retirado, significa que, enquanto ele não for devolvido à pilha, nenhum cliente poderá ocupá-lo, pois ele não se encontra na pilha de barbeiros disponíveis.
Para ocuparmos o recurso (ou barbeiro) selecionado corretamente, utilizamos o identificador como índice da lista barbeiroList
. Assim, temporariamente, o barbeiro retirado do Store fica indisponível para outros clientes, pois a linha:
Só retornará um barbeiro dentre aqueles que ainda estão à disposição. Ao ser executado por apenas 20 minutos, o modelo de simulação completo da barbeira fornece como saída:
No caso do exemplo, o Store
armazenou basicamente uma lista de números [0,1,2], que representam os nomes dos barbeiros. Poderíamos sofisticar um pouco mais o exemplo e criar um dicionário (em Python) para manipular os nomes reais dos barbeiros. Por exemplo, se os nomes dos barbeiros são: João, José e Mário, poderíamos montar o barberirosStore
com seus respectivos nomes e evitar o uso de números:
O exemplo anterior apenas reforça que Store
é um local para se armazenar objetos de qualquer tipo (semelhante ao dict
do Python).
|
Selecionando um objeto específico com FilterStore()
Considere agora o caso bastante comum em que precisamos selecionar um recurso específico (segundo alguma regra) dentro de um conjunto de recursos disponíveis. Na barbearia, por exemplo, cada cliente agora tem um barbeiro preferido e, se ele não está disponível, o cliente prefere aguardar sua liberação.
Neste caso, vamos assumir que a preferência de um cliente é uniformemente distribuída entre os três barbeiros.
Precisamos, portanto, de um modo de selecionar um objeto específico dentro do Store
. O SimPy tem um comando para construir um conjunto de objetos filtrável, o FilterStore
:
A grande diferença para o Store
é que podemos utilizar uma função anônima do Python dentro do comando .get()
e assim puxar um objeto de dentro do FilterStore
segundo alguma regra codificada por nós.
Inicialmente, criaremos um FilterStore
de barbeiros, que armazenará apenas os números 1, 2 e 3:
A função geradora de clientes terá uma ligeira modificação, pois temos de atribuir a cada cliente um barbeiro específico, respeitando o sorteio de uma distribuição uniforme:
Na função anterior, o atributo barbeiroEscolhido
armazena o número do barbeiro sorteado e envia a informação para a função que representa o processo de atendimento.
A função atendimento
utilizará uma função anônima para buscar o barbeiro certo dentro doFilterStore
criado:
Para selecionar o número certo do barbeiro, existe uma função lambda
inserida dentro do .get()
:
Esta função percorre os objetos dentro da barbeariaStore
até encontrar um que tenha o número do respectivo barbeiro desejado pelo cliente. Note que também poderíamos ter optado por uma construção alternativa utilizando o nome dos barbeiros e não os números - neste caso, uma alternativa seria seguir o exemplo da seção anterior e utilizar um dicionário para associar o nome dos barbeiros aos respectivos recursos.
Quando executado, o modelo anterior fornece:
Repare que o cliente 3 chegou num instante em que o barbeiro 1 estava ocupado atendendo o cliente 1, assim ele foi obrigado a esperar em fila por 3 minutos, até que o cliente 1 liberasse o Barbeiro 1.
Criando um Store com prioridade: PriorityStore
Como sabemos, um Store
segue a regra FIFO, de modo que o primeiro objeto a entrar no Store
será o primeiro a sair do Store
. É possível quebrar essa regra por meio do PriorityStore
:
Para acrescentar um objeto qualquer ao meuPriorityStore
já criado, a sequência de passos é primeiro criar um objeto PriorityItem
- que representará o objeto a ser armazendo - para depois inseri-lo com um comando put,
como representado no exemplo a seguir:
Observação: como no caso do
PriorityResource
, quanto menor o valor depriority
, maior a preferência pelo objeto.
No caso dos barbeiros: “João, José e Mário”, considere que a ordem de prioridades é a própria ordem alfabética dos nomes. Assim, inicialmente, construiremos dois dicionários para armazenar essas informações sobre os barbeiros:
A partir dos dicionários anteriores, podemos construir um PriorityStore
que armazena os nomes dos barbeiros e suas prioridades:
A função atendimento
é muito semelhante às anteriores, basta notar que o comando get
vai buscar não o nome, ma um objeto PriorityItem
dentro do PriorityStore
. Este objeto possui dois atributos: item
- no nosso caso, o nome do barbeiro - e priority
- a prioridade do objeto.
A seguir, apresenta-se uma possível implementação da função atendimento.
Note que o objeto barbeiro
é um PriorityItem
e que, para sabermos o seu nome, precisamos do comando barbeiro.item:
O modelo de simulação completo, quando simulado por apenas 20 minutos, fornece como saída:
Observação 1: internamente, o SimPy trata a “família” |
Conceitos desta seção
Conteúdo | Descrição |
---|---|
meuStore = simpy.Store(env, capacity=capacity |
cria um Store meuStore : um armazém de objetos com capacidade capacity . Caso o parâmetro capacity não seja fornecido, o SimPy considera capacity=inf . |
yield meuStore.get() |
retira o primeiro objeto disponível de meuStore ou, caso o meuStore esteja vazio, aguarda até que algum objeto esteja disponível. |
yield meuStore.put(umObjeto) |
coloca um objeto no meuStore ou, caso o meuStore esteja cheio, aguarda até que surja um espaço vazio para colocar o objeto. |
meuFilterStore = simpy.FilterStore(env, capacity=capacity) |
cria um Store meuStore : um armazém de objetos filtráveis com capacidade capacity . Caso o parâmetro capacity não seja fornecido, o SimPy considera capacity=inf . |
yield meuFilterStore.get(filter=<function <lambda>>) |
retira o 1° objeto do meuFilterStore que retorne True para a função anônima fornecida por filter. |
meuPriorityStore = simpy.PriorityStore(env, capacity=inf) |
cria um PriorityStore meuPriorityStore - uma armazém de objetos com ordem de prioridade e capacidade capacity . Caso o parâmetro capacity não seja fornecido, o SimPy considera capacity=inf . |
meuObjetoPriority = simpy.PriorityItem(priority=priority, item=meuObjeto) |
cria um objeto meuObjetoPriority a partir de um objeto meuObjeto existente, com prioridade para ser armazenado em um PriorityStore . A priority deve ser um objeto ordenável. |
meuObjetoPriority.item |
retorna o atributo item do objeto meuObjetoPriority , definido no momento de criação do PriorityItem.
|
meuObjetoPriority.priority |
retorna o atributo priority do objeto meuObjetoPriority , definido no momento de criação do PriorityItem.
|
meuPriorityStore.put(meuObjetoPriority) |
coloca o objeto meuObjetoPriority no PriorityStore meuPriorityStore ou, caso o meuPriorityStore esteja cheio, aguarda até que surja um espaço vazio para colocar o objeto. |
yield meuPriorityStore.get() |
retorna o primeiro objeto disponível em meuPriorityStore respeitando a ordem de prioridade atribuída ao PriorityItem (objetos com valor menor de prioridade são escolhidos primeiro). Caso o meuPriorityStore esteja vazio, aguarda até que surja um espaço vazio para colocar o objeto. |
DesafiosDesafio 15: considere que na barbearia, 40% dos clientes escolhem seu barbeiro favorito, sendo que, 30% preferem o barbeiro A, 10% preferem o barbeiro B e nenhum prefere o barbeiro C (o proprietário do salão). Construa um modelo de simulação representativo deste sistema. Desafio 16: acrescente ao modelo da barbearia, a possibilidade de desistência e falta do barbeiro. Neste caso, existe 5% de chance de um barbeiro faltar em determinado dia. Neste caso, considere 3 novas situações:
|
Solução dos desafios 15 e 16
Desafio 15: considere que na barbearia, 40% dos clientes escolhem seu barbeiro favorito, sendo que, 30% preferem o barbeiro A, 10% preferem o barbeiro B e nenhum prefere o barbeiro C (o proprietário do salão). Construa um modelo de simulação representativo deste sistema.
Como existe preferência pelo barbeiro, naturalmente a escolha mais simples é trabalharmos com o FilterStore
. O código a seguir, cria uma lista de barbeiros com os nomes, outra com os respectivos Resources, um dicionário para localizarmos o barbeiro por seu nome e, por fim, um FilterStore
com os nomes dos barbeiros:
Quando um cliente chega, existe 40% de chance dele preferir o barbeiro A e 10% de preferir o barbeiro B. O código a seguir atribui o barbeiro utilizando-se da função random
:
Por fim, o processo de atendimento deve diferenciar os clientes que possuem um barbeiro favorito, pois neste caso temos que criar uma função anônima lambda para resgatar o barbeiro correto do FilterStore:
Note, no código anterior, que, caso o cliente não tenha barbeiro preferido, o get
do FilterStore
é utilizado sem nenhuma função lambda dentro do parentesis.
Por fim, o código completo:
Quando executado, o código anterior fornece:
Desafio 16: acrescente ao modelo da barbearia, a possibilidade de desistência e falta do barbeiro. Neste caso, existe 5% de chance de um barbeiro faltar em determinado dia. Neste caso, considere 3 novas situações:
- Se o barbeiro favorito faltar, o respectivo cliente vai embora;
- O cliente que não possuir um barbeiro favorito olha a fila de clientes: se houver mais de 6 clientes em fila, ele desiste e vai embora;
- O cliente que possui um barbeiro favorito, não esperará se houver mais de 3 clientes esperando seu barbeiro favorito.
Como, neste caso, temos que identificar quantos clientes estão aguardando o respectivo barbeiro favorito, uma saída seria utilizar um dicionário para armazenar o número de clientes em fila (outra possibilidade seria um Store
específico para a fila):
Para garantir a falta de um barbeiro em 5% das simulações, foi novamente utilizado o comando random.random
e adicionalmente o comando [random.choice](https://docs.python.org/dev/library/random.html#random.choice)
que selecionada uniformemente um elemento da lista barbeirosNomes
:
Na linha anterior, além de sortearmos um dos barbeiros, ele é removido da lista de barbeiros, o que facilita o processo de desistência do cliente. O processo de chegadas de clientes não precisa ser modificado em relação ao código anterior, contudo, o processo de atendimento precisa armazenar o número de clientes em fila por barbeiro - para isso criamos um dicionário - e o número de clientes em fila total. Assim, criamos uma variável global que armazena o número total de clientes em fila. Uma possível codificação para a função de atendimento seria:
Quando executado, o modelo anterior fornece:
Teste seus conhecimentos
- Considere que a barbearia opera 6 horas por dia. Acrescente ao seu modelo às estatíticas de clientes atendidos, clientes que desistiram (e por qual razão), ocupação dos barbeiros e tempo médio de espera em fila por barbeiro.
- Dada a presente demanda da barbearia, quantos barbeiros devem estar trabalhando, caso o proprietário pretenda que o tempo médio de espera em fila seja inferior a 15 minutos?
Enchendo ou esvaziando caixas, tanques, estoques ou objetos com Container()
Um tipo especial de recurso no SimPy é o container
. Intuitivamente, um container
seria um taque ou caixa em que se armazenam coisas. Você pode encher ou esvaziar em quantidade, como se fosse um tanque de água ou uma caixa de laranjas.
A sua utilização é bastante simples, por exemplo, podemos modelar um tanque de 100 unidades de capacidade (, por exemplo), com um estoque inicial de 50 unidades, por meio do seguinte código:
O container
possui três comandos importantes:
- Para encher:
tanque.put(quantidade)
- Para esvaziar:
tanque.get(quantidade)
- Para obter o nível atual:
tanque.level
Enchendo o meu container yield meuContainer.put(quantidade)
Considere que um posto de gasolina possui um tanque com capacidade de 100 (ou 100.000 litros) de combustível e que o tanque já contém 50 armazenado.
Criaremos uma função, enchimentoTanque
, que enche o tanque com 50 sempre que um novo caminhão de reabastecimento de combustível chega ao posto:
A saída do programa é bastante simples, afinal o processo de enchimento do tanque é executado apenas uma vez:
Se você iniciar o tanque do posto a sua plena capacidade (100 ), o caminhão tentará abastecer, mas não conseguirá por falta de espaço, virtualmente aguardando espaço no tanque na linha de código:
Esvaziando o meu container: yield meuContainer.get(quantidade)
Considere que o posto atende automóveis que chegam em intervalos constantes de 5 minutos entre si e que cada veículo abastece 100 litros ou 0,10 .
Partindo do modelo anterior, vamos criar duas funções: uma para gerar os veículos e outra para transferir o combustível do tanque para o veículo.
Uma possível máscara para o modelo seria:
A função chegadasVeiculos
gera os veículos que buscam abastecimento no posto e que, a seguir, chamam a função esvaziamentoTanque
responsável por provocar o esvaziamento do tanque do posto na quantidade desejada pelo veículo:
A função que representa o processo de esvaziamento do tanque é semelhante a de enchimento da seção anterior, a menos da opção get(qtd),
que retira a quantidade qtd
do container tanque:
O modelo de simulação completo do posto de gasolina fica:
Quando por 200 minutos, o modelo anterior fornece como saída:
Criando um sensor para o nível atual do container
Ainda no exemplo do posto, vamos chamar um caminhão de reabastecimento sempre que o tanque atingir o nível de 50 . Para isso, criaremos uma função sensorTanque
capaz de reconhecer o instante exato em que o nível do tanque abaixou do valor desejado e, portanto, deve ser enviado um caminhão de reabastecimento.
Inicialmente, para identificar se o nível do tanque abaixou além no nível mínimo, precisamos verificar qual o nível atual. Contudo, esse processo de verificação não é contínuo no tempo e deve ter o seu intervalo entre verificações pré-definido no modelo.
Assim, são necessários dois parâmetros: um para o nível mínimo e outro para o intervalo entre verificações do nível do tanque. Uma possível codificação para a função sensorTanque
seria:
A função sensorTanque
é um laço infinito (while True)
que a cada 1 minuto (configurável na constante TEMPO_CONTROLE)
verifica se o nível atual do tanque está abaixo ou igual ao nível mínimo (configurável na constante NIVEL_MINIMO
).
O modelo completo com a implentação do sensor fica:
Note a criação do processo do sensorTanque
na penúltima linha do programa:
Este processo garante que o sensor estará operante ao longo de toda a simulação. Quando executado, o programa anterior retorna:
Observação 1: Note que o enchimento ou esvaziamento dos tanques é instantâneo, isto é: não existe nenhuma taxa de enchimento ou esvaziamento associada aos processos. Cabe ao programador modelar situações em que a taxa de transferência é relevante (veja o Desafio 17, a seguir). Observação 2: O tanque pode ser esvaziado ou enchido simultaneamente. Novamente cabe ao programador modelar a situação em que isto não se verifica (veja o Desafio 18, a seguir). |
Conceitos desta seção
Conteúdo | Descrição |
---|---|
meuContainer = simpy.Container(env, capacity=capacity, init=init |
cria um container meuContainer com capacidade capacity e quantidade inicial de init
|
yield meuContainer.put(quantidade) |
adiciona uma dada quantidade ao meuContainer , se houver espaço suficiente, caso contrário aguarda até que o espaço esteja disponível |
yield meuContainer.get(quantidade) |
retira uma dada quantidade ao meuContainer , se houver quantidade suficiente, caso contrário aguarda até que a quantidade esteja disponível |
meuContainer.level |
retorna a quantidade disponível atualmente em meuContainer
|
DesafiosDesafio 17: considere, no exemplo do posto, que a taxa de enchimento do tanque é de 1 litro/min e a de esvaziamento é de 2 litros/min. Altere o modelo para que ele incorpore os tempos de enchimento e esvaziamento, bem como forneça o tempo que o veículo aguardou na fila por atendimento. Desafio 18: continuando o exemplo, modifique o modelo de modo que ele represente a situação em que o tanque não pode ser enchido e esvaziado simultaneamente. |
Solução dos desafios 17 e 18
Desafio 17: considere, no exemplo do posto, que a taxa de enchimento do tanque é de 5 e a de esvaziamento é de 1 . Altere o modelo para que ele incorpore os tempos de enchimento e esvaziamento, bem como forneça o tempo que o veículo aguardou na fila por atendimento.
Neste caso, são criadas duas constantes:
As funções de enchimento e esvaziamento do tanque devem ser modificadas para considerar o tempo de espera que os veículos e caminhões aguardam até que o processo de bombeamento tenha terminado, como representado nas funções a seguir:
Em cada função foi acrescentada uma linha:
Que representa o tempo que deve-se aguardar pelo bombeamento do produto.
O modelo completo ficaria:
Quando executado por apenas 20 minutos, o modelo do desafio fornece como saída:
O leitor atento deve ter notado que o caminhão de reabastecimento enche o tanque antes mesmo de aguardar o bombeamento, pois a saída do programa indica que um caminhão chegou no instante 0 e que no instante 5 o tanque já possui 100 à disposição:
A situação inicialmente estranha ainda é reforçada pelo fim da operação de enchimento do tanque no instante 10:
10 Tanque enchido com 50.0 m3.
Isto significa que o produto estava disponível nos tanques antes mesmo de ter sido bombeado para o mesmo. Fica como desafio ao leitor atento encontrar uma solução para o problema (dica: que tal pensar em um tanque virtual que antecipe as operações antes delas serem executadas de fato?)
Desafio 18: continuando o exemplo, modifique o modelo de modo que ele represente a situação em que o tanque não pode ser enchido e esvaziado simultâneamente.
Neste caso, o tanque quando bombeando para um sentido (encher ou esvaziar), fica impedido de ser utilizado no outro sentido. Este tipo de situação é bem comum em operações envolvendo tanques de produtos químicos.
Uma possível solução para o problema é utilizar um Store
para armazenar o Container
que representa o tanque. Assim, quando um caminhão de reabastecimento chega ele retira do Store
o tanque e, caso um veículo chegue nesse instante, não conseguirá abastecer pois não encontrará nenhum tanque no Store.
Inicialmente, vamos motificar o programa principal, criando um Store
para o tanque:
Todas as funções agora devem ser modificadas para receber como argumento o tanqueStore
criado e não mais o Container
tanque. (Apenas a função sensorTanque
ainda precisa do Container
tanque pois ela não manipula o Store,
apenas verifica o nível do tanque).
Assim, o modelo final pode ser codificado da seguinte forma:
Quando executado, o modelo do desafio retorna:
Note que agora o veículo só é atendido depois que o tanque é enchido. Outra observação interessante: o que ocorreria neste modelo caso um veículo retire o tanque do Store
e o mesmo tanque não tenha combustível suficiente?
Teste seus conhecimentos
- Modifique o problema para considerar que existam 3 bombas de combustível no posto, capazes de atender aos veículos simultâneamente do mesmo tanque.
- Construa um gráfico (utilizando a biblioteca matplotlib) do nível do tanque ao longo do tempo.
Criando lotes (ou agrupando) entidades durante a simulação
Uma situação bastante comum em modelos de simulação é o agrupamento de entidades em lotes ou o seu oposto: o desmembramento de um lote em diversas entidades separadas. É usual em softwares de simulação proprietários existir um comando (ou bloco) específico para isso. Por exemplo, o Arena possui o “Batch/Separate”, o Simul8 o “Batching” etc.
Vamos partir de um exemplo simples, em que uma célula de produção deve realizar a tarefa de montagem de um certo componente a partir do encaixe de uma peça A com duas peças B. O operador da célula leva em média 5 minutos para montar o componente, segundo uma distribuição normal com desvio padrão de 1 minuto. Os processos de chegadas dos lotes A e B são distintos entre si, com tempos entre chegadas sucessivas uniformemente distribuídos no intervalo entre 40 a 60 minutos.
Uma tática para agrupamento de lotes utilizando o Container
Uma maneira de resolver o problema é criar um Container
de estoque temporário para cada peça. Assim, criamos dois estoques, respectivamente para as peças A e B, de modo que o componente só poderá iniciar sua montagem se cada estoque contiver ao menos o número de peças necessárias para sua montagem.
Comecemos criando uma possível máscara para o problema:
Na máscara anterior, foram criadas duas funções: chegaPecas
, que gera os lotes de peças A e B e armazena nos respectivos estoques e montagem
, que retira as peças do estoque e montam o componente.
Note que criei um dicionário no Python: pecasContainerDict
, para armazenar o Container
de cada peça:
A função de geração de peças de fato, gera lotes e armazena dentro do Container o número de peças do lote:
Note que, diferentemente das funções de geração de entidades criadas nas seções anteriores deste livro, a função chegadaPecas
não encaminha a entidade criada para uma nova função, iniciando um novo processo (de atendimento, por exemplo). A função apenas armazena uma certa quantidade de peças, tamLote,
dentro do respectivo Container
na linha:
O processo de montagem também recorre ao artifício de um laço infinito, pois, basicamente, representa uma operação que está sempre pronta para executar a montagem, desde que existam o número de peças mínimas à disposição nos respectivos estoques:
A parte central da função anterior é garantir que o processo só possa se iniciar caso existam peças suficientes para o componente final. Isto é garantido pelo comando get
aplicado a cada Container
de peças necessárias:
Quando executado, o modelo completo fornece como saída:
O que o leitor deve ter achado interessante é o modo passivo da função montagem
que, por meio de um laço infinito while True
aguarda o aparecimento de peças suficientes nos estoques para iniciar a montagem. Interessante também é notar que não alocamos recursos para a operação e isso significa que o modelo de simulação atual não permite a montagem simultânea de componentes (veja o tópico “Teste seus conhecimentos” na próxima seção).
Agrupando lotes por atributo da entidade utilizando o FilterStore
Outra situação bastante comum em modelos de simulação é quando precisamos agrupar entidades por atributo. Por exemplo, os componentes anteriores são de duas cores: brancos ou verdes, de modo que a célula de montagem agora deve pegar peças A e B com as cores corretas.
Como agora existe um atributo (no caso, cor) que diferencia uma peça da outra, precisaremos de um FilterStore
, para garantir a escolha certa da peça no estoque. Contudo, devemos lembrar que o FilterStore
, diferentemente do Container
, não permite que se armazene ou retire múltiplos objetos ao mesmo tempo. O comando put
(ou mesmo o get),
é limitado a um objeto por vez. Por fim, a montagem do componente agora é pelo atributo “cor”, o que significa que a função montagem
deve ser chamada uma vez para cada valor do atributo (no caso duas vezes: “branco” ou “verde”).
De modo semelhante ao exemplo anterior, uma máscara para o problema seria:
Note que foi criado um dicionário pecasFilterStore
armazena um FilterStore
para cada tipo de peça.
Vamos agora construir a função chegadaPecas
, considerando que ela deve sortear a cor do lote de peças e enviar todas as peças do lote (uma por vez) para o respectivo FilterStore.
Para sortear a cor do lote, uma opção é utilizar o comando random.choice, enquanto o envio de múltiplas peças para o FilterStore
pode ser feito por um laço for,
como mostra o código a seguir:
A função montagem,
de modo semelhante a função anterior, é formada por um laço infinito do tipo while... True
, mas deve retirar múltiplas peças de cada estoque, respeitando o atributo “cor”. O código a seguir, soluciona este problema novamente com laços do tipo for
e uma função anônima para buscar a cor correta da peça dentro do FilterStore:
Dois pontos merecem destaque na função anterior:
- A função
montagem
, deve ser chamada duas vezes na inicialização da simulação, uma para cada cor. Isto significa que nossa implementação permite a montagem simultânea de peças de cores diferentes. Caso seja necessário contornar este problema, basta a criação de um recurso “montador” (veja o tópico “Teste seus conhecimentos” na próxima seção“; - Na última linha, para contabilizar o total de peças ainda em estoque, como o
FilterStore
não possui um método.level,
utilizou-se a funçãolen()
aplicada a todos ositems
doFilterStore.
Quando executado por apenas 80 minutos, o programa anterior fornece como saída:
Naturalmente, existem outras soluções, mas optei por um caminho que mostrasse algumas limitações para um problema bastante comum em modelos de simulação.
DesafiosDesafio 19: Considere, no primeiro exemplo, que o componente possui mais duas partes, C e D que devem ser previamente montadas entre si para, a seguir, serem encaixadas nas peças A e B. Os tempos de montagem são todos semelhantes. (Dica: generalize a função Desafio 20: Nos exemplos anteriores, os processos de montagem são paralelos. Considere que existe apenas um montador compartilhado para todos processos. Generalize a função montagem do desafio anterior, de modo que ela receba como parâmetro o respectivo recurso utilizado no processo. |
Solução dos desafios 19 e 20
Desafio 19: Considere, no primeiro exemplo, que o componente possui mais duas partes, C e D que devem ser previamente montadas entre si para, a seguir, serem encaixadas nas peças A e B. Os tempos de montagem são todos semlhantes. (Dica: generalize a função
montagem
apresentada no exemplo).
Para este desafio, não precisamos alterar o processo de chegadas, apenas vamos chamá-lo mais vezes, para as peças tipo A, B, C e D.
Inicialmente, portanto, precisamos criar um Container
para cada etapa da montagem, bem como chamar a função de geração de lotes para as 4 partes iniciais do componente:
O bacana desse desafio é explorar o potencial de generalidade do SimPy, afinal, os processos de montagem são semelhantes, o que muda apenas é o quais as peças estão sendo unidas.
Imagine por um momento, que podemos querer unir 3 ou mais peças. A lógica em si não é muito diferente daquela feita na seção anterior, muda apenas a necessidade de se lidar com um número de peças diferentes. Assim, para generalizar a função montagem
, proponho utilizar o operador **kwargs
que envia um conjunto de parâmetros para uma função como um dicionário.
A ideia aqui é chamar o processo de montagem de maneira flexível, por exemplo:
A função montagem
é executada para diferentes configurações de peças a serem montadas. Por exemplo, a primeira linha representa uma montagem de uma peça A com duas B; a segunda, uma peça C e duas D e a terceira linha representa uma peça do tipo AB com uma do tipo CB, formando o componente ABCD.
O código a seguir, apresenta uma solução que dá razoável generalidade para a função montagem
:
No código anterior, os componentes montados são colocados no Container
definido no parâmetro keyOut
e, note como a parte relativa ao parâmetro **kwargs
é tratada como um mero dicionário pelo Python.
Completo, o modelo de simulação do desafio 19 ficaria:
Quando executado por apenas 40 minutos, o modelo anterior fornece como saída:
Desafio 20: Nos exemplos anteriores, os processos de montagem são paralelos. Considere que existe apenas um montador compartilhado para todos processos. Generalize a função montagem do desafio anterior, de modo que ela receba como parâmetro o respectivo recurso utilizado no processo.
Neste desafio precisamos apenas garantir que a funçãomontagem
receba como parâmetro o respectivo recurso a ser utilizado no processo:
Repare que a saída de tempo de espera que a função fornece é, na verdade, o tempo de espera do montador e não o tempo de espera em fila pelo recurso (veja o tópico “Teste seus conhecimentos” a seguir).
A chamada de execução do modelo não é muito diferente do desafio anterior, apenas precisamos criar um recurso único que realiza todos os processos:
Quando o modelo anterior é executado por apenas 40 minutos, temos como saída:
Note como a produção de componentes ABCD caiu de 5 peças no peças no desafio 19, para apenas 2 neste desafio. Isso é naturalmente explicado, pois agora os processos não são mais paralelos e devem ser executados por um único montador.
Teste seus conhecimentos
- Acrescente o cálculo do tempo médio em espera por fila de montador ao modelo do desafio 20;
- Acrescente o cálculo do WIP - Work In Progress do processo ou o trabalho em andamento, isto é, quantas peças estão em produção ao longo do tempo de simulação;
- Utilize a biblioteca matplotlib e construa um gráfico para evolução do estoque de cada peça ao longo da simulação, bem como do WIP.
Criando, manipulando e disparando eventos com event()
Nesta seção discutiremos comandos que lhe darão o poder de criar, manipular ou disparar seus próprios eventos, de modo independente dos processos já discutidos nas seções anteriores.
Mas com todo o poder, vem também a responsabilidade!
Atente-se para o fato de que, sem o devido cuidado, o seu modelo pode ficar um pouco confuso. Isto porque um evento pode ser criado a qualquer momento e fora do contexto original do processo em execução, naturalmente aumentando a complexidade do código.
Criando um evento isolado com event()
Considere um problema simples de controle de turno de abertura e fechamento de uma ponte elevatória. A ponte abre para automóveis, opera por 5 minutos, fecha e permite a passagem de embarcações no cruzamento por mais 5 minutos.
Naturalmente, esse modelo poderia ser implementado com os comandos já discutidos neste livro, contudo, a ideia desta seção é demonstrar como criar um evento específico que informe à ponte que ela deve fechar, algo semelhante a um sinal semafórico.
Em SimPy, um evento é criado pelo comando env.event():
Criar um evento, não significa executá-lo. Criar um evento significa apenas criá-lo na memória. Para processar um evento, isto é, marcá-lo como executado, utilizamos o método .succeed():
Podemos utilizar o evento criado de diversas formas em um modelo. Por exemplo, com o comando yield
podemos fazer um processo aguardar até que o evento criado seja processado, com a linha:
Retornando ao exemplo da ponte, criaremos um processo que representará o funcionamento da ponte. Inicialmente a ponte estará fechada e aguardará até que o evento abrePonte
seja processado:
Note que abrePonte
é tratado como uma variável global e isso significa que alguma outra função deve criá-lo e processá-lo, de modo que nossa função ponteElevatória
abra a ponte no instante correto da simulação.
Assim, criaremos uma função geradora turno
que representará o processo de controle do turno de abertura/fechamento da ponte e que será responsável por criar e processar o evento de abertura da mesma:
Note, na função anterior, que o evento é criado, mas não é processado imediatamente. De fato, ele só é processado quanto o método abrePonte.succeed()
é executado, após o tempo de espera de 5 minutos.
Como queremos que a ponte funcione continuamente, um novo evento deve ser criado para representar o novo ciclo de abertura e fechamento. Isso está representado no início do laço com a linha:
Precisamos deixar isso bem claro, paciente leitor: uma vez processado com o método .succeed(),
o evento é extinto e caso seja necessário executá-lo novamente, teremos de recriá-lo com env.event().
Juntando tudo num único modelo de abre/fecha da ponte elevatória, temos:
Quando executado por 20 minutos, o modelo anterior fornece:
No exemplo anterior, fizemos uso de uma variável global, abrePonte
, para enviar a informação de que o evento de abertura da ponte foi disparado.
Isso é bom, mas também pode ser ruim =).
Note que o evento de abertura da ponte é criado e processado dentro da função turno(env),
portanto, fora da função que controla o processo de abertura e fechamento da ponte, ponteElevatoria(env).
As coisas podem realmente ficar confusas no seu modelo, caso você não tome o devido cuidado.
O método.succeed()
ainda pode enviar um valor como parâmetro:
Poderíamos, por exemplo, enviar à função ponteElevatoria
o tempo previsto para que a ponte fique aberta. O modelo a seguir transfere o tempo de abertura (5 minutos) à função ponteElevatoria
, que o armazena na variável tempoAberta:
Dentro da função ponteElevatoria
a linha:
Aguarda até que o evento abrePonte
seja executado e resgata seu valor (o tempo que a ponte deve permanecer aberta) na variável tempoAberta.
Concluindo, o potencial de uso do comando event()
é extraordinário, mas, por experiência própria, garanto que seu uso descuidado pode tornar qualquer código ininteligível, candidato ao Campeonato Mundial de Código C Ofuscado (sim, isso existe!) ou mesmo algo semelhante a utilizar desvios de laço go... to
em um programa (des)estruturado).
Conceitos desta seção
Conteúdo | Descrição |
---|---|
meuEvento = env.event() |
cria um novo evento meuEvento durante a simulação, mas não o processa. |
yield meuEvento |
aguarda até que o evento meuEvento seja processado |
yield meuEvento.succeed(value=valor) |
processa o evento meuEvento , isto é, dispara o evento no tempo atual e inicia o seu processamento, retornando o parâmetro opcional valor.
|
DesafiosDesafio 21: crie um processo de geração de automóveis que desejam cruzar a ponte, durante o horário de pico que dura 4 horas. Os intervalos entre chegadas sucessivas de veículos para travessia são exponencialmente distribuídos com média de 10 segundos (ou 6 veículos/min), e a ponte permite a travessia de 10 veículos por minuto. Após 4 horas de operação, quantos veículos estão em espera por travessia da ponte? Desafio 22: para o sistema anterior, construa um gráfico para o número de veículos em fila em função do tempo de abertura da ponte para travessia de automóveis. Qual o tempo mínimo que você recomendaria para abertura da ponte? |
Solução dos desafios 21 e 22
Desafio 21: crie um processo de geração de automóveis que desejam cruzar a ponte, durante o horário de pico que dura 4 horas. Os intervalos entre chegadas sucessivas de veículos para travessia são exponencialmente distribuídos com média de 10 segundos (ou 6 veículos/min), e a ponte permite a travessia de 10 veículos por minuto. Após 4 horas de operação, quantos veículos estão em espera por travessia da ponte?
Em relação ao modelo anterior, este novo sistema possui um processo de geração de chegadas de veículos e espera por abertura da ponte. Uma alternativa de implementação é utilizar um Container
para armazenar os veículos em espera pela abertura da ponte, como no código a seguir:
No código anterior, filaTravessia
é um Container
que representa o conjunto de veículos em espera por travessia da ponte. Cada veículo gerado é imediatamente transferido para o Container
.
A função turno
é semelhante à anterior, apenas com a inclusão do tempo de abertura da ponte, como o parâmetro tempo_ponte:
Note que o parâmetro tempo_ponte
é enviado como um valor para a função ponteElevatória
, que agora deve lidar com a travessia dos veículos quando aberta. Neste caso, basta um comando get
no Container
que representa a fila de travessia dos veículos:
Analisando o código anterior, assim que a ponte abre, a linha:
Estima quantos veículos, dentre aqueles que estão no Container,
podem realizar a travessia. A seguir, os veículos são retirados do Container
por meio de um comando get
com parâmetro no número de veículos a serem retirados.
Ao final, deve-se criar o Container
, realizar as chamadas dos processos e executar o modelo por 4 horas (ou 240 minutos):
Quando executado, o modelo completo fornece como saída (resultados compactados):
Portanto, considerando-se as condições simuladas, o modelo indica que 170 veículos ainda estão em espera em fila ao final da última abertura da ponte dentro do horário de pico. Portanto, o tempo de abertura da ponte parece ser insuficiente durante o horário de pico.
Desafio 22: para o sistema anterior, construa um gráfico para o número de veículos em fila em função do tempo de abertura da ponte para travessia de automóveis. Qual o tempo mínimo que você recomendaria de abertura da ponte.
Como o desafio deseja uma avaliação da fila ao final do horário de pico para diferentes valores de abertura da ponte, o primeiro passo é construir um laço para que o modelo possa ser executado para diferentes valores do tempo de abertura da ponte:
Note, no código anterior, que foi criada uma lista, resultado,
para armazenar o tuple
com o tempo de abertura da ponte simulado e o resultado do número de veículos não atendidos ao final da simulação.
A função ponteElevatoria
precisa de apenas de uma pequena modificação para assegurar que a variável global naoAtendidos
receba corretamente o número de veículos não atendidos imediatamente após ao fechamento da ponte:
O próximo passo é acrescentar, ao final da simulação, um gráfico do número de veículos em função do tempo de abertura da ponte. Essa operação é facilitada pelo uso da biblioteca matplotlib no conjunto de dados armazenado na lista resultado:
Quando executado, o modelo anterior fornece como resultado o seguinte gráfico:
Em uma primeira análise, portanto, o tempo de abertura de 7 minutos seria suficiente para atender aos veículos durante o horário de pico. Contudo, nossa análise está limitada a uma replicação apenas, o que torna a conclusão, eventualmente, precipitada (veja o item 2 do tópico “Teste seus conhecimentos” a seguir).
Teste seus conhecimentos
- Por que utilizamos na função
ponteElevatoria
a variável globalnaoAtendidos?
Não seria suficiente armazenar na filaresultados
diretamente o número de veículos noContainer
filaTravessia,
pelo comandofilaTravessia.level?
- Como nos lembram Chiwf e Medina (2014)1: “nunca se deve tomar decisões baseadas em apenas uma replicação de um modelo de simulação”. Afinal, basta modificar a semente geradora de número aleatórios, para que o resultado do gráfico seja outro (teste no seu modelo!). Modifique o modelo para que ele simule um número de replicações configurável para cada tempo de abertura da ponte. Adicionalmente, garanta que tempos de abertura diferentes utilizem a mesma sequencia de números aleatórios. (Dica: para esta segunda parte, armazene as sementes geradoras em uma lista).
2 Chwif, L., and A. C. Medina. 2014. Modelagem e Simulacão de Eventos Discretos: Teoria e Aplicacões. 4ª ed. São Paulo: Elsevier Brasil.
Em português, ainda é comum o estrangeirismo “framework” entre profissionais da Ciência da Computação.↩︎
Em português, ainda é comum o estrangeirismo “framework” entre profissionais da Ciência da Computação.↩︎
Aguardando múltiplos eventos ao mesmo tempo com AnyOf e AllOf
Uma funcionalidade importante do SimPy é permitir que uma entidade aguarde até que dois ou mais eventos ocorram para então prosseguir com o processo. O SimPy possui duas opções muito interessantes para isso:
simpy.nyOf(env, eventos):
aguarda até que um dos eventos tenham ocorrido -AnyOf
é equivalente ao símbolo de “|” (ouor
);simpy.AllOf(env, eventos):
aguarda até que todos os eventos tenham ocorrido -AllOf
é equivalente ao símbolo de “&” (ouand
).
Para compreender o funcionamento dos comandos anteriores, partiremos de um exemplo baseado numa obscura fábula infantil: a Lebre e a Tartaruga.
Neste exemplo, sortearemos um tempo de corrida para cada bicho e identificaremos quem foi o vencedor. Para tanto, além do sorteio, criaremos dois eventos que representam a corrida de cada bicho:
Na função corrida,
criamos os eventos lebreEvent
e tartarugaEvent
, que simulam, respectivamente, as corridas da lebre e da tartaruga. Mas, lembre-se: apesar de criados, os eventos ainda não foram necessariamente executados. Como não existe um yield
aplicado aos eventos, eles foram criados na memória do Python, mas só serão considerados processados após o tempo de simulação avançar o suficiente para que os tempos de timeout
tenham passado.
De outra forma, os eventos lebreEvent
e tartarugaEvent
foram disparados, mas não processados, pois a função corrida
ainda não tem um comando que aguarda o término do processamento desses eventos.
Aguardando até que, ao menos, um evento termine com AnyOf
Para garantir que a função corrida
aguarde até que, ao menos, um dos corredores termine a prova, uma opção é acrescentar um yield AnyOf()
(que pode ser substituído por “|”) após a criação dos eventos:
O yield
garante que a função aguardará até que um dos dois eventos - lebreEvent
ou tartarugaEvent
- termine e a variável resultado
armazenará qual desses eventos terminou primeiro (ou mesmo os dois, em caso de empate). Assim, para sabermos quem venceu, basta explorarmos o valor contido na variável resultado
.
O código a seguir completa o modelo, testando qual dos dois eventos está na variável resultado
:
Quando o modelo anterior é executado, o bicho pega:
Observação: a linha:
Poderia ter sido substituída, pela linha:
Aguardando todos os eventos com AllOf
Para não deixar ninguém triste, poderíamos forçar um empante na corrida, aguardando que a função corrida
aguarde até que os dois eventos sejam concluídos para decretar o vencedor. Para isso, basta substituir a linha:
por:
Quando simulado, o novo modelo forncece como saída:
O que ocorreu? Neste caso, o comando AllOf
(ou “&”) aguardou até que os dois eventos terminassem para liberar o processamento da linha seguinte de código e nosso desvio de condição if
identificou que a variável resultado
possuia os dois eventos armazenados.
Comprendendo melhor as saídas dos comandos AllOf e AnyOf
Agora que já sabemos o que fazem os comandos AllOf
e AnyOf
, vamos explorar nessa seção um pouco mais sobre o que exatamente esses comandos retornam.
Inicialmente, imprima os valores dos eventos e da variável resultado
, para descobrir seus conteúdos:
Quando executado, o programa fornece:
Pela saída anterior, descobrimos, inicialmente, que os eventos são objetos do tipo Timeout
e que armazenam tanto o tempo de espera, quanto o valor (ou value
) fornecido na chamada da função env.timeout.
Um pouco mais abaixo, a saída revela que a variável resultado
é um objeto da classe ConditionValue
que, aparentemente, contém um dicionário de eventos em seu interior. Para acessar esse dicionário, o SimPy fornece o método .todict():
Que retorna:
Que nada mais é do que um dicionário padrão do Python, onde as keys
são os eventos e os items
são os valores dos eventos.
Conceitos desta seção
Conteúdo | Descrição |
---|---|
AnyOf(env, eventos) |
aguarda até que um dos eventos tenham ocorrido - AnyOf é equivalente ao símbolo de “|” (ou or ). |
AllOf(env, eventos) |
aguarda até que todos os eventos tenham ocorrido - AllOf é equivalente ao símbolo de “&” (ou and ). |
DesafiosDesafio 23: Considere que existe uma probabilidade de que a Lebre, por alguma razão mal explicada (eu realizaria um teste antidoping nos competidores), resolva que é uma boa ideia tirar uma soneca de 5 minutos em algum instante entre 2 e 10 minutos do início da corrida. Modele esta nova situação (dica: crie um função Desafio 24: É interessante notar, que mesmo quando um dos competidores perde a corrida, de fato, o respectivo evento não é cancelado. Altere o modelo anterior para marcar o horário de chegada dos dois competidores, garantindo que os eventos |
Solução dos desafios 23 e 24
Desafio 23: Considere que existe uma probabilidade de que a Lebre, por alguma razão mal explicada (eu realizaria um teste antidoping nos competidores), resolva que é uma boa ideia tirar uma soneca de 5 minutos em algum instante entre 2 e 10 minutos do início da corrida. Modele esta nova situação (dica: crie um função
soneca
que gera um evento que pode ocasionar a parada da Lebre ainda durante a corrida).
Para este desafio será criada uma função para gerar a soneca e uma função adicional para identificar o vencedor, pois agora a tartaruga pode (ou não) vencer enquanto a lebre estiver dormindo.
Começando pela função que determina o vencedor:
A função soneca
a seguir, cria um evento sonecaEvent,
sorteia o instante da soneca e processa o evento no instante correto:
A função corrida, agora deve lidar com as diversas situações possíveis: uma soneca da lebre durante a corrida, a lebre acordando, a tartaruga ou a lebre vencendo. Essas diversas situações podem ser facilmente modeladas por um conjunto de lógicas de desvio condicional if
concatenadas com operações do tipo AnyOf
:
Não devemos esquecer de inciar o processo da soneca:
Note, na função corrida,
que quando a tartaruga vence enquanto a lebre está dormindo, a função imprimeVencedor
é chamada com o parâmetro lebreEvent=None
, já informando à função que a lebre perdeu.
Quando o modelo anterior é simulado, fornece como resultado:
Você pode alterar o valor da semente geradora de números aleatórios - na linha random.seed(...)
- e apostar com os amigos quem vencerá a próxima corrida!
Desafio 24: É interessante notar, que mesmo quando um dos competidores perde a corrida, de fato, o respectivo evento não é cancelado. Altere o modelo anterior para marcar o horário de chegada dos dois competidores, garantindo que os eventos
lebreEvent
etartarugaEvent
sejam executados até o fim.
Uma possível solução para o desafio é transformar o a função imprimeVencedor
em um processo que, após informar o vencedor, continua até que o outro competidor passe pela linha de chegada. Por exemplo, podemos uma alternativa seria:
A função corrida
deve agora utilizar comandos do tipo env.process(imprimeVencedor)
para inciar o processo que determina quem venceu e continua a corrida até que o outro competidor chegue. Note, no código a seguir, que apenas no caso da lebre ser pega ainda dormindo, a modificação do código é um pouco mais trabalhosa:
Quando executado, o modelo completo fornece como saída:
O que é importante destacar é que o comando AnyOf
não paralisa um evento que ainda não foi processado. No saída exemplo, a tartaruga venceu e o processamento da linha:
Fez o modelo aguardar até que o evento da lebre acordar fosse processado (no instante determinado quando o evento foi criado). Não houve, portanto, um novo evento criado para a lebre, apenas aguardamos a conclusão do evento já engatilhado na memória, mas não concluído.
Teste seus conhecimentos
- Generalize a função
corrida
para um número qualquer de competidores utilizando o operador**kwargs
visto no Desafio 19. - Construa um gráfico com a evolução da prova, isto é, a distância percorrida por cada competidor x tempo de prova.
Propriedades úteis dos eventos
Um evento possui algumas propriedades que fazem a alegria de qualquer leitor:
Event.value:
o valor que foi passado para o evento no momento de sua criação;Event.triggered:
True,
caso oEvent
já tenha sido engatilhado, isto é, ele está na fila de eventos do SimPy e programado para ocorrer em determinado instante da simulação;False,
caso contrário;Event.processed:
True,
caso oEvent
já tenha sido executado eFalse,
caso contrário.
Existe uma dificuldade inicial que deve ser obrigatoriamente superada pelo programador: compreender a sequência de criação, disparo e execução de um evento em SimPy.
No momento da sua criação, todo evento surge como um objeto na memória do SimPy e, inicialmente, ele encontra-se no estado não engatilhado (Event.triggered = False
). Quando o evento é programado para ocorrer em determinado instante da simulação, ele passa ao estado engatilhado (Event.triggered = True
). Quando o evento é finalmente executado no instante determinado, seu estado passa a processado: (Event.processed = True
).
Por exemplo, quando você adiciona ao modelo a linha de comando:
O SimPy processará a linha na seguinte sequência de passos (figura):
- Cria na memória um novo evento dentro do
Environment env;
- Engatilha o evento para ser processado dali a 10 unidades de tempo (minutos, por exemplo);
- Quando a simulação atinge o instante de processamento esperado (passados os 10 minutos), o evento é processado. Automaticamente, o comando
yield
recebe o sinal de sucesso do processamento e o fluxo de execução do modelo retoma seu curso normal, processando a linha seguinte do modelo.
A figura … elenca os diversos tipos de eventos que discutimos ao longo deste livro e sua sequência usual de criação->engatilhamento->processamento. Note como eles estão intrinsecamente ligados à questão do tempo de simulação.
Antes de avançar - e com o intuito de facilitar o aprendizagem do lebrístico leitor - acrecentaremos ao modelo da Lebre e da Tartaruga uma função para imprimir as propriedade dos eventos. Basicamente ela recebe uma lista de eventos e imprime na tela as propriedades de cada evento da lista:
Basicamente, devemos criar uma lista que armazena os eventos de corrida da Lebre e da Tartaruga e imprimir esta lista por meio da função printEventStatus
:
O modelo completo, com a impressão das propriedades dos eventos, ficaria:
Quando simulado, o modelo fornece como resultado:
Repare que, no instante 0.0, os eventos do tipo timeout
são programados imediatamente após sua criação. Quando a tartaruga vence, por exemplo, o seu evento é considerado processado e do lebre, não. Contudo, o que aconteceria se simulássemos o modelo por mais tempo? De outro modo, se deixássemos o modelo processando por mais tempo, a lebre apareceria na linha de chegada?
No modelo a seguir, acrescentamos à função corrida um evento timeout,
logo após a chegada, que representaria a comemoração da tartaruga por 4 minutos:
Como resultado, agora o modelo fornece como saída:
Repare que no instante 9.3 minutos o evento da lebre está processado. Isto é importantíssimo e não avance sem compreender o que está acontecendo. Quando a linha:
Aguardou até que um dos eventos tivesse terminado, ela não cancelou o outro evento que, por ser um timeout
, continuou na fila de eventos até que o SimPy pudesse processá-lo. (Note que o instante 9.3 não representa o instante de processamento do processo, provavelmente a lebre cruzou a linha de chegada após isso).
Até a presente versão do SimPy, não existe a possibilidade de se cancelar um evento já programado. |
Conceitos desta seção
Conteúdo | Descrição |
---|---|
Event.value |
o valor que foi passado para o evento no momento de sua criação. |
Event.triggered |
True, caso o Event já tenha sido engatilhado, isto é, ele está na fila de eventos do SimPy e programado para ocorrer em determinado instante da simulação; False, caso contrário. |
Event.processed |
True, caso o Event já tenha sido executado e False, caso contrário. |
DesafiosDesafio 25: modifique o modelo anterior para que ele aguarde até a chegada da lebre. |
Solução do desafio 25
Desafio 25: modifique o modelo anterior para que ele aguarde até a chegada da lebre.
Para este desafio, basta alterarmos a parte do modelo que lida com a decisão de quem venceu:
Quando simulado, o modelo anterior fornece:
Nessa replicação a lebre perdeu por 0,1 minutos apenas. Um cochilada imperdoável da lebre, MEU AMIGO!
Na próxima seção, veremos uma tática mais interessante para resolver o mesmo problema a partir da novidade a ser apresentada, os callbacks.
Teste seus conhecimentos
- Crie um processo
chuva
que ocorre entre intervalos exponencialmente distribuídos com média de 5 minutos e dura, em média, 5 minutos exponencialmente distribuídos também. Quando a chuva começa, os corredores são 50% mais lentos durante o período. (Dica: quando a chuva começar, construa novos eventostimeout
e despreze os anteriores, mas calcule antes quais as velocidades dos corredores). - Modifique o modelo para que ele execute um número configurável de replicações e forneça como resposta a porcentagem das vezes em que cada competidor venceu.
Adicionando callbacks aos eventos
SimPy possui uma ferramenta tão curiosa quanto poderosa: os callbacks.
Um callback
é uma função que você acrescenta ao final de um evento. Por exemplo, considere que quando o evento da tartaruga (ou da lebre) termina, desejamos imprimir o vencedor na tela. Assim, quando o evento é processado, desejamos que ele processe a seguinte função, que recebe o evento como único parâmetro de entrada:
Toda função para ser anexada como um callback
, deve possuir como parâmetro de chamada apenas um único evento. Note também, que o valor de env.now
impresso na tela é possível pois env
é uma variável global para o Python. (Caso você ainda tenha alguma dificuldade para entender como o Python lida com variáveis globais e locais, um bom caminho é ler este bom guia.
Para anexarmos a função campeão
a um evento, utilizaremos o método callbacks.append(função_criada)
:
O código completo do modelo com callbacks
ficaria:
Note como o código ficou razoavelmente mais compacto, por eliminamos toda a codificação referente aos testes if...then...else
para determinar que é o campeão.
Quando executado, o modelo fornece como resultado:
OPS!
Aos 5.4 minutos a lebre, que chegou depois, foi declarada campeã também. Como isso aqui não é a Federação Paulista de Futebol, temos de corrigir o modelo e garantir que vença sempre quem chegar primeiro.
Uma solução prática seria criar uma variável global que se torna True
quando o primeiro corredor ultrapassa a linha, de modo que a função campeao
consiga distinguir se já temos um vencedor ou não:
A função campeao
agora deve lidar com uma lógica de desvio de fluxo para determinar se evento representa um campeão ou não:
Quando simulado, o modelo fornece como saída:
Você pode adicionar quantas funções de callback
quiser ao seu evento, mas lembre-se que manipular um modelo diretamente por eventos tende a deixar o código ligeiramente confuso e a boa prática recomenda não economizar nos comentários.
Todo processo é um evento
Quando um processo é gerado pelo comando env.process(),
o processo gerado é automaticamente tratado como um evento pelo SimPy. Você pode igualmente adicionar callbacks
aos processos ou mesmo retornar um valor (como já vimos na seção….).
Por exemplo, vamos acrescentar uma função de callback
para ao processo corrida
que informa o final da corrida para o público:
Precisamos agora, modificar apenas os comandos que inicializam a função corrida,
anexando o callback
criado:
Note que, além de adicionarmos a função final
como callback
da função corrida,
modificamos o comando env.run()
para que ele simule até que a função corrida
termine seu processamento. (Experimente substituir a linha env.run(proc)
por env.run(until=10)
e verifique o que acontece).
Quando executado, o modelo fornece como saída:
Uma boa pedida para callbacks
é construir funções que calculem estatísticas de termino de processamento ou, como veremos na próxima seção, quando desejamos trabalhar com falhas.
Conceitos desta seção
Conteúdo | Descrição |
---|---|
Event.callbacks.append(callbackFunction) |
adiciona um callback , representado pela função callbackFunction do modelo. Após o processamento do evento (Event.processed = True ) a função de callback é executada. A função callbackFunction, obrigatoriamente deve ter como parâmetro apenas um evento. |
DesafiosDesafio 26: acrescente à função |
Solução do desafio 26
Desafio 26: acrescente à função
final
um comando para armazenar quem venceu e em que tempo. Simule para 10 replicações e calcule a média e a porcentagem de vitórias de cada corredor.
Neste caso, podemos criar uma lista resultadoList
para armazenar a tuple
(tempo, vencedor) de cada replicação. Os valores podem ser armazenados no instante em que a função campeão
identifica o vencedor, isto é:
Como o desafio pede que sejam executadas 10 replicações da corrida, devemos modificar a chamada do modelo para refletir isso. Inicialmente, criamos a lista resultadoList
e, a seguir, criamos um laço para executar o modelo 10 replicações:
Note que a variável global vencedor
é atualizada dentro do laço (na seção seguinte, ela era criada logo no cabeçalho do modelo), afinal, a cada replicação precisamos garantir que as variáveis do modelo sejam reinicializadas.
Uma vez que as 10 replicações tenham sido executadas, basta calcular as estatísticas desejadas. O número de vitórias é facilmente extraída da lista de vencedores:
Para o cálculo da média do tempo de corrida, temos diversas possibilidades. A escolhida aqui foi utilizar a função mean
da biblioteca numpy
:
Quando executado, o modelo fornece como resposta:
Teste seu conhecimento
- Acrescente o cálculo do intervalo de confiança ao resultados do tempo médio de corrida e simule um número de replicações suficientes para garantir que este intervalo seja no máximo de 5% em torno da média, para um nível de significância de 95%.
- Reformule o modelo para que ele aceite uma lista de corredores diferentes, não só uma lebre e uma tartaruga.
Interrupções de eventos
De modo semelhante ao que vimos com recursos, os eventos também podem ser interrompidos em SimPy. Como o SimPy aproveita-se dos comandos de interrupção já existentes no Python, pode-se utilizar o bloco try... except
e assim capturar a interrupção em qualquer parte do modelo.
Considere um exemplo simples em que um jovem Jedi tem sua seção de meditação matinal interrompida por um cruel Lord Sith interessado em convertê-lo para o Lado Negro.
Construir um modelo de simulação deste sistema, é tarefa simples: precisamos de uma função que representa o Jedi meditando e outra que interrompe o processo em determinado momento (lembre-se que um processo é também um evento para o SimPy).
O SimPy tem dois métodos diferentes para interroper um evento: interrupt
ou fail.
Interrompendo um evento com o método interrupt
Criaremos duas funções: forca
que representa o processo de meditação e ladoNegro
que representa o processo de interrupção da meditação. Inicialmente, interromperemos o processo forca
por meio do método interrupt
. Uma possível máscara para o modelo ficaria:
Para este exemplo, o processo de meditação é bem simples, pois estamos mais interessados em aprender sobre interrupções:
Portanto, a cada 1 unidade de tempo, o Jedi fala um frase para manter-se concentrado. O processo de interrupção por sua vez, recebe como parâmetro de entrada o processo (ou evento) a ser interrompido. Apenas para ilustrar melhor o exemplo, vamos considerar que após 3 unidades de tempo, a função interrompe o processo (ou evento) de meditação:
Portanto, a interrupção de um evento ou processo qualquer é invocada pelo método .interrupt().
Por exemplo, dado que processo ou evento proc
, podemos interrompê-lo com a linha de código:
Se você executar o modelo anterior, a coisa até começa bem, mas depois surge uma supresa desagradável:
O que essa longa mensagem de erro nos faz lembrar é que o método .interrupt()
vai além de interromper um mero evento do SimPy, ele interrompe o programa todo.
Mas, jovem leitor Jedi, temos duas maneiras de contornar o problema: com a lógica de controle de exceção do Pythontry...except
ou com a propriedade .defused,
como veremos a seguir.
Método de controle de interrupção 1: lógica de exceção try… except
Neste caso, a solução é razoavelmente simples, basta acrescentarmos ao final do programa (ou em outra parte conveniente) uma lógica de exceção do SimPy, simpy.Interrupt,
como no exemplo a seguir:
Quando executado, o modelo anterior fornece:
É importante notar que depois da interrupção proc.interrupt()
o modelo ainda executa a última linha do processo ladoNegro
(basicamente, imprime “Welcome, young Sith”) para, a seguir, executar o comando dentro do except simpy.Interrupt.
Método de controle de interrupção 2: alterando o atributo defused
No caso anterior, o leitor deve ter notado que, ao interromper o processo, interrompemos a simulação por completo, pois nossa lógica de exceção está ao final do código.
E se quiséssemos apenas paralisar o processo (ou evento) sem que isso impactasse em toda a simulação? Neste caso, SimPy fornece um atributo defused
para cada evento que, quando alterado para True
, faz com que a interrupção seja “desarmada”.
Vamos alterar o atributo defused
do processo interrompido no exemplo anterior:
Quando executado, o modelo anterior fornece:
Novamente a execução do processo de interrupção vai até o fim e a interrupção que poderia causar a paralização de todo o modelo é desarmada.
Portanto, se o objetivo é desarmar a interrupção, basta tornar True
o atributo defused
do evento.
Interrompendo um evento com o método fail
Assim como se provoca uma interrupção, pode-se também provocar uma falha no evento. O interessante, neste caso, é que podemos informar a falha (todo)
O que são funções geradoras? (ou como funciona o SimPy) - Parte I
O comando yield
é quem, como você já deve ter notado, dá o ritmo do seu modelo em SimPy. Para melhor compreender como funciona um programa em SimPy, precisamos entender, além do próprio comando yield,
outro conceito fundamental em programação: as funções geradoras.
Começaremos pelo conceito de Iterador.
Iterador
Na programação voltada ao objeto, iteradores são métodos que permitem ao programa observar os valores dentro de um dado objeto.
Quando você percorre uma lista com o comando for,
por exemplo, está intrinsecamente utilizando um iterador:
No exemplo, lista
é um objeto e o comando for
é um iterador que permite vasculhar cada elemento dentro da lista, retornando sempre o elemento seguinte do objeto.
Funções geradoras
Elas existem e estão há tempos entre nós…
Uma função geradora é uma classe especial de função que tem como característica retornar, cada vez que é chamada, valores em sequência. O que torna uma função qualquer uma função geradora é a presença do comando yield
em seu corpo.
O comando yield
funciona, a primeira vez que a função é chamada, algo semelhante a um return,
mas com um superpoder: toda vez que a função é chamada novamente, a execução começa a partir da linha seguinte ao yield
.
Por exemplo, podemos imprimir os mesmo números da lista anterior, chamando 3 vezes a função seqNum(),
definida a seguir:
Note que a função geradora seqNum
é um objeto e que o loop for
permite acessar os elementos retornados por cada yield
.
A primeira fez que o loop for
chamou a função seqNum()
o código é executado até a linha do yield n,
que retorna o valor 10
(afinal, n=10 neste momento).
A segunda fez que o loop for
chama a função, ela não recomeça da primeira linha, de fato, ela é executada a partir da linha seguinte ao comando yield
e retorna o valor 20
, pois n
foi incrementado nessa segunda passagem.
Na terceira chamada à função, a execução retoma a partir da linha seguinte ao segundo yield
e o próximo valor de n será o anterior incrementado de 10.
Uma função geradora é, de fato, um iterador e você normalmente vai utilizá-la dentro de algum loop for
como no caso anterior. Outra possibilidade é você chamá-la diretamente pelo comando next
do Python, como será visto no próximo exemplo.
Exemplo: Que tal uma função que nos diga a posição atual de um Zumbi que só pode andar uma casa por fez no plano? A função geradora a seguir acompanha o andar cambaleante do zumbi:
Diferentemente do caso anterior, criamos um zumbi a partir da linha:
Cada novo passo do pobre infeliz é obtido pelo comando:
O bacana, no caso, é que podemos criar 2 zumbis passeando pela relva:
Na seção a seguir discutiremos o papel da função geradora em um modelo de simulação. Por ora, sugerimos as seguintes fontes de consulta caso você procure um maior aprofundamento sobre o yield
e as funções geradoras:
Uma boa explicação na StackOverflow: What does the yield keyword do?
O que são funções geradoras? (ou como funciona o SimPy?) - Parte II
SimPy vs. funções geradoras
No episódio anterior…
Uma função geradora é uma classe especial de funções que têm como característica retornar, cada vez que são chamadas, valores em sequência. O que torna uma função qualquer uma função geradora é a presença do comando
yield
em seu corpo.
Para compreendermos a mecânica do SimPy (e da maioria dos softwares de simulação), devemos lembrar que os processos de um modelo de simulação nada mais são que eventos (ou atividades ou ações) que interagem entre si de diversas maneiras, tais como: congelando outro evento por tempo determinado, disparando novos eventos ou mesmo interrompendo certo evento já em execução.
Já sabemos que as entidades e eventos em SimPy são modelados como processos dentro de um dado environment. Cada processo é basicamente uma função iniciada por def
como qualquer outra construída em Python, mas que contém a palavrinha mágica yield.
Assim, como descrito no item anterior, todo processo em SimPy é também uma função geradora.
Um evento bastante elementar em SimPy é o timeout()
ou, na sua forma mais usual:
Imagine por um momento que você é a própria encarnação do SimPy, lidando com diversos eventos, processos, recursos etc. Repentinamente, você, Mr. SimPy, depara-se com a linha de código anterior. Mr. SimPy vai processar a linha em duas etapas principais:
- A palavra
yield
suspende imediatamente o processo ou, de outro modo, impede que a execução avance para linha seguinte (como esperado em toda função geradora); - Com o processo suspenso, a função
env.timeout(tempo_de_espera)
é executada e só após o seu derradeiro término, o processamento retorna para a linha seguinte do programa.
Portanto, quando um processo encontra um yield
, ele é suspenso até o instante em que o evento deve ocorrer, quando o SimPy então dispara o novo evento. O que o SimPy faz no caso é criar um evento a ser disparado dali a um tempo igual ao tempo_de_espera.
Naturalmente, quando num modelo de simulação temos muito eventos interpostos, cabe ao SimPy coordenar os disparos e suspensões dos eventos corretamente ao longo da simulação, respeitando um calendário único do programa - é nesta parte que você deve se emocionar com a habilidade dos programadores que codificaram o calendário de eventos dentro do SimPy…
Em resumo, SimPy é um controlador de eventos, gerados pelo seu programa. Ele recebe seus eventos, ordena pelo momento de execução correto (ou prioridade, quando existem eventos simultâneos no tempo) e armazena uma lista de eventos dentro do environment.
Se uma função dispara um novo evento, cabe ao SimPy adicionar o evento na lista de eventos, de modo ordenado pelo momento de execução (ou da prioridade daquele evento sobre os outros).
Um exemplo de simulação e otimização
Quando caixas seriam necessários para atender os clientes de um banco?
Considere que num banco os clientes chegam segundo um processo de Poisson a taxa de 5 clientes/hora e que o tempo médio de atendimento nos caixas é de .. min.
Vamos considerar que a hora de trabalho de um caixa é de R$ 20,00 e que o turno de trabalho é de 6 horas diárias (ou seja, cada caixa custa R$ 120,00/dia). Se o banco
Simulação de Agentes em SimPy!
APOCALIPSE ZUMBI!
Entrada e saída de dados por planilha eletrônica
Modelos de simulação são usualmente ferramentas utilizadas para simulação de diversos cenários distintos, exigindo a manipulação de um número considerável de dados e informações. Assim, uma boa prática é armazenar os dados do modelo, tanto de entrada quanto de saída, em uma planilha eletrônica.
Embora o SimPy não tenha funções específicas de comunicação com planilhas, o Python possui diversas bibliotecas para isso. A minha preferida é a xlwings.
Comunicação com a biblioteca
A biblioteca ganhou meu coração por ser de uso simples e isso inclui poucos comandos a serem aprendidos.
Vamos começar planilhando nosso modelo de fila M/M/1 criado na seção “Criando, ocupando e desocupando recursos”. Nesse modelo, temos dois parâmetros de entrada: o tempo médio entre chegadas de clientes e o tempo médio de atendimento no servidor (lembrando que em uma fila M/M/1 tanto os intervalos entre chegadas sucessivas de clientes quanto os atendimentos no servidor são exponencialmente distribuídos).