simpy-simpy-a59c5bcfade4/.hg_archival.txt0000644000000000000000000000017213323151071016623 0ustar 00000000000000repo: 7dc8a5286acb335c20950fe8bb88b23196ca5138 node: a59c5bcfade4d2fba1958af242eba1564b81a48d branch: default tag: 3.0.11 simpy-simpy-a59c5bcfade4/.hgignore0000644000000000000000000000023413323151071015337 0ustar 00000000000000syntax: glob *.egg-info/ *.pyc *.pyo *.swp *~ __pycache__ .DS_Store .coverage .ropeproject/ .tox/ .cache/ MANIFEST build/ dist/ docs/_build/ htmlcov/ .env simpy-simpy-a59c5bcfade4/.hgtags0000644000000000000000000000100413323151071015006 0ustar 00000000000000480c9c2f4e17b006bcac4071add939fb88d2c54d 3.0 152ef01b1267cf8a753774c3d66afae95e739466 3.0.1 4c9390d76241f89a5cb50baf54fcccd6dd45f315 3.0.2 3c17cc7e52a4cd866327fe7e18167cc6145706b9 3.0.3 77569827abf65591cc963141741ec3006882d0e6 3.0.4 c1566b24ba27e32e92fdfdaf01a4ddc3d61fd367 3.0.5 e11044f09a546b4844e95b098a02e298d8afeb15 3.0.6 ad434ce74e21304dfe220168a97527627310804f 3.0.7 19592f56f31e5d0bcc9225da0755c936e0d6fb00 3.0.8 9858a2079e685adcd389916bfb44ca4d8694b5ee 3.0.9 f8653514008e38030106b802ca66366571d8e5ac 3.0.10 simpy-simpy-a59c5bcfade4/AUTHORS.txt0000644000000000000000000000076013323151071015426 0ustar 00000000000000Authors ======= SimPy was originally created by Klaus G. Müller and Tony Vignaux in 2002. In 2008, Ontje Lünsdorf and Stefan Scherfke started to contribute to SimPy and became active maintainers in 2011. In 2011, Karen Turner came on board to generally help with all the bits and pieces that may get forgotten :-) We’d also like to thank: - Johannes Koomer - Steven Kennedy - Matthew Grogan - Sean Reed - Christoph Körner - Andreas Beham - Larissa Reis - Peter Grayson - Cristian Klein simpy-simpy-a59c5bcfade4/CHANGES.txt0000644000000000000000000001275213323151071015355 0ustar 00000000000000Changelog for SimPy =================== 3.0.11 - 2018-07-13 ------------------- - [FIX] Repair Environment.exit() to support PEP-479 and Python 3.7. - [FIX] Fix wrong usage_since calculation in preemptions - [NEW] Add "Time and Scheduling" section to docs - [CHANGE] Move Interrupt from events to exceptions - [FIX] Various minor documentation improvements 3.0.10 – 2016-08-26 ------------------- - [FIX] Conditions no longer leak callbacks on events (thanks to Peter Grayson). 3.0.9 – 2016-06-12 ------------------ - [NEW] PriorityStore resource and performance benchmarks were implemented by Peter Grayson. - [FIX] Support for identifying nested preemptions was added by Cristian Klein. 3.0.8 – 2015-06-23 ------------------ - [NEW] Added a monitoring guide to the documentation. - [FIX] Improved packaging (thanks to Larissa Reis). - [FIX] Fixed and improved various test cases. 3.0.7 – 2015-03-01 ------------------ - [FIX] State of resources and requests were inconsistent before the request has been processed (`issue #62 `__). - [FIX] Empty conditions were never triggered (regression in 3.0.6, `issue #63 `__). - [FIX] ``Environment.run()`` will fail if the until event does not get triggered (`issue #64 `__). - [FIX] Callback modification during event processing is now prohibited (thanks to Andreas Beham). 3.0.6 - 2015-01-30 ------------------ - [NEW] Guide to SimPy resources. - [CHANGE] Improve performance of condition events. - [CHANGE] Improve performance of filter store (thanks to Christoph Körner). - [CHANGE] Exception tracebacks are now more compact. - [FIX] ``AllOf`` conditions handle already processed events correctly (`issue #52 `__). - [FIX] Add ``sync()`` to ``RealtimeEnvironment`` to reset its internal wall-clock reference time (`issue #42 `__). - [FIX] Only send copies of exceptions into processes to prevent traceback modifications. - [FIX] Documentation improvements. 3.0.5 – 2014-05-14 ------------------ - [CHANGE] Move interruption and all of the safety checks into a new event (`pull request #30`__) - [FIX] ``FilterStore.get()`` now behaves correctly (`issue #49`__). - [FIX] Documentation improvements. __ https://bitbucket.org/simpy/simpy/pull-request/30 __ https://bitbucket.org/simpy/simpy/issue/49 3.0.4 – 2014-04-07 ------------------ - [NEW] Verified, that SimPy works on Python 3.4. - [NEW] Guide to SimPy events - [CHANGE] The result dictionary for condition events (``AllOF`` / ``&`` and ``AnyOf`` / ``|``) now is an *OrderedDict* sorted in the same way as the original events list. - [CHANGE] Condition events now also except processed events. - [FIX] ``Resource.request()`` directly after ``Resource.release()`` no longer successful. The process now has to wait as supposed to. - [FIX] ``Event.fail()`` now accept all exceptions derived from ``BaseException`` instead of only ``Exception``. 3.0.3 – 2014-03-06 ------------------ - [NEW] Guide to SimPy basics. - [NEW] Guide to SimPy Environments. - [FIX] Timing problems with real time simulation on Windows (issue #46). - [FIX] Installation problems on Windows due to Unicode errors (issue #41). - [FIX] Minor documentation issues. 3.0.2 – 2013-10-24 ------------------ - [FIX] The default capacity for ``Container`` and ``FilterStore`` is now also ``inf``. 3.0.1 – 2013-10-24 ------------------ - [FIX] Documentation and default parameters of ``Store`` didn't match. Its default capacity is now ``inf``. 3.0 – 2013-10-11 ---------------- SimPy 3 has been completely rewritten from scratch. Our main goals were to simplify the API and code base as well as making SimPy more flexible and extensible. Some of the most important changes are: - Stronger focus on events. Processes yield event instances and are suspended until the event is triggered. An example for an event is a *timeout* (formerly known as *hold*), but even processes are now events, too (you can wait until a process terminates). - Events can be combined with ``&`` (and) and ``|`` (or) to create *condition events*. - Process can now be defined by any generator function. You don't have to subclass ``Process`` anymore. - No more global simulation state. Every simulation stores its state in an *environment* which is comparable to the old ``Simulation`` class. - Improved resource system with newly added resource types. - Removed plotting and GUI capabilities. `Pyside`__ and `matplotlib`__ are much better with this. - Greatly improved test suite. Its cleaner, and the tests are shorter and more numerous. - Completely overhauled documentation. There is a `guide for porting from SimPy 2 to SimPy 3`__. If you want to stick to SimPy 2 for a while, change your requirements to ``'SimPy>=2.3,<3'``. All in all, SimPy has become a framework for asynchronous programming based on coroutines. It brings more than ten years of experience and scientific know-how in the field of event-discrete simulation to the world of asynchronous programming and should thus be a solid foundation for everything based on an event loop. You can find information about older versions on the `history page`__ __ http://qt-project.org/wiki/PySide __ http://matplotlib.org/ __ https://simpy.readthedocs.io/en/latest/topical_guides/porting_from_simpy2.html __ https://simpy.readthedocs.io/en/latest/about/history.html simpy-simpy-a59c5bcfade4/LICENSE.txt0000644000000000000000000000214513323151071015362 0ustar 00000000000000The MIT License (MIT) Copyright (c) 2013 Ontje Lünsdorf and Stefan Scherfke (also see AUTHORS.txt) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. simpy-simpy-a59c5bcfade4/MANIFEST.in0000644000000000000000000000024513323151071015274 0ustar 00000000000000include *.cfg include *.ini include *.txt recursive-include docs * recursive-exclude docs *.pyc recursive-exclude docs/_build * recursive-exclude docs/__pycache__ * simpy-simpy-a59c5bcfade4/README.txt0000644000000000000000000001104413323151071015233 0ustar 00000000000000SimPy ===== SimPy is a process-based discrete-event simulation framework based on standard Python. Processes in SimPy are defined by Python `generator`__ functions and can, for example, be used to model active components like customers, vehicles or agents. SimPy also provides various types of shared resources to model limited capacity congestion points (like servers, checkout counters and tunnels). Simulations can be performed “as fast as possible”, in real time (wall clock time) or by manually stepping through the events. Though it is theoretically possible to do continuous simulations with SimPy, it has no features that help you with that. Also, SimPy is not really required for simulations with a fixed step size and where your processes don’t interact with each other or with shared resources. The `documentation`__ contains a `tutorial`__, `several guides`__ explaining key concepts, a number of `examples`__ and the `API reference`__. SimPy is released under the MIT License. Simulation model developers are encouraged to share their SimPy modeling techniques with the SimPy community. Please post a message to the `SimPy mailing list`__. There is an introductory talk that explains SimPy’s concepts and provides some examples: `watch the video`__ or `get the slides`__. __ http://docs.python.org/3/glossary.html#term-generator __ https://simpy.readthedocs.io/en/latest/ __ https://simpy.readthedocs.io/en/latest/simpy_intro/index.html __ https://simpy.readthedocs.io/en/latest/topical_guides/index.html __ https://simpy.readthedocs.io/en/latest/examples/index.html __ https://simpy.readthedocs.io/en/latest/api_reference/index.html __ https://groups.google.com/forum/#!forum/python-simpy __ https://www.youtube.com/watch?v=Bk91DoAEcjY __ http://stefan.sofa-rockers.org/downloads/simpy-ep14.pdf A Simple Example ---------------- One of SimPy's main goals is to be easy to use. Here is an example for a simple SimPy simulation: a *clock* process that prints the current simulation time at each step: .. code-block:: python >>> import simpy >>> >>> def clock(env, name, tick): ... while True: ... print(name, env.now) ... yield env.timeout(tick) ... >>> env = simpy.Environment() >>> env.process(clock(env, 'fast', 0.5)) >>> env.process(clock(env, 'slow', 1)) >>> env.run(until=2) fast 0 slow 0 fast 0.5 slow 1 fast 1.0 fast 1.5 Installation ------------ SimPy requires Python 2.7, 3.2, PyPy 2.0 or above. You can install SimPy easily via `pip `_: .. code-block:: bash $ pip install -U simpy You can also download and install SimPy manually: .. code-block:: bash $ cd where/you/put/simpy/ $ python setup.py install To run SimPy’s test suite on your installation, execute: .. code-block:: bash $ py.test --pyargs simpy Getting started --------------- If you’ve never used SimPy before, the `SimPy tutorial`__ is a good starting point for you. You can also try out some of the `Examples`__ shipped with SimPy. __ https://simpy.readthedocs.io/en/latest/simpy_intro/index.html __ https://simpy.readthedocs.io/en/latest/examples/index.html Documentation and Help ---------------------- You can find `a tutorial`__, `examples`__, `topical guides`__ and an `API reference`__, as well as some information about `SimPy and its history`__ in our `online documentation`__. For more help, contact the `SimPy mailing list`__. SimPy users are pretty helpful. You can, of course, also dig through the `source code`__. If you find any bugs, please post them on our `issue tracker`__. __ https://simpy.readthedocs.io/en/latest/simpy_intro/index.html __ https://simpy.readthedocs.io/en/latest/examples/index.html __ https://simpy.readthedocs.io/en/latest/topical_guides/index.html __ https://simpy.readthedocs.io/en/latest/api_reference/index.html __ https://simpy.readthedocs.io/en/latest/about/index.html __ https://simpy.readthedocs.io/ __ mailto:python-simpy@googlegroups.com __ https://bitbucket.org/simpy/simpy/src __ https://bitbucket.org/simpy/simpy/issues?status=new&status=open Enjoy simulation programming in SimPy! Ports and comparable libraries ------------------------------ Reimplementations of SimPy and libraries similar to SimPy are available in the following languages: - C#: `SimSharp `_ (written by Andreas Beham) - Julia: `SimJulia `_ - R: `Simmer `_ simpy-simpy-a59c5bcfade4/bitbucket-pipelines.yml0000644000000000000000000000031713323151071020223 0ustar 00000000000000image: python:latest pipelines: default: - step: script: - pip install -e . - pip install -U pytest pytest-cov - py.test --cov=src --cov-report=term-missing tests simpy-simpy-a59c5bcfade4/docs/Makefile0000644000000000000000000001076313323151071016134 0ustar 00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -n SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use 'make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* ./html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SimPy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SimPy.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/SimPy" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SimPy" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." simpy-simpy-a59c5bcfade4/docs/_static/favicon.ico0000644000000000000000000017755113323151071020254 0ustar 00000000000000 hV  00 %f ; X(  IIIIIIGDBDA?̀>66!UUUYVRVSOTOMwPMKMJGJGDFDCDA?A>B?>6!;96gF.. WWQ/WUPUROTQMQQL/OKHLIFKHEIFDHEBGDBlB@=h@>;?<:=;9;;78[XTZVSXUQWSPUROTQLROMQNKOLINKHLIFKHEIFDHEBFDAEB?CA>B?=@>;><9k^[W]YV[XTZVSXUQWSPUROTPMROLQNJOLINKHLIFKHEIFCHEBFCAEB?C@>A?bc_[;c_[a]Z`\X^ZW]YU[XTZVSXUQWSPUROSPMROLQMJOLIMJGLIFJHEIFCHEBGGGeb^d`\c_[a]Z_\X^ZW\YU[XTYVSXUQVSPURNSPMROLPMJOLIMJGLIFJHEIFCGEBEBABB=6ccc6gc_ea^d`\b_[a]Y_\X^ZW\YU[WTYVSXTQVSPURNSPMROLPMJOLIMJGLIFJGEIFCGFCRicKde_gc_ea^d`]c`\P`[[-ffM ZZZZZVA\WUrXVRXTQVSPUQNSPMROKPMJOLIMJGLIFJGEBqcMTjbFUUU\\R[WTYVRXTQVSPUQNSPMROKPMJOLIMJGd^YV\YU[WTYVRXTQVSPUQNSOMQMJOUUUa]Y_[X^ZV\YU[WTYVRXTQVROfffea^c`\b^Za]Y_[X]ZV\XUZWTYVR7sg*FodWibhd`fc_ea]c`\b^Z`]Y_[X]ZV\XUZUU3UlZ~k|i%wh6rfGmdXibhd`fb^ea]c`\b^Z`]Y_[X]YV@@@oSnmll~k|i&vg7reHmdYhbhd`fb^ea]c_\b^Z`\Y^YU9rqponmlk~k{i&vg8qeImcZhbhd`fb^ea]c_]vytsrqponmlk~k{i'vg8qfJmb`\halhd`xdd]!{wvutsrqponmlkj$yxwwvutsrqonp{zyxwvvutsT}|{zyxwu~}|{zyy7~}|{z.u%rnqpkm~}|{q u?ttssrqj~}}{zyxwvutsro ~}|{zyxwvut?~~}|{y*yws*w~}}||{y=.~}{z>{4?????(0` GD@DEC?DB?CA>B@=333HECGDBFCAEC@DA?DA>bUUUSQNbRNN;MHC5JGDIFCHECGDBFCAEB@999 ;;; >:7<:7777YUQHVSPUROTQNROLUUU NIGLIFKHEJGDIFCHEBGDBGCAC?;=@>;?=:><9=;8;;7y@@@ZXSqYVRXUQWTPVSOUROTQNPPP SLL%PNKfOLINKHMJGLIFKHEJGDIFCHEBGEBwDDAGDDD"D@>C@>B?=A><@>;?<:><9:::#\XU[XTZWSYVRXUQWTPVSOURNTQNSPMROLQNKPMJOLINKHMJGLIFKHEJGDIFCHEBGDBFCAEB@DA?C@>B?=A><@=;@@7]ZW]YV\XU[XTZVSYVRXUQWTPVSOURNTQMSPMROLQNKPMJOLINKHMJGLIFKHEJGDIFCHEBGDAFCAEB@DA?C@>B?=CC7`\X_[X^ZW]YV\XU[WTZVSYURXUQWTPVSOURNTQMSPLROLQNKPMJOLINKHMJGLIFKHEJGDIFCHEBGDAFC@EB@DA?@@@a_YYa]Y`\Y_[X^ZW]YV\XU[WTZVSYURXTQWTPVSOURNTQMSPLROKQNKPMJOLINKHMJGLIFKHEJGDIFCHEBGDAFC@CCC```c_[b^Za]Y`\Y_[X^ZW]YV\XU[WTZVSYURXTQWSPVSOUQNTQMSPLROKQNKPMJOLINKHMJGLIFKHEJGDIFCHEBGDAea]|d`\c_[b^Za]Y`\X_[X^ZW]YV\XU[WTZVSYURXTQWSPVROUQNTPMSPLROKQNKPMJOLINKHMJGLIFKHEJGDIFCHEBGE@oGBB6@@@cccfb^ea]d`\c_[b^Za]Y`\X_[W^ZW]YV\XU[WTZVSYURXTQWSPVROUQNTPMSOLROKQNJPMJOLINKHMJGLIFKHEJGDIFCHEBGDAFB@@@@ie`wgc_fb^ea]d`\c_[b^Za]Y`\X_[W^ZV]YV\XU[WTZVSYURXTQWSPVROUQNTPMSOLRNKQNJPLIOLINKHMJGLIFKHEJGDIFCHEBHCAj`gbhd`gc_fb^ea]d`\c_[b^Za]Ya\X_ZW_YV\XT[XT[WTZVSYURXTQWSPVROUQNTPMSOLRNKQMJPLINKHNKHLJGLIFJHEJGDHFCUibafahd`gc_fb^ea\tccZUUUZWTUZVSYURXTQWSPUROUQNSPMSOLQNKQMJOLINKHMJGLJGKIFJIEImmKlbWicbib'ZVR>ZWTZVSXURXTQVSPUROTQNSPMROLQNKPMJOLINKHMJGKHHU\YU[XTZWTYVSXURWTQVSPUROTQNSPMROLQNKPMKMMIB^[V]ZV\YU[XTZWSYVRXURWTQVSPUROTQNSPMRMM8`]Y_\X^[W]ZV\YU[XTZWSYVRXUQWTQVSPUSO{c_\Kb_[a^Z`]Y_\X^[W]ZV\YU[XTZWSYVRXUQXUPffa^dea]d`]c_\b^[a^Z`\Y_\X^[W]ZV\YU[XTZWSXVR@jj Njbe[hbee`gc_fb^ea]d`]c_\b^[a]Z`\Y_[X^[W]ZV\YU[XTZZS%zhG!xh.tg8qeCndOkcZhbfd`gc_fb^ea]d`\c_\b^[a]Z`\Y_[X^ZW]ZV\YUmlfk~k}j{i#wh.tg:qeDndOkc[hafd`gc_fb^ea]d`\c_[b^[a]Z`\Y_[X^ZW\YWdmntnmmlk~k}j{i#wh/tf:qeFndQkc]gahd`gc_fb^ea]d`\c_[b^Za]Z`\Y_[X mqponnmmlk~k}j{i$wh/tf;qeFndRjb]gahd`gc_fb^ea]d`\c_[b^Za]Yt,rrqpponnmmlk~k}jzi$wh0tf;qeGmdRjb^gahd`gc_fb^ea]d`\ccUwtssrrqpponnmmlk~k}jzi%wh0tfwvvuutssrqqpponnmll hyxxwvvuutssrqqpponmf zyyxxwvvuttssrqqFy({{zyyxxwvvuttu#|N||{{zyyxxwvt.}t~}||{{zyyxxY~}~~}||{{zy|!g~~}||{{y7s3qXqFqQ~~}||{uHtsrrqqpp~~}|}w>vuuttsrrqq|~}}||{zzxzxwwvuuttsrrs~}}||{zzyyxwwvuutts~ ~}}||{zzyxy&w\wvuu v'Y~}}||{zz.wwwv_~}}||{v yyxw~}}|{{z{rL~}}|zAn~{ +QvuU/|?p???0qq?PNG  IHDR>a{IDATx]ytřU_sjF%˒,,_ 1ƀ9 ؗ, Mrnv;䱛%/lB^؄10[6agzfzF3\=4U]]ݿ_}WgLQ(SbV&))r,f`,f`,f`,f.@!ݑ(T=;?Y״$p8z&C\Tg2YQe6(9D= aK;/DB-r.c}*<a*خ"gf KxB TwRA!z*'"AC /=# C.ys`tˮ(nqd& =Z* H`~{}na24NKRf4Ty-3%TŃ..OJzO)?|hI]7yٚƅG9w VLM8r />? @%f!f*.{r ||Y u{?3 9yi3Z=KcR];쒷>71  lu w);W~/ݷ-;: 8Uq@nG{/>cpϤ<し)(PcZ>0='m~JiM]w~g>Н)~|{G""/[--fK[ky*y॔FҾ2m k9s-^Mu'=0Dqqd_{p#>NZ^vdf}Z>=<LFL9<r<8QBi<ɠQ5 #XZ}8AjA)~µ}r牎vP=v <89jo|FSp)9/|M o`G\85Zxܭ8 a#zt(V5m2Vy"f-T.\.Z?SKN k୪ʌH)r&ۣJT M"Xyl_U[55׀@9^˧z.6i QHq~0a>}jêq@$(-64K - ~ćDsD&÷c,P~㗬W}.J)O'vqe w/n`FLpl7 Q"C)wpr4ud"uKVHsLü@*{l:nHIh/5_J= k5>:"vN j39*[/0/E} ʳ\rQS.A8 xKԈp\(6bxz 7>}>iRB@8sOc+#K0H 5$ suq ,늸:"g#=<(#at9i#:fPhܼ\hEǕgk" `*]uMHY:ez`wc_G kkrCpl>{Fv0Xc/^qec*c=x8>9@i`mZ_GOSPT}U[;^[S> DQ+Oz;vͯb-JҖ-gO6 $(*t>6_<;uՎCN?ZTp]}ry:/nδ~0OSܙӱ7> FL?fTXs v9zxtF&D4 gOnXuiqXiBFeSd#_o  ƠT`E8Xvx-q ISBd0 0ii.WJj9|J̡¡ 4(C{C +p'$l@,DZchƒ`5+g2<*`qU;JLjjz/ү^wY* R ׸H5(=`55y}j`~Va{vkfīKsDs:m<)6~\R'd,Y-lLLC TNwhz;4kR`@UZ Ϫ>!u@a)޹B(4`ج04g&#<4ʳpNd8+e`G,V⁛[VƁ5x mjiW%#2᳑ [4;0|զ-0Bb̿+E)6(@sRVBH'bkFSPshi1k-6r{xrg#Œ3 B_>mnY9,"E^ݼb郯}-w.` B :3RԥaK4]kCd']7PJq](OF~'a 9T)6@BzO4Y͔LIyzG_z!XgN%FB~De א3't ȿx1kPAa-ްe-w.O0u)isccBATL"L \hGlNeةH'|RoƇc{??rd`EΝNZ}[_L >1OE3B~+vuNي\ֲZ;[){ P"T@8 Qӧ_3rR&at#z=`Ɗ҆|UJPT@8^=}Ow9pNS\vU\hmMGO?Il5D@/b][e _[zjAJPJDI9̟"R30W<ˏB; 0^AkiG 1{P^U L|57 H$7Ίx}#ڑŔI 6QstHG=>+Dd(~ ?!6P٪6(T7K4FtPb[rC 6Qr&UZ]U-_I-IjV5YJ6 &!Za\;S?ݝx@`ZMT@P ` `[w.[#v}afb'p! |$03`RI]MOVn7 61'K#z)i(_ 7@=,6nKMW,Y-LcHy wO ʧq21iY?x= .t>jJy4_x52bt:.$8o|3çOicy}?["># Q`D#Bau Zŕ5uR- $$.gS \]o?[s2"X7rxB#Ɗjqk/t.Y^b;a8Mݧoly1c @' ,VaWZfM}Ü`R7ʼMolۍ>-O`G{p8HF, DQ1PP/Ħ-ji^a: TmZtw]3o cӎ5`[V@,uŚ ;`;ygh`>D46R `>U7.o]yb> ccG;^=.&f 0 I`"IZ7^wWjzJ W^; lwyu8MvB[}>RPoPhi㒇~)b8؞BJ/3DPp۱{玭,+q'XƀǮCaP=XhA&رcI1M`~Id@UF^HEVxڼ* ^(T #ٶ"v|95$Ir_ d:Q1odhhZNN#j]vٕ?[ Y(ᐒm7Ogb$vlsRb٪O>x3r݅]mD K@ݟG H~=ڦ`0!q"Z 72%:(Ah]hk%\AIL Srns>>'_gTS)@SWXVV^٨/39]q$o?zryL|vnCC(/蓷ݾݳ׋<٣[X[ڬNtqgP%82o7.b{Dw,!J*++笻vK~liOj%@_u_MM͏A%Ɋ9/ ?o#ؙ Gf/slEaI]T1jqMӸG?D3ϓ755}'Y|&/H>x}OG}΋JˮlΦd|B&9Y}>|O@d{?N}oGV'i>Og |o Tc~o|>g7::zlnآГ}6R @֭oh6ūq) 6p||R1``{cLO28WRjAOx&*0nbSk55xÇ쳲'b`JlGSNk+vUUUḌlbaO 45xP sЁ@'bWtET%`^悿 1Qƀ O \6*>> -sq\H8xǿUe/X(t eNg&:mDŽHu6 4Ko~p ̮mH*4_ Ų̰g7r|. ( Ga% >_ @H-+q33YM..3; &ja0G/KUUU5xa)Axu>V9ަT%/ͲYmeLF"ex5/O(FO Z-KR7 =SǕpE^5L&G,T||{]'Pj5]dlvd2[ a+#ol:X-Q`Z@+TH> pw|rO¸۳a`xDq+W-n{ѮΓ{^Y*.ijnLE)S#sK mV:@ .X)C}{?(L`۾]|[r5P(w}dc6R곂t2=ooԺbIrzxVEQF\Mf`ZaOm%pPNxl~= _rIqoi;+_BАpOשǃĎ J{A݇P_-+6766y^^\( =橓_1IH74} a8k9V^^4F@`tw׋`D|xSe𡑆Mkh%.ь}^WWGGGv8L#%A P(4p}_牣GW,k]q$gN|ݹ[Q#`5Q!e7!$9{NgRC.n`o;}2!$6=z=͋n% WooN0ul9JE 2H ;^ `I`{aCP9K#`=^|ٕWd2Udڙ<>q,ho~m)]-RV4ox\}`ZS}Q0ӕy} TQM(=VKx0=`/,li;348L;s\ݲ,kffj3~^EQ:OT HfLE 0D}ɚJ wfY[(:jmݼMI#ݝ^Fҕߧ+Y>تlhR*DlsRfJM0x f5":2so#~yLj^T]9xY I=~VYi )D"Ǐ= fK 4^ Q7\`9qkf"d*$&I"+4B70B'.O)9?@%u٢mmx{/%Nt+$E~#^z:%>zz/=SsBI[R\% Yp% Yp% }dG^8IENDB`PNG  IHDRx IDATxg$ŕ-ҕ9޴4ihV8!@Fb F7Ns1{ѝW#A$! ߴqhofUQY9esV,HDS BA{Q% $ F@n ?˶kpل:H5"S)GG=IlH@0ň@AaT|TàB|VE0 4;HUU^F~K ֻ`-\Kζv;Odǜ5I:\^sv6oki;mgri-өɦ츖< hفd#vm}~e @€,6 "Eyҹ-,4ws mN<5L5 mnh35IMs5 FPPO̼Nkvm/GwrYeώq q>{Gz^x ;ɺ- ;e0Xg L$|=`]׻a^𑶥sXkX0099 ̀]D=1rlC7l{igH~ϑw=y8 ϋٺc}cjJh 2~e˱g][=XҦ%(ڽpr ;Gye7qƝ엟,I߆@PmJ@Ⱦzx=1絑ovom|8rPQ" K"10 @ B$}4Uvvkgœڻ4 :U pt.e)gؘ{ܮg~}gz &1_NB`@% lI&a~6[gvdD 6 pyeٴ;rv9bt/  la=itS $hfH?=U`~5ˎ]ޡ00d$#"~ɸ7Lyzt~ܣ7lt Myn~M$$H Ќ_H,@rl>iOFTgc}+r7ykG /o@A◅Mwz|_}] du#u9B2d}17^I!\ " @3 >7Qh׷z{f5>h9XɢчPU\c4_2-6m캝fP~$$H P ~M`uF/u]6s3J'#uj6124;~Cp@7 դ $}~o0of1>' #Dįp!~Ojfr!,HB`@~Y۾ \!u'[?ݫah3OXv~"~e{% ~{rtw`)5! @@?#Yg^vMW~ ?БIw?]( ; @.eRSd毻۳;3y!PzE H+7XH|9׸VЦ|2"Г)9>uF"rg_=fT$Gd[(co?i͉?5+CtJRmGr(2zwp $b1BS H|e~ jfG1 @T@sD{v֢_*D, v,A" $H P/ Vq}._Drf=?XD Uρ'~7_/}@) & @ z@Z+ZWHh)?ԉߩ/ 9 Q|,7x:z@D?O޼x'M~{٪ [MW(?Bqn"84s}o%F H*BB~ .K{{[+ֆND4ruiIK/;$E(K k[>@g`$#"~"`fQn֎-C(7 I $H Pk >Ǒ?v}y\'u9#wbOt@< @ Ze-u'X.;-mYF}y}%[Da~DJ/?l<$ @ Zb{? ?xUϴ45}DNU#~#"`>;q[9۲ H"$nv~tR4qYy45oT7L޳L]C7N hV@ ,X\ѯPUM,`4#sx< @ 7 @ ,u_ͶKP[ݵEE{"~F"~!#w׏mF!$ h:@ӁbX]FɮB})\˄?Tk_NGF訓vR-mZMKfx½q6_(qB!>Ppj" HL%"ؕ֌O]xR_S%įWZmnao;p;K;IpqkС/[mX!6jkAk֢R2|l/` yP._$H 0֬O|G񄶞?ܩщċL^ka|rx1 &|VlM<'/CGͿ_9?;wr`S(HS>њϵ_ke"Wvٌ?n{:$+f3sNj lՓEK;3򹗷@>@DH@`*XO;ٜ{ŧ;J‰NGrAFx䡽o.ֶǼEp @:OcrMOcq,S*įP("|z$~H# qh0BdYN{H咏nڰ)t f“V{ Рd u?+Dя?6;~5F"Ͼ#( Kߌ>3S\Zŋ} @Sc?:jͩJ8i(D>lo-B?\/o?i;J_Sh`c0y&(D!U=۟JoB5C~}ﳟ?>P,c62)Ծ+fďVoC9`4Pj@PmS$>+zg-*5#? r%4(IXynh: T> @WdB/jįJMH [J?Q&Aa*.P0Gv|J+խkEIE< ¤C %n<;M,O!DJ|Q>NDf m, [FP `c¾CH.Ὧg_/r2dvƧO($EH4H&vY?9Ӗe* K?Ogg>t`0d>",|2 vOٵ#r.k5 Ѐ @V'5xN Sk⇿AΡ 'o??8f6)1ϼhtYrP0`@"@0iP[hϪ}. IUxvF>k|?lŀsHSFDE),G~ ; Ի; ;laspG6ΐUr}J{E@Qz dBFvNxF40DsHK&rc(ª}.5Ojَl;ʼnvzŀ_T  9S?QӴړqP|S4H&t~&ЗUpį&yGS 7U[iTG85E =F+܄/E/H5)H*pqh+݀4iFwrk|Xi.:Wqe=ƪ/ޭjW??|J]YB /nA0K}?@>ԏ?CgK[6"O9xgt^*Q(N{MikOyfQi>A/Xf>zɭA#č?5N0E\ Ȉ-11{'[/l', s?$@1H&dۧLC B_Mo{Oj?Y !rOgkqeua/]{G 㲋>r(!A B_h9s ޏ3 .F`S?vUy.}g5B rbT@J:4-V5~Sxf5{!O(@R>/~ۮuDAFD]gV៝9?7]>wً qMIKl=K?0MHZ_pؖ͹›HQ:8o3Cursgf(/8FrzrP)*H}M%{Þen*E7Wxvf1.KG@! d9X_h1)į e\ԼD'֤`x-g!x`" B T?f9c22/)u~(ڗ/%E(weG@$a h{>z+J_RD k&J v+\{%Tp_;<7m(7HGf /f)sM3{qG&~!EDMD7!wCSK\Ak $a]Ö{iq LQɍ_WA{.S7"~{f'C.  Aغ/|{E) 3>/[Jj֕C EDrD]mmV=,hژ 𙧜YbhmSrj?P?RFjDKƈC~UY P @oehoJW%D"`Kį[gG,][AM j%dUw(n*QEQT5ԯPh{- TmMjiT̈scШ5?P0fvv!W "~?"`"~f?Ej$A [\z9M D}0"7{S`o?Ej$~[p~@įpD~D5L xn@d~_u깉V})75&]UW#|6dsr9r'8le3Z.ur]vӴrMoaXaa0LK3 S3L}cff:&u{& Y6eFD!վ7~ErN~hȏ u䇏zg=?s^ڒ9p@~O~qO?!|pA exٵ=0g5g7/6{4ttDa' ;I!  B >f ][%$DzWSک=oޑݿsK@jNȡ@쓉Q~/{? t;}C۞J_]wr| ֱW?!Bg0 ?1gބ?"  IQv IDAT p4W)lto`OOA`?9_deߏ;P _5w%xy) YTnd eBo`K2ER[z0Ƴ+O<-Sk/]x3l<];dvAoLlj$\i$96mˑ?~a3_cz'ʤ?T\X@F(_:ӗi@MXP~:s01@BD(GX@2GS/<9g&q`Bh4GaO=Pļ^xȟk$ iĵDB?)O|݇~#(/#AYjwM0pP~v@&$mgNm+'_aSlMlc+Jnc̫p7?TPͿFAC:z[kݑ_T j SgrV [vkU_{1ž(oAx!OZԜuǬ0Vx6(@;0v{ߑ!xOx jWCj 0v\֜[DJ}-]}G/^5(_%~1xb$|^?@R{_˽HOo<3|=zWn*N"rN2[{|֕"Pa!k$@Ȱs HxHnХ/8n{ 1_|vWiU3|q@b4o1L-Oݛ~Ԏekbן[pk1!h E=Λ^}'>xE([g1Hj $<^}G->Ax+d3?}<@  ` ;Rf|K= Wg.YĜoYZiZ'~ۆ잧NoZ\%Q٨k.Դs/=Hbkװ1Q 6S  /b}떅تV79/ H'w5:z1kˣ闇AA; 'RAHhi/C/(¡ {R;Pc-C aywY~0! N9,q''fam&=nBl*k΢eܹ =C׌i]9@܁_ݽ3' O)H6u^@Lj.(! ?o?l,f$ q%/,]۳#g`bz|*ce֜ =3.]N~tGo現=oA[$ɞ9Yʯ ȿ@X$b#:"d|w:-ZGd b  Kgmze[⹌g=l:-b<:+vqƆ쑡>{H~!{h~CeL~VI~I$SDui`MΌ &J N?52qR$+UC= p(xao[,m9Ã}o 7DZ$?,Ilkl<QDhI$~ޥuDa}MSp% e!F̠<1~1 ppO,:5M|{fOdǙm0<HHJȍ?bŽM~a ,5GYM&k+ ~>QV PٔҲ]P @`6Xq-l03 "O* DQ n!ASd/@E/yBͅ 6B'` мp>": 憬&,!:3y'p?N} W:Ou(9@|DM2/A 1o "a!apyp%LZ *~G6 Oȉ?v=s#߶_M?H|ݲ`J}$z%~ f[gؕ^ y@ DOAH>eLȶ2r$ R`WJ@$^7HMM&"~uPFOj>]W}sz- N5f$RPx(B@+vzf[}}B6,ɉt XߥTިac104hCoy:,\qbҚGCh%a CIߨVc;Q][3?|ԫ( <"@oA  6 }Rˬ/7:fhZM 'Xub} #O`@(x$zėKZj2vp=J~ZhWurLj/[o~탉>4u#$cE6f>  @' 3θ0qK~jE;E7<;787G@6 1 UI 0PD@&Ξo.TƯzW$~qΣodՎz=(4䡖'@BP @ CL$P( 3o]OhbD{H~P@$~ ty`7wzAN2!@bP @ x=P 09*[1Qev!cCvS9{&@(ˆM0r/z?rMW. W,.sUɻ7N6;Y9D Le_?92GP !@L @@"`P<暯r $Pqtj w؛m#QAW$ H$f6"`/޿Ǯowjs|}r9AƭnlB@6/H& $yqp|m]:' CKfg!s;f~54QɈH* HFDw.3 ep;?w< b$| M̼1| J1xP{CD"l9<<';(A<@B $H ̀|@eW}dI3TGC}kSsxkW{oۊr? 5* H0 wBsɟ]ךT@r+KVC%ȤgGJ " )Hx 06k?3g?؉)K>_#sj HA >٨=z7;⏴^=mDFHv0>>O6ߑz|H47H@A+"ؠ@=zίu3 ݭs4Լ͔LyOnH!"I4'H@@)W`^:TFC=g@_ߗW3Po(@s $xHdJɀ-m/]Ƿ,K gW0Tפɉ_`ddйDBA2$]@ݗI%NU*_>PEpY(lÃ>~oo\䋮$$H  0&?C ?2 ?0PO[>36CRV&A!/_,60 : H!dd@1 ^ع|9Dw.3k&Wq |g{__&*A/z HA22DBJ"Lu4:y ?~ 470 : H(@'<܄wtf"#"~7%S``?|@fj9R @ t dD_zc1&~|"~Ѿ p4yCǃ[SS~@ $! P| 5 +?bqD' ⺎\/3ySc(Y (  $$ st~χZpj<"3p ďza/o'|r! !P>4h х H u=Fg:{YםyPp@w~rί}t; Bz4@+yKtzc(I@@>Q6G V\~U۟B^*%42kpJa0/.}7Pz e! pELk2-_NgaA^$H  ńn]d%6NnQjp/|4w9rY^{o `WA.[G !H@@>]" g\k: Rw#į$ K1&w-mo IDAT;П[nxӮh8ǀHaP%w.#{sYl{xꑽ }"E,6cus{@Q  bS[O=/qʙIW,?o _tQBȻyCaE |? =p$!O wv(4ed ԔQ" $*-0)] >+c1o"@'"~ 58`/_>Ȥsfs^ы_ jGtC1[79ft4MK{lg蹧zK/mۃBS/ u)H@H~MRaSl|@{k %Pܤ5(1#(/ %podB/~~WȤO^`  4"$@cg5V#`a qe-[ʼނ |&xjҡB=C/3S坔 '2+? ^)cq(Cpl]1@F,f8&Wtpy1Ǽ9_x'M!r] …! "AHS,E:t^֜X ޔL'\w'CA ш(5r3Mlgq[>smC!B!vTwW J@(F᭕_=dŴ5z7|B}G ~_H<**"dϜi֪5W_!1}jBj L>M6Mc(7 [~1z[^ >{UT 6m_atDŽZ߀S J rsAhm/,\zy~M@M C" C9`OG:Vuo(EA?* ~#JF$Y8eGgO8azE u `  9 u^ ?$~'tRU%~5}"?sE 5s.}٪UBAQ  L $M`" bzhn_PF I`'piuEq*#W_Ba7`!麑;^ٞ=B-ZmHD.߈5r[x1T7JǣΥ^S*?5~~v#0O;/E&B@6yi't>_r QQ~v)8e5R=RdMͫr.5?B_PT?f>/<Վ[GAȳ(6qU8NQ]sfH%9:DZ!g]'tщ_tQ!Q:tej^s? QY_\~6Uc;40?6iV"APM=~Z`Qi B".ziLEWMkz(S2# "cVBePԼш_C?*"SCk+g2Y/ZO?h6g#RF4>蹬.$9,YHg[ks-Ιo.;/bq5O2RU!~U?eY Um&Źpy{~B^6W65@9$ H2!8"[h_u'ǎ$0&)$S*O(j2ß!~7o}Gy h{ M+H@`3F P$PeP\֝_qʼnw[1-G*n DMGmgvgѣ?[B^> EFHPa !9f} Aex&\%o}OgS>Dj ~f߫?o'_lf޿!6qMWwz{{vQ>^TAj4(GpGx\o3VFG(ՎՉQG"~M\~_(&Z?u @ DL-(D}Id< "~\yU8 ++/5Udž_r_$# @ (e==pGZ`b@kEo从]Y"*.o<֦Bm}c ɽ(@M @zjr06 qK[r1;s ,_}ySzf3''/c"OS ᖖϤw\OO:-mZ⅕Wv)8eC45/3ԦgƬ܍x#כ0 @)p"p Y3@.Hl=mH?hj'SrNrݺc -s1Ћ[n(E"~/ԼSK5qG6v'~5<4h  XX0 // [il;O;?ajҬ'#"KN45o=r˚< _}YkW*E5I,bSHsKLn{GO0q ݀(k}ɘ(=5&jч{^B P>#0,I;_Xb / 1c '.Zfi> ~FJ2RU!~U?oB7@&X^ x܅Ƭw]8ibsaI>.ӊ歝^ү^}- $jt@#JX&Ĩ(JB` sަw'6ΜkDuLT}=Oi96lsxxͻnCY zS(H&r7vXؚ̜ǚ3kuGNjF! I<f?{<}.U~3ϥz߁ EsmHpPqT@)gw.gwdviŖuj !AǝXrWaץF45?PSm"#<|&I_$VO&j ̀ PO[t9g؊ǚ,/*N<5$zx=#j0;ҩw&d\$H4Bz ȒJd@<1Br͉c͜g. 9N45oߧt_(_k?أ7+(<?'5RH$hFT(D1 p ccWŗv22ykޏmˆ?2xm>Cو,_jH ЬIBO QO B /Ye%EV̰hjz"UPڿ Ek&@`bTT(jT@2!`Y\}|1ִ.9'1[4uYԼa6vщeS{CC/KC$H bD@5*1VWOc ˗h;j .JW4w'?XdTNܮ-=sW_y^gtk._ H5$LzEmsW^EeX05\%p3?mͷ]M ?D1Gdbo&EKX1=cY:cVlM34yy/v*UĈ0>>w_fKCp.5g @ B8x1`@ޝ0(_@U@gOc݆9/>-8TN[v?{<}.Uĉp`vG6pۯ=ҟ? 5M ho@ $V"₏hIU,kiMkHSZw_,+G!o.2 2TQ12+;fKg옭i5Լ~eE ~o9͈6Rw{{v>@C!iP+-&<]-_shuP@CQ|&?A}_e+,Z<M;6^;R?Im>7ګO6_?@ a DA r9v5-_3{tOy'n㵫1gFuσ $ՅPED 2$tWYxR4-wb6^Hgr/lN`ltȽw\.~Yֿ߿.y a0@&K!/Ĩ?LX V-[teKKk[M$zx=#jWhvC^g]X]H@0bAP,\jի̘1&!7'枷|' O$ L/D PȢ"A @|޼9s-䯯f*h:tz;nAq?ـ?uO@F/Okn"yOJ7"ppƌYN8y <P!ώ|Ň,֗-,ُzOH'EM~sȚd% u '_xR4Mjk7U/fߛw (٧_.@Hj4#V"}ֶ3|is]HSF)oNUCGG~gSw $ z`=Q $O Xv 'ɲb1UUl`xh`߽w}p ~,uσu- $6  rD BMDnPQ TqaB ҋwۻg{ƌ3nZ_nGlۻ1muM7H"Ay|Y`[Z [Ow‰֯߰Q7L*È?{<}.Uf?];Lo?E(@ =|zߋ ȡ XTEĨ@[ܒݶ:|s{iZB@c{_جmrx&E BzB)[X(3%rx0 mC(gkCܕe3+r_Fnz=xDFruҲm ZeZ(=S'BЀ{g #&P(A_:o?l&L f҈_U$~KU)(3f#}67 $(X /Ɗ `ÈC2Û#["LC?[WGW+w>/'oP !{o#I}#שּׁc{zfٙr%hȐ6قD`XA CH ,ChÂ.s}U]]YUy_#ex/"2+2*w/^devnQՀD ZaYOT,+w51Zit^ԠϏo;TdH.E_* $ 6\tGu _m}h,֏o.wԥS- ur}o]p4F'LG@鐓V(EViEsF A`Q rP" 5W &>OMǹy8qSwnsS|-N#  ]&* HlKZ.߸f0 >4;35>xC+ t댑nV@D H: PH1~muwu,8 g }ЏKCu D" e5 Go$W % Y(oݺ]Q?m>!,wc>ы~S!eQ&qREQ7I,2$R LS@<N>O듩mn#M;1X~/>FVFCχ JEFED"Jbx# 7^ n~rPܼ p(/Ҕ2 HdS`g2##(^Һ- A 2]"2$ɱmy IDAT*6\ut:hb _<=ESX ]5CED"%m fdgfnp)8P=Pw0K"َ/5dDdH$Rr&޿wZ5oHĴ0%'t1N1RAt= B^ @p H]I@-krPLD.Rf@I8.W{z"7$"@"M)P7yB!pӾd|w8EP I(2$a9 `U3(!K[_ܽc?#6 6B:LdH$ҸE4b`usky/s+<,<YdtH$d,0Vrܪ#Cc~P'3'nM[/ol]!q$Z%fV=Scs~7-?}7a6*2# D"5CRR&QdiKXg鐩@x)NruZcL{ G(]/'@ߋY^(PdH$҆iMjҙZ4,6 =_=(/_`6L:XDj#QDrKa7k fQDnNu{u7@QG/|#aFCDD"- z0kP(tNbtq Ƙzl>&_Zع_yl&ztH$7e$ƭ5Z0 ppy7@pc[mo B!"@"%M!=u]m3,E:08^HO,_u׮(!%@&MED"y)-oL|=2+/>ҟ:  vH$/b]?Ƹ~Z [e~;xyŗ߁} #"@"%2 %n ,N#:8Iy~A[qZܵ_ 5W P6䦌/|Ÿ@#7]6ԝĈ?L1_}BЛ q`ۉ Dr[&W߶m;?}Cx)7H$|?ڻw^M5l# mXTGH1lW 9d3_kTc=Գ'º]R]R:G}w;^1,[|cotqh>oϡ#ŗ=d#;Dd+̭^nAdO>ʛG"@~e}z_A1 ~^>߁Çk*(Q}r7KV3k>2he%/PK\5l3pkg [Bww@]p3C]aX:գNbiCNdR/ e*K w'A^Я[wEQ@+Ud:w'~xx@EF?PKo lbnݺw׊Ro* Mu[i4fWo\yM@l@6s<B큁޿AQkMPJ|Gi6͡JP3Qd07?- ~ɥ ,fNOOLΝdaax'@@S=.*/SoZG%,d?`O.\2ϿJ`&#ٿw:zl_N෈#|uut Zܯ5_TAK/wW^5yOo2dt?3BP{'9WKjUZ!_j-ß埯e/̀P@ HRG- ;o~r8_V_߽|(V%)& kdp06H 1CIC@-#|ϯ^˥̀l8bGi?2ѳlUoGw ?7zzbWvm:գ ~T5`fO@jlfn~ў}}u*[JE=Ǣў~5W*.gKT;7LLLAܵЪd'/8l۪~X_W_}n]?cotL1~=Q\.;S2)2( Zo|/ľSno3"lϛW:)@8bjX) WrKΟAUVj^糵b߿bB~/>sӑr?m~'1 ?d3Śa[ujl @JԘKWphh_sR7,?ÑH[pxPe$K *x#PpXײ~ݻO~9;^~&_r?i_sW Hɞ#Ǣ8}hCT^|3gO+Qq>~V?:iiPӥ:R,FƒL&pueK&@5[㷏92cϞC=HL8z43[1_GoL7^Qn(@4WTA믛б}{N?u Ug3/?;hJO*?H>l۱NΖZ,MTmT,|>?J$ff'FFИk@ȑ[#`+(}ƿG!Nz닩lT,3OM% @b]W_{.}~a:b+yWD)/p*h۶c'=W^:U5uA+˪ed4MhU5-ijFԬjMCAQ+|*E}>- )JPPQ( <miq$tC8g j^wt*=H5n!y 2 ~~[0<]n}n*[1-c']L}~I0Y3]"xg6즁_wnx~vm?j}m!3|x 2'ioG& kSْC-cimc8u:@ӠN෍kg>oujF@,2m.ao׶m;ՙ@]~r;~b_S,9*|hcI{~En5'G+V~f)L Y@"2#D_};˭" Oc,."qn8y3;HgeeFq hS o~'zvLK7-|57 @)(FC乭%gy/.thVfI"Іr8odx={}5CyxL햶阯 Ƙ9)ou'1- ~ϛ[Of2|&`z<l/K/#eW#$.Ĵ?j|.h $$EzGf8= xʖyxe_Jo,Q QEg4yf$y$2m$I?f>۶:ұLv"ܯug1Laבx<>q E$59f:QeK|޼j<˴zk_b'75oq/1vw/+(_B @{ɪx˶m[we;C_I _wu;. i:tZ|_"tc7+?ve_#R._|L*%>o^Y#?oKKWP*xE}$j8}Ry'ꀺV?NbmsoZ)?9*c/Tl|Q%HS|wc,θG`~Zlo}mN>O* oDWϽ===_F._j#N/ aum\;yk!3]E,2-,'`0sߑ% ~bC*P(ggA P*^dZ_!^[`2/kN9_WEN_=##++WdP.`l@iI @ @t=3%cqs#VPI:D 񕫣F>Ed/`[@dZ_ BoP`9- <15w jPo =џPu |>HRdrja~$UiJr![NdZP66ot޽YϋÆ^m:+t ƘD=~ytqvාQR1˧L*%t"ɬWleЧ$*k!sD%o#nέ6H7 ~>o8") \6l6d҉t*LRL&21Y~e YX@~& @ɦiؽD~q_>u{utG .hFY* \6dL:NSZ2X/ y- `FˢV XPD5%j_^# _H2-8 ԝܯug1:5jj.KeD&NdRd*Jq9yh C_[2wz o?W)V'zC MqWw^Srt*H$VUUe5\uc"0n.Ed5؉SG#蓵pON|20Y{`dK.^`>EH=~ i:t&6;=pq~q&N&P,^zbxOOc7_M֔? tĉ_)t#uG\ᮐ#I&cfg``_̀l=F!,UG с-[޷n(T?qN_9J$[zlϛW:KϪ#g̀S`U kݛu =+}!,˻ 1Mg\u#kM{95{?O% зg}y ^}#EƵZӄ??)"2 w޽#H /q~t؁Ao}\?4`~nLyfe9Em?r%2'?~EQ8΍#[wnz{ ;Vw y X[Pd6Y mY918'P߄/_?b}ڟUa=ʞŽ NAc?It"l> lb,."/_PI:D*L&Q?3IԪD;>$նh&"W[u{utG .X[uڏQ>'1o {<͕U#D3w~~-;iשyɤ_<ܿ?.[t!ؓD`%mAu_ ;)_ Nb\?kj޺_, k׮|\(F~|yF_OCd6A#@ 2e=O/ߴ:ԝVl IDATu{;tƈx6^F:?B)P5MV {=WW.IPw ~[՝N&]!Mi8 ?`03FDrKd3IL{_rᮐ4F:g~Pi"dqq@ "Q/ 16~b*[e:8I'n] g"=9~qu'1~?NbH7c~?6Ed];NCnor鰗nx~vZo^1\e\+} )"F ɜ:v`S;Љ෎kg[ǵ ݊8ˇ4j% @"mTdq ?@8I7nYCouĘWMb?ϧ+0HM&J2i ?0Ȯ!K.Gߏ5ͱm}3_/@@$E;[?E76~U%Dbl6jHPdpp%~ Z O{r1W|FS;z47CNjށB$5 Mqz_?~w c;gqptFDhx2lBL_g~'1 ~H(w5$5]dh4@/Ln.`7N(q- ~fkB0.?3Pdg<U|~S~ =p+q&_kS>@ϗPx/G.# ً zn4{PyeiuMq@ h,DSom-Hne=1&Ӊwt+8ǹ~ie3~4$6to_G5(ojLSnvO?\E:%UU5yEZv1t1lc4[;)øeLjpԯ,c#q iZRەS۵OSylF@c#@?iKE@)yl  5IdWxyێ/" :i snݸJ$Jl6>Z}ܹ}={n۶;ӻE댩lϛW\cϚcI 7 ?"[޾kyQQ,&Ř7JV^"jK TU&gg'{{ݻێ{lABI;sMCAogiZ&ISh4 X[7r(gMQ"P(筪 UNFPB++w/T48q/( i9yL˰:"@2H=o{7 bH$VI)lşWӚ෎il6Jdbemume5/JJ'c箽;w>w 9b}<ĥT@r]d(MӴ w4nsʒpg/{wyI@x<6z+dPfߍ%o'֒p-PXw AӇWsL*NœR<\(ЗJ:N>O?w{8jOD"[ @".2JW㵵ձ+#_*wnGQ6 @豈J;ʥ, Av B`a~~ma~~;z'Ne :b+5˦sl2&2t"JT*PU)qت\>q-C͟Ϥ CFV%yrه/R($~m@.K^t/gPfFhʥ_WrKûN6. <&/e%ʒAё /{Wl_K̔c7~ן@InIA_  >F 0QёnٻXgPni6>6zeԌ̀ D>QdK?>>|ݻw? Ag;_f @rM2EUYN_PP @/~[e@ϽG^Էbo/k<W`??ol^~4a ?Lʀw`g~T ~y\{wo]DtU'&1e%2c\v}7}@S]\ ` g5h4ƥkYPT୴blw_/͟=p%bJհ,bqOFQ[E bCÅ/^f@+P&<|Prw/2dIu]C@&c3Bӱ07 X_9y̻ǘ ϑ H$D;ui+R3`tzra|yiW^}whhJLf_hmmmܾ|!?L "}߿{_| y95&'UM߿YTZ}64+7%o42Tڥqĩsw8젾T~CMS++?RSDU0h_j|NzG ?/lyVWgh |] 0]奥x|dphx/격yn/-?bo Ew<YTUx:r>:?xBw '~x zCeeH$WD[3{Q \l秦'ǟ}7w4ٙWʍDTd7D}fa NN_:ުS6 P( gߋ7珳[>uŸ.L^__[zԙ"P_4uiqOUU]~+gInZAi.oJ*_?,=5dK_e,SO>̱c`+_4wo߼ }#g}r?q㐩F#`Ff}(7d/xD"mn]!Mi8N#2*kߋت_|b3kƪQ@ Sm6\ZL&86?=oUK[FTYsB*?VUsDwn޸45>s/|cph^{:$ (J7lrෂScGÜ2=ff.:rm7 bYǕKGaXu{hdۼoTB(0:nJҝ7(_gVw6f/?y5|cIUP\[@Pg4 ӏ?qɓ/D2x?Y7$X_$#jNa"tࡷ|~tm.]),)imްF4"Db' 1aӎkϒ̽a"@&Xm~1M $BoN=~wHpn=iӓ7o\Khxr9Bs:4(^Z?232|>tc/ue-j~W ̛5c; 2>649 ,dt(_.޸vߟhO=ʖ-4mvqr >x F8r,G}m:n6rU//i 禍e 4f ͪׯ]'x}hSO>J%63XF?'Y~')2$>cJX,/?'K~ p$f;ݑՕ+.|N7稱/8^2| $2Ȯ,$Æ;iZ_@}eY ~0b0bYI@U'{_xSP0j SL.K>oPzxdbCP|Jwt*9??7{ka~~ \Oy#Uc.&  R(ҷo];W3)c~֛4B >'64ߗvxP~YXyC)ZmKhߥ1 xѕ}~B!199~-e0Uf8T(J:|ՐiKPDχU=K>#641e4,+ǿXIi"" G;DEQ[l>][[z](Nδǹ~KƟoMӪ7ԞYYn_T&sWNhCYc>s_LU[Mjz}NWp%K`nfj–0&ZZ7TM+} ?N 4oraR!QIUH/AhS JCeG7,>:6=h7K<:zb8^̴'%%k+ ުJqi`l'WÈ ߮X"fMG?6C#JJЏfWK(-rKו,.-\޷o+a1i8o϶''D3EQzX)Nƴ2oc.9=:>~HPmor0a0v)* ԡ"С0XW/Yw̋͢1hd,0}[ P,MNM܀ȦYolmWKD["F>CH," sEgLժ|np~}Y|­ڮ`UK jAjڮNZ2IDATR @H3܋i\ש2B0 p5Ar@IU .@^/`rO8U'}g.mBrƀcc5M+~"! k[lvM JJ캽vsB< jsuj~5hSuX*jc I%2$l-5c?4d$C v7_[$%]qR1Rw d@7rbmmnu%~A}#h *0R $@4v|u>c#Tv8gplہH$DDHRl,`&  :3?73Cv] 裟 cm&!H dqMZ u͟W뮄ZSJSϗF&鈁H$R2וWe96_bB"z0-{{zwF{p/}Pț]y %U͗JZRF|` jeYD f#@&զ^72$ e`& e/((jW'|`0_+>|>_ϯ4hB1_(R!_|1i!2yQUI&24dPo C;~y\.9$LW?VewQ',b)L@ |_wԥ"@"Y 1>CLeFP< qh¢j(?Z8fHD"uHUJ0 l]ZNw'Ř$ ɉ0<Vc)n'H$2$w Q^|Q|za5`6mfK{ED5o@7֢RfBx3(O""@"YH#z{%/"?hJ[D" @"#[ _B Xi_fؚ7~#H$dH$[qM_2ynVK?`-2g2H$2$#M;;洨_~j,I$ z[!oM$ Ԙ`OVd;D2  I 4|K'EH$T't6ͺD"IEDDI$fgB"H$DD"H.D"PdH$DB H$ ED"H.D"PdH$DB H$ ED"H.D"PdH$DB H$ ED"H.D"PdH$DB H$ ED"H.D"PdH$DB H$ ED"H.D"PdH$DB H$ ED"H.D"P?TiGIENDB`simpy-simpy-a59c5bcfade4/docs/_static/simpy-logo-quad.png0000644000000000000000000040206713323151071021661 0ustar 00000000000000PNG  IHDRM IDATxyp$}}yԅ4w7&}(J$JӺeْlɲ-ea{1މؙN3[zG=:h[E%ͻl]Uo@%H%'"2~ ;+B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!X BȺ@*A!BHoپ) !l:6ۗ=!mX BzB!Q};ۿU i( !+%N!ty;++OyۯW^B!$%N!'y#ӶQ_}<7mpwB!/sB!D:}_d_t'~BPBٌt)NžEˋ!mgGEB! tB! U.S`WEt:xc,MB6B6EDi[s"ŭO=*ŋ4>j>i[} !0BzB4Iii_R4;,*fuyݗBH@N!("0Uaۢ^%%]9֋}o}Q Y'O+֩rUO!ˠ@' =k%ei%唾X+ >*j0a6 ˮ4Lb Ӕi؆U0\C Jͦp\rg3?wnpgN]D p8X{X?[b_o*UKoi?'Bz tB!MR.s$ɺ._}.>:2h TdQfE6hTLgXn[nhS Ө!`A04 !0! i! WJ8p\ W8g@8ҕtHtA40pd[BW4Zm)eȹfәn.S33gܩ_8q6V#:F1bb̲-tBTU@ e@.m,iB002AEY~o'|Rҕp;R*.<]ʦ%!hIEHr6¶/IE4֥;!:!()=ap1CCFА+}Ҷ-ػccξ!Rl-l7L@) 40>K29 yXlz.+ )%ɵҕRI #:-98l|u/LS6_9{ YњjϢ=5v9OZ\mi>'Oڦ !( !&S\?wh3"v2Uw7GFmvYml*raI0(ȞniCZ!kM בmūEVS0+ON'fًͩbyg՗_ŕ牗%(ʣ։H>iݗBHBtP|h0l&Ё%.\webOetˈ-}zM쵫bea0tJYn 0JTM @vq%_Hh!,.v-_\| Ҁx+܊IӼ&{u]my#BHQ}/y"n?[l\e3ЏvYͲbbIJg)]7BQny\97בh.6L]NSr™VS\opxɇp ta l{f]7JY!M :!YJLo'\_;<4b7.7lq7 C,9⩵eM 人 +Nv[m%wӮ#-,s x~}=ܜW?0@*ڋr !d@N!$ iEyu5MzF~j;+QYw}nhߐqEf\crBT!P5,kx:Ĺna3G=*qRw2:ZBbѕū S sO?znfڙ>S4Pr@ڨry`.}mql#M:!4i_xːuϞh_7^54F''{pL7̫+qزŘ'V yƍ+Ӱf+v[nO-λOOm?vxe|sOO),qoVp~OX K')@'E<u~ru7T&_]92"wG}Պ능y]@vF8\.4vZ6튀gR&!zQ)6yYґg)θ"Ӗ'' :! >IKS}w{ #w`roh8odW+N{d=XΞ#ȦU%-\{s/E9vpm,^u^gϽz)ӏ6ha[h'i~D uBHCN!Qʽ"b<8Oy@#~bFC\iW~"[n#i?ny'~UJ*۽E? }L'%:!k@'#(O_nx9`=~TNad}=M"37eQI\qM 5L N:_c[/"VzpOqB@N! [5{P6|#m-vU1 %8 ue \MzY'Dtˋ4T/Z%.<:ŋS7q+g)\D(W=IG$ꄐB˽׸أJbCCuwVƍ>Ӏ)% _]bSV[Y>]礦DgJ78ڮss?ÅSa޷'֣\`)<? E:!k@'&k5UZgӷMj{|'|KK%Vq3.WY^vKZtu  u /:W$%MOϳo=srUO* yC r! :!t/a~Q to4m}Wok>4hRTa9C(桹X^(Fwͣy!h-:g.K?Glq]#H'+]dq#4otJM\YZX-W?S^.mEy|tˣfai 4ЙWݿ97X=Z7=謇 0NGUPBHwE#ؗ5wړwxphĺߪ`]#E;c;0xǽ^]@V5H-ݓwXU8&,gOLD\ERs|s@N}{?~$|]vUAW;Ⱂu tBYҔ-^9'w~FwbTxlלj0ky– G@̔7/o_,.b+׽`s#5H!D=Ri0cX2Ҡr~_?QANQ:!CZ?l<`W0)|/Zb\$u s7]=%λ5OloYʃ_2\󐴀X:Xpk Naqae{[ePBH1NXy]ooqNv; 5XΞ3@הz9]B6M9DZS?'C #O?}O`}N{@P'LޅBHr຤[6V{[1ĠTYΞ#Ȧq70WK s9tVĶI _m;A ~9BBBґW7[M }U֡9Fa\]"El}u>Y^8h˿wͯ=E<\HLæNY:!$T~anYusnG7}YiWN ,gk;&% 1arviFf7za-l5,C#! tBY!8r[nw>8zϻQm:lmRaB0M.]s5/SkMcA.`7kΞv^=F`SBb@'%F݌%969C[_Sպ8$]JY^<~5גF׃A60_|ɥw/9d6?DV<SovB&BoDyܼĻ?1{߰,1 ]G4q5r560WK0/Аyck9乶H bؤyK{Qȅ s) ٤PB6;Q}Fh/ʽv 4<2j' 78E}5w+%ixlB( !<)?Q?hp~CVvAxL{Q-W e kM%nyf!zqnagoŠH ο.ͨq !=:!d3W&%Խ׸)Ԭ>4>_a}Ү`ie8^e[@o1ĥ[Q:yIe s%z\Z TjbVO J n {@'ltv+iw&{Q^)bج$G\(5&Ғf m o_rȸcg:% mN*΃ө]yqq+a]rt{ʝwT|am) PB6"qX떇 3˯A!k-5ʹp\eM(%!Ews7[^,gb(v087ϙVkn5P+ɗ2@<:e"\W|?2*nwrͧp CNQHQӨy{;7ǿ8I7"{/]7,gOCY.u׵]S`T.`0LS0LK4 C@,/ZO/ܫ.`Z10drr͇`Xng:~NNH귗[XAcc;Zna^,gk)+ͥAN)vu:+r=0" ,.Jgq^g r9gwf梻0'[pܖt%]Hwi( =W$`j}5{XxfFYfH'd^'ys.""`=loy/58z[QN=%3(WdS`p֝~W_zuv\lϹZ\[¨7]3jV&Ơmmom0!8^zIx)3s-{vYlPBz,LGt tBzA'*qF{n? 1 #g9*%q5g9{s5_W54i9G|qa F-rEp]/o{։Z]Q  bϮًS/ zฤCbtBz tBH/f`9? l c1Gwס9 )̋4T8߀lR/=:=X-[%(Ѓg*͟uⵓs=0d E/5:v5}^s { ,}gB2{:BH@N% r{-pO'BltJHk촼2ke +IWF˒HGȨq}n=DžQ( !L8> jamCJnGQo0\I.rr5HL[1?VS^8tmCu]S{<W昇Qe.:yչ88jUN[ C$ Ɩ6}%'?h\9:ODZI.ͤ @민۵:LkS猟y/ @ru ܍+LR9d4'ۧ>\rPG2@OkE 'z՚j^nƏ_zuYu-D5b'=`| ڳiz&]Cy.#U^恸=h5-;9杔jnͯ}뵓itУO;~ ,gWs)ZIJ?zgzqdpv`I{k]sɢT?{7__\/9PКI!6h!%n:'3dYv[&C1HDq_Drh*J)̻ k/ԋU$)YWlTGT9Kp͵ s]#~ΜrN~Ź%Q&t7=i \O|y˼@s3<a̓bzp5AGτPBփ)Ղ7v 2k-l_\u#vCG .hHYu[,~ruEIS'r5;2,y;y\ᢄ{X4{ϺґiIK˴P|*51~ʯ+NG9Q} !!)ՂKgƶR[S茟)2v]зc\8~\0-Din]2"{朊+qZ-s]$qGM&#5'Rv̴ ]Fe8)[@n\r[?l-D{BBB&nvv"PI]w{c3n8<$QO Ɵ+bY[hJыny i] Kk=zfܴ;Jd-۵ko룓v? a Nb( !e7j>E_3Z;몛;iF=8y< zq@"i,(5sridyuBnͅ99eM{䘧]fa3H.!H];;PуGߺ꥝߭/wht VmyQMObthYh ynHmzCG`uZ@v"PB$IlG~m=#݅nr4gR%9SM@%λ(&q^0&#=Hu^[27 ^u,]_4 B^/Ѥc'peaz뼶D8[zs^8xq>hD :ʁmw[QuYKΛyV{%/$ unknQ/s5W_Jg@01L&3nB;x;nqusŒd}gӠ_qIWov=k PBtv`- o~S{b֭蚛gM|8WM0W&A)r bV s:{Rg<_ʾD__w]``ĜضÒ9L0{sa?Ap]OF6AfιqYJ27T?w5qaA( Q :!Dyy؜8x`pJX|M<4~2A T"!`\~'}9!Ir8O ?R#u%1N[bdBGx{'.wرJKYIQzd<@u;Jx=@kKݣK! !)@'&JǕ5'?D!Rstzyj(Z 8uu i[ʩS6uiK#̋d=-!ǔ֌=^Dɥ~Y z]8E#2?^a~Xz:@+…G؀qPBtIH_v:vw7πRaK\Eh2$~f.ZyHb1)[l0GpwXGFH5ۋ 4en]^"B|=Ҕy;+Φ 9!@'&K:9y׏n5M7]sbKWyjp|>^Ռr.9|tת7*]fJ|E4 vuT[ECe噛v"׼ G#';^R?fKs{"}K#,L%ߍ[*̉]ĝo}Véz u'a4~\JNHBE5Vtb|i>KMα9}⇢QRr_|-itM#@& IWU ro-XI#~ߧW*կ=;Up7Tn4v;aDž(+, +U>iխ* BPBt0hմj:~_cUyig]^rcVu:x `sxM|#|}U1zs%e {bn\ tBN9χ-ضiJ yԘ \}ha^8'T0W gt\\u:Qӎsb9.esˣ[]7ӼS2ヿ@!j5cױj4PQi9;!) q$QJg1~!=E\P|<\:ÍR4,EwrhKߴď (nre6aϽtd'xEȫGRz=~}-onflt\ 24W=$Pk*U'c)} aBœ PBpC7}֖"際g Tu\ s񃉴) ָ㇆Q!JMm5\zIy+".LyJGo7]Yy۽+隫ʪ- X6Ơ9r30<> !$脐"$3Of}q"Mw--]7I3n.p.na1?FwͻFl)$aϿ|5 $Qfskn!R{n}*~TE; I3)#F#m֠(OSNaNH ( !y RC K'MwV.UFn!7Ey vOdcnrh!1Kh|);6zj }>֑+OV#$M&-TPX}c 7Q]p@#S_z:!PB?04 my[Y[K薫MD\EtU5| }\ѭ5m8"-l(,m7m'8xZ]pڙ~TE;s0 SX?PX-y}P @NC{U50 TMaf1bM4](2s5_W]#37Xp% h \}S]گϠVA]*(&ch }we7[X6缻yІ,4Ogr}%+c؍mr3,s:{( J?oz[]wg]ٿ60`\%% e2"U][1vܟt}k]t>e#t BHV R_rX-Uh_l5oe<ODH M ʻ-OlD\K:_@!p"^iffO~՗/uP^ ! A! d=85Mw︿qЈx+\,@lM7DQy|j%y ]TC:&fI⿶S =hN"\-Aa,'jGkVe2AҖ&!`\9si[yfuywODyά:Wt"uα9}R`!41~ΤH z-[;¾9tB@'d!U㲫*.& \MѪ{ݮ4o3 ̹r`C19Qa|. WCEyt 3-@0>Xm- 6VƙI#Σh'脐Ĺ~nѾv`XrٓyC&W 7CZtqו9LtMnj>bf5bKApre=[a^\RHSY/I'$ tBHIseտ%gw,kuuϹF9)]ěg=5w/[n$ 7vf%9Js(5It*F/9`݈==y87lZ8:!$ iU [K~[F,My _8A vb4rt r#Õ늝a)8uuu#mCWO8g?X-~!^OLH@Gk6{K WuuCWOsV/F]\[˥%Mtik\[X]"-KsJ8@f\%wG脐(49zKh8,QcHs a^- 7c7ϫcG BrX+vd8rʮ-/īr~绶L:@&;- uH S:!$Ie76m1vSPSK0ג a9cMu y^k3~%g-?*_]+ʿV}tr|HV]{q$6jtNH 脐4T%s 8xghX,L7e:S$±u<$Qk={;\X(9Wr4$ (W=Eȃ?]K -Z}\K{u$j} cKQ%iuB65$FT_};k'vzS)4\wGKWle : YM4:y e T!4Ε+ͥڢ-@ח2p%Pv{/z"]tyu.uv B?b|5:2f^mU ۍi-Kp˵ nqN<&FvUz__2RƹMS SzdSQ !~JӂOMfߐU۹ߺ]jt%!rmRpT 1Jih49R7GGD^[umQy¹"08p\l( !QDwZk4vcKؽpC7@\:Ėna9GO A\e>̡0W#W.昧K&Q/=$6Jì5́PpH,q'{  FBM)5oON%~]W,9-:ëO'djRwؼykKs%PQ_[|IX(W'!= H78\NȦ:!#/X؈^eq rM <\yN]?.P_qJ䁸=wӜ1F=_j 9t*?4n3yt(Ohzm-#cdFqiD;:ٴPByj8z]&2l6[o 5[^,e %Z[AtCc\( 4j}r*I( {'Guά>ݢx !ER+$]ok?kvL;ki$f)ͣY쮮κN!328$g@D'&Is\qB㳻%'apK_6j ۏ% 65/&'~5W'tWCكJULP('jv[H}C:@6',^VkD{#a2ga3ȠHgO] $n*9X:8F2Ĺ"]b@-ս%IfuȔHg/<0 œ/7O|ciW;ݠc~r/0߈w¼ 15~p s:` O+‰O;gQk}t !ڌvgL{pF*j4EQT)*"  E!EFFCELm]Ka,k^ʔ 81qF3Lqc Έ #}Op ں;2|,s|h},KikWDZ4axJNd8XC :M<^&*#H6-,v!0o ۯ>Jhqחkk4/WK<2Z^YTEo42d2%2"I#RQ(eAzLWj?q b|}s؆HĄGs=sO^e{A~Uy@QN$e##TGXߐ ?Q>8gfCߕÕg- atNnN"}S8u= Zf7ee mCL=t?6k|3"%$$H)ɰNh+L.={->^0Q`2lȨQ9w1Џۭ7׸ .8OsGƘ?9Fwc|t|̲|`qΉ 7׌:ov&J\]j7-m&`@eJwI{הm>`+^`׼K|=%ZMzMVT}q>ZzTͿ|&^< WϢUHJIr#ZZv26ZOVY#KbDg*$v=+gvQ A1%IЛ=f>laNT8Ta^H_edW)%W+.γ`C:m t=9LN̈ aA9UO _x_e9[m5tyt!]"!E%Y[[V/'W_~xzz<˚:^3H.,&'-unlkzHIJꍏÇqY6~x0H윚clEXsK?]6E]MT.-j?җ D$(*1wAIIt";cD#"bqpN'ғGZ2Zθ4mdٗ$mܵDꘓ] W驺s$#"zyյ_m" Ӈ{NcJ*63{X-Q_].-7dD$E/K8:Ko:Gç$ G}?xތ_[+_]/>m<|phQ&%5%w ȈӉ@曆Egu t>G܃/çq^>c!ݎ2u˷Co iWO]?΃Ur.tE  /TP"["E0[\6=/niy7cǂ\*F]sׯh[][uԱ؎6%5 q6nn#mݶL(:Yyީ@B[dʥO~9w z@blԴ:)eŷI_ C#KovTJ7% s/`T)(Wޕ7??% p;MU'mY&:G[m게>wӥWQ9q4Rߝ!D?0$0d+kkT#8 :\[ IDATMQ "b\գy๽By3~k"b.5IGϾ"|zxW-r]ǂ<$MnLtjcHvzxnnD:D-KSo>VeX'׎M(Twphl0oTE2yWGڑ9Ig@`8q3"̛G+k{6$pbkA jԕkܻ~xuY.ӖȍLؖyZ8k䢛e'ݵgq% x#C۞>szi31!Ezu ):n^[tѠ$g[ZG :iBi9=KcSI54Q֐,̉Js ?w+z>i4)m"<irsu(Kn@ Xoz$Ϫs%η-:hj`ƈ vZ9_: &-LTo}mh ԦEYDR|"Zjƣ%JD!5 pے\Q6GdȊ洦IT"=i t!ȏ-/_o4g# M<9vyr~׼ D^F7:o_\,"  7Yc_㧼Hr}V96 ck^X\uq}inKYSu&]|yLV$m堛)6P| zoK\~z+oMNIb% Ke[ACnѣ'S9:G@6~e79Îy>(L鱟yrW0 n:x&*Uy>%YyxOԫo]&mź JvЉӮvݏ%ӎDtyrJ>5 sˈ_h'×WQp846!Ivao2]y}#E{_;QD r_$j!rcJ ^ %;uA,iCnyb5}h.ʉ<ݻx}'G,e'+jJN*Mu $ʻg!Mm=s;涴,z-?mnZzz쌷{3zq-gB0zP^8|I<颡y1_:Z 1i/'|?=)ž9t͡HkZPD]Dsk_G/^<ԚО7eZ{@ƶw"@'j,-ǻ5&O{Jsz@hoG.uuzxѽ.W9.1# SҶ-︿6o8'"\ѓ/Ʀ,w sA0lm%jIeԺvNNE .7Cy3~ ?ǓGǕ_<|*#S6xZsW{*ke:In|IR>)pxf5AA[yH[=_Vҵ_ț_~=y󏪧/>2G9'_5߈W¼f!ԫ/,|i> :(]T0C zm#B?e eh:y7y!Y\s";G_.hDu,c1jےts>ߧ8*bs..v꨷K)4;a#HO c3Qׄ9Ѻ8W;,tqA. T t}[ZKsmιk0lsˇM۲]usSm3y>} oj卝{ݕ QuDqޯHV_9DkKj߄Rxe(Or!ޞ6t䴷+(g|#~:5*گ?OkJ[6 8=5OKgwsrlsqt:. EM7Mq[1Zg5<:mIXhK{x~_||eo>c57Bw|xInSF$ {(N:C :Y\b3 a- =2 j6-o7vI<-# C~^ڏ=부I.e~ *t=҅sw0W2D9tөĥ~Dv?xF\fg|4S*Qg4zmp\q 4t/ &@z3.]]C 2~zF|6=6᷍./^ NF0.[Q~j9D}ڿh誟Igs]) i;Ns5uAP귪_gl&zh@3)%;5x;aFp!>u5%=~̴&̓Dy7A#mb.$Ҧ&dQ犈d‡_F/[ 踨U]+q>œh \PqÍ-kH?EH ts[wWqyߌ|gdwCvs@V_p:)+~ s""'z(kG+j V[Aof8\sJX&N>A<2]&Jl%Uq'r_q ĂJQ4@/W|uAza 0լ#j?~E[krݬa^<_ D:Hq`xHr͇ oli ؉a1)1ߦgmh_O^ںihfHfkjܸcg'^8رKL UӇDw-O Fl#Zx)_|Ok_ֹu{I׭ղiuN`(@`8Iovi߱O6nyt-M spJ*s#KjZR{ eMeOsXmy,e_7YH憛, O9xoB]ݾ޿}=|_׿*8~m #{]/%—6HlpӎiGX\TDtS0aNdb)D/E߭" 7kޞej/sjVgOZ@q͖ܵ tI[EcQ2vM3m jvuW}Ϯv|u&^FFN]$nI/#ƉdHg?Ok?96}23]څ6^90I@%ߪ~e^q"¼xT͎)70n9MdDש9Lq7]us@k'A@:+c0FW׼<5D ~Y?+?_]QdOc7/N7\w0:t-}s?M_Bg׼,aF3Fe?XFͷ0 NY8OJ{[@rWIu']h]7z L{t'/> }Ixaʱ}ž|fdyQHj%]jR0o׋ͿRK?>ߠ)d+i;\tdD ͓ ?rN(["wr`k?]]%j۩%۪'U&˲ 亐O*$ӱpŸ.5שp:sB+ߍ߿:BGN;<矘棑T$6>==&-p7~õk<=y3 c.Pf&0rtGi4ҎB^A729疛sN4 ɯh+EbLtJ2q`0OLG]ߦ/cn:c;pų CBZtI MJ?/ݸV1֑޾S{ |{Lj҃ny cDݿOkY0h6q|Np䴘B\J"e޶\nejflˋj﵏_Ej>׹msK< l*n2rb#Cm7*g _斸%s#JSsz)]0Rbp+vHŸ=k&M;8"uIKY~Ácb bflzQ|FNs51"1R+Q4]+|ͼJ-Z휧?w=7 \t,@0$j`~D~nPl([nم&) ` Wl<>`MnT]Sm 궿.ʉ,NɞVs6EonnFM|p;z#" ;v<*?훚cR(jL?6c¼[{n>x0zA[:ץFҼ+ht @\ƉF ċM<)}h Ң|SZ`1Ou\ҭll۲)RȪ߆͜i57筻R}q_>ADu}!;M=͜z?{L~qa1_^T+wn4nf*{,Y0ǂLkO*iւs @0\ă7,:fG)3kMa^Z7="s7q0cDaCᣵZ-7=ϼK Nܜ'U뮺9_6]x7m jv,m˖ڪ\xp܎73lSN>-n|j_.KQk~KݽBw;΁+`:D"Qgc9Ɗa԰]Hg/l]hƈVWhgMry-ԒR:$I"]_W}brus޺>G]PhU7 %UvO2\]/~8xO>ܣ{sj>t7Kn~s=s(Kvǀs  \Tp|оb{TU*2[^d'}&{-F=,r(m9wxAyH.WDw3]?3j溙 t7Ļ6܃>傪]|bzM;-fm6oˢX3huUE rqq^.ο ^IX)8c)pˋyl Ui4G[3k60Š&H9HOjY/&*]uWw]7vܽhe^x8:LL]brjMOL167>EWƌ#7(-./sW겪.҂ɈhE())uAnsMa:&s,p&&4笚%xۯf KLa3vʈ'ZHɕގc@ u8WTbZʮp]lĸUw=o,%X^GDk\^wSbrdTU#b2J/@c6>#a)oYXT)67>qT*XUՕ(Wr \Gԫi .n$lw眻Ĺ ΍pnu&).(HroG:@sy)]ͥR{71 IDATDٶ "rЅm鬛mbrq 6'[o]un/zyƺ)ma.y~_Dl^s>XP bՠ|syą`{ąDŽcHBA2j(F* C u)#%X\V^Q]u-$ɖsE2J>*τf貇p jy؊hm%Z*.A)eMO7ۚx^[Jy,ytt[w[|kn禃ޞ4u\⠻.=!8@w]T0ߧ ht7vND,.kec(,6孇+2/W)}gd ,W]Lru\wcnN6rųfͼHz]H,zLn;c' ttk0٦:J@7~sLoAuWotaܒl[nAFeS >֟gU7tBO wv=%^nstv׺M,Kr l]<!Ӷum-m; \X\[}Qm-79qo? v#y7yj.MA@^%=MP% ;fY=w ,b6OZ{RPw t}vcl]Uד.^G@I@0 –9 'v@[B9dP%w#ۭl,b<csgttsp \ے\jNI۲,mE .Dy|>汦DҺGw-wT|kmV\͡,Mgn'mIsxMnIˤכE .Dчny}dnMv"bBĝ_Έ p}=˞$]q)e[Iben{ @`1Yğonh6Smt׶,b:㞶-u%ml# 6ERۦw覯\_J7wk7.cllœqf&7`DlA1{ze )4iמvA vq:yɲC@tt!1q2|H=*Mfv=F7;_<&Am{ G@9D'Рm3œhhG!fSX`{ȚVt.} x ,⏹aUy0Z3tWw0V`Õ"g߮y4phlH3MlN+{݆n.K7*6^l&~ Ip <)IlI4mC=RM`:I*$rw-YAp=0<5"Tر'"j.͇=mPaݜ7\Z7=$s_1'wT46z?.((t@*t5R0wTf/$&| M}uA]Ɉhd;rFuNmt@2l*m.IAjR0fu2,O; |aq1Avn u@`D$j*Gl.pj\Q!ҝ)ڹ#g>r tnٗT0B@$**D[F*{Ys(#rW@%FG+ m r]Dk  һn >9f?|Mqns]:D:@`QRۈTԠHIOOVVaq6j LvvWqX_wP4?_UuaSHw3;QHP&.RJeDYny$v˝!JpUjEuҼ>$ wo|6iW8h\9R &MRxDaކ8/+ve)7;i~Zl;`>pJi) :)AҞ7jaD5 e+5Ta^mr2Ǽ[œwe)^R;ܳ:h%C)6ZGŷ׼nK:^z mjI'˒"JkvQӖq!,?+~ۓ|O7ѽnVf\!Hc/! BkC[IJ?_[{ Z.QTBB L tO[<~D+EһgŁ({¼ EvTRwe P)ULލ^˟{ZJ{c0@0:Q\YA;8=ˏ)kM Hc/35\c؅~DAk|߹i^#)%MLqoJoX3+gvS'~Lgo5t"}c^kϢ$mv+`@`0II)!/ry5moФ׬FC'Q. % qtWhsEtQu/`*ʉ ]v~%vuTJ7e.則*".ț\979<|H?zJ;Yynf$Kzt3]f|6dYny)] [L1Fld'>~S~YQwӓ楓q>/=$Mq=ȯrʛ 0/(5܀0}D^'&KW+ړ"5?̑цR@`0ImWo(#|ZnAM!w`;/l^u:tҿ0~ƣ7[%=  &.o3SN_;6g[e!(k^T'%u5 \)"S;gf8T8iu Ĥ9:Is]@0$nƵMW*oOm~v Q^H"✉IcUص_>ec۲`\)($9 &Yn Bq.{gf1wb+5D? s#~_ zF9l@>7=o3_jQtWhHmO="N#Ns\g0o 9 y0o:LqNAe_<}!ç{e3oA98r쇣8 &iX yssμĞn׼=s!'04vNx:98;?{#"j`TI@0梚"UŽE# |gp[A+wxe g/݌օ.fsw@`pqUSiq@7Sv9xWxr{8B (o OL{'Z=/i8ơaZLΕ]mU @\jZ]wV*)oqa݅V_}(>Z/"l2:|Y]9mU{$§P tn'UrAX#uկ>Tv<~}kIe41b^O/] N݇N+,@@`pIl tsk൱ /ϋ,q0<*S0O Qk#?S{?}dO_E+:h1k9W8Bf \\4ޱs,Z0/!9 0Oƨ2>} ,s=zFDO]omw`@`pIrK"'"of9z{1@}P͐He/$Ј"^"<661ɯ'uGᗴ;6qeZ!9s46t[]xub[^@|g.t#sSjfEny17 y&gw/‡?/uֹInzZ;u \1}@tmֈȓĥAy|k. RB|[ezmTDy4Wع]{?t%8<:A_EKbr+ۜu2ܱݶ4)vd&m)us.VNLL}2)[GK8gy1o#E[osC_y=5=o.m_ۄy$ʓ :+=&]=v}%;rJ};dޑΞ঳yS]ȴpu v9k|yUHBKHRTNf@N lrc'"O)⓳|yYz[Qjs*$~G(Ẻ*sOK;v~ŷ*GƧh/\_•MuOs&mmSKrk wP6Twγt1:FO_Ѵm~R1R9(V8ŜEg'gׯ|Ι˕)EZ=ז7ŶU7mSn.m):tbqz IDATwDJq 1#}\sKn n9ٝqܝV`*GGNwlaRa'uH.-1_Pp䜛W r[] Q=*S$7G|#n :\xu*cq58W{_D/h|ߺAcnKOj INYU$Iˈة(h)׏yNPN)ηKl+sv{!;o_ʡSB.>eOkHEIjrDv,: -LC5~5r; {Sc 0/rB5 K?%㌍ 9}Y|%rf-/_y'ۖ:e  Is;M\z塓ޞ ƈm:{?=*C @:{)E4<'՝>9~#^N8O=rUOHz#>smztG=) vungtu=^Ə@JS#S3`;/kY24<:t׼)"I$vhd]=r2T젹hieFOIFH]Pۜv"Pvt:ÃKlIBț)F.y!`NguA^lG}% Rd:{J?c4߼vrkNŰ^_5j>'%iDnztOD^m‹_ e:`yg;꫟]s./vhrӽŷN8ΣFz-VO<_5G=tw+@0<$ (gA3Ho ,宸9w&e_I:`Y<656ߞ~u7ީsPվ$z:rT(pw' V$ۄy5ADHص;x?'ek]q?Y%j1[y\ͳRPD2"yA+읙{߫{0AͅDs Y,'v5/2 Gy (Fޚ~P)[w_~;]wx[^ k/ v5 nU-@0\Di\ts$qh|t!K PNmMg:hЅy&ۙΞwTvWG{{s|pjPe'*I:geq槻;(tgTw^[SjA1F%!Ig}x:/&~)]w%wIgV\U#̬GK/Xxz4OD5Z躧mχ=]%! :Åk*D$"U 1p=ncw!}uhi}<|/=?W2Dy""bTuEZ-ey;,Z9 h t,-Al얻 yTk~vsU[͂}]i3@3=٘y4T٩(yß72mUOD@,p :ÇMuGtŭ73;~*5Qab\Aդԋ9N E .;02ƾrW#Uyo~IV68.]7%:@0|9[E4KLga3HϤ[y!zbN .*8gS_w3+gV*=}(׈(sQr6N6&@0|$`\,=cB.g*o; 8e(;S0O숱Regv8xػx?2޻"UwSq 9tyuM@0K* "VTcf7۳;ŦL<%hϊr: 8r< ַUvpbs.V<{ |D+u I[qY ˹j"@`XIJ40Suq?= 8{/[%ѳ;XeWO_~[ x\H|N;  /IŏlP( D,s81( G9ۏsW!;><ъ L0w3+w5ZօJ"9Jm'G~C:Ëm irAr uHYV,WY¼9ktjޏz_:BgvwIFcKyZ?n+i!`H@`q 9م)uSN.1blVo L/MAQihGJ1}bJщ)hZgOQLt,ei䴘yoD[I)]w%wΎ!M"aW `W{?Gl:6m^3OBt@;=y:XP|ϕTT暛ms nl>ۜI3=A"&#gGFXm>!5flb;0'J?JS)"FĹ`A_yP =S'ơq;{zBq>0@0ܸ6nm> "oᥬ%{ }5׼K"HgnO;aW.z'竕+j.:2H{n'C@pK`2a:Rb!q"X-;v((LYny m(7cH"bLTGɩiCǽ^V!MWmh'r xrl#K;}: BMxbN.;u&rbҬ9Ƿz8W~' *|wPeo9(޽vpz|{rF brI)IElSv=: 0Qta^}PsT(5ʰhI1o ׼rC{{c^ׯǦ٣GOF .&9D;\v=:(`=ҕ.?P__*|qBFv^\G}嚷g3ɑy~2.X/ũ׃"hIM2=<,4TLHoި+s{;AD=%z&5 qs*$~M)dX kP(co+wt0gX}r?|U*_شo'1 1i<t7#"^řh\[V'͹9 MYU3K3.䧿%."-&Brc@bqЭzçs{o[tѠ \May-"fpsqD\PPF7R})U߉\&aM@b\8s  %:'"jO'&t[Eat]s<~o9~WZBШб 7 [xhe⳥KUL"tNbqI.ʲjRGOgU@swp;t:tA(*g'gwO]7/9-䒪مzbrp`@ ̂EBF8oD$֖ڱsI>EG:~ A^L-YΞ3HEqBǧg_8Q[V[b[5DTF'+_gV?{o˒"2k!yH}VG#gݶg^  ~`b՞ېe -V<\2UY_D.IV/P_dFUo/]$O$KK*aX,Vb@gXXi .Ktח⭷~P{¬|.W̸ڝr"9r3b!uqބ~㻵wƒ֣V "k'ufOa1X,Vb@gXXiI\S}\3^Z*nNjV{hσ  ]Pl!TTC潦_IՓGzYߥkuqkbRnV]Zn-&Ra1X,Vb@gXXiI,Z8m~7oLKrc4$he/,d 7KG5GjAMܞ(~k7ٌa >TvY,Āb(\u߭p;?Bȱ<Tc~ʵcgny`?:sWBYo/] Wެݸz[-v6Z-8u=OHs ⁨ŐbX%bQ*zIW;;^|-[>Zn9sͥ1T*f*<TT ,:rb~pj~7~5н+SD}b:r5JzMl~" d 0*AFofd~?Sϲ[Aq H5 y\>}^Nk:~SY,1X,Ns`<`{;nݸUq;}R'\Fq[ 0]}D]sgH2j5׃߬Z^6`K$GbHgXb@gX>eʘ%Y4N@w :y5;0/]Ξ1s6Y6$%L6ߺy|Sx= tٮcPgXb@gX>QN:yF|wׂ2Sx:{عC#k5/7w_H11=+~㝚K w:6)ObY,V\.c8`m9zS~R0E+ Qo&P\ w7:x ܰ(gGbOwۍm{;ѐp wSF'9=j#< #[%'Í3T rr(%@šTk߼ZRp/j="PqQgXb@gXY6ݵCozfIye͜:=w|+`1{J/S  l}g@Z[[9{{Ozַ Ysiu|Cecr:E1X" { #.G/ެM^ܵr2?.cZ-3~ny+HQL7[+uQ9@NNMצ\~qw2+ Ww,2XE=K3X tUTgaa /]mvf<[Z57ӠsAdُSӀ8cuq:CY{wĀb6;0:mǛo~7|cR'0N船#[%'Í @`#Y[NIffW:ֿVt{1 #̋SDnN/lm.F2_ s~v8rNAqԅeT4X}tA::!1Xa5J@Hho'޺FzcB4:cZUByny62:Pn0 ʇN6#`ke|]oerC<ۻ-[KSoC; IDAT?x?y{fs`8T'>:>Fu8gn3~$TC6wɣVZ wg].gX%X,ְJT郎uM{{F﹩=&牤o?Any2F"-ƼܣRS{e 3¯xI]^WgRu-ٺ$@@,? 6X קv1SI۩i:lS~}.r[/6o>-kd- 'EL@\JNJS"BȆh7zy'C&K>y9b,t5@%yƀ8 A PǀnzBG?zo}=pkk̝k~sߊk>cL@uUDټ[7ւa ^%fRg?BTRBlx_S}_.,D1X`@`'S=ynVpG|N_ f`09*`TCg5{掏60 `'k짬 Z]Ss R;/_Wrt(@ןtKzƮrl6nݺ5}⵩ f 2 /+ZVXݕ"@ʛ2 gHX0O=dj>//?i2e+eHg*:*Sxq֨] -pY9+ aXK)A)xjC9Vpxp߾xb_p_XĀb5 ".aɳ牫NzS?a7(-/,"T̈df?7`^ םinhN"_ny0 l}vI6׹+\u$vF{Ǐ%뢀W/x㍻߾ycW&&'n@ 5bF9%%I @~T } Oh_?A;`z@e]y`,1X2r,0PYTftף&/K쿜ۯo5[) 'Í3kͮBs לJAʼ 'C ioNlG q(^?:jOV;N=POQ ^a/_pɋׂ0ڨ e!BRȋD !u)@vw6ģ{ÆQJXoܖ-yt|kX,V b@gXe `ɳ=s=:_~v?/&KJ)~ `^J{$4p 7=9O# ε{ծyo˥a:+2s_ eģ\yzݸs:;u.J[ݟXDT+;H&T1!  $RJKdM!@^#R(kLtQ0ݻ%_* ̻#vὯ>30ܷ3Y ŀbV'.`T,Ma&C fx:бsG\nʿ ?b3B)f妓ids4t9uEܞ69gg] TW^[< _磟rK_=݊Ջ#~fssuPL'Iw2t$},L >i_bcUW\Y6Úh~#|S -/-~-1ny #R5/zPߤ"r}OROu{e oUX(IUjЍ5}!u|h[oAJt=hY@)|WdƠ5jn9W^;;;{]'¿yb:ŪJ *Cv ߮x4ٺz#h^4Ʃ >Rȹ6f_Wp\M ,q]rɯP+W k<8g6 ,v F-wRfvԉ~bL@*Sΐb,tUҀ+׾2kǠD^(/:['yJ V~8̎K ՟Y,V b@gXUuegLjzǪܼ^ ]/Q9OgtT#lRӧ:Bu2(kn<۶@vEy|y!<}Y,Q~4ݹRPվ3ya'@Cs1XCbU-۩+'ςxmyZ+ W匊!&Fl:{]5w6ًV.J0/r%=\PҚ^'MbAp}=;Zm!rq;.W=F_M8wUk:OR qar;;?|f(=6VbHg:ŪZy)xd%@画'w䕋QH1SGS,+Y,I+u==*G_T`e1Tw_^iL5b6p'Ẅ幫qpO:0mZu{pSIO}|nE cQ/0H2ݦHpo>' }̽2+ É ӳ?Yj5eT'3XŀbNZ>vmPaE}˕hRw^_EMVnpy*9]in*;vw@vlf@+^clۺ`9k iڟ:FW.Wt,{IᭋW+N)p'=W!!tuJ䵀5ҤbGpp05: a$'$WL`#Y[O% ļSَZ<fYyjy7Mdxj3}-{‣$(G|P_Z` ̻}=jY`ދ5w B@9/vv6{gbJ:: QYQ+O$|1yAͰ.}i 7`̰90O` ul'\7 k߀hv'NFys۰ .X <nkݙ(կlMgbH*)嫗swM; =xbe::-Q?rG}$';/ 7k7kM2Dtv:Og/%ȸ#5ZyMro|um!܆i 0 C-pJl .0w]'ٚΎ}RBoNMM/?{ B*OC:A ,4cxAI@|L.\޽'6^{&뎹P- tuڢ`۵Cmȗ+KynpejZ\ G\'\nyUa `iAvLOhF9{zsm*trsq\yj*;/gv,0H;WnyenOaq%gO 纓i/t+Y,(t©mG{+;kwoNM˩THg׼*0 6P%Mp{o"4Ӻpxtfv&WO^?wty? $sT۽>#/0Pgn[X6[>Gr~J)dsbᇛ u耞{1X)b@gX" ҩ:.)gxbw[,FWog.Y(FSCk^tO,\)!7da\"0`ܫufMRz~0S|5ju8oxkN2(Αxۭ2S#G S'}qDqu1XŀbFEUvDzwV kҰ:25`ދ_iU5Q0O 1R)*I GC-5\`*yt7Nn#sb61ݵWV9iE tkO,|x:4:SM-F/O0{G)dmvOޏG?{YkZ I 5/9O AMThӳ)r)TADTb62ϚNWv~IW ;^FၺW4U+h6mljw{{WیrfXJ ,kx>5V2z]n"TEh Fξkͮ掟@ mbxvIivA-si0h:7 ɃM|կz~0}Xp`0*3/ל*EtvYb@gX*g䀘<9a ijGptFxmbJ6JיήUI :snXB!F-L F9ھFB3#&6Ng1k^JfM,3 _`86'K@:_ܯqϖ[0Q8NTl(VLLNXju.Y,Y,֨ c\ :mK _-wVn -w6`>L圻[H{%0.7ô+ v%nnH$myA_8>Rg/†\k"PoSZx䪥tx4o59[ ͌cxbZµ0SSӧSiǙΕM;.bj{8໰eyY`ދUScFE k ŹZN%jQ3(1XY,ָH8^gAo1g:;y֜͢ "5Kj2wlUչg(7n6Unׅ>k$s\vs0K#QPڡ.b 5s̟$Mg/#Nu; b8:Q\VHgXgzb@gX$ga[6h{i>Zz33uxҰU|jṙ\`*]B`ZC6y3 Sfm#6qM?"ݯ&JOgv$S$.L\ך`rG-Uw)rliV7Qff>a6s#pob6/+m_b;9 @k?\:О5ՀyXúj8RZvdMmd7ҳ s/t54ePP=xHsgmVoܖW'/)!N̍$<5D`^8V- xxJ97q5\/.WN\vg]5v&fӿr==Y~5g x.uu03TY`ލ5dᜆq͇c+p ىރ.WP|b31Xq³ɠBF?wxj01{Y1}]SRQ8? ( IDAT\9d))nvYn1aیcߚZ\`nC&sz;hqӟSzٳ^k^(5;ۛCN dCcΰY,8kHϊY[ێT70w c.D bҍ,!Ǯ#嚻6q]: mv=5N|dbjH%f3SPmy4gWs9WСZ{{:{`ةХQ0Kq/b+ף`:b@gXgA>P !OsЍ!YXUkfeyњWP'W O ;>a}4a\f;΄i}'\ZmX Rǁ}bă6єyoz>׳Xefc[hL6pks3Y@+Yc:b@gXgA ew}{TmE;O:BΕ00Rv˳įjp>`:4cvMv;fۊ w2q{,90+x@#$_hPW甅F-I-K8wAAVn4/>&yNvXJ ,( ~1[N8-GxQgekM^3ftȉ潦_IR:ϳQ;b؍էf;H̆a3>^jm[LLp<rZ>N.s'sý"Q}ElǼ,0csAd7,go\/V l7ۻ`9~y`:b@gXgMxj|7$tvr[3̬ʚs0/)7lC;A늡M0Y߭2@P O%Cm҉`l8V^0)T_}O5kWX 8Di)E8lH!'gg:>< nXgR ,,|uşzO ݆ï cػxELNI!kq4s$PnyeaskZPNLNMԕ#&dI2g/U@Nw^4:gGY.8W ?[!9q &.^4޽9rг4:wb@gXg]ڵ]2I25<\_U֦ڸr3<5%&~;ny ^As o`Oe8uӔ r0G9+ZE֡Q))}?&| d~~T~[^UBRH!ep!VK 1o)uĀb΃\<']pp}!@mF;OONJyB 5B8Q0O Qk^861ܓ̟V4L$/. 'v\$Rmܓoi}{mhG㚗q0=0/#VY`I)aN뵣 ?JkY/zdf΄Y,yQ0wm@@v]|.=5:pb1pBAv:w:=VP `VEK@@M9:b@gXMQo0{ 1M| mGhEH3K:薗nnyr^[^ =&ro70k[ݾh@ǜj{͛djCdgDy Dtd7 ݮyY@ލ5d2>{t;{a>;} xS2 XY[;B5r'댈bGQNzZ}5or}1=;xBnin'7xJ{\\PMLoϖϯs{/jmYNw8'㖧rY_tŹn9܋bWetSWL9p1ȨhxʭZ!jY5/3\L!'عv\sG2j2aSSuuSmj0}E]pƣկ|T3kO-,0/B<' ðݥ/:Ng:.db11X. }< uN`EOڳjBɽ|sBi1SP `C07Bn]Sr\p4#& TǮ#u9ݎ3y<}Oڳ~}E9TwcJKSsj֨yN&Xk+k+?K ,ֹ::rr9~uS8:PGߴ_uV'/FsRN4&siD2ZkT0!ԝ;8xR )TN&/?N0  z]@uyC-{OeƩ5/g4]s)͍?W9qztb;1X,VWQoA zr]zR~YjZo%y) E9u˽!F-L IfM7 @OTF'ð%Ա`\MgЕ3u|G/ +Tj\7]_S+X HًN-ޔPj=so)w.%0u~ĀbX/5r(w9T<qty9 }A,Wid7 V_4rnP`ܱsn0/gA/N Xm^+CuQg쥠ٜZM2xW6w| r:&) (kN]t).7ט`r)99]}"˗v©),'X9]v}LeV`^xTA;;_}Wz &G`QFOi:b@gX,Zx,<( Z/⨧S';E1'7Kϭ€:b@gXlr9zk0As\ox`nV7"Z"h\ku 5pgYMLf9smL&ǸF`t݉}Ep?Pmgqq_ahpp>ط8 ^1<5 9nyMR 8<:˵euCtB3;,0X,VVmQ A4Poo{WGA]Y1D(ሺC;@T+)'XñK'G`)sԹΥW{o6չr-0.T 32 . Ţ®yY`d\b'S+Gϗ/>} ](ןY,Y,+HٮKg\vAx.KOm/=}Y, 9QPJ*0/Cݫm^cNAFYN)'M© g%f+dqn*9.W;lЦ;}n_ey7V9qJqS c+պ-R/EQ0<\.::ĀbXt5Uz&y}@rc@쩣{F/''1!ͦA>t\iU4i}͵-03hss9f \s}[O 3ɜ_=`W`ʃNu`g.W_~.-ǀ8dq s/tNkr̓i׸,rĀν/ZOW?sLNDr5ny?t:Dӡ:<ӸQY:hN G'fs`po.88A6۟% 2y߉&Y(/-O<_caϞO?}P=(P9uX)tNinzu<ϔNաNT}PNl{:kjC{zV6"L;L*aupYetgqSۋhGiPJ$3%uN︬s4ϗ~ݕ/`n^ckt^Qs˵;jXoruI)ag{g᳏?6 tbK1X,V9£rGSnz2[NgQn2g۫sgQON˩zSԅ$;p`;2t7N^\uyp^Q؎f6x|Gm*ݯg/\$tz@]V ܱS@]No.5 t?]Z\|ߝzs 7/ɰXgK ,UHxs섻SNM\@G/ZOHMMfP&\*] mp|Lr],ͦaN9QN󠕙0=Hd>֚zi&!>9&~D< @L\oݯ4S2]a;87^㾷Z/>VCW! Qϟvu_bٔM! \[ k#3k׼0/ =N;j2w)xrӏ? c8֠pyHX+t:YQ}A 5Htxg/ QZXz]  ŧ1aCP[0hTs?ZN&Ԕr*1̾r.0׍SmV5uUwc  p@],ګ"<{LeutvuGx}mm L8tj zZmA)UkzcB T0+1j)[>&0^vn3ԴspL5w:)NَcqdkWd~묻WSk>IW>AVIE ~WAu(?[`Z 1XWbXU{G,?ׁ==B5GYئKD@+k[Wnץ|r"J3C]R.ABg8.Hdcr887'*}ikҵm @ٯǠԱM_g>-Oˆ\n{sq?]8=g TwI:[bbFC>POiz$.=K2:^K2}P ݭxoy>ژ=>/ )idr. v2tp+Q0TVc7ܚ.OmF'gtw׹%}Seyn_G~vgXen8Di ck:\9e!o|޻?Vj5xze8p|bFK>bpZƯY'2nFe덏Uks=ڝ~~\XNNɉ +\ hxgbzm6heRkىer1hes+W70۱8gWDZ_5嚏(Uk~@]$NQE6]/Q9L%C;܉bFO9hи<;r]pt啂u g涶PB,!.^dz1Pk;t7\j| \cNAVj̘=@yA+4 {S}E}vt`ލUNjw`9q|M'GϞ̽7_~J<ž.YNZbXbX*E=ع%% 'vRxg N[ "FsB SkP6cdss:;xMum `Ni sb3$c0w+/wzXF( {Nq1N5F5=x?Yx0\ojw<,ֹRbNw_ IDAT[w5.t<hm9I䒇LN͕`NO WlD}ԍ;7믾~"gb9[dcӋrVB~} z]g& a0<877I sp'Ijng̪.F-^XaH)%ng^\KitSYV젳X,( cuLn|qӳNw_q:;[g::AjꂘRr{e{l$4ί*Z;Z7Ρ(ukP,s2>}șuN Ϣ[b ;v>0ϷSnyO-%@@ԉss2~rp~Xs-t?@E&O:`'pN+.Y jF7wt\0ޭO0AѻΞR7tH^pԚk*1u4 9`ޟ2O/mc9> l%bN!NE`^(Vv>50Ͻw!,?[T:,SY,$tO vqL庉T{i!Z܋h% !B0F3b˨[k)pܝvY׍5@ JbcCz]ofM+7$*q~ix:{8e8wl5jRJYyw:9t|K5_b8`YY,k꺔 0'r%jz0ymhu6DB`b2hHI%f2E`26jN#y f㶁p: `:SIҧ۟v擾p$fW<(euty8B_|_<-\v $3εEcX,) ֱhRР [xg!L`UV#Ovf0{ڵWnܽuJ)BSu u$/*C3{ϡLcqYuqS$QW{QYk̻JS:ܧ@Xg.mUa!(ۯc8IB8=!I| s/vY,) }&kMY.yqicg^)͉B !DwL5~r vo6cT?^9Ddb; zu \Uȝ&(U(@˝=rA>O>Y[5v}yt::-mTw} ^4@Q`a~Z&V e֤B֫R,,\j?r`sߋ]oA35ޙ]40)'(|N 8CG0^ qr!Lv*v.Ojܗ^~ ]Წ9ϓbζ|upT/z^~hcgO. l4Z.io+M9 %MuWoqY.7n f;8o׺q o@Ovt {zyqacv57?&!bqC~wn9ʮOgO볚\,K:b3P)@ ? ?`<>-l(ڍqKzЬ'(ڞ]7+.Gl'sס̹\ `N"~^NwxeBj&_?_W[8#N-/w1#* 뫫۟I3\LPO(ts#vY,EC5uz_dڻ+0g[뭗a#IRH)4[==ݛ pocL2G9Ժq v9|Eט:8fU1PW +{6[>$͏ TB8j k=Ae, bX̦R}Wr9_vvWb@)Ãpuh;O+d8H0Ls̙ a2^{[vp캧<0=؎CIQXObԀ̋*f%!DqOSdpԴv SGb?1X, Mץ3.K^%,9/LN;s|rmP(! z F8Cv#M'zSFBï7\=ZkBAѬd?Qq#gZ_uqOۇL( uϞN5/ {NrS6-݅u<ÀyͧЕ `w{kO>. ֒SNMm[[uY,X,u>Y':[i3'K'Ip9͕sMZ=~su+W'8Rf %fJ+PH0OՒκQ-vdܱ[Cy17O I)``__,?.lB{=I\!] nrYbbXYQ0\IA NwMWq:{K /n/>_^&&SA )۬ى\sO'N>vk!}|or쨛[蠕5n rcq+r\eT)F޷0֎sv}Z{.w9 tbQPODMuįqQ ZtjJfr}:v6>\~꣣x^a- @NpTv2Q7 ©TyG#]b.}wo50P"'Ֆ/~' IZ?c;ȸF3aXY,UDÀzx'tIʭɚ>KkսTnE핥{_-ϽX] A,@ jz="1h\i(ݿݾbfCg̋*}O?qb;)ϻF}TT t:Tùc=ǙX,tb +ҭӁ(WQ? )O^gz{chõŧ/"!D01h됫OMO\=rskNNS iXCi=׼ },cU );k^b`sv0RBbc}}M֞iwd(xf,+QbXY]su#&m<赞@'s%˓`+9ڕkfoܾ|ׯ߽y+F=xmFb6ӱ6Qߴ6voV;n}Mun9˝2~y@J)_䛯 N\O';z z]tEQ|4 ,VbX*Tt <Q:UJ8Ԡ2Ob2n=>d}hf!@!z V:ƼWכ tyuu2Wގ[2!t@]łA`(Xɇ<#HyrOt|K59wMoGpb:bVXׅxiNA̫L,Gunv:qg{s{;FV 0 JNg!>u2PM eyY`ދu@CfQk^8Np^Q2/ts-ӿZ.d'xڽur]`bJ:bNJY֪ؑ 5:̛XT ] 5\}(`[/6ݟ{l6F kAq2;'ᆋuN\\@Y_łnr%TOwۯTJpxyZoGpb )tbW%jI?e§&7ʬPJE;;{>}91YZ٬K) s_W'tvO գe],T+5OH ;;۫_|Bp-֞X ĀbXqP[).S^6vn@AXO4&A/ \e k}82P$m:\},rKXeyXtqnjA{Kvzp۟/?>48۳Lk\svYŀbXqUZ}佾^سoZ;QG!\{O//m]h6d,O"\5P^],Vp~1sŠ|~PZ(7qї>L?y8gbbXrp}V7~k+0p=Y{%>0Ȱb(tb5I,I7E\u={6>B (ꬭn޿v ðVtP/2:4Pkoόk~@U$qO}TWQF,msb/>b*c;<{5ŪP ,:{_u ԗ>y nho{ UÃmU$611f~3Q׼L=ł p ]Sܕ=5g|{{r*1"yM}/9C;UY,u.=s<(`5M4r1zQ1c6_z2x~iݭZ= &']kԓۥ[XCh} 8BB{ٳoRmȞ=w  9UY,uU4@UwMw@|OqY] 4nq_$A($őY4Үִ\[.gllmGΌL3ڥDR @h}Tuu}!3*#<=#<3#,;3#U]UOwlmGBDD\ 2Zq*ض {{;kV;x*缡*r`I : (#+c=^';[mשT׏zGޮOmsH[U/-1Yڭ[ݻCwXeB8zr99ttAirvʵu h9OâZIWtv},>GeBP(QgU:CN|TܤPF<fB%mZIl}o)jV-|sVk A# H$kzYIVUg+YAE*ϫAѨܻpmA.#Ri$;yߚ'M 歷DY]10`vVcSO~xE#z,"#PAD5;HEwH{a3^ǢkPw8::<[wFJqDrX-NN |ZUh4k7޻{gtd4WmˢIRp]57%zY֜,9&=,~<,o坏?{*4Cb<↶'{Á  Q^=nb5+ߢ6e"֭shǷU&ff\΍;Ӆyy\=3m΀TMVߵv5 gg=O7{ C#UٿOtAIOTd^$Ѣ{bVyϞȇkk׫hX.OΗT߻ NY=jUT5mQE,ݻXV :9{͹z.fA : tYe0-L7=`KXߺw ,ojjrβl)塷}GZ޹:WeCʕ TFmw|/Z÷T<|h2H#'PA{^vIhlhFgPwvZV]]zTٞu]7eE>rNUMhejŬ^ e5"g)U.^=yځ^yw1 : e'<r 6Q5(íݍŅB>3=sziWM5y ;emRM%抛6r{'b.dRYP'Pw#GiJ˲`wʽ[נ*uke ; =AAA=gOy'~hHG }U_{w<2J)Չ\9kRnR pw^1)K6E7ۈF^BRXרBם7Zם?#HAAGAlr? IDATk0kSr6+\'zccɩi'߸)6%ZY=j9wVBk{j}hWE<F9G  ل':ש(`'ˤxz}cscc~~q\.D%S#&L~y =dK7T}5%yG+~T9:څ&G7 AAMܵ겡*nJ5HRL. bS҇jx^x)O2]؊ڽoA[yΫ}` H@AGA 24^65|./'kwwg^ϟp]'NѷNUHcj/YjejK1 @b hWY1t#HF@AGA_hǝ'N=/m[[ۇG['N۶㨛ALK[%O*kR `;򝝝tQ]T=gGAށ  u"amYŜݖl]qӒȬ`jydV\a3&v\lmmxRbV(( 2S=@ "98oyO{xDybrjVgfwI izLTū 5U%?$Toj++,˂VWU K:oX=G  C**a*묠2hvO8u.fvѷyU+&Z9 MR57% b;v!~rJi ϽCTA"APAdPޮ]WYg~-Q\rg,X꫘ Zt_6aA#fIMt%g!PVV|;Eמ{d(aPAdM(~JHy9e&'&Gf G\/ rY4RNJ})`z^cޝo4UhWЃyyA {A:r2=< [>FO8+x?|SS VZ_jzRSa4plw}؆ ȐtAy`O w:]s SۘYYj`F(\֔'M^?v:t۫!A&  q߃I̢Gy^?{olݵlۘ˭V!nk'K+P xVF},}.RJv1uq+ 9XAGAGjh6a '.Z%)"bbwX}o6]Lz_!Xհ  :  L'ДLxizffi\]˽bSE;G!٤cLIG i+mommh{x;  qGA]6ܝB6N!GMh~/~ztxx,3^>*]:uY;79=~Rʉ~drr.Qr\r֩vA$##  9WxD _'*gWV!R? &\eVjSw(8 U!23  :  ,I%Vtvػɇ}|{_p-^xUZvb$RvS9%lۖUEˀY2 'CADd^k_E> XKm&\I;) Vvcnm*n*'_Cw~zCN|h~ :d # "CVMU÷yj0p]6\G}ޚΞɫў 9nf({(v#&5Jı%_e 0( !鼙y۟|sGZ_ڀ  M̞Ni|n W{h4n@Eb[udtAAT{VbUe֭?kl]Sj_x?/BmÂvE1z]j.m֮w,M`:9!5 zAtAATIzYMtz^̲p%I>Uԇ&Y]jS}Wnhb޹/ҥ lKZAge=~dtAAt`%]T= s m^WW~`YzUZ~cfboȱLK9 Rb]7&G]єSDM mbY O Ȁ{A8(z 4( so߯TX LWCl59q䴛(52E(D{P-Cl₸r 3UPXAGA$-:b'L m96K`/+~rc9&f'SʉYVa+Ҧj ]*!ŝ'xn # Iue.ɝmnJlDk״ZٚcV{i&&ZQ:} MDaG  H7g^%=xTÕ_>Ox1[}6ˎ+p (yq?]4#HYA1 dtv;z~]UvFCfc KzJ+P QDF* s+4J{_t,bM@uq3o   24uaOpe س3sm˶y"jf٩+n*&KMkHs9C5s,rG۷j|0LG XAGAy5.ڱtln_!0wm\VETrrŦҭU9eYԳΜj.-GϠ# -(:!4ίW1j4 KQ솔6! ܙ0JW[|Ϝ;e۶DaG]e(gPA1Y%=X~|}zZPJpgHM}XnV9&Ĝ6݀PsV57Mص'_n-Um_.%A :  Fz@#VG5LVM嘨dm8BlR̓yMV[SJ_?seh8Щpw8I  ݀wfC4QzضΜ;ˍREZ9Kq}31=={GH( tVï-{+W_A7U5U17eJ̣ LU͓ ]앺`gk4xΫ@IG>נ#  d({9W=J=Kl#Eu o]j]ha* BkZ^^>+vlրY HAAGA>,ㄳ߫SJu8]iKV B-](o WY>W{/Ne#666uٗ]m :gO)<bqbUE)uFypuhWEUDA :  "ЪC[ɹS_pg$Znd(NKyW>a<4%qݙry%gZ:[;o ;AAn!+MA]\Z<8删c\ E!'X1r 9lS,sO/<9pmzIew ]AAn+Dog)RLqZcpyW\3kB1Oi; rO,X>wth_]tt( t$t[ gfgl!-t6.캘k싴E懳Dztz˹禦Ohss!4TKG  H-:*˧\~ڶ,W\k gpv 5t~[J!ϟ\?{p%]g6xM:$AAnf ./<66v-xX-m*]6wR>8r}i42>ѽ߃5:w?DAAp|r_(\Ǒ9.4Nd&4^csʦY RdӘ%ߑ s">X}tK|l۞k{ h.Z VAq$=~˗^bX|ٌ `bz'KӇ)\ wn$;,s7N,V><uG0( t AWEON= n.7DRz$Z3?=gXp8;R(N͜s 7>|QOW+=x PA:t5=37|ln=7\kV̕"7)զrԃÔP n.wfdthidem*%l#( t TV*zZWr9|v~q+)=ˢMr:Wk65')jqOJ);;:6+wy[9/;ۖA :  ݂=g'bo -A_Z>yq~a5|M̉)9`1WJ)ǽLygZ_J痖X,xM:h  HUJmYg%snL\TMde(ɬ~VGusg͍ Kvu:AAn7=Sp)J^^̣ p8xi849Z؎{bbbѱ+wH=9/W~A9P6ɡ` Ïlr8Vσٹs.|"Vq=saw.]y8;?3öRyڝ۫x4w:B2GGtAҒVA^܂y=ŋ_X|yH,Ɇ[Q m +'O-/U+Bt^dpq̉## : IQgVqurq7n07; ֓sʬT˕6W/ɾ88:>ݏ 8ٹ (ɮ͓8 YLVG{Nyxpuǽ?(g[˒knĔ &@V.<پDܔꃜ{`[x\ze~Dak;ҁZXAtAXx=r>[[* Izxͩ|m;V sxk/,m;N1" =ή9-!wgSbo1OBZ5&G9? @>Εj_Aw< nd1JP #sGp9{O]?{sTև@"uzIA"V١6D8OxwʣOvEMUMAgf8;E wM!|b~po._>99l[kV'Ѓ |hljx*;dX:4_{)|;_̿=:>'q|۷YYWyק=D~mq_' }XM Z>}™n}FW+Z'2*r&R 97wa||陃{w? qrYE]  : qGR9?}z|o^wRQr| ΄eO ?u—_: V׶  +ף3:xr`b8kO<=ϓ>fZmjyZm"'##hNV@$mض=^(^X\8Q}Ayc ; (h#Q,_ZꥫgFvUޫTE_ ډ(Fv^~ݙ_p﹎;/<6˼K4WdU,/L,CbތN\ֲ|xbyQ~D׳Cemd`AAGGQWp'jwsg߸o}? j1jYG K3_[^^}bjB-|}t{8PEVm OU)D+.^y. _ևݔѴM[Q[~ IDAT-sWGFʿ67?}S) h Wߠ3T*>W-GGY7t̙_[Ub7WGq8{gy3+~8s<:rcl|mP ގ 1(!(eUs7~/pߗ<Zb*DE5~M)ntTzsjz⍫O]}%wmZVU8QW&/vYAx?°8^~[b"躐R#iCӖϔJ#o,.-<Cd}fdAAGGOEbΫKo=3˟|FĜ=>,s]B\~o9:WxGG[F#Rg?\ ;\Nى %yuvxWkrjmJr\cAjyHrU1ofu'R Er\^RA{WGtytPYt9Q/򳯟?;{yG9: 9+̇rJIvy۶Jҫ'Ϝ ,LІ_ݭ@, OY=~d=Ea~%D2yPA'/>sqIo"NUsqU,}99gV.{zjfnݣChz !_M2P#ȣgY_yӧm LJ8+W܆Sޝp ,OOMuɋKK'ԡ-lyY}.d} @,ZƮS] YAu+*rSO?w…f;vb!•DN*zNG"-VʇZތJշj 6ǻZOo}n#h=@ZyȎk4XAGDEjl")+8=5MBHQߘ4m÷mKTu eYyuܜ{szfk׮sF`kkhw^e]T}yv Vؑ,l<;$p/2+/_\>DӉyʬU ^)j)1P<hT7J'r:tN !Ȁ }fQ}旧s699@C}o]Zd7.w9k'0X5bbN╫ ӳ9'_\Czuvm9 ~$dϴHynĉ7n |o/XyE{P;::j: 9NMV:24X,ܔ^aت4]=dNG goJ7sRض4:>:vჿu/Ԝ7=tdAAGB${p5}ƥsO?s[呑);rwV-O'Ү('3.>:63fO<5162ݿ kق.+0ϼ(H/}W1q*J^x/FGF4sDUblpvHkcBm( CUs3nwE9BP~{“s cyA9+uj.?}ƋOb ϧϫ .#yh7uV׃/$'2p:IV}owo%< ;! orZ'arX-@Ң^5<9fkG''_x?m`gR-w]r?rHkZ9ňk 9 !Vտ[jО4 =& 6):"H& : qrΞPFpay闞vTz'[?Bkœ#HFAAGG&l< :9{vqo|T.n!L]<4Vj7).d!f"$g[lݘWrűhyv?^5uy/Z'( 9Oyބp_ԍgmT|XL_wJ~&ڔGzėr s%ͬDCRQ*ݻ£؀y~b A;]t(9uji=WrMJ94/Qӣl !:{Ty}nvKK'8wkogs|ydoɴJeo:Fɫc _&c=/_z_ l>Tͳ*Y\/+:e@ё/6|O;L$Ԗ]2Cm@)@^|zG?ڝuhNx&I2j .[| "^4bQXYALMM>-v}?|w$kn 9)E:&c\ZI;#,.0.,.ܟѼ&y-=۱'L:{/=z[O?xooBEyr.v¼}r?^ dT*3g#qϿm;M9<7!+9'sns,nsc9fa\/y{G;;;Ӏ(5c2#`Wz9^s<15 J;hMT9[2xBCw-LZ3iOtqi^z7e}eۭѲR<[ L9_\?E={$k7@sފמoX=B}ƄKWjyyrLy?޲8+Us51h4j?(A : 6:CRyrnJz'-A7`;};E~s%=Z%w]=7Z.]y?on=w>*TaڗW]W `+8?OzJU(Z.a's\^_Xf!_y>P]16ԬiXg'i'+ {0GV =|ה[e?AbAAG!"&nn鍷^S3Jw[Mt sg`8{ӽͷN>xȿl,ŋppݍ[-h!l~>圗Bt*(fI"2Q\ z0O:oضS]knJ *&"Y))10&91jP\!}|eYbBHyfW^|^xw>޹h~ݠ7D%Jg90]WH_a$Quvכǟ|Sg0_(>pjytegҗr9iD:Er+j)s def|F<\.˯~u·)p'LYj9'.BHa{{>lGpw~w?ԆU#謜,{{/ZN c,#nw? @իW/>wBuDZ !Ю!! 6U171ϊ6&ԆryvK1$qjj4w*pGМŽ͙E3Y= `Ar>\y_͹o\ \ms2 %(4%, #9::kcc3/ޯU_Y`{{{ݻ{\T_aΜ*_:o_BF !9ƄGh4.4%]aS9j{}'ͬs:)!PTn7.(8zP$ke~+o=0-b\srƙN~/sٮN՜eS2Bl(ڶs<2rvdtf?V*lxskGZBpYU'2a2iy8GƠU326T<\.:h -lb)@%X|3&`sH!sk>jP$*d*r@NWOb;#o9>KVa@<*+9:9a>>ES(<_(SNޮղDz̈2%C[-Һ(孷j΂Ǝ)P${ƎN÷9<ͧgf#fkN:ifbjnV" fqYp,\wޛRinnqڵ+uwݽUBWT΁\*2s l &1$'AEu\ξuQN) ^:O[5o;N% _ kzpɬ^V1N/b4CjZ?::#Az7l{O\=wͻӝr&jnJ[o#j)x~@8` 쓮>W.3'=uV}R=x{tΝ=hΊ g=R B^_η^0I<ή]_r'N\,}?Ě!Hߨw6ЗsSbn"+PQ1|DܔgƯYYɉNs~/GGw+Pr"dAAG ;睼n=979ēY\5˒ۅ7`ld`DqrK2$b.M2 99{qs?ݩ]{{??:q?V\xUOll]J?ol:o+߼epB~>=sĉNJ\EOoۯϨm9E297)զ\-X-+Gz77|ʆzReBUAt*7^~^[^BlLTφ49&΄:τlIqN˥/{kwO7>W*Wͽ[キMig/o,NeϪeT%_{4ey=~|p*_,s unn.{>]ΩHWZíKSbߍB0̯x?dpP/6h4M^{š#:I褝7t5W]h˹/ffbS L<l\֢ħ^|SEyZYzW?ާ!qĊ':%:1K2]Uys tt4?zSM\wsūk5G,28(!|}- i )є}ZNR;#LdE t+k^ ޲,Tۛy!+Pm=Z[-GLt?5O {o}孯ML7m۾Oc1ԪL/S9iw|D)x͉(MH8& (߭GMe˝B@(Ditɉ+{6.َ{[.X`ݕ( 9\,ꊘgEzsN973r?!^=uAAv^%=,7/,, uF}g<,ͷL+˰P8MOeD(GܩzmY!`QЛBΣcEcv˫>+V>ZkJFZ^TQרT*A$ؘHdE;ދ4Cp"I- Ȉ[,sNT?[̗9:\Z@r@?(N "N5vUMdj,SB-V̓eDz0&9<&v>G>U+k*ᬔ# Ct/[g.9yc-5:&{2qrZo3!扲;>h&6P%',B.rL,ғ{^56QmZrxTWneh}}Cnو޳9|, h~ll86V(J.s\ۙm{Xd&)˱#Ŏe "kF;>2%&r ujY)Q6-1of2%禪橄J=!Ш7*;w7 ;0%; AHέ3ocőgyKpvbeBhJ#t,bYe˂q<) RЧ&C6~Gu<<hx^^}n4:mhWJ8F^ۭywT4 IDAT[ǭ|ZڮZu--)Z[e9[JżK6>[Ǖs**$1q"cZzJ"ױ7R]E)PϊeY(9ę#nfy< J= JGBz ߣ@Mڭ_PEX,ijSb9 O(@ 9˲e[6?ȷШ7@))O/McW軮 ה}ܔHw1QY93RZC Kyjq'+2qGL ř]N o__l{=Ḻ[-dN뭉]1%zY1[=-"Tߛ7{l@@Z-h|ŒČ+nNK3wE2McBr:ZeM̏_sYCY-ofecfvBm*G|P>+mh^}+NUc6 A Y.;җ~uzfPv'4rnX01ڧ]jS/^X=TwwB{HY<ڠbeJSds+2%杁)ryRniZ̛%P3 5mB)lmm}~:"R5a P{& & = >o{CV-eDcJM䘒r,}OEOnJ˴MI 'RJy kT͸jy3+ 9MIySRLjyl8YGQG t_flN=wO,)5o0pei폘Rp_eBcnVM昨GWs49YsjY)r"= s3tCٻT-OeB6nB,˂*ID5 <(][EñbXzW(L/NfZY}jA> 噐|S.z&$<6nNY5J{$s^y=csHkcBl?:2%7'LH'ԦrRٸxcέZhVs#f28#yTgm]{T_|K[懳e=DŽPQayTwGMHPkdḌ 4JsBm2'o}*ɏ6&ԆryKܔz%>q'j5"1G!Gt1 OyCÒ.sr'{̞̘dYg 2V/z9\PKVk&ҙ<gW_aJλ*]I1)Q6cy3S 5ӣKBmZ̃ŶeA^^' O'{:#H@AGsn&\t=303e.?d[']D9܄'Ƚf"z"TuSZLM 'K"#T1)j9X/Rɽ{qlBRwxxjvrd A{y\<"@^|oOrLTBz&qNbޱ0ƤaBm*gż1J%:BѨWw>uqır28#yxr<=WϾo˲իf+ jNVZ1tNteΉdR%Gj9TuS2tdVW\/ڔ0vcBmhjfvsck11oGch>|4ύx27Qp# fЙWEwk~܅~u)SlPGRpg$˔w6x~d}97cJ7Bve,njdjn؅y2%"^[TA[Jy-6  :Gt2٤pGHBm* b8˄WgCM}XnfFγZ5s }M+l0ǤP˝>_4SB}DNGļ 1oL,Ԝ^]ފyZ ECih=<d@AGtΛ ^筇ҫ4=7Mo̞$iN}P11= ;s9i:a9FTNV_[L9sb*1lJ΁8R9@α9E_r{oͬ̈9s⟠scͲ,vIZsY\4#Ȁ zs{V;'W(I??.yMyt'!i:E%MWs3Y<ڠb5Vꊘ-P3&sWxg~V1є9&Rߧj]hJrNA,(Ѓ wK5\_ <#zM-ԡ9I{`*G8+uj,rs #lGb5_4Bm*'[bJN4?B-DjrC@V^{nƓt# :`BmÂfB%mBU7tEW< ~9&ڔw0&Ԇr9V^[=$ԽsN9h4/|fog-֐GtG6k;6k7|+bY'{ҠKbZw1O՝ J, }j7I+Ԧr4\ڰ;b.\`z9snBmZ̏{9]18x_ 5ӫ B 1O%@4jw>{]6IH5,PDob8[ '+{3S_B-'Bi5bXڔfiȹ뻳)17誘J!):n-L9gV[97%Nѝ7)ޢ[Uc%!rd@AGL g>ykb/QzOc}UڔW_M~XlbB5&iZޠr޵ iRUǮN+9YJީ昒>ɬxKOiRSda<&FwQ1/{#hJ(1B3PWlml|{w;PBY=n8vP֑A:.ts8ʼnWeYU>cb"81>~~0}} ^nluNՄXGmZarZ{և  㟒"@'Ԓ,k~7r;pR}3X>zLj@٧Q C|+`8-򖐏_iN/#r:{%&Pg~ "bm C/}Á?Coy^^zNm7t`ot2{'A:"@'\4]v[5?~ 9H˽f;hDZAyfkf(Ǻs~PcS b ]{׸9s$4v,(O>&b] Q3ΌAp ~ ^o0{`y^{ pA8([$Ha0\ƙx=i8R$XL0\&Wo/}?O åj#H1Pg j{(q0|r~`ͼZ4-yL@LeGAn>=_jAtFc X/-\p0:vu{^?`}~888ײ *OirM$ҍ:x&]:}޽[=|! {h>3 }Pch.gv>iG@+%_Bj,|+>T@͵JhU6wa@A~A` ~^nNaW Q&@vvԃ([ bXLlY̸݇ i#vI:4ncBj@u`/0ܜQT 4dWG>{5v^فtcv}-ha |/d~vN}tڭf&T|( ̓(."8Ϣǯ/^p.tDN"Cy,\^=zdik̽c5 1OBp '2s l2^h@8 yj/Ac j `W*-?y^`njwvۂMjtO *<dz^,Nwa8DN"-IΥVk\!?|poCNXύ!8{(7&oE|~˕-W.[3{˽>%O$K IDAT>^ɖ\?(]BX.rs1և1 `Aw:vuzy<{AG"i&0.*ɂA^v2USk/_k6ٽEpNPN1"@'&ů?DJ~:cN V>GB>X@mc\lX@W0ɀf:W&`k" |n&(Lnl٧~=l4N}WS~@$V:e 2X*0i9DN\v-,ݿ+a8O bED>|=653o ;M @%jS}O|IcP<[Dב0v:0ADЩa uěn]L(׫q8I!tI,4wqB? u hbo! Z}sF❮xF jibyd/K0O3b\|[t4`k ֐\ׁ`{EpoooC,aB u ]iklS%uҍ:&K7{.:{R4<7PځHZN>P$42a?Lα|2``nP{ͼH }jRNg4BV^qqfg{{8vbC !C6g] 7 S ;7io\Ory_lX5ƂrS8y֓W cgEgT['eMʒ@u e,>I%tiZ*`ʠ߹wq $k zhss3/q z\e8ǀ\`cN1Ԕ34yMj Kq=e}a,1+`o/I篱eEӿEtUnRoYY4rQmGFU]yE<="@'FOmt~Yp8Tاs.z`.j̓@m3s `s;L|PS%=_Dj[{gۯ^ Ʒ9B05T"hɠ'{fL?l'dd:sҍ:馊qrGU-t`n`P X}jLSj@3'07NJ ׭RV(@0:OAF=eT&ZMgʷΔv@$$mAmfU}3Dq"@'Tùhq8w1m":V؝5W07WB9;c8 S0ͽ&Z}_&7#,tvu4p΀p8~n݀1` UtѭdSuvUe}/A=(ޓHK/tM({@\p?[(G i ,kʱ|| 5VwI,me>i@:^ca 1;RZ S0*(aA<{yq@?z0ΗL{|[QS xtcEN銃9_rb[.oMjdyZ`<+ݜP[l S0 ͼ&рڮS^-W 5 Ӹ-c Ϸ6=G> zUOuø)CBڋX2(7t I$I7M 6 O* %w @>y|?xZ{}5Cu z҂p̯[5O|Og71<7߽LfgГ2<ǡ[w:M`PƗߚY4I#tM0\ra8_R*zg=x4󹁹ؘf3!'zN50TYa>j k" 1嵔G^X|@mw$y#`<8ǯG>:rX0ğu“ҾVH7V褛$-{+/RNv3UG >-܏Cf!˖-;}&ZhqK-L|~p/&܋ǁo^>GCw@+07GcyJ/ʖ' {7[o^S]9&t7L:딩^H"@'t\PVGhÒ8 ׿m70>kSE+$eee-S% I$Mnt= \.ٸbĆL`.id }yj̓08i\V0ߤzz^)axSuLv#_`>ʋϤ|e(/X>I~W;??<~9k56>¤h-0~٠)[n7`7O|K`:[޻z<~?9\vs!|=l]UnцD"t/ dZV{0ȌX}`5knΗ 7 ͽ&Zit^xctp`8]_5*[͊1h4o?{+8r~{Y8\6Ϣ"h$ :)~y=S~ 2/C0+}?l ??`s @mic%-kZXSy^\.lx$Xs]@}[aH9:iŸxyN }7TF1ss+/ -gXhS7̥Xp-} RL@j,|+>V5&yN:^ٓn0 IP0\y8$ :&Igl{q@mc(0OC8\ssF@l@mQ)o@70~^pOF0 _ ԩ\b`zLd`0{7Fcp..ޜϞGg> D N"-I,-?/>+̤߳|("2{yc½U*Ds<3C(O欋d9C9HOa]@:*ܫ;~Fv_xO{{{/`Ӣ{ x]6\>},DZnD?P^zαܼP+ l&?j<0ܜ[ |fSQys\\lk`~*Ps-2jkIDz 7{ yt4-+VluRk"'H@NYYnzݕ;˂`t 6ce3 @8o>0ɀPRX^P#9V >5Z>9W1gs޾},j{s}p_ww2! 'HBU9D:馈xhܹ{o JSȏf d[j9jΞƙLiC{9"PC^W-Z*'x1`'vFϜנ:ՂpGzD"DNZf=#8fk{Q(p8$`ff^5ɂq \e87bsvsq@` yM@jL6 8_bAG ) 87;ټ- gr͹i$i&"@'4v}po}}}{ '#2|qd(qBa4 -P2|<-yLs48Ogd`~2#Ƅ|=t!t:}0s+azx0wD"e"tҲKb;9秷=xaR`4=-/'Tc5ɂqteD/ 0ds8_`07NJ ׭|0O| 𧳏\bԂV('#0 ðyqcOaWeeלAI$ Ue㞣& 9Zs| ܬ/|(qB6۱CyTr}+[us,(O#ol1e&{| v'<]wa8~18vj]&H$YՕ>PKU@=ԝPc,`ܢb!\@+%_@j,96̯Za5 %0lw[0k~sѢppT#8'H"@'-D~A6GXu4P+d8$_`9m07 Uc^#2|,07Fj~LylO Leh7;<11+`t09u=TQ7^H$R"@'-d7+A]vs{.3Ј@8o>0Pe!{ayZ0O07o?XYZ ̯ZeX1NJC([9/'ZwD",ENZV, 'VWWcIH `n ;#5O^+|!\8+%_H>s#63i2ZeԳs=[=/^ Hʱk@41'_`>ʋϤ|Zc%o<[0D]LzE`N"H3:i$Z .zù(=x\վ84j PwS0l0O܀|,07w Szo 6ie*Z`uF>~ss:Ao| ϚGrI$)Aѳuw'㺅{UMZ07VTcA`o`v-Ʀ~_7=MgW|@=[8s BpTPHʒܰ$+t2fz l֭ZGpNgj̓@mAZ=2ms0W3/G83qJ~ᓞh:/eF@=[0o=G5 1\{͗uD"ENZ&1pת0`4 8op?I >^yr, ^3r5Y$떨pS)[g luL4L4m]gP>s47]kΓs<{>P,>vd( fv>iG8+%_i>s!Lj0jPeI5l*vI$\ENZv\*wnAT*{Oh5Wف0&PcA8$-Pcnl`nyq>S-Ѐھ/]tp;KHp5G1 _8`n-Glkq$`'H,eu{Y]ѵ f ^v0,P{e)&р 2; [p2GNpn^Q=kFG}сtDʕI$Eu.8+o6qmI>> 6 O&n1|^WJ0n X>ys8GsJ"nڹyVUl<6HO"#t2Ht5zsP({xgB 2ggXPnkǺi\{c޴po/|S4* ʗk8,(Q4aXÄwp*H灝D"Me=WT*UoLOo|2VT f^3 ;M @%~jS}O|IU ]#c89̅!8[[2DʭI,E+čr>3`,jL2s̓ snP GԳsI\ (rDUqaZt O%HYNEg|w~0>mm>r>FѨC+ݜ r 2j}L\@ 2?MgCr>{\㺮نH$)"@'-Tן_/wlfZb(GXP=,0 fi礪@)*% G^y4Zq/Ƶ蹂9W6D+n?XQ(xuDI*nemmo |-k+ !ri$Vd 杔GXza4&܋a?^v(_lyUPW f%L fL"H!tҲn?nM  1OBp{=\Q~_35ɂqg*a.S8_`07gUJZM{g  7-dpL C`z%wDZ AIVW>v A_\yj㐼8 2sd)R9Zuaf IDAT}_&%0gpzn>X`nRX%H:i%[?us?;B> 1OBp ̭6 ͽ5O H-RuK,48Og/̯ZZ :#Os yBJQ]؀D"r-tҢIjI9~׎S18op?d˧+3r d ^XPnw6C=I‡{ Y-g-SbB91u5a\"H$DNZɦ; ΢ڏ B9pn<>I9I3s/,0 n6i> @wX`~*#\0l ^v`n]0uWZ4L H$)"@'-DZϞRX~}@u`XY󙂹V@B(m`: Ru Tr⃕5sAck8G 1|evpܩETtDʽIˠp{w A>CԘ? kn-YXaǼi}^2s3h4ǔRNgycev %Z3cfTA]z).z9DZIETSܣ׀Rq hP^ ' ^pޏg*'qLgOeuV ?`}ފN늮='H:i%u(_(Cϓoec=@P> >f^)aZ<el4O)!jl |$=;i9B0VXIx&7gՑH$R.ENZFɦj}e0 D3ss @飇 ɛO2phpNru_G X^قqVV:V/*)h:{$Ef>\cL0oj{ ԮN{ab_NI$ŠhR^-"qbX}f@`2cy`.0۬ІC~KNh5)I4]{Ql\ ?[0Nw, GwbqUlт$KYkE;w$ As@115V{p ,me>L>vX½Eh^c5n #W`UP>[ WD?<3Jt ƀ.\tt+QkWr=Ѩ XF@lX`.(,Oic%g*;O⿃}aLy97sE@mӟ[pWA0 i8;EkыbR.}7^_ 6 OX ͽ^WJ0n X>W| 0UF@ z~i:! gp,q81p ⪤jz;9DʥIˠܯA/ e-ܾn=PcA8$c:d%ky|2s3h@mߗɮ=kv^/jl0Oj,xs`[p׮2kDʝIj $jTBf^E;kSTcyJh4F@,C4^Mg}a <orleu#H .tҢ\=zW7% 8vq <1$/rq9b!H‚@>bS x-e|䕏ۥ4;cѠ0+UA@ѴwN"&t"Js!3VyW9m07 UV9PRX^P#y~#k̳s3/N\|0*XPNa|[ŝD"r%t"Hu[tQ)U6[Y fB;0J Ԙ>̯[X(63ib f^5&'a9WZIw C9r **ku;E:tV}Fm̵]4҂̓1&5TsnIlҀyU*ق$b.?0LcPT)+WNI$RDNZTɦ_OzX:kTB04a! .0czef^njLY|Z%lG*5[sj, Tc9W#qJnt2H\ҹ"qEtҵڜ~0 4S(Otqf>S-_\I xdʖbV@9&ʭ7la ʵFN sJe/i0W:9DʍI(Yp* 75 57gX0>i:W&`n5/,}pɔ-W|@bﰀs}WKޣ]"}~$)"@'-dgUg'xc({at9;c9ޗi\/MӂDK,0#5Ϥ|eF@ ~X`L  fx`> H:㸮[cI$RDNʻLIC} s&@\>iG8+%_i>#/l02ق"b.5C]0}'9:]\I$RENZFI@L'Ӡ9<8_607߀<*07h} H>K5]XKzj,T =,>\;9S"A'"H$MUIS2.[1 ,g X =mg̻1yK p PnTM̊t b\׭z|X>?] I$\ENZd%Ni링󼁹挀؜ ڢ"S07߀<*307Fj~LyQ\a04jW`Pm>XDZ0 qYRƣ2H'H܈tS`r8)xad9+9-cP'3}07o?2U@~X`ntebx@3&{H<~SfB(,"D"Y=@9.Pce:œU 0ǽ>c;5["*Pc G^y4Ps2فY#,yPOWc@~c/4FT N/2H˙wH$MaLCcGs«4M>! BP7GX("8O"%O`yί.!L=O8Uj!hh l5(h'4*3̸J@]gFAa#KRI(N"HeIS|JrO4l~9o@w1|̰C^`Ti=&Z!E^z8>&i5Z{;GX>b; 6 T;aϒ{08n EY6HȒPymwV+)@d03]i0j,فyyr~kcu}cֽo/WEG`侥i `Cټ/yCޘ%i>5^D [*pBq8DʍIyWtf[TPοu//AP(T khx (c}Wv6n7fL&L~`]Zރ<^__sF>Fƭ G^Vk ōsV^q||\L Z>cteN]uN|e)$)W"@'-cӽ[߇P1 z& 9c ?..g/^31 oy/'ܾsʭBT-JB 1'/`}'dVH <̀1v00 )ۿ߷?i: %~ic~=&Z 0:/SxYȕH*^t {.}]Z_~c[Ṭۥ5ώ^Mg€|9Pc%7%G08 z^wJRT.RVWnZ]VV׫jP(O~tC-ycR9g 梈߲ Nռ8m6[N|o o0x zai;Gk+o߽].*%Ui7q@so9V8nq |@ ]'H:iY8ǁ=8::88>:APiUV, czW F1N}7ن {0 t(R|@ϜݍŸp0pΎ뺮㺮Xamm}u}֭JRp EƘu1Ff *gYs,0@hXט_d^zGWAAA ~ݺIZ]fh}iUZOx{,8P(`:Cn+ʪHHg1k~ίo)JL3icx5c o~40ُ~:=˦2Y=|wPX}{}mmzRB\8$|G^y4ZYù!&GŌ1p`< ~ el\^\6. o0+=-8qϏv||Jrzڻ6'|0,0O, UHΖSD"VE?Pv3Q4+#Xp`+dA/>A c8 á? `0t;voVmzkK1lz=!~o~ޣGcHnìZ&cQY:9DIP](ZϞ`Aճ޿{ZA85P3g v=OEYV>q_={wo^ z0Βq@gE)suU>Fqh|vzxp?o눲0e H(lDBoNkzVkAw0^I0tZ8wנIvލ?wBWUWūW'PAiu{vouVnp$o8 @bHEQxcOώ>?wO9cK;8j#Pkyŕ!0D"-I ~'8 ܷ_|\[[( h3s ȣo'?^8k>1!=qPn-T `AW`ί!`\~8k p\ZY0쀾82Z4nyI? C?Uo Ɠ\u!g : x:ɋg8;;J   G@RA:jl:D"];@"`FE$QӓJv1;s @m9ћ>ۭ@;~ϕ 8Q=G0B|5+ZU^]T|յ۠9Og ̯Z"Gk7ᛯOˋ30 0 0ۏ.':I7 IDATdQO*@Al\*++l̀GX`fp <%1h7[66AolDI$JqH-gU_z]ogogseeru]q1cjKE(`o~/~ p>G t]4 gl]^ I| lwڗ~X,˿y<0y#W |ݝOk 2N>D' Th/f0tg{RԪSpU?FLʾe#@N"TE)2h,/wv6X]__.WF_r ,t<. =~=L{* gw`D' [(wMdssD [ns*4zO9\gȼQ^MʓfNnC蝜v;Z^T$a-7Pҡ1VH4ݝD"MEI]5]PWUgg'gˋwJ|hr LG [8v/=_^~, Ї2粌ye|Bs/~7P >++x`>ʉOγ^rqv}a@hzyttQ}0e(* ߉ZN}qy8,JZ~w+L>-Ws1h[_ʠH:i$ʢ߫]5M~jGGkk5qn`c?ISLE8n_|y8eL\V6U_2ɔ]~ à{arY꼁8pvzb6jggɯϏA(.v5ʜ@lZ,:&eA攂 ؿ{w]- p3~pc%&NJ6w?z093E5]:s TvD3WDVrn_\^4*Z\.rGkw_*+5z4G0| nƿ(NSI$BR-/3:׬F|owgP(ڽB0^=-++ ,AZ 'gϿϿv0}yOL ee.uKZ׿Nת+[$gї4[.ieZ7ϟ| #hyp5oo+Kem`dC 0`f믿/VAfllg~/\,hYNw|'3BP\cAG Գsqn={׾wAf:אɤ.:3MDvp`0Njj\l8WG`>]ii\^8xa7/v:cPFe/NHsY\+zYY);?;=i5/N+ZRP2@<ӠMN9eAqg0(IkVNusx0iVn}GZ>Pc9g2_0jP #a0|???uUq||,Ge.Aʏ&@. Cx}u@X` 08I+$ьszeݛL{H$\ENZtݲbrqkZv}VVjzrfU%*C#}c 0w~٧n.azsуl?W5=>AҠg>^__g]?c8lP>uxaw90ٯt1h L'MoILq .ϏIrT*Sd2+^W5cahų L@ߞ7@"Hs:iѥtb)^OQEq~zrtȀ;Qxd2,|5e?m~/?<Aj8D `A'Ms#p"<;wXj|-;%=_g q:o^j)A>ʌ-ZQ]5]vɇ@c`X/5t:ZRT6D-&Vb5&+ <^; .~Ar o7 [fI$ENZt`[vK5U[]9Ã^sQ*+x-$z1Ơ?t_<ۗϟ|2{8nPZ}sP{Pys` \=}9==yx!E^pI$i"@'-t"pl&rayaq,WnV[-J0 W`s4|ٯ6߽ 0j*>N: FŻm η]`~߻sj/zl0ftȁǼÁzfs~Y]]Ƴ.A x[~ ͕G[(h5h ;1 C0u'[[|y-&ec & *1D"*t2H5e]UuIԒ48dCx~V*3x$n@I\σbe.}<:<܁ lEi"(7sLavdlJ{<{FJ޾}G#`~`~JPK ,S~ռ|$NxEeM]Wj*m\eK^/|ټ%ד+EM.燎x+cnvo`1u;[zwI;$:!+ JhtuGI$i"@'-dףM&n:zT#OZc @8ʆ8?`w]kE@nRl"9 E]j 3Agsԯ3AwaPEP1l\ |6@, sk$6oH:lT졺.]<Eq kk]PuǂX>R !DڧG_yAQ\1MuUdž+¿D"Ceɭx(|.©6Aֻz^,WJ+b9Bu"<}o{_~t}DtQ ̣8<sto TZ{ۭL.¨3Xu$8RQM||ҝ;D1}_ytN>ǖNh6GZe\.ٕN+*1!_ZjAQ8Av7?|P:laCec:q+;.H$ENZf¸L8EY\k5;[N\./(O/ B88{Wfow L/.>iAkcUF=3$QnuƷ[sxѣJۉ=º9x>>jOm@.q{g0D 0~ߒJvKwQ2e/ꀳ `o1pz~q1ь1`Abg{k득w_ZCDb@g ]t{>Dޏ $iIENZf.) DQEA60h4CWWVoBQy{\v/"1߬{yH *PW_0 Fh8^>rbQJcx ժ48'_~w睝Z{aFw]z:vNla8D"*tҲEvkC Umpy'GGJXU\uב33Ơj~]n_ -ʈyɱDAde_>թ@-hPˋWo޼=™+<˲:SuOz{K:~2ӊi^{ZlJ Ӧ ZX`WzowtEC߱m%H7X褛 Z(U(?C/0 {^wggkWJ\Z)\ҶSהt`?9>zc^G7[(MNΣ,z*@}ڤEk}gkkk+k>tdZKQ||esƷVo` Γ`4O=Q >9 i>NtV[Y)-  fs׿y2y|-$8'hI$)7"@',$$zsNk7n--bR.kr!q 8|g|'E/JIOR݇[,`Lv։].\$ {_0\٭AkG`vk峧 t N%DC6/Y{RB9WppvzS Jvut`> mbqVr`owo_:.`1jfX`eʎNH$ENi _,em! n׿VjZ py,(W8v峧o>9=>ޅw\Is'[@N629 `}ƝzXP#ńr$l^zkke'_%>DjB9}qױag;A|mm=P, gMaA!jGCh4lWcF4Cu_O綛D"f tMtGQC.wY6=:u ^x8Wnc$3`~h0$˂2|Dt{zyd?p:z͗fsݫ^T$@ǩ<pO`3^0![]vlzGjVz#jUF@=;07k᳻-_Oo_]\LPmzNV+ 9>b&a?mmm~n6 4ˮ7IYsь4H܋tº,C¿LAnj]^*RP(Jiuz/>߽}2tѵT$@ d O`$i@ *Ho&̢ZԀX>q~#=R3gth5mnBQ7:& DmsӓbZ뺅[iyJ0Zᷯ_}< eI\fY{H$RDN2(. u<^"mllm'B-nX.>5"*A `0;ѫoo^~;M2e0aYaGϪ>;}n#I;TRkխ>ձ7<]7?t> IDATan}Ij^ * SLax'@ez5O+tFpKj%ީڱ[,^ogkZL>xf:^O "n募~n(ҽwJ*V*wҝ=y\2qi[3dC~C]N/nZK]&kNkHc}kO>>>z,JoOneAw}^]~8wVWEk۱i׍{P!gMk6ݥn>8:|7_,p ɲLãƟYtN2]OrVe]U?~?n-)6ptp_Ż!g32~{n~ttm~YŸf"?~>b=~p Ё3~ 9~BΪ7"\Xcwi|0xp*M)z^oœO?}쫯7nx4<{`p<Km8Z 0k)Rz(VCr\{U+d>NcWE1t׍FźŞ4@euIyKo`juJ-++Cz ֎ +DMk9ߎV??ɓ|-@8s,vϽ^ё;]^Gu&4¯ÞNMz?Xat`V(injl6*t@ws?7e>Lu7UUO/ӽxo0}dY^eYL,(VAN7_m~KWD\j|qRM}]kkrםׯ_>|dd@.3䧭o@}?ht|mo wnn2!n~sL:vZW/~`g{E'r(&?t̾tnBg_ʭznF@Bmq5W nklXޛ2zӻv w]7sɏGŪtw[=`+z˨T7Wwsop}ӿw+]lSfu=Ynw{t1`UǛ*noz(;Aۿ}h4[EV?eehl}B}wyFh'V0=0{Jb8 nS*QV UXu߯kuk/rJعjӽBϋu;)O yw{2_K`uU/==/+W;tnofAN>KUC^k[yt|>W#u8)ED騳~e;ZwOM{)!Ba],`jLZS3k}~oFN'vz;k=~UVv{h8E1o][XDžXUά Om˻F^u}wnt?|"<Ef?V2[UXwEy}{џn=*e^[9z!sy:#'U^o]Ӫl*U9BzXhmb,9 U}*xpmЁU@soRµߘqtk:^Wma\OӍNe=F+=V=y'^飏>ɲzDwaoF&U'˨r_z*,b:loE1XR9xVi?]Z8$`unP7鶭n}Xyt`y*!ᦫz5bt׈Sq*s*;J5M./uY|7}]̗zUs'2}ݔTխTv-&nGwe6-{ Uc;[bt%y1YLhr\ML40nt˻f=ﱪHh¬?Ϻĺ;'zkoosxΝGKb nv!RtweXP㔮|ƖCv|P@/soAݯ[Ͽ z\+[Us~ ʊVzx .2:[#ֲ ;,.TK Kcks>eռkr-nWe'!}W\GG^F^X)τ b ?{nП[?u))]w{_^Uȵ%k\XPu>ԕ]Wuyk[u(|SOkBtWW=w[ogradn:W彩X.O=/^NֹCXM|v>4cЮjy soǦD@ǺA^EƯ6wmuQL[ 0p#rx~mS+Sb?kS: M,+מu2%d2|.L7xWU5N0J$ҟC/c?T8tkkbW@oVt+@!W(@U *$2s׮n]d6[tA覥qouYS5 zbtk0ڛ7VZ9[V0O["bܞ]zJ9<_t/OgQSs|w띣uv5sc]BmMl~<<tA[O 5Vt @Z^[ *ji|Ŧ|b;|豐ù~kxxx|ӧ(wTowַp,c)&娽;;P3xB;Y{}^uM]ΤJ [ǩs%\nvѧWڮK?]Эk};X:$X<;.ͷ?߉d3-^|^z*^񰽱񓜽6@^5@ށnHw`nyusק' t+h[su[ 6@:pbAݪnFaznmt܎MűBYCޯr.~Pk~eVۜ%^P~jelZ*鱁~UD üX?\Cfz~fUSM\VPuoԅ:jTU5vWs]khtl?_*|70znyYiɲL/>/yOZrY;Ϭi"g;D +z,k;u(xj:Y; BЁ'uwXxW! ebzCrc;ݝ?_w\4oHS{=^rYYIpxt TSjrͅng0nyU@} $DЁK74 U[S*")ۄˡn~Cu>7`ww{>|ao Yhkg!V̮,񠳵KuRMurxϬtQסHRaJT4B@X5]y)A*6bEj2~ytl2ΫT;/+W>O0w<^^WNHX(岾ipASzU, /ZgeY&訳d2ٗC p)U r6>|%4H7*erW :F]lx\ח:Ub2ݻeM9S-_d=dY&ã^w{tՕ^U9szt8=c imԔW/nt^bP?ڛo&~[)~ӥW~Ejp0n,d:Nv&@f{>tXg(jL 6y}:pVuUհO)+ÃWEY^`X"kI -مFQw}cVC=ao)IS'nAk|(N&"Rnlע,3&WO'j{]|R^hacBEQ{{lHpUWO, eǎN~%#:TaK(r]}OYffݘT'yqpNZ ֛&5^5h`puOOY'T=:ܟwyדe!UUXGkR1] xp3mwUEw!]ew}7>5RkSUJ5H1~72N:TC[KZm#4fSYGsEd2766~[d`~{EjZ 'J yސ^o~Kckp)TGZ~+7GJ7V]=eww:ۯFͺy2 t,d<mmKQG2#$k{+F|WL`eZOwK~z8{x<>n6ǿQ]kbg/c=irhH{k]{MY#ȱwy]Qw^ a18*pšNUϫ Q:lVA]F;;;NӉPI -l첔O^wtI~?q`ҁ.̭n .Wy8gϋ^ 'W>3+xr\_gA⬁b-!}rU=]w8<|柦ª+ԝ}aN:tm@ b8AJD($.Sgw<.:ułeg rc>dvP8kX7wTXQtt@M< ݽ{/ƍ7x< u/r}%'[_?4tVnة@.t5=l2Lh(ʣd:,DRCԳ<ϲ<eEnk g̳]N=%2)r\7_,߅V޵N`%3Rչ7O[oy;"rrrכvtۧ<|}|woyh4<3YY)yHydyv$K&gݪVZjsʲi1)IQ,'EYLRE16ի"r(gxx -9 )h8< Ļ]keO:g$[w&e1=I"EYeQHQS) ) ) ˹O]\g{nb*;sV n6+Z\s}*7$HcЭu7놜u#[aݺvV> =N1~-:g>u0@N]o+XQtϪUѧ}|Dκύ/gAݗȵYiktsw.CǙB:aD@tDWߵ,.u*)!ݺ:~^:WUѭʹ(:@ uwwǥkwCƧP<S+"cS+躊zG tq!|jmwp+,xQʹuw] t=/d3XŢ?&:uS]׀tdB\tpt5D-ڐӳ5%cݿM.2_P:Zy \3t???=6_q発2@2COaoH\z8_.̭C V VEfCz,92[%_Հ uwUPZgpM>kwQc|8BЭP~j^Ȣ=t\Ck=6D@ uvDN¶Rjyj8wU??oPtk8 ~|c \tZi~v!=zh@8R .~<'5[*TU9sNg8>IDAT#,uB_5Nt]-ugt L΋!]1](S="R`v<co^PݿnO$nXBu@o).|uJzl8PnUCĶ8:JUHU#6͒\(niEa\lXQtqTAc!.2?{U<4MϫfYf@wcǦ0^+;k : U,VխYzoز窺T֭sn:Z ѝ=Ţ=tjzkXDlU|Bz\5-%v puFN?O Wu}hEZ㋆Ug0Կe Vw*- `F7p-j^TJN ݄sn(Csnz p5]'8Hr@X.<O,IENDB`simpy-simpy-a59c5bcfade4/docs/_static/simpy-logo-small.png0000644000000000000000000011600313323151071022027 0ustar 00000000000000PNG  IHDRks AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  @IDATx]Wu.9Oow[wl| Ha#",V:m)ORfYWN:[B.͢xiN0?O>O DF{-+ {*)jrMӧJt- -D( $M:R362ޙ;Uڥz_ H/mF,!)BsRpA!#0#vshamnfAH!3Ś CS:bHAX>3^|ŏ ȦicLo3f& sC2=<09b6#̙,]+Q?uee~  ,Y ! #E.N&38P&!H*itt&NfӪ/ӐHfM5>xhW eD)6DٽtkYӨWM>UAHDP`"F /b@; t rp8R()#` ƥ`I&GV@O%ճݙm;_J| xj::83KXJvY4aAI29$KޢlP;S uZV'fN)Vh@DiX1AHXm|믻~ ^ ɫ?!!}SyOBpgU?Ւgy4 80-ɸ ԘC&Gp00{lk]͓HnH%Ba#̏jo_z:oUJIFP)SyG7U@hm*/Իn /} 럌iс~pg'mIaGP : # =us vz^L|aPq7{T? h;ڴZpO>C X=7:LY5V槾qM5jϭEp Ypyko/nH# DDt8 4G3EhL˃&wء1|EX>| 8(<qE?fп w3aA]m&KH ZG^o+ tø-ƒ#Q824-v`C47 =^!DvX>:x~'u")_>/ EteY 9*eߗږ@g吶aD{TkNa\cN ]3p O[ 2`B1z8e/<]fa{`IF!$Yڰ¡#wz=jl=ZNNg ey)ۿj}xQi>g\ONcq=豢b㊲/I7Y95ژO>) X-%lO QxcK5q 2mEIo`m.a܀ lyI<*r؞n]61Yee!r߀Ӊ@0yz'>Na E H%n&aGʵk7oZO>i LO*E\\I `‚-{6ni3"`E8xCJ@d383㑬3k8swg} 8(0U$ZGLD^Q;%e|2GN]!ӣXG`Axvt:Z #8ƃ`Zz7Gi^;\l7oa8mP%\_ .+|,U?TVbI$_)pQ`JFqc`Mq>0p:hptn;G[ E'/vx7kq=fE'K~p>{qbW_ׇBZоʋL#U'3D0SD ZG0}$ ǵfnևa! ma{XzHcX8 J\KŕH7Hк8dn|'=+)+> bjk/ ,ds٥ŀ8GQ h%n;V51SSl] i| L أTͼTRd&A;AD"Zs"56 ɭ=3'M^llt⑓)S-@A DlMEq}(8+Or8vv\G ^, 0P^Юwqz&"7F3ÚB'VX\atfabPs.ki\=Rd,Dpg)SEZ-*{'ck`R--aLq/i;alPK.Cp[!4p:X=@o?ܞd:;Nd1`A/Pn܀26uK1lGCj H\<%̐|)wT𡾮%+>| (H!u^2qf,)L⺍bPCa?Ӳ ]m3xBv;ouN3E'y|ZQx*tƼ IR^ 5 9!r'"xg:}q'e:| )pNQ Y&an%X`y7%|+ I l豋۸vAxtw= />==QoGяdL1ƃ氛JK&dJ*xx=7@jwm]+qDr0^ rb儀ŵˍ^QO`'[[K.|)pS@:6ZG]EDj%9"' #ߛ7L񆔈9A$! F nОkpm^JgS[ fq ׉d$^U9RZ5DB&`[*1TW47On{ڛBW,]Ҙh"H0䌓{frH^07a[!ݝߗ܏&[^ͥZ5wK떴R1Q1 Gkǹ.Uw{+)š<,(4/Cxv7P vY&[ Vpymԙc{U7 |C)lUb[]R;SZOժW?@p9BW<.S[G;p ބfӊ$Z;߿3SO_[J>W>6ޫ {TOw׋ӷʀz;9FCf9@x_z.Dx%ՌPJbg# Tա= v*-CK—s1 qӏb:geomSH7g:rj+PCZ "7n NcKq#2RLݟgØ FYA}?X-^{Ld<ӥ:NJ>u(Urkʄ0ۄK3Ymz0PHח:߭a:a] gȵ{BnLmxv$-<ٸ&f`% >蟻\utqxjyȪ.&bъʚhL(@Z #qZt ꭁH@ g;Sua{4:BR#Wc,_GLW"wVlf7]jC?|ygMH  m%g?ZʾTQ /S[ :Xy)ײfa5ˍd@(^X bMvd2Ӻ F#6b1u2s4imPV<"U<]-YJR԰<5T# .1fT0*cF)f&dSFWGٱwG/RXYsU=28yBmg k3/M~k.:.o4_r{(j'4MVEDK.5DGl_?܋6~Uqzl7z(ٳSVD =[,xڵA4f*8)rDax1.kz>EU Κb: I6ġf6LoŊ"S75^DD9wgW}*ܤ <8K?"M@+[Ix_j*;-T Kj+(0=nBEp*XZ0.(*QDO7#۶M׫b;ȁSL2TKz 2u].덊#6\VH`{vͤd2muf[RQ ?f=$;UhF+&!4OcCe@)4D{J@pែ"SMIf#w5 C1> y4& 4==5R{~W_K|Ze`4۔ R񂘒 IQ8m%v2tkÐv.0[JR*M8@e=s N>I(ǧE␱k ?YO8vB1)6Le:΍;>z0 ք<2p {"u3FnNkg8~~n LZ۹? @46Cpgm4G OWz) 88(x4#(eb-O׿nxlGz򺷸p !Ux gQф;unNߚ'zƶO#%"* n <=GKt;WS୧F缛}ӼڸŢwoE wo}JP|5TC %IJX4x q~9-Jg/^`xl;+x<{{i<3lwr"YD|!25BpZ;;+*q⏸#N8]'TFWߏ s{8+> 㭎g1 hN(tϧC"OgMe-@ Ye;7]:z+lp^!N/a0\x}g+µ*j`c5>&DI-tF$B*Z͙[?Yt56lN2L P^FeVJ_%:z^54 8Am CD)G`7iLUTw|/7BNRx%1X_oLcOR@3s:N{ؠ)6ש7+uuB~KNe@R_Kh G3h~7Gplz(`LM7۰e>|AP} Kv tYZ''.9uJ$sӼ3i"RaĐ# S)GnF?V`#,ō\ҿ[qAx7R[1"aO7x"?unAsmZf|I^g;RT٢()D'@Pb95B9b-*M~#nNHvGqB*L*V^"?WKcmڤ3^/LX)ࡀӮmoVI^{Lt$sU*ds__GWinH\"%fEsgRl$2^n0hwM@@r O.+E?;y2yߍ"M:T:Dg|v|S෕lDҳbx;vpF!YiO5'6 zg2Y2P!n 0#eǯ@ҙ.pX]Vm|~|W}O>va]dz+:9_:8ONi{N,Do.kBcOsCan%1iDk/o[> k!y@Xҧ> _$\1vTW` HX!'q?1&3 #[-\}[3߻\օJ:W斍B Hscn-wk-!ԕ( 9P/}׆^MG!8ou^q@tZ53ׂؐźgUVD/nu(5Tx-9CaY8t.[ÅkY=pT EO=gq䓛6%MʬV̴Q qGq5yhH_O53xv V-ăkQ/ iexSn؞ ONf i^BNb‘G@L=,\4M=?bGćTҀeò{&:N 9AlhST:|:`H+ł{u7D_vu/^^G^ˮsT2kʠbY+o[ mnC5 q%2̎_Wc{/C͚W 0sޒಙ|ͳ(zb;TϪODq{,FӥB#N@\0;#-y<%=Hv&faaDǓ&&_ia[J?zq_OܗŦM {`A^۳gҲZٓLݫZZZWgmzjlugaqϒy\871 y y8yj7Qu;p",DUžBwZ4F[gQA5P]]<3bF r=VxBs?k3[G$ے{VaZ쌱jN.ę]U#"9#7Xb{2.SQ%!5sUvW4ci<uc<\yhptn;-y"ˍbعG Cx;s}_;kMvFwz3u0*03YdP#)7bvm:Pz+:|F)zn+: -[2X<Qٸq755ql'X quqUƵ7Zo;y(D/OǙĮ*^܆єSb5/+5,natȓt79o95 ̔B x:5Fiw՝CWW(}OʮgQR:>C_> FpzU}U5=E?6c!(d7b{anuqXy¢v++[;8qn}gk߸qu,okݢ7}9J`vO4B&Bڂ'MVnݺP,VE@WT6wZjƋhI=T#Ok,g;++@n)zkG +մWEBgܖCr$&1Î!R o?kQ4%35|w~0,7CђP0e ^G[¦3=Mg=jTbϾxa9%ɐ1 ϲ#_VU/ʆj*! l=SY-e0LAKœTi-£D_5Xnݧg0*i_Z:~usߑ?XvO>N#q 1 4s]70z hnMu0q't-=Oa$p|`vVwcQWy3fM׃yu}&p/CPZ t[d0ogF3 `ơf3֮k43=?~"h2p`ar߰lE0^0KЧA/pv ?,YH#v^ػ͞qڼ\f2GΎO=[)T D<0 B†r ]h‹B]7TC5,8YFH+lvZIhHg2o$}MͯG@7 p4[0Ǒ TG <7ҟ&vx:YT!Wr9 C7Jr9@hpc̷^rdAuU|CתblK:kY,k|˲iuiwLfk"8$ٍzjfmo[ù3tN\\`Qx،W> n*䥔гQ䁼_G4Yl##0Is>dl&ۊĎdV_uj*m=k֨&xoXҲ cw8A K;rmp$>]PS]8n T6% ^tG?]?g-[ 47E w._veEŕP4zX[[10(#h#@`e],{rm5F2eT&Xb3]$Ӹse0#sLq-[6cҕKbEkpmi-FU„;`?`"kayAZA$OK49]+nOjm{£e2R(k/d9jb7wꆾ$ 2_BWB_,ۙ ܈RGVU/>e0G+t$-))k\xKx#ǀ֣swLHȠΎ"Ǝ^HJ^xckWrkk6.~ ]kDA^赳p@PYb,Cʄŀ0cy4\eOd.QQ,_ ƢѶ~l_> sݍ[[UUuS8~?rN%Ӄ4a1,@`X8‘0[W]upE?ڷ{7mt`]] 2e=Vƹ=J]yd$dV)Q$?ʃ;|b£E~WAsA.$,w’Hy=!L$=.:ͩݘ: `ꌝ^Z\+6 .W; Hc&84'~l~*W=S%@#uEվdm{$T%ұBegɶn7<6<2C7ÍjDŽB3>enYU:@w?`Dckjk*+3(3p@XSA~C!LCNPY J.p]99ILi+t2Fż tmdhE(ab'===0#8]yfN~00W Y4;WD`d8Rf!4I\^KnxB4UBzn߻<>Gn G"h\gOB5/&fk4"*cx 낋x%rXۏGGnr`XdǥLFxI lAa݉l @Ht2a- C1;Q ! ־[Fޗ6+p!ve~A'f#<ǽح/ 3SS`c%` ]ʭz'-bh%&ZNw&\u;OՑEzՙYk# "(,YT.K 1!Zx' ۊnçD [:&ݨ;n03lrZPӂY@q'2?x"Nd2A/6ख़LSƢ1N8>FbSB:娃qiGy[C 7^xAWTE7A&d!ji*4O:GH~& aT+Mf"3!RJXwQ;M,ϘhVɕ ԁOk>#Y71q)(C BH*7%tPn8w W:h:"x.Ydw+ D6lHut9 ȳ~á W;yi0kjpSPFz8¶_t=X Y?O]aeKV7Ϙ?)QH]26oVKK+*Ľ;~ݮL{9-)-[ a90]v@]76I@n"8F "gm WXXfώ#CY/ :1: [d,&i~(OnGy~,1%CL@P<_ϓ!c4'ѢeV :uYh(PIKd)[#7va "oIxbLA/ΡbqC5F )B`Z_V^. lזz"cYjQ[es7VܱG浻#}Ͳm9wpJ nCϟidx IK0 f\FkAt[kpc\`IN)Μ UUNwu{?NM">D#ד 3-_BBNHN/i+yh5o$oCutW]]Ҁ*uAi`HU ulJuQs+5i-{-h; B#] f ޗ(gzj`șr0f~tuͪWvtXa& >hvQؠ ium6 Z8| ̓Aoy<Ӹ,phpYɧ8T敗قbq̓yVPB2Xe|Ч ?Žr. ')@h&ETIzޭ?cmm}՚5 <]Ý\Bc+Q"z' q<Š`L_X) K7LXf 0V޲na;p5^B!;WAHBy)`1֘S)>c&G!yLwzfXhSlhld>ZkKyjrAj_ t5O*15p2ud+ bjI/p)BpWOw7 鉎Bso)^5`Ďxtb3oG!@:I<$'Ɣs mk MIa` V:zP5,Xv~{&S LnV܂ Stb7:Pp[>@@wK@ van-"9qݘGl!̞x?񃻙5qjǽ$P{,MLeT%4±Y&&1it#y#<)L( TUV4 qƂ=ee5?B\^u`Uyeč= 'd|Dls>sBoܺZ9MF!<7Hw+Ɂxk+ot`; oȹP{pv^!@0!oUWOnF%- BE$.>˚"SV]?' WsIFOjfi+DqLTͧO=p`;Oؑ}Wq؂7VϚ\}G$U 8"Ř Gxu[XtjՃ.^ 78 1'Eڀ۶>PTTtf`#>L1Iib>dX<|s0)䛆B-Qe|&nQ*;8gKIr@aw$ N{6i5U%edQ(E-JqZ KRH)%$&/?t&t,L k[E@($ĵSntRyd Ϋ"} o by؊F|gkߔCfQ[)@2$sR pkBu&PP'~H.g`60g ;xhX Hks;3\/`rC'm㠻 ^pa|bW֋,Ǯ)qM9>>.($pJb6f%b/h&FԮrj?bm]K8j;uZ/Wj:ylx?<;cNd+f,[ST[qg: -4W'1ڈ%0dң0L4Jc)]ۈfZC{4X!cÙv|削ӛ!<3kcQx8ډ8& (WzvSa ~{zZ;;Ѡw&8ƍt6WtzIYټX,6!#܉ȬHHAwu ߅K$<àh4VgΏb|LJpX72/7Mū3gL8P}d36q0^!k;pN!ڳ`ȨF,!<8XsOVEа}Nɚ,S_ <}/VmRghBQQ(n4%vZWx[GeM>jfcq;򀧵q +Gwy*d5S{_K< 0jfJޜmc\ ^2q 6toJsU6Ft'GN8f\7cEWWVUŊs* W J,:N!-cЌof;ֿ-2#7m}Mr!Jpɻ. F8""dFP&fc#3k;w< 0H=%*6$Q%f\|Ν;X wMY_zp/=}ңG)Y*/SoxE3g͹*6Ȗ`+( n^45+ѝ"fqEf7pV53x-n)uZU'ZW^Ʈ(" ^Ԡ2,7| Ij{_?IqlX!D)'q3m'8tE+eE!Q&a)8Yw{qźbvXC=rc8 eUhuZ˩Գ~(k/#f\ ,0cRrTΚ-6}N"n]tk.6mXl^8T;4(McCl,n[[ 5)wIJKfl3g.l9u(ф +>w! Kyxd7$XṞ0|E`ݻhfCJb涊_x'Nw.6$Tݔ1m 4NA8Q휻paGG$57Wn=Unۿlkݎ)R`2&媀ãӟ쾥&< 2\À|ukӭ&.BC'ݣ י`גϪ#g:sJ-xOHmk.S]'<3pyŊIke  ܑUcdF!C6]hrS }Aۘ;{\OVX A8%.'TwJtww`̼eeZB74t/sn_=oڒa<@yj, (W e5; Y/pl-c[Tgӏ:x7f-}bm[!3.+^ZQm?Ow4 ,[X1x͘U=t#[TP`l5 J bJSw0Ћ4su0MLm}wa <nU;ǶދO,^ts,mCnuw|}"Bw^5X }.Unۅ`{"-qUr\g=3^:m::qt֬ʃsV}vf1C",vRfu,٢k?un sl{=*a0NB%];1$)Ow!MA Bg^>YiRs++9Gzďfv(;::=rfv믽{'fxЪ91W]ډcWWWi]z^yɞWON0OL6BM/Z49&ƢG<$>,ng'ayN=d_v 0OV!OѷݑmW&6 !Q*܊t=.E5d_ _ʜO:TWـZ:m.#gdIKa†~xuufSRRcߞm:z/Vbr8)m O FlyrgPmED:;:L\`AGOSӤ? WDʀmp&{apaRK8T,OOx ⓶_<; tBoWee1ާWGZZpbњ1)g+tjLd#ĺ(//-mge>j0(DbHCiixƸ*[2{6䵫83|f~շJ>ܠ8p0U'Bh :wX#O|f Kou"墽3>QTvJa0 (.GwP1Z~QG[5UQ%k̈́b&ߞ9 v_{'KT.IMjAP` }(i`ԩ~=ïƒǏ9_h!<* ̂i;mf|;@rۢ8( =~ζ^ٿ;P5CXY]ӂ(PX-˥_Y%ι|ʈ8jb Į©a"~Hfo&߮"A?ЎKB0]5cFv<2v kc4-fsb3Ɓ+@.C.ZBRm}qΠOx*v|Cs@I2XP爴 SX'mq޸V0`Pz1C2s^>00p®[#qIaعj'ܬێX_33{:x\d %Y!~ꮞڟz ̼+f/^);sVQs4d9UG!v![) Joj5::"vx#.JcarڪZA hul(N#1 +*[xƍ=[Lr yPA#I$h2Nl"5񇛜F~-3F),$42j`|| |9f$MBpc U.Gln*3<y0` nc]/lKlm2 dӒոA~YܒM%n7П͜hɞ>ِnڿ3݄h܎ mKc$^[W=3lmC4IYMX-E4P$? D`Wؐa&Qΐ3CLwOkUW׾νիGCNW9sʗtvʤRu{*Ir#{Zs p`Ҝi4u W~ug׮uH 9*1=wP(k#×~{!Ϝyf d:~f 9΄?d8c uwTaV?|j򷟔U WZ(ԯ`/M9ƫ9uTU1h˘ĝ%ҨXQE#;>^jHJDheaJY}zfa IqwdžTdV_?;ޫӏQTl`W8kc<'kzJB˳ZneƙS\ŋ]R[81{$uCYє g(ڭ~u~~ ϐmd^މ|Q~8=mgqk@|[z=O>Q,6vƠB@0t"o/cԋ2b9DF3/b{MnOD%\"ȗ$'Jt=3Ews-SHn~%VY=M+g:hYzzyQ!^lAb}QǭQZR*dW¥߳KFrߐ.YG>3q(xw46+N?ߎMbTQn ny|#O~&!-D"qF(NԾp遏OwRo\zF'qq4*{lsдQ?*_5O }Cr;Vjڮ"ЪH8cf78NaQSyl] Cxڱ_T0/za '"H>XT R?w˫~pk>^~I*ߑsN#uϢ%BXʓ1jCkg0+5՚}# 5ٚ]yZXϤ+/Uop1veoϾcg=C'Sb`K Ԑ=xRY @ݍ[9'g$ha<?8=rJW*ՙB!s{pϧW2gd+132KC‹ Ak%9zp  Gw"aéq` OlCo뀑Si0P*OKt_xO , l<` E#DD-b'-Z:$r q ӐL `RۨQru΋#I/LXQrEQ4*Jgqiwgm?_w߻/ Sg?X[A+u:s_ީik: o=cOHzQT !26@Smèfl`NPX^ P_G~^bha^88^۸랻Pl`;_W@ 6@8R`3#@g+4]&Ġ8Hde|-`*Z G^~70NX>|%Z__ r J(YvZyɦy!aC= [ P1& .[:CI$b^xz%c &MX %_fJ쀻x{?HG1]Zd@@\}zڷWේ='(EiĀ~ᩧ"+_?^@/+*d3w)?Ehi&[ (Db7eQ݂zT.N:FP<[vS+e2@p QM[Ъ'zūiM_~[ffgX*Ycj$$Q{7C]<:n2^*Hˣ ]; &TUTz*yO;'vѳcG,m'YPB×UzJ]v:S ʑŶk>" y48u'g?Pz&'//$^&ùgGI$K^^eY $Ũ9acOٶ O''&N }#u%  WNIqѸîd_Sy>(lڽ`;h4,n`+r!na -"ʋyYmPʼ kXS NY9JIx;=)s!eE%')吅uR:?ȇ^t r^tӍvrCc 6"ضuMQ  ]_zG0o& T>iUQ4=,v/S0P(*)c!lC|Qf` Rn. mSy9$kKDWǓJoˣ(u'Nwu7Cg{׌_d0w@ҋ"=d޳p~ȯBWqbmX57BVɓO:m.n wUz`FwXeQir.Uba#fTj%Ӹx_ŵ-yw" ` W9Rݱ[0 $tU#i^A&Y7aMn m3;)*#7ՓN)8;?Ǯ~  p!SE^>w{YmrԸÀ`ycV<=IM0Kb EJR:;qtlƱU9QL ~[akCv+Z= 6iv/S^]YgC#=5ŭv+%t2{fmmƃC/in2"pvb@xuy|5t%ncn4Cj/LC)$<+CVCEI^X0 u~>g0p~7@!Q@ok"Ak mm\V{r ͢k?n+ l/Sz S'' d-9\(c9Bt*\4 v:@=QdehTLN}8l'ґ=i9݅LΘ6ǥtϊQ%잁Py$̃n/Ub.4I쏚WhCWjxǫ}n@|z"Ղ]uE@1 ʧʳ,6!KEu4O&[>w?G?֥\n=P.緶\.͞g^,f2!n.8 w,=WyV^*>ucFGSbvd@pRg=}Ft`{Ur5W̫߆ue!66~؆JHA@y):7^kbUL\įpǩ [LX!7=}rcccVx !:M@޹MqdRUt_T#&CݻGTr7n%NZ?dyLP%ܽEK P^:ѽi@|KxjؑO`[1ˌzIF' 6, :ӿsXD@\]~V_3 e5`;aV8̾R]{ԋˠR U+LaBk.#,U=Pq(.MtBj̠ !"~]#ȩ7HzLs)gJ7}pĸ3YJḯ%Ə}7K2ĭۀStRptF.lw]NZ`LX+?%PuxxeA$-!?)zȒ@.Wѥ g<˸SNQS1Lmxg DsC9$>㲌-4w'%J3&iՏC&;;;;raBOhVm@W[{P~9o }0S̭]m4F#L( u;ak<ࡰXaS9۸HA}+^JfVkغs PFܼ@D2yɖJ%a.v$uu PؘN.`2 A`ـCzQEq{jw5`D}ue obeOdb#`x X Y"o*J\#,FcXTtVkXk~JFlWdo 9laʬC+ ry+=xbz(htM  12N=#/ϵH`gyG S/eq3f%njAgJԕv_.· ȇ~H2=)%ax(/. ̮*6Wt.z˥71H,r'vX$?JHY \M kLjO 5`g L$[f ”%!1ax{ J0W(WGDea,$$F/2<3l3tnhe[ | t-dyy$Kďc[1'Ii~oMNJ ;*-.FewV*nB8:^*}^\/Db(ͦx0ܖB"hAt+OBhM.9kw: $UcA6p‰^G+j#cXfU8P'U.$ki_`C׭6Xa,dXф@T_NX0JzzWbR' BЃAλ;Oy0ΪoT݄^zp7¨Qۍ,#v5~ڡoFV,dx.`$9E:rFCt? 0 zOP QNVSOgJd7=Zouު]<0I&FZZ0k\ND\z>^ᇆp b+_  a*[eJDQĢֆx-m<VBxtm%S|EP4狦L HCKX\<A  S?T1]˃|yIj/RƁ;e%EYSUPF!t ŠqRv.^̊#(X5+պ6ɽgk!:r 1'q 80TǞ~94s 3 @&:xɛj9%/SyOu<=U԰b2tBbUd`7`-B4}d2Q!:aE"P|V(毀 gt15^-_(dB! y>%iGkG^P" ĚV}=H%rKl+s<n3Z(ԶmHޭʥBZ[)ӤP?>J U,<†^ʃbzQk$Tg3ejJX⫍htO,34N^|wkwd(C>*}!TJ0L[.:@*)Z8צ^,?Z^kͺ1jۃT;GH$G+cс?2/?}Tv-N]FA Ld!@`/+3#DL; HnLBT,(lȋ^ol S"@҅| @(Q2P&c|"9G"P 1T10xATSgx`4 s8=[#SIW77ཎեY2LyJ/X]0n<;K d4|V@{M˸R۪e3zz3UKt_um# nv#&8:}ש&o '~_ӀmC Z0,˗ f-ݬoWק/fhԺZ(,N[웜O~׾N&@fl֗ԕ45x g~?Օ_y̏9q=GF#I(4/8_0bݯ0oiAA>mqKVeKFP\H$:w/ 1f|+^ ej}?8Iaقdӕ]8rgiOm@fES+A~\p+Vf;:eaR.#vP^ý(SCeOGy.sû!`02Јr2y#v.Ӌ?>:6fC qIf:}X(.6/T*dFEG;H&I>\oe/x$:_~&TbOGIXlJ8\ BpV "ABR)s%C_FYI=WD*>$zgqHI $Li'+52 0a)N"v08 AByµpWјUS\a[ҵa|ujQC<>VˈҠTB~vS>~X UFCexsM$c 3ِ4tGmz#] WM(R)~_l,9DV<DL8c{3S^\"7*[y$λ428~Ma\bSNS#y3y6q60wNe.y7-9d9=mQk}BGA%v~wiqi]~ }"!aDtkDbőWTPl.{o-,̽30` `]ǸDPH[bqtV^̥[k^ۿ/Qxs+Ƿ#L,Z@Rc k[4tdr|$Om^ԑc'o=QP< NnJ>1xQb~r[gf,1O\g1w muZ>og#eqϛҪ@ NDY0Nwr'Q4z1wc~o Im!ǞlNxO›aә;ɍ\x4|uΊ8!Bŵ+?"GF‘dWXʟl2111cq Msx&,ŋGJ0&B;[TжZðaU]Wُ?' tzmm˥`86aز1-ʇ\9XɃ8BzviPw*f,--/.^st'\Fk 5GQh騽nz٭,Oח3v8v@'zvkm7 raRnkavnz}cmK=u4`DCd ?|嗾r0pʔd_X$`~ueqefz2>BQT+Ry0y]Bc0++N,::plhxP@jbzRs+sMe|v4 yH)V0;)!a1 Ñlndzf/ͼw \+d0F44X}@_} |4݀\.=x`#n驫12yW\sg~*MX:)Wem’;*k ~~&o;QXzٝm蛌L!ƗOο[xy -9 Gc WWg1e5SLale6,!,Z_xw} 5'WfU K/Xɋ[h%lC~KßP<XO-/u줲61^>tzcr\C!zi}MK_c*fX she!Ö S0?^q(a㷟L.c pVBj P_E=Wq]_} 5n +A[cH-p·ϧ0aAx͙ {gbsi[G7̂aŏ/]x ҠeWZW!-i nj.g-L]y8p1(߄r:Ao} 5 +DVUTjTbd {}KǎWM[hv46 {s>l7(l~7k3zW+2=VBA.x8n"5 Ra=;8)]_} 5@3)Ecvyȿ21}}09EY'a: iE8:Ya\m~&]dF^4LakJ\ƱB5v4fMea,CʡK c/bY0(T˗0-x ۦkr‘ gaZ}e]_} |pk@KSXΖ9p{XF@1X6L+W_SF_k>0]17Rh"0"gСǒCç(E؋UXnklMofޚ6c^-w2q#/K_} 5piF4 Tfp:ZtXGqq^38؝E{?:.뻾k}j@(D `p <-EC #.b7s1˾kpjF5 X11;妑ֆ87PB5@_gE_aSN'lxcaƿk' |7Ufd*IENDB`simpy-simpy-a59c5bcfade4/docs/_static/simpy-logo.png0000644000000000000000000022721213323151071020726 0ustar 00000000000000PNG  IHDRl_ IDATxy,]7j_^oߗ[R.  b<€9m<0sY1!$DkEjuoߗ+XezYs"_޼ $?._z B!B[E7yj~GvzB!BȆ'.w>-UB]R[B! 8q=}k#6yB!BHCHib[lȄvQA$xqR}&blPB!B^w"-\ʣ[Xl˘p}Ri8qӜWf݃{톰g-4\wzއISG˓E;!B!d͡$OxTK c , -m?*mJ}[" SLj:R!)L)QRBPBJ )\'lDSpy5杺ژmLU90LTn޺Ҩ5\}]/^B9uO#RB!Bz z6W<I^p-b0J#êPЭ;iӔvT2[EEӱ0ńń&P RHݦkoW"aVy"*fRBY(%)W܆a7vC\VތSsKKQ L1{UݍtChېO{L!B!w 'SQoxX.36o@eH = $د[ $ IO2fLe}B#*]תȃC4pm\ubՕzfnN7kswKjԧ̧#8 B!|P;<,â\ =^/lݴ͜%ϲ߰^(&t p]*a;yyJEg=[?ن _@H!Zs<[qQN/̪%qunڹ}Zcͷ;b@#I=&B!@_{ⲧ˄MGKG3P=Y=8*|P(OR]sU0oڥ`M.$P*nxg.Ш]n.pgqƙz7Y̢;MlEhB!BH.(W q5b<1w~'J6SMKL %FuCsQ8MКC-`S|llN!BCSكo|an?^ᣏXr`P/ϖ>dVuMiWUq-I.̣U98$߻c6VWϝroxB!'*|!!?¡G]Qc%V9)ZlQ݆g OƶW]z~ZU]yw-=X-[ޞB!BD癛|{ubZZŹױE(=Z{{-̑ӳ-|v\0,1P}ܜv{cW=d\\#B!>=9C_~rJ)t.֥|5k[U^a)0թ&󍓋H PGμjV tsB!BC6@o$+(:al26o7>bpylq/3;ղFg6ǶGb*! uCl<}C}z{qu9B!܇lTx́LAHP_ hjַX3;nZ^3թ{?<8NHQwضz㊷VȻBW=xN!BYlD.ʇ~[qЍfhy@>qKa׶ٻ]|-ΧŢ8mypaM_K*%i"B!?MyX/{ ~WbQWءW)?#% /=^ ֔U co,=B{Ǝ|%\/}-[:/mrv]xt k'݇I} B.d#%`ۘ|o-~%U 6r!B!(=^+btBA!sm\=&P'y'xm[9ϸ]rme-K PFɏy:TJyHpTo\×sw.o5ċC#B!{{{#"ηn5~g+?iSB@f <.kGU7gt/a= BE=T̴wCy0`-Xjn5Py͹|ssvm2(+ ]J폔0Ю}ڦwgb|B!~~aq$F~~Ng&D0Z0-0_?kpyfb*?~O*a>DzX;7ݫSQ9y͵Ӻ8N),#viW榽*ZKvkB!Bz~dp9?ÿe9H!Tܜ1-E)3;ղ陷s44f<?=CwKzscnm> < j{p_BaӓN!BHs )V~{z8^gvv]eaW,~s @@?^xG0 1&P/ۯu{B媆wcXy{^^ҵKy<a|xi6|܌ز]ߢobyΧU64^ wH'B!z8@(kP(]mU gWany Hkſ(9w.ΓZEKp-KG\RREo4|GOB!BIG ?߬T*"Wz|y'%.ѶDzI/yP ꍗgNjvy8\c%ʃ-Y=t]Zv;P\)%Aylr>}i] { !B!}&áلG><׷W^HU \)*MI/ _Agid;n|y{ aqhz]]աglتMyncH:V@apHծz>27B!>G􈸬4Qg>>B\J (N[-S;pӰ8uBc]5_8|z n߄og {ګA7Ws iu>=0 bb _8*7fu'B!zXK2XG0YPt_+ y0_ mTic=i+YǙu4-٘ggJ\Yf |lPkԠDC?E:!B!,޿j5RD`^a}N [yg=ymڅy޾w#;d' w.L%bV20 6m 6o{ׇ s/ ܃PwB!B=f =7"ş5-Lzᅦ|8:} +|:0]5̛ԻI::Q|ft <}A΃4/txY`[~e<8Z5ϜX*G?c}=B!{zaetq1}cٳ Lqͻ&/WcuyJTf-|{sztvzڃ'tLhIwB߬?i=싉N!B=F6;b3(_ƊMyMPM}ڽmos# o.yHU΁$;M Y*cbWך^h~y >y=QJ)^׮\poF7B!JG>O?{c!ʁFȉ-20OHQŹgF(@794/K ppz8qa8#H`XPP~alfR`heHX'6oB!B=` 6Gb5ENWayLUTTys'򆻷8{$́.#[b`PΟosEp8{9~*4Ml?!Epő9B)vh8y-R8n wB!Bz"_xOIMXky^a׶<ͺξj9GJVALlҞؾp3wwjw#3# tMe¼j  LSL?ږ_=勍Wfnzwܼ0JȨVvL>4>.? 2XHzl{02!?z78C݃M^tB!B IY=phN˙{$rL{)S*V-=0,90c@"O<:5+ެDQŸUBIۯItA!Q! V}Lj/{)эwG^Zru) z .aA)Upܻh;ai(L=hgsjq u'B!dYsá0_^ߕ˨Xn"-Z)]K6r*ZMtrU΃|&&6?hes !B!H ҁ@X}_RM5(790if0kOxD]$<3ot@'̻H5{Z]G˃B!Uf=% A7umtaMmaU܅@gɵ^2-DNcم !fJ MShRt4S. k:4]B IDATI )CtMBjФ:%h^{!5tS+3B!@Cu4%&\?=@7s))}7vꢪ֪^uaVxswnyo87.yӷyZ[6È^ Prpv}dhL iA tS(!a8{ZBEBvë}y5z !BYcU@ m:(nP/"9b sriqh:%45; ӯ7^<P67;/-(Dz4ռ kN;m2&룇'GЈVQo}OS{ޭ0_~* (u1{꒚9!B!~ay8]߱KZ(`GKoy0 cG0vM/7_^.9ӳw;h|o* zHO#,w!<`"ϝr;u6yѱImnĥٺmU=c] %ꂚ9!B!~([^bm}Q/MV{= coדPvHME/8yq:|O _7H{һD-,Ѓ?s箜s;^x ؾ#a]~mVʞG|2r#R(x]7v3B!B!*%iłVn^ŒlcV9%νX\ U֋߮֫[׽;zh_GXwy@G>@:z0[H/X:{^vɽ}=)whT];+H,)to4h>d!B!*Ёw]Qߪ Fign`ۊ0O@엟k|˵gyRsS;hw+LI򢇟 _7/v># EYXzξls'ٔʼn]Sj5,RB!@zM۷FGA/X]9梹7 l_/%Þm[1 M):* ʚ3O\֟}51>߼ r]7Wu+'l`IDžkBY/ح$:W݋077aqR{7{տsݻ_O]_ȺYs̃}BU&HljH>~@s_^ZF=v4=jG 솪=9wyi IB$I*e&N|'%ͲOST%i=$B6$:*VÍ^YMaܻ0(B_qս`=fA"u͓=K]u]v@_oh ܌g|qS΅x8uK7y4-?p=W[wQ  N!nN'M[+[' o'JeZFHY@Z^t aP(Ť o/yJE|ܿ*c}JߕP;ߴʳ3}^E0 {Ax!0q { fq]v!톺]]TWōWk fW{iI45L_gfaG\"޸s^ X]/P4-(o y][unWݝ09vEX;Y8=|!Fs GCKC^C+"bݶ󍅗o>Ut1>}We@Y%Q4YGv 5ǽƋh}AsB8Dv8Q [vZ 0XKe`e xÕ\,q uLHC:R.> /\b"4! ^>\~}&DQX\- @Ȗ]sPSe6l[]scj}ta~=?{G]-y 5w̹uÿ+ϣQ{pB)&Ё>YLjW))́nypx\p$5Upc.9Q0?f/֗kߞ]A@O>d!uMXZʁcfet k@EQ1LY2L9Z*q&tM$ut] RmMRrϿH; zA,)  Vh4-/5jZtnq9][f;7n>U&{`)Y]g N$IGã砏nַ$#S*Nj39 gV%K ,-ۧ70n~SK=u W/uaS@3xj\TOo] Pw-c n^w/,-x7њ[,)Nv?8ߠ9Ap" \-lKӗ/okrt˔u6mШ,! :aBF4 Tڕ++wnzowZBo bhLu B!-֤t]iS ]9 1RJɡ13oj굋/EA+= _<:O*!}F? \ 0 rn昷U Og;aRٍնEu2Z< 7aq /ɦ&|n b豵4^n\x/Ub {ɉЈ*@q@T, &rbYE#B)RK ¬773fo{wn:3Z ڒyXl<>?lA7!]3ҕR#dUQ P.5/B 13?tœ3/|^Xږ i&; r"f vm7x,Л[i0Y\yj0B@V*Ikl4M̀P$$๞s3?YH[;6_xY4 }T@lGRPBF  UҏDbVA۾ 4s'NN=Kq#.b, |,RkaHGtT .ruzFP \2+n]LN8< Kkh{x$>t4KX|Ѳ !l\:hByRAAQk<[A4(zWUBG=!kD? 6oiBԠ[aaoPyt.AʉKA|"@phZI{{azܹNA̞BB]G( !,V{ouξQߖO |;@Q =g=1 LtzMsDRIvօ-OzIzhC,*£bœBH#Qߏ(P)wyxW_=]Xa%g{ .Fo=Ił*x0G7 gt $`XZ!-Is֣ցv5ڒ#BO")y"4G&|'N 2cb+]G91 (kWRP@y<+)3=桶{@e~ ZK~EPB!$,ng8@(N7/ˡ0+BKhy) 1*[l  Pb*;Rijv+qLLKhcBoB!.ܡ?kC(4h;E:!=_:s5Z#aR 2߭0Gq*lzI0CH'B!QyжLi{?=돿7"OYc@AAd-Զ22v7   bljq4G qi B!w&k o1?Oi#7R Ls~YA?{~cNC55e=9].Q͞rx.Z2-<9ulq;O7Z?ᬛqtB!Bql1HЧ¿ % UMM٪<`V<%$$~]x%5)SI+H cGML/J|Z]Gy?Ch st4|B!X\('>Rrrp;' IxУ#삔{|<$7%wMzy/y#+Ke)@J薅Cۦ;o8h@tB!< 照~9hug;!@!Eh(y<62w9Ǽ>mY<1Ty|RY%ԣB!dU4rYNٷ^/zxz@QG[C*cmlyn?i;l]y[j$B Jrm/X),ԃ焐E-zp(Bi~9}PҜ~W5_qdZ^Ja wL612]s͞V@+q_0'=B_Ra tr(%&ngϺnGVwsBRKnj'U$亚g׶ٽ S)*brdB{jchM],4u3;!~HG2Ol@'+E!a٫[~)\{) zba!H⑇~w#3U;O'x( j{%us7 $xՁBHjf] IDAT.6OlFNV(jޅ^-NO:!)@Zt -zhapqD"FVe ƾle&,(vѽwzv:8Up{ uB!@'R@ePrjӯW 8DBB@fr.H]Uv0Nj;<7y񚆱4*<ҜRM4ŖBY<|A0'wn9 )*E L*aB!BNV?aUsgf*E:/hvPpmѶleKM8{J݆wu ͩt)kbX?}Q7uǩ.h\ v !P5RENի g^s._=*񾐐&$ЁΥLE`Gwyiz)̑3}=on-! bE>c}=(VPv ~tÞ,A&B tfH 18 lTI/@C/ 裻0b{5ϼj3O#{,8f"߿ecFy|kvmaK/IaqKQB!O@gw#LKlu=os^tB@h t @\C;r͋Cޓ$psM89Ƙr_ L9uSwo\vfqb^uB!dm\! FE_i| @ў0Ҥz t@^͛]'"g6e0.V=*S!+ʐ^G/; %!(ɚ"`r |i Zs) O= (nك8&{ǒZ<Ӽ<j0+\Mab0CcڻyǬŹXBRma:![(ɚ#+z쵳έ71_{ } 'к|rB!X^z>[]UP ePR@H RB,襊Y,eahT (R4M MkUKԒv<:it#~@K`/ygܫ[} %XcvgWy/3ԉmU'V,On2.8qo:Nd_@{SX tǜB!`;W=°GbyH-x^.m9:[O&&X(0MQڼx0 ~~Os];О$,ܪmۤ0 a ^-o+[* Ǚ%O`e@{ddR֝=u(z743 3B!$![9ok`|h[hnmRqj7or.s[e]WK(I&zP.{߾ކ/sI O? t}z0Ph80Տr .0k칼"l?yB+Uʐ-G1.q4͂.F!9]Ks_o|,>,B=ؖB[Xx@ Λ/j՛Rx!REX6XRXsN _G\{{0FͫޛCrB[y]z#Sms{7@{e<(8 !ʐRE}prS/:w+/&G\L=!B2z/)3-ЙQtΣp2c}I807_FmM2=~RE&ڗ(i~D5J{7Gz i[u=) 3a)ƞlށnlD|?Ju00hā·i=Q9fl-z4<@:!Ć/.;}4|y{DžGZs ?7 4L3j΍ל#cZe`D%R oPĄ!GďVKoK+dx0F&=8 nAc9x\k3p Xk: !v6@.}uċZd%1eYA5o͗AOl'5 &*nx߽sNH t=Q\XQc~Lj" *Cě`Ԋ gzGJ(CbXO9hоcܹiF;BEu뙯ֿ8GSZʃyy}!q^8ZܺSfZ9g(Kp'8@z/ܞKQxXh0^Ԯ'a=goZcaҠRɭgv֦&UUڴ B G@0P'ذYܫKޭgZ*|dky b91;;@o.W)80]Iu+*3̝lhUkOt sxBŅYorN5Sz=w8-z" Onޡݾ!k,Z@w*.BlL.w)ЃLKhy678*݉<^rgf;(n\(umM"8'"Þt͌o4OXQ|k-X;PZ|$nB Xmqcׯz7w3kBlX^zI! D{xw=qٝ;b3-݋E1tmkh iҢz@\Wn6%p{/U ci0Xx bP[vnGdebE!7HWLKj7/}ß!~ V;K90† clz#̣M4Ml*#G3O7//6О{@__Dl罾ٰZ@bsP=p{k}j`HBXU|.G2LG6,,cqN0 #k*̻l>}ј#n,ιQ›{KM|ܲzJj{/EA{&IYE4-;f4 a hݙS/זZS ˭IWVq#u{^l{N-SZ0C;}\U0d8_K(8m?]5Ϸ_Փ!i9ߕ_7~4~HU">5EyyTdqwb}>פ>[i;R:#U߂B6)/ڧ\{qi_D;VY4E+R(lխnvoۻ v{=`8;陛=nZ2)N+EX4Gx/߫WŲY*"232222 ^erjÀ "wv&;-1K1]kQD\gAJ#rH"<ۡmXG £ly=N]\.efH7k >|V3/! fYAzckjYe86FJ"I}< QzLjxP3ˈ9Uj&g3QrQ^ElCw+$j6^:1b,}3[WAoAc EsL^='\&0Ӣokx/{ 9n3gL;Ao×K[qs~>MR k|G&/1>牘8lC2IuȞUkW;d u`P6Q ?a2NEf.,Y)M& Q"EKĪ=˨4zֳxVO}#Hfag!Q뽫wg0S'\9Jrr1<6,'*`ݿz>5~ g Dkr+BA/c."L CdD}¼F/y|lqDZjzuv,W.w?g#ͼ׹}Ol~HsjO睬ȟ5,|cTuJJL*/'F_N$>EyOT#&eT#d@=L*52AJu]ӈ꺮X1nvY., 㸣9x ~G2HC8O=ắDU#gYGoRթd"iM%zR (0CUUM'@' V!TDap9crs]y*dlq:[e&o_-8O:uhF&Ƒ'&+Q_̨lڶ%LPQFE :U Kǵr.,ƘvN'ܙ{*"ncVw5/1x!_,Aqku$ޣ;']M>Nzk-F}ٛ/cQAƱs;pY 5@wX.ֳszU+ ݁/E#N۬ PSB<͖YdveyD!fr!3 * UZ3"j2M-/^yOyB&i>f9 Q@ PW$כN%AB5ڤ5C& m Q @9PAA8'sB8bO3L2O32gP,>{#w߼0<`j޳WlclxAX^ϣ^T&&xQRyٸON]W[MiVڢP4*B`PJUp JHZ\`rN\f޻nYpmw-^>?ug"wCw|JZdʏ:VlLR(2ҹt|j}kWu$RuQDm&H !PAA@^#w { lpnaqpswcs'_(ܱ]~|#qor;͒XwԄHcXI'p<9[[܋k{$i#$Q:j@#z{/OFUӍ0fӄ9STJìXYɻ;q_-X,2PF-cYx>1V •;j!ɢ]WfKm245T\#6uKW M"/ꅝPVӺXUy#ն":ʞvڟNU]ߠ(J$@ J)[T{߹ڒ/IR8x 69csn呱tmltb|;Jr~O˅wwR8d6NnܦiN3tA '6yR$ݤ=Ӫt ڦhRUQ54!xBBe+IEx>!EO=/o* ʭ=* IZg.U}N(P (%B{iX^QH[KM~޸QxWʄQK ^$ _3<_j QVYmpp㉦LMۣhVJi;N Bi$;y (!T'^\Uw2^ \,GrkOGrVW%rl$i]7z75 Fj[%@ J |X|6c" Ji2dZܮUu;\* '>yڹBa*;11Q!J(CEI#d&Ӟl^տno&ܧ')U:-( =ÿo%@PH nuͤU۶[^%+o}:>:znxR{Kj-,4ʇܯ:(E[٭no>?mP8TRFyƷ錹QձNxf`UIUHB Jiݯ4#xUKXB}:z&-j =&߷mrDZS9\^ٳ##'@y%#LxN=޻aPߞЭ4tNG)iϕZmPʭ9ON9'(-p(s\Ls=q-~.F^/d[(܈=YI]t@y0S f:i [3lߤdVu$A#S< .$%bN}y+s (T}=&z=cSS|wYCV_ LZzu>9<19~ݻo'Ean%%y͆-]mͫڞNkRJj$,l BN꺶 ;۱niDz?rGG|096144tQxm% Ks`yܓLePAЋCνk^mp5DR]ݼh]M!B܂/%S&+UQʓP-hVU4@E[fٟL|ygu\fqhz+01.)W>uQ)R\*2DtH ±}W޾5uT4YI] 4xL_^揔t3if3L畴ȟ5,?Od^3s󚧮>瓘G\""%ҷ{DqF`ܕwDZP0ilJk5Υ,q*8`G "j IDATc TJIC{Q[gV^ijjj[~sw3Itةkꠦ YYX<ׇ@iuo?  M>26鏏}V(&7 v2';&fv=ؚMM-t]ۢ( \m$=I93\K[? ō67>|'? q%12}Mt ޜ`1v{+}U'M4W#TN=vglsA[ɳvY!1w@_URū!O~mj\XoatyP7#;;C'߷~s TQsy%A#牴}r3g<"s!9q]$悘{Xy$M<ץPS/=[׏ BKW2^εLGGsߪULkj(CuqDzFo|vgFEڸ,Xۮ{ڪ)L9&{ vKuCC a"a'ICLx/5-=[mpc閦=jnTe Q]S Uc $o9ȣa&ջj?:tG;/a.$O @sϽԪUMc%yJmf"x"z9驉7~p'7 ^ g x9[7LmܮF虌VK(Cp?|>es@HS(@)6>ss ~#9.l yM'.d <;w ok+~ݘkؼCǬμ%&3z"a84y@]R} إ3]jnQoy?9rx}0) ɭ;6vuwui0NH(ZJv& \MjREQ(dBbYnRPJSJ\q/b1<1X\ܶPm o/d z ~!n=ifJi;QHh* 1mYBGJvEUo:7rĉKbIۚyٯ7}YUEUVQ1#LʫA8TIӺ+/ktt|WkY$kX1 " A,Yq;&(UV/Q=jDУ(Kҏa@}c`-y pǟ3u}U./˚U+m,[pU8iԞU4j i 7?x-zp%tY(I子cV4UIMZefL^js/$,'$IS: *Qݽ4Cவ[׎ZL6[(B(8(T(ѩS}BNJihPQT© P ZVI$5N2kb^GQ..IxT␴l^C:{Je/u'UxI T͗`\s]-wڵkd{Z-BF(9(M-'ȕt:m|/ %MQB˞aP(BL{{|z;c4Nn> yךcz{sg~yφWB.TeR&3Q,5gYƸcqGՉ4Y+sY|nq6ot86|:Uvg\{6eCQwY`,B@~tZ~4n 7~Qxӓ¬FEdFq :u~4yE5fِV=JIjt\j"M F\ V7U!"him>[>Ǧ#VCw.BT*|oU6! k[l9c.]]ҷW9-gTQjv۪JH#MM(,W['odxٔQf>+ W[q,Su| (_sTDu*eF$UI &广Hlu%"WsQALYGާ/z)S4X<3\KY㥩Ě9RIeKRq, .xDӗl[(_#On=(֬̅㲰twc@ΖdN 6پziYV !* !Bs2MM_ݺ}~--=1UhEysYY7s7B!y/y|? z㧅wr/g+^\tnl|k.e=JQݜn9/"@ϵt( [` ?^O #Y9x}?ZtH7-zK; `Y|‡c%e)-3rLrHgQATD! itKRjNK|9D ֐GKKxYa)4VpGr3hֳyC2Jl?"E.Ke/ 6pcLjp]vϿmu/\'1'*W%ش8aޡ,s}\ ϳ#ou_E瀞^H}hSc^us!=74қYN8E^|'29N1f#t xSBU@xu!H'0 =Q|1ļIfMZ(#U71Oz f\&auyjLsRMyI]V/AE+P=u- ז ߥ<𨒩{'+K4>U,a['kQW Ƙ _6x[c.@ΤvͶm;#$Hzx1N-xC{ Dr :[ o^qlÁ3ЉֹJ{w_[n\to&MFS=Q<v5rLJ%i\R<B+9Ӹ,F٣C4wڳ.ɫ1R9 cIPÉa۷?~੃̇ rԥWZXmQ~dS}@ސs{384(knTް-rAXOԗ~$_N$F u>7B3G rn# b./h_\9L'~&ڙֻ/PAb1t8f.# Mg5Q4Z@,I+΅ں3)UEzPgxcIz@]*˹"]_P} 9)AU""qB%M뷣$]cJuC9?1sַqO> Qi{9FЁ I 3 ?Hi{o,w۪N*IhD VJjv\9dZjVdDQ{aV4/|YI ^Z)tHiPj5ҶUk)rҷI[m3~mO=xcidZ1fWuE'4sv'^z1L8Y7kg[S^H4dP)tYk&w}k՛j:I24?a U z#9;da2d T}&;rOgmR27JcuX]*I5N0;` :N󸠟&;{qآWW.[Z 8+%rBD̠^#F y,6YW.tzzZvTQaQh3mj:\##tXݿv?w˕+W.\)O$ORJF1su9xZ,f>Xlv $X#'hTO&bYlGAsecw2-5NǦ SGc,!<\Pߥk&}?UWgYX1#t*1j' եu0C.U;dZ.} tt4{r? ZeY?].e^>X ˇU+CUmj5yzj}W=qld_ضmp/hq]-G,u[5=?Dr ?iBc,/ІTf(FGpl愒j1{a\' CeҠV^[O}^=o>W-R@.kk֫}ڋAF Fw7O/"p ' À* :y SGK$ or/Z?2t3pl$Lj#F:9c4 5&1ˆ$H2C2FUUr$/~iTVHeS99 NJ"ݼi"a\A_1'*o,Է3vwt+wYu 01{pض1\HυyUQ$ElU?~ussgC!EP%Nb}vY<L%] 9'ہ49Zs+:cȴHre%wDvgcCOv$J&Id1bpv _jBz.e}Q M9.H(KgO{}cEUqzL9@ W&T&2慤"J`0UXʹ*v ־z ]6.٩TR6PN..ۀ=yĜBļZ~eߪtlCPd-F#֎jrgڮ뤝5(pgzT5OJIo~@]nkO!瀦#~ݫJne#݅~[LR@k'}i 9!C(' {s8eNE~0#/*ywTUˬ}@#F4ܺ牃߇r $=F0iEkj<GP^rM8]Ծv}=3&{Y/c8]t(asSΑ7 O΅ì>"B5&_v7" ՟NYGsS|÷OOJD֎ϐ4b^/9 Z&~سͷO}utɎ<`[K}b.I+ʊoZx"@؟a&J[Ig ##?ͽ76_]v9/$JCոpV"aGmek7^tP,E&iy8:pP';ŸjBjhr\O~眹|ιAcNe{[gj Uϔ(ļ<‹z%ٖrlA"C?lWxgqez^I lnxdd݁^+">WLЃ_j`C< 9@; {pq1۸E]yumPA 4SbD֜I9PFH%R]A+Bӂ>(ՖWM}^'% (-n^.R{Qx}irh㘠@ʝD҅HrHzʱa]8Lݸ WvR@^M}IDvQYPZ.Z(uzZ!"e?$m0XUuމy:ybN ]k6"fp:1 ߉^i ŭOZ'_?ǿmqoepT5c@S#5d5)8 %ˎdj9Ϗ<F4w({k7IFT@̳Dy@ɯ8GHݗl%je)zZa {<` CRj^U*$"rBg.fƍj/$ĈC7u6?w^ٛ;g,b!VVC`~IH=."*%Rwn>s3G 2]o~#F%g_O5~Ȋɝey%.!dUw[;niSjN'W %ڷơxREGzzT{-F H(0~߽k;!=ϛ($&shpf?^ZSҖGz蹓֙nջV[7mYKVO= RmKD;`-Zr&rW` d..ů %!Ĺ|bW= U7Z^-R X~ǿ /~Di/prZB# u4b[LjR7~|}xDM?]+TÛ~ʐM]*P L(gf_;/{P+KH6ҋG)LaW;q/qYcPmI`hqon $"Y1^۳ͻ輦?xo<|.T]ksujڽvoMI;.GeFG%إc*J+QѣrZcys4ļjGAto8wºIFZ,a PubW_nEA1}6mٟ5t/%dZǜ Y'.8 KݱrLaRGا[(y9T - `K޵.TZum@{7S5B*t$ņ(#GIK:v&!5H{t%PK|~Abd^n矘y_y;TW"敧1zo2&ZE@c >cy+FGs( mڼms#3o> j@.w&4*9_k~in2dۈ^3*}v}>D|!=?'ૹouNo $*a+Lѭ-_?61G^ rwrܣ}OP<-6q0L"$T,rk "p䏬 w p"$ imdrrxѷN[o4E(U^6V#X% `%8|FuvYNjCj1f  ϋ,-f' ύ>klv9$]VӉOcĈcA1íU-u=ϻ("A꯬+zGk?M^*;J#ds.@P,p>|ӹbE S.gzu%.~Rp vAݫ[:܁waӚn9>T7H&܉{.7٩0a|^Z1A_>])AE'QI[>&9 v&7lfliԻT*d!5؞WWI<+"SA[tuJ**wVW^byg41~8c]tKAΣ9ooz?:cR L˴N1,ծ D(\SOlߧ"A@A8P L[#|dɰ ^7:NSttn#s]~} ^!ׅH m۔"P553GS`/97!}d:Em#dJnHEL=;{,wSڶ7=ju?ӬnBຒgW( X$vyv~Z#"ZyPkvy"y3ג)Qܻ3|3| 97FT.FxK`hz 9(UZ6o}#}-,ōp! _X, %EnGKlݸUb{{N@GT!߽܃O4:B.'n^p?|o(CcasFnԃ0P=LAWjTZ$>V(lcևn:7 &#a}d5`D_. QO0ܷ{m&0ٹymjms{+Z S0&Ųw v.Pey*ljHC`:$ɵwļV~uJ(&'O8xPmj:c% ߾2̷8b9YdR E#9s6Np;c .cp8QB( JơQULk{ ×R C`qmw@ٲC[q}>iU>x¨Ugcl A)Rt O( S]6GG'x}-z-:=Ӊ۵:2+;yp~:o}6&苇ʰ*P4Y^t0uă&N6=3}=} p qͣr\Vj|p*'xW%ݨ%ܩvl-*yl\/%J>9ylv6q""Jz+1b;! ;lnPM[V1V!fu˘W@JPE 0d2L%ҩL2jR:8c1Y|BN)B* l~*NrSeی1eesBBE Lf"Ltk2l2 Gs$ͪ69#"Ȅl׬SW߸Û) /KHHiMͭ=A60tl%nzO]^/ Z<2=AovN+ɯh&F 7M޶۷A@ËQCS_M7+5Š);~(8OAxE ˒u^̄'Ɗc;O^IYԿf]L9LyH"MKR" yl̗BZ>ӱ)-$UOXIC.sgݺu2i\,0qM`O9rwܿ;==5^(rv,P,/J* ;1L4D"LRij*Qܶ6991:`U(|P(l.zxF1k覙L&D"Huuw^EHuϛ=<<OϞ>AEF\_ :|_ΘcLMMOMMsfWW{BG9Uզd: HlAOswRRd(<%Wy@`מ6yL^:m7>,Hn$;$,EVXWW͎V+ӵ+ٌv+>ȴzvVf҃d&$vOtwu*V(w&dޑw= <; >8G 66v0ni ZzYe7ʼOx_W2 yxZ6Krfe^;o|p˺9s~$_Il@og8">?EU^:x-['zy [o3ˎRij_uɘnt8hN\W2gN(nlԇ:;-9$,ӜٛfmRM{$]_E"wCz>GGw_~C#Oc}!}!nݐW@09$\0d`yFuѥ#_|u/ۙ7;pl-O"F&ofrh猁n@;N{LY`Hv#03 'qP'߇J]f_xn۾eMgs4 fk^{gõ6Wjoޣ,I|gϽVNj?]PfBtBp .ÛEldD,]Ru2f'Oo(7<\xduڶmp/CCۣ]vpc U;rO,jY^8axD1t:=9226 ]%Sx⩏J&dN~¹?ԝ'zd?t kO%c 8MiMg^|wʟXEp͜ڈ4싯 ܴe07[g?.]\sYBV\`IvS=b5Iy4`T* 3ܹcOyp||Ji'W=Ml=^{-SҴҹ'vqy.`^k]F_y4#^ӀBR pbDR>7 e1χlsoߙZ`;vعk/nܸym`]w إK,߹JY `ÊYtNq> 3X&C_j="@r/1WyΝK۶n߹{7tMOdB3h685tڟhweWo޼{~ITalr#(RЕ!Bxh sу.K2+Gы.E_rrٗR%FRϣHg񛛶Y<{/ߠ9p8_;o}ż#lxsW•y軿oǁJiaܣoؽ{מ>x|bVyvy6y= g.E& ՔPQZGLpt:,>##_YIܹLnٲgddt[OBH. w":CS7k{T._xlM;GGǶ$1Z1t*d2p{|bPJ/ZW"Y{6lUD'g W)b[6kl;l%ƀܰ~~ſSg?sG^7I{i峖U'KPɓ{AA7 T_Mϯ]tqڵ<>F0ޤV 8ڣx`7Q^:>!֍+M '$2)\NMZܔ6D+&@lw#q00mJ߽sġ_.!$q!؄h)c8ˍxqn$b%T-//>}/[,eMC&3}j-I IӀ*]:Q4~aY%?|Q^tZv׿fRG yjWk#ſ[[pAʼ1xm vBMzIA&1Ùؐ=!|hKo  `ڕgBjs1Ơil}ꩭ^719\kי><3=umyyiJcHR3L޼'W7?^zLNĩm">gIK*肑 7'Q׿yQ6jyr9 z5Ak>p| .+tk)HvCQ߼`_u8M ) 3TݦxcfLomܼy4J^uw(x(Km̿I~ B}@Vrٙ鋅mĨJ"yP@ƲԽz[I &d&&&TPc)f߸qîdÎic"f̃LJ&f'JgM+'iv呦ܙ%Bz]mB:C{~mx|i}`/O/i0wTJ̺yjy器1ρޛʃd)*z9~ +ӼoZpz~uJTkM"مt"ci&9VlD#ȯ=p^02OdV {?oF|6^Ble۲ޞzoDovaA{?9S-ܣ;R6](n'Rٔ?ĝKyҕJ6vd.M\%{$iYW_=yn!ّL8v*McZ~m/x{lraE-:oh ¹ԓ3T&2(C9qۥ4H=-x=E4iZRu]'Tz.0uw$~U4ͥC2A䶂njpMH5J~NFS^n/Ǔcb_0wyGmV*FZVzp@Lr|e a i>}g-:pa>t=A=ElPj!۪]*_:i)"1Cv_%axJ}?G GIx{@Vvc Ї,IrjM{Qp8+GըS Q@O!8eV2͵>_Dk-u?˸_kbی]8nB܁~35DP.Q{_ܸd^9q8p 1G۸*Wwt }^T5޽a/qD1ɲJyЕ:)~CyyCMOH[VŘ{}vX sg%^HR)Md3I 8Wor?eB\g`u&F%ARrRnvx C& 1qk2.,П#c|Х<=u|Zs1z ]Sb§ؠ25w^*KZ)#-eUjBv-`wc ICŀj))u[~xiYEj5$,;aЪ~?eJiG煮fpIn"ĉ8OvF峣?-}L)?Ch%{ڷSU7R:uM*%f߼b}ͺpXX|oʃIƗgy=_]} oA1" xo+\n &lS^q=+.lh֝r/@b8ZXSjafɪؖ]" ΎiD8W 5u)6-1s М~_dJq2߿m]-]yxy)[Kp/ `-OFǵI>Z]SGR8?@KAWdtp4,UKE,KAW?*p"n]zك}0b)ߍtշf%=n(- %{7\yMKkƽ.Cq jW}\Y.U!4MC~uK6 薵c OT Գ[ɓ7/W.w)+p`;G#?1]STuH߾8)}o%ZSs>,<wT1{{MAWılŤee4WB_@́ Tg%D[K u cRL #4znStg n̲_Nx<>Fm_> Wzd*/-s.;ߛ1ل7}R҉wm[R"]g d؇p8],Pt:Sry ܒ"l{/H6: LRN\b4d5R3k CW4Id^{Hdpc"^P0DxX6[!B>;SݟWC8`νe{9&n;bV u_z|RL2FNplGS8v!zp']7JRZf`_C:9LV h ׋ IDAT]+zקԵ󕏈WRD`:wx,}G|DX;]W?q U(-a̳yp6>w({ɇCny(cd򫿟 oߣҕW!|537k$kUR4i(NSu~Q^9mEc @@`*ӥggX5p8ê}c_*@WV-g~=0>[Ig`,Jr} U)[ᛅctH*Ƌ0ʰabo?eC8O"ٯ԰Ii+)EF\f+n^2/괌`R8٘O|FzM[346OIӁrU.1O./Ypqw9Jݖ'ݶ|?[( x%G8/Yyek > )'?!!ڦ͛sG>z~]nN&&WOD6 V)Y׺uF)Kuš*5/*9bK'p{`Kd9ؾ*NbNNuB>q<56m0wFҹŏM'!+R~oG qpQ寮{Dk+]`lf(!7~=55u .87ŧ.K$C5 uk $>J!M榭'ޫ|rRe G{%'Nmm P,? ?0ލWppλ_ =]bّ8e4 ׳Kh;0浅(>/XC61cL冿~+;G'6/; y'JJJJJJJ]!U1-K*g?Ui"'} s:HyX6' /:Oz\ʩ6~dgn@йRB|xP[<l:;=j?)G;!,Azp#T*]P_]:{Je.qԜqB- /k|zmÅ=sY/S܆!z .W}ɼxpD iKŬ '͏V\0 (KR?ēH:yY 0,Fd߭l ym&:1膱y֧Uk]srp^N녒)nJb^;-Vu{$]c yQt4V(}8Cg q9Ua}yEHE:>] u_w76+ךлY:7^~ Е-2qI:Ho6-5|v`-nE2B6m|țoLߛ߮]| w l`fJJJJJJJ42mm[wo_7o̼8_Cƞ;7y7f}#(7z"(X~7O1d@uPy@*J0) so[eoAkbyBfkqՋKóxa/]III z,.ثoZ7]O߳,9y]>9Ǘ 7l[G+*Խ7=wD_I|jmtSF֙򉻷{F|{<@aB.zu"ۯXm(0c>&oS42B=t6?~v1/K*B[_j"y@ME(u@ZWs#ϋTC PPs>yg90g/eC=k<CDᎵ"ޝnx3JX:aܦubD[i:z|c$MbsԠ̉d= :/i6l{{6ZmEQv;ʣސ24ei$HU?A|s"./ z'I3/g2݄MBQv<(yg=Vw#6C_ړ?ݻ}f"?8dZdf-?qI@|sD0F=m:O4BF*,JŤ*CÕE{ np"sšD0.C^1\`|W3QWZ.M*ef]rb5%eW .YoW&lxuipد( ͔喰a!Q߹;2vt&=IE@m]W嶆yM|fK:W&p珛74pZ_{ixXKe$ɑL*#&"ƙb#DRpZ&YaԲ`Z2-U*T)"/-G[t6 Zn>qX)<p.VO׷&C{9׼WΚN}~|6q! +W\Cb8>@"2hESpnj?8{[wL˅»W/~wB?dYyK\۾}#iXz{$`][hCly; ~mHC{_7nO>RYƗz$jWq@Q`LV8+ t?֏7I5=%0iC: Cg:1`:t!D#=Q0J@lbeªL+EV*hT`JrU%viuW(1c߸Yl6I|zSuC=0M@zfʺ{3KG0 V u`mO_zy8p!i7@ùs#~nF*˲0 mJ?|zjO>'"oWgXT~}% 6 ct_2lM;`YNB|p%0)0to}y絳9,|cqtMUURSGEi:Bx.5LZK hЉNCui  ` RQؖ 6-ؕ2LBM˪ABlr^r1]~eUa\YIF!]duϞx||~- ]X&7n<)/yb]u:\0~'7#COM6 )E}z޷oύ+sgaA'5_t%~fqSt_RK%,3ajC2Q (ZT+zEHp?<&s*S9|[z~E?މ_w ޾<{vmΟ;^ږJ! :[:ppaq28!t:=7^M&nX`Lt]$Zj#۲mܝѣ58_){xɾD|]P< 399lv3΀yȂA@0gw03?oIBII⤤Z$^8'Ԏz%+B:.sq0_2@=lɼ}K/〈^2LÁΑ T?b/d&¾rxa>N=%7X;ypNO|囯S[7M3 ` `T㶌iOMlL&ug""ݻMرd~Hqlf=YqKsߦ'IIO\E4}nlmOR,% N#?|O8~ o1u^;#OdzQ̇?-ptFߩCDVxkT3r) a8gd'R~oM%!cU1  v{j(7α7ONO/|ܝ'2s8V^&6o#JyGwde%Q` ;GG0$b"4 p<|.Be\2 {y2(?J3OǽهSK:$f:aWԪLjPQ9Ȥ7dӖ3!wCk:ph3{7n/S[$|ㅅ+.].\" 8neoL߼KWok(qGta|7W6++[$ݧ}F9sHݵz P㝒RU q^K<ݟ5 چL?<2MQwii/!~hWFǵMO*Rk+*9Odh;W?]|q8UN{_o0 qpx=.]H`1XN\f|C#?ؼu>{b.|zw?ȗˋpC05̻?(1>swЁ#c =Dem\GykV=Q(vpyyre(L8cvR8Fb}P_Lq۹E](ժVPp2AlK/'?h4w\fSGP~΢-ti>hgo'G$>K4 XY_UE9#o{rN{ܱy# ߂63_\o?#84KE&y0{@PB>,,bU{䖒Q[yYR*Jp~%8w4x$)Q;%:͍k Į>Ig]j÷}'J9Ÿ0.Kճk>YYyrfRE];W9>uǺsqD>dbps[7qӟhҔ!b;s1q0@ cԉ^ucR׍I#z)[?R)Uέ-OJ%p3p!I΁oLd{yEO1A|)ٱM69rOG@׍.N`!p-yw[ֶlV*)>$,Gdb{ۢ\@?f-+ ZӖ^0TXsOVNlڦ '%]7f7?;c&V: F^ZL߹t:=7|o-Tӌ4$oO郮y҉>4]t`LҞ[T*'WV޿ZTZGxxgTzFQ PLٴi|S;< cKi]0YU[TPUK~Ȳs:G~'VX\;- Vz"Ik86 k]z^*n]_T|M5ԍ6.j8}3Wϗ=}@;ґ̯= BJΗN>#8tY.D._;;s]O 1 |40VKr#a@JӴT.\.''-R9VX;:x/,, wXVF]v4aO@z[n7 {`!y=pw|̣h7Bm2  zOX(w(1a\ n^Etyfρkڸm|{&M#X~vÉ㞋]!9̗v;wos !p3ބ- f8BL.'&62˗zJ\~^=ss̳.yE+7,7tY eJmٲc[OmxftlLktzv=0npl[rl?`f#l!u%$,uIb!uyg8Wzr%_|50wӺ=3e]MI:)VKv9diwHo.K,&flO ޷ZfQϽ׼RR d/fsC/Nl،Jfݻ]͊YZZO->}e\nt.0R]#f $ 0LtQ|MqzDwCP젂y{s˶KKڰIzƳ׺$:~(vIڍeDIG!݆NsgZ~ؕ ]=A7~hFjb^U/@/ê'ݟ.Nk_wA57]0mM7o_̽!!:igA0$81;y?gB8\K< ٻ ]<=Ypgc@\-p]s9הA ޱy"zEy`=m JȗJX[WN$N[Ogd|;v5)ܡ\`^lހ,PB~2ax<"֪G]浅EŽh({7<>3^[e!4znfQ,)Kqƒ%9tX]$>brD!{+p D y7b8u^N~8O`C~}=k$nM kV3m si&JQ3+%jC!no/Ag. -[ kZ>Gw0WV2PV\KsW mZI]%IaR\t˺JN{(u;ڝtv·rܦ׎K"ws \C݇;ekpFCy8׳--5dk QryxYT=-&X=x`&g"E&ioqKqKҮ1()) q+% q s.ks>$;γ2_ۿ;22k4\&7R{\,;a`.Fmg5{; w={< וWo# zRX_ IRQ[qK\ϫUŪ>IX'$ id WGsN=̜ZȻ4u p.zϳ2/m!d3oΣcڛsw96Qe!0ߐU֨![e;[sޚ!0osAwȲKKY}`*)))))))uYнʒ9H`5M/%Ķ]wΓerэ^ܟөIJ>[D{sPx00 <.Gwԇl\ ғVG{==0ws׳;seoU%:1Q\$1Y꣤ԊPZsu?PՍ><=gHq:}5[P{PҨ-h`c.цYp[S`^l$@W$>`[z 條hR)o5Rw%TF\Hu2=_~ #EmdF7ܵU炶KMCs>GٛgT E:xrwTy;rCݣ  L7AIkzx&$5 j\뢮sojUIR&{ŐJ<=O Oܻtf `jv<`nߞU픭-HbNmܣZ^ͱsX*X~vrp@]̦?8.WRDOzB.9^|+cco9I@:y7iee`ևucHz&U[(^=(s vlpfX\Z<: 9w{r`tKS νQҮo]ܮÁ@O=O=Mӳe[`U6%R{y([js)I!@l͗hk~C1}J 28CUJayy&tI└1Hb%wt@}Sȳ༞*Xt̽ww܇ wd{rQ$ˢl1zrKG]р"wo!i8֧`Ǻ0+Zz}%QRRJZt{S)k][w<)GE={ @}A0ڥVVJrVs恅(nk3QzMֳw̅H{0h_^Y,KO]ZEW4ĵ.q=V())))vC,Rg;[Lvހ:y7m4J,z?({ InC<2|Q~չͻ[#])iJr @qϕV=pOmڴ巩`^9JY[NRsE.WGBzwwuK{k>XUaāpe ˶l˲iR)/Lk* wJuc$^)iH^}z,êyfgHXirXi{Ω^Y,vcj&[^VRۆl-ķ:qQp܎7:?e8phĝ*E+f)_)W+zbRR.L*UJb\.,XV_zo' ȱ7k]xv?'MCq:)URzeªA{ϵ/ -EqM"UgYP{O\b*+!Unloܱfl{p' z'*fTRi\,JT[Y2mlfٴm%J7&p _].…s1]=GV=܋^g}nt|˄hcnxsD`.=nTp&I;.߷FQr F,vs1FmbTɗʅR\*V*rѶmfv̴,2ea*u8ۻ$IR\듴w,qKs\@1묤KACqm#np`-7Mgpv߮;jk syH`90JMImR,WVV,=m fYRlx3[u+ՉCzQ}ϓL;5<2{<}ƍ_Y5`  qלoϨuJJJJJJJJJJJ=R=@'§'9a#۶x-΍))ZLi F;l +`ۯdsAt_<2|ױ ;8j*))))))))))%NAZ[n=BmDZ~w&({t0z.Ы(;@;`؎= {QwW2죳wo>Ni-58^s<)&%%%%%*+)%AI]&N* Љo>O=_# 5 I7wi gUSev'!; phyz6=0wKe6{wSjMWb' $)Iڍ׺$ nX]c yQRRRRjMxS)q/n4́((np[\rV]({(B[,'0Ghujzjs./kյ98rpjO^}"T\Ťa L{7%ɫRKj$IR{ٟΦ&j˨Aob Cy(!0{`_\ <;V[Q!΁A{b0yssYR'j+rZ$^ʇL] msdg 'yK@#e>G[0-t]0{QP ^wh02 /C#U MIIIIIIIII U]Lg7niJ%L4t:r8Fڛ֠[b0hoV[jCQam@y$sxQ@n|P(g;!0@ynB81)3()))))KIW[0R3Jbk1iyuۍ{>g펉_0ebii{ʆwٻs{f񖋓(5hRRRRRRRRRRR.h;w{U3 4ڣ`a C5(؛B W6M0vn҃ [5ak%Sf"vPjO4' so$(uI[%ժV%%%%To5yft|aYJɤv &lb(r TL֐-A{5ڐiusaܷH9 |R\RoR$c|S\ϫU%>JJJJJ(̃Γqd; #RcZ].hp+\z_$C쑶FC}g^ ڼ$G ;5]m?-"SRRRRRRRRRR kg&R!jۡM0w֨ WgRЍvko֐[` ؎=N{;/ՇzME@WRRRRR$6)))SUϹ\ߴeˎWbU`ݔ){[b,3qXY 5-ķU(@n7-AwCjolP 1`QQMIIIIIJbO %$(ˤէ fMtcCC]'9\Ά4 0ol~_Fmmu8oM0ڛW G s%Au5NW,ϭEf{bHu TmZc=[%Slv2lI]0:y`aPPyt)ܻڼ߫[]בu]mEWQ@b%xWsok%%%%%Γčnt 64MlhwnߞU픭-De (gs p{}0lDT)trFj Co wERCE80w%%%%%%%%%%={l.2ߛMynkyH e%n =Vo)s###CcR^yЕFI}kz)g$yeǮ5w{HH(J|z7>B7PB" xaݞ]3;]+ٕUczfv֖ɪg2OߛA({BGDus>Bou&=~BYwuX~vq^|sׅiw޷7ՅNcF'  f tn,.7%  !4d+2@縺tQ~}$˵59jD7uy鯝ahk\Ws/Q~W6NA ˛NM.%|Y7i;7Ͽ-vcr3 z}>a>v:=0mԉ_[]8ۨ)֮aTXrx3p w0)YTk:\IrRQ. {&YY7 u fP7;-./dYz'V_~V=0 ?>ǵ :=N-̭3 s-1pã껢~%Òěf&qM*_c9ĵ3,dz E qỲ#qH Gs8puClv oUBWs˟k~^a>y."V<ۯک0Q'>èoNW l{|ߧnuO'ˠjk~.a~ s v<|^¼:ƄgQMNbWмo^I]}a;!o19 W*j͛I˯l)Ӎ7&tCU] @8p3,bʵAW>4t*䌱 ,.1)ɫc 6`d="6O)/P|O棽w# ڰ{g z*=)+:3{za`"gH2)s8x cl8G0r\~a۴q 8hp ek,_ IDAT )¶nku3B1G`R̻ *T|6L7^w;t)|y"bs"v]v], dR-6m9H,Ĭg9\0́zP\ a%Siq09oc@I}qyCjs*{UxH/rPuՉ{FA^-^XO ?(?ӵ3Y~zzQ>kcY@`Rnl;[<u|=?y*]__D$Y%$Yi%D},"]/ Y͍\X?^U?\-Gvl-r4M,I4Od3g^oEa;EYОwpM$ۛF)] u%sGߺ7 JVV;AB I$Mv$Yn&ol*A?{w?\v]_4C Ɗdg4I,MN$=x#My^ʾ?7 g v4egea- 4Ð5`li.96^H"8~lwO^G?>/De1 3Q5=9̏>M{x4V .36殪mZe` dԕ/;Q$@}jOa>-tN 0%`qw`H\plw{0 r}4][ Ʋ,x`繅}Y}!$4iO$i/ N$diOi'K~ hk=OtdY?x7 xyު -˾50X\:n-ZLB<-ŭ$,I'y4'i֏ xoi܎ ! Be߲W;[[ a绷[|o\ -x̎t,r<>ȲT<,,eqKG 4YIBYqqɶ^lի]  ۞미6+ Xp9MxR\v.>=\6Y3!|̖yB B8*WVnhEHb)Ti$A~G'E$,"툎8p;\i٬Mχ׃1-ap?x`o![|Ysq,]bɛa57(2S4fxh=x x {~>tEے^?辰'tlb/e^~SfzdV>>n}\g6KpHSY:N~Gq}ݻ|@ +bmy¼:?8?\}va>~:}03S*+N9=QF<(RD1`\Y!=q\s? w燞5\ \qqgCK!s)2e.ifqQIQ&qG8I"YiL<<˲,FYN2@Qelk5Lp 2 CByqxc=k:z;;.+ߜ3; E.s)$D<,ʓ4I$J[G64KxNg@CupVxq\9.cqAp]7p=/t=yN຾p28 ;q$B"yii&U^8\Lzt/W'Iw{s{o+?WG9i\w2̢*+4(n|ӯk/}y~xۏu[n=p\';3ƴGտ;'a^^':^kFt?0gZBBH)%cs^ifo֪ Stf;@-mIQTiC "r,Y(*73;Iss`>TM!cR! 373m@E•7LE"Q1vP_ !^V]1Tp3Zg@jdg9dhBDψM{.zesE[UD<֦(ϕ=䥝)wsRrciHqFab1j-9sY6QĢ=^G݋>/L9=>s#dYfYhٖa1`t(^դU֏);',*;e5sχϣi%̕mP FiFpKU:R{8˴c{*PߕDVf),f^<9&YN Eww M!@ )r!& 4Se%r[i];lt/RG}yn̓aT:ϵm. !G?z#T,MS;/}8o?{;a#Xr';.8jIuzʵRC HVsE.D'Bi 귺q[+}_tוBC|ׅ29?W J̨b@S}B0@ 3yMAji.h( AQ.\Ƽ`9¶qo\bg`ֶ mvm׼nc=L݌\;AćƆbfvַ,,M+ngeksyۻΪL (O҄xDSnB2sƦQϠۼjgJ4*[_f|(3tzzZ:E㵽J erL7l*0YiAbQ oS*}VZqӡ!Z~ɼVfMN7WtO v-Y@1OWzR;u\*68wfis!fvը?soss`ssS6 oݾť[ͅ0  |o2,/= Za^'Fw ScÁ*ȑy<Yy%i'IP:7}߽ͅ;=cu|ZxuX]u)$s=WsW?)(.UV6Aff3MB3Ӊ岛k険ꘪDPm3C~6ק6h ^ ]@B,2EZԳ좌zE-uvַm8ڬp2Z2y*݈0nTUJdZAuZazx*lvVYO727>8IU0ل9dk[E,iZOoU6= IPo w0dxL|ׅVVg|SL]Kݱaҍ&Mn֌go.'ELZ9y;sR|Y8爒r?@MH˪5&QfL#oq̪rgiy֣wޝ;wrUk7I-5 ,M,cgQeQĽ(Jz8^nizYNyC{/_>;:<_{[oAyvH0ǒq#7݃1) ȟU@̴R u^0[xԢM5/wЛ<}Eg2Z3 VE% ׿*m_u)m#rqJf)]fc=>B613-^օz]H8/D7xQegYdTfgLo^%e7L;nV[V׬ȳӳU*VUf.BƉZb/Q1Jq>kyiq)Ҭ,wiy98F t=kMUFo{O?o,aӋr. v;u8Wk%U3'=cQ&X۶vvv׿C9xa>º2ibmz.LUhО-ʫh˼Z&L{{ll66m.1B=)TCUif{>>Wis]\8+|DUxT=mK)m93nʾz]]5Ӌ:ò6GUV_&A\5w0-UJN?KYLf} n >zRS?}?^X\/,M)wZNnYNZ0mŅ5֦@KqgO~;cO9)ӦT^9ӵ1diX˵4may^CYSV%ept?k;L% =89lz%y F/xo"^L.P Q yut-PJ}{Џg*L6[Č~^)fk}0K,kYm6;N+dU3o[?*;f`t!զUqٖ.׾TgRY4 wUӼ*ms1`ٶ=%Ϯ3mTM<阏b˗/>i;?ѿ{ % R nM2gy !h+zSۢ73J QM-ŃAW,I{y~|ÞGօM⹔25DG. U¬bauVo45{]&oϥWz{Ufg[ܷtݫ2uaciƶnw3nuތϗekݶ@o @ue͞H7LGleu[*==\u*ڮ NoQwzʖªi{ֳf[dt`ف]yV_o}߆Pf,rLagWdo>{Bd(G(Jyrf% nmm}{{?0_g~.iY~(O){u//n; ڳ_|;!LYU$ )vYkSV 8˳Tܪ2Yea9Tf XUR=ݫ_;B]SY㴰Y{ձtj}~٘ƨ>m\% u%MߧAau|ڳu>jۿqt調'`YO>G?mbo4aa9a94[yq8zOgϞ}! oZ1>מ>:Fg]Q1oӧ_ӭ[w9Ǟ<UХ~)+UL1 ~ң;\S$M>vz*g^ Z${~{qywq)&)6eeDh]Xxu\!ϲ}0`&c ,j<6zuLuwkڇe۶fysYNv;nǯ{12byJY^Kgayu(/U8ƾd# Ah4°8ò ppt;?'/^|BPs%;(FnWMml6VMa|kyebyiז/מtwsm P>Ɯn]7Dylv]wBW󙛺OK!΀(/_RUE G혾yϓs{9ujuYH-ua_w;'>jngۻ]^\&3;e3@&#Ylmp' `9h4FW6yHNn 7+Ng?۝/QE%(>,m`{mFTRp{~(}ח.LD6sڳi(+ANPzUst܆<6Anns]ЎGGy2->_mQs,n䫍+0.0g`lbع7n:\GOWìqZ iҽ1@JMb"K!D B$ K:|B4{Y9zM$M˜M+8QՕ KKT* /V6s/0Ju1 ϳgj=Ea# 6oiWI]Ng 1l4J}{G(8n c! ~rgQrCu  Q%жMj,qoowiF\5v3 04K?g_|/ʾ\5V^s5i4Qtg Fp[*a^ iUawpn},Q6PFV1bFIgl.4f` ^qh!Ã_iKH⟭{AAAdsh5Rʴwq|8Zcv{O/gYzR+adz?ӈ:Lq.sA޲C.+/@Ok!`\nlg(lHެrp}>nS]81= ߺCs˞k>)aƯL&onnj{WZ&qOmuEIU\c`^[j8{<99y6*.#Ĺ>J/   n8t`\(Ĺ99W@[k/ !z]?RB s <~8TBHoFA9.l݃n t ~w!j]ޜ]gp!\ 8Oq|x:,l{ꧻvRy%)lt'ce{Uw  k}1 Zy_}_0&Bv{;G^{}=mQb\C/9ce   5,]a R_X5qW"B(귢hu(NRo^8Q翹w_<a>~r,/GYIrC`N]OM   sqJD)jѷcs:qݭn߹`J$V1EG<à:>>~qyU R@W"6"AAAf\@WFR9u|zlu:WVWHa80sqqOvE:`on67VN7  xMOӄP]˪v7s:nu~ˎ ^kXnBcH$:::R¤87{]+}\}ѧuz   ^Ry*t/9X\y'=pnRuO qϸ5|RW]0(oZGk(ټuWQU4MLAAkuo*(CPL>:ZN8Mi  1 4=@9#` i s   . 藃AGDywvw>H3>v >{3w-~7(*ٚh%  `H%fiW>{τcZW;|s=}y}j;$   .˰H,VpNPxC@E)UŀFmB۪Y:AAA\$/}.q%1} 6%]{ tU ~G-siD &0'NAA1GcN(Ÿ}Zv}_7 t_)ܼ   $Я]ٖ:q&ŵ0MWf$   . WM}87anԕ8׷X qNAAq%@z)ԾMڮ.57'   >( Mm g#   .דYpAAAW דHAAA\C?/\ow;IENDB`simpy-simpy-a59c5bcfade4/docs/_static/simpy-logo.svg0000644000000000000000000003011413323151071020732 0ustar 00000000000000 image/svg+xml im Py simpy-simpy-a59c5bcfade4/docs/about/acknowledgements.rst0000644000000000000000000000153413323151071021666 0ustar 00000000000000=============== Acknowledgments =============== SimPy 2 has been primarily developed by Stefan Scherfke and Ontje Lünsdorf, starting from SimPy 1.9. Their work has resulted in a most elegant combination of an object oriented API with the existing API, maintaining full backward compatibility. It has been quite easy to integrate their product into the existing SimPy code and documentation environment. Thanks, guys, for this great job! **SimPy 2.0 is dedicated to you!** SimPy was originally created by Klaus Müller and Tony Vignaux. They pushed its development for several years and built the SimPy community. Without them, there would be no SimPy 3. Thanks, guys, for this great job! **SimPy 3.0 is dedicated to you!** The many contributions of the SimPy user and developer communities are of course also gratefully acknowledged. simpy-simpy-a59c5bcfade4/docs/about/defense_of_design.rst0000644000000000000000000001771013323151071021765 0ustar 00000000000000================= Defense of Design ================= This document explains why SimPy is designed the way it is and how its design evolved over time. Original Design of SimPy 1 ========================== SimPy 1 was heavily inspired by *Simula 67* and *Simscript*. The basic entity of the framework was a process. A process described a temporal sequence of actions. In SimPy 1, you implemented a process by sub-classing ``Process``. The instance of such a subclass carried both, process and simulation internal information, whereas the latter wasn't of any use to the process itself. The sequence of actions of the process was specified in a method of the subclass, called the *process execution method* (or PEM in short). A PEM interacted with the simulation by yielding one of several keywords defined in the simulation package. The simulation itself was executed via module level functions. The simulation state was stored in the global scope. This made it very easy to implement and execute a simulation (despite from having to inherit from *Process* and instantianting the processes before starting their PEMs). However, having all simulation state global makes it hard to parallelize multiple simulations. SimPy 1 also followed the "batteries included" approach, providing shared resources, monitoring, plotting, GUIs and multiple types of simulations ("normal", real-time, manual stepping, with tracing). The following code fragment shows how a simple simulation could be implemented in SimPy 1: .. code-block:: python from SimPy.Simulation import Process, hold, initialize, activate, simulate class MyProcess(Process): def pem(self, repeat): for i in range(repeat): yield hold, self, 1 initialize() proc = MyProcess() activate(proc, proc.pem(3)) simulate(until=10) sim = Simulation() proc = MyProcess(sim=sim) sim.activate(proc, proc.pem(3)) sim.simulate(until=10) Changes in SimPy 2 ================== SimPy 2 mostly sticked with SimPy 1's design, but added an object orient API for the execution of simulations, allowing them to be executed in parallel. Since processes and the simulation state were so closely coupled, you now needed to pass the ``Simulation`` instance into your process to "bind" them to that instance. Additionally, you still had to activate the process. If you forgot to pass the simulation instance, the process would use a global instance thereby breaking your program. SimPy 2's OO-API looked like this: .. code-block:: python from SimPy.Simulation import Simulation, Process, hold class MyProcess(Process): def pem(self, repeat): for i in range(repeat): yield hold, self, 1 sim = Simulation() proc = MyProcess(sim=sim) sim.activate(proc, proc.pem(3)) sim.simulate(until=10) Changes and Decisions in SimPy 3 ================================ The original goals for SimPy 3 were to simplify and PEP8-ify its API and to clean up and modularize its internals. We knew from the beginning that our goals would not be achievable without breaking backwards compatibility with SimPy 2. However, we didn't expect the API changes to become as extensive as they ended up to be. We also removed some of the included batteries, namely SimPy's plotting and GUI capabilities, since dedicated libraries like `matplotlib `_ or `PySide `_ do a much better job here. However, by far the most changes are---from the end user's view---mostly syntactical. Thus, porting from 2 to 3 usually just means replacing a line of SimPy 2 code with its SimPy3 equivalent (e.g., replacing ``yield hold, self, 1`` with ``yield env.timeout(1)``). In short, the most notable changes in SimPy 3 are: - No more sub-classing of ``Process`` required. PEMs can even be simple module level functions. - The simulation state is now stored in an ``Environment`` which can also be used by a PEM to interact with the simulation. - PEMs now yield event objects. This implicates interesting new features and allows an easy extension with new event types. These changes are causing the above example to now look like this: .. code-block:: python from simpy import Environment, simulate def pem(env, repeat): for i in range(repeat): yield env.timeout(i) env = Environment() env.process(pem(env, 7)) simulate(env, until=10) The following sections describe these changes in detail: No More Sub-classing of ``Process`` ----------------------------------- In SimPy 3, every Python generator can be used as a PEM, no matter if it is a module level function or a method of an object. This reduces the amount of code required for simple processes. The ``Process`` class still exists, but you don't need to instantiate it by yourself, though. More on that later. Processes Live in an Environment -------------------------------- Process and simulation state are decoupled. An ``Environment`` holds the simulation state and serves as base API for processes to create new events. This allows you to implement advanced use cases by extending the ``Process`` or ``Environment`` class without affecting other components. For the same reason, the ``simulate()`` method now is a module level function that takes an environment to simulate. Stronger Focus on Events ------------------------ In former versions, PEMs needed to yield one of SimPy's built-in keywords (like ``hold``) to interact with the simulation. These keywords had to be imported separately and were bound to some internal functions that were tightly integrated with the ``Simulation`` and ``Process`` making it very hard to extend SimPy with new functionality. In SimPy 3, PEMs just need to yield events. There are various built-in event types, but you can also create custom ones by making a subclass of a ``BaseEvent``. Most events are generated by factory methods of ``Environment``. For example, ``Environment.timeout()`` creates a ``Timeout`` event that replaces the ``hold`` keyword. The ``Process`` is now also an event. You can now yield another process and wait for it to finish. For example, think of a car-wash simulation were "washing" is a process that the car processes can wait for once they enter the washing station. Creating Events via the Environment or Resources ------------------------------------------------ The ``Environment`` and resources have methods to create new events, e.g. ``Environment.timeout()`` or ``Resource.request()``. Each of these methods maps to a certain event type. It creates a new instance of it and returns it, e.g.: .. code-block:: python def event(self): return Event() To simplify things, we wanted to use the event classes directly as methods: .. code-block:: python class Environment(object) event = Event This was, unfortunately, not directly possible and we had to wrap the classes to behave like bound methods. Therefore, we introduced a ``BoundClass``: .. code-block:: python class BoundClass(object): """Allows classes to behave like methods. The ``__get__()`` descriptor is basically identical to ``function.__get__()`` and binds the first argument of the ``cls`` to the descriptor instance. """ def __init__(self, cls): self.cls = cls def __get__(self, obj, type=None): if obj is None: return self.cls return types.MethodType(self.cls, obj) class Environment(object): event = BoundClass(Event) These methods are called a lot, so we added the event classes as :data:`types.MethodType` to the instance of ``Environment`` (or the resources, respectively): .. code-block:: python class Environment(object): def __init__(self): self.event = types.MethodType(Event, self) It turned out the the class attributes (the ``BoundClass`` instances) were now quite useless, so we removed them allthough it was actually the "right" way to to add classes as methods to another class. simpy-simpy-a59c5bcfade4/docs/about/history.rst0000644000000000000000000005225213323151071020040 0ustar 00000000000000========================== SimPy History & Change Log ========================== SimPy was originally based on ideas from Simula and Simscript but uses standard Python. It combines two previous packages, SiPy, in Simula-Style (Klaus Müller) and SimPy, in Simscript style (Tony Vignaux and Chang Chui). SimPy was based on efficient implementation of co-routines using Python's generators capability. SimPy 3 introduced a completely new and easier-to-use API, but still relied on Python's generators as they proved to work very well. The package has been hosted on Sourceforge.net since September 15th, 2002. In June 2012, the project moved to Bitbucket.org. 3.0.9 – 2016-06-12 ================== - [NEW] :class:`~simpy.resources.store.PriorityStore` resource and performance benchmarks were implemented by Peter Grayson. - [FIX] Support for identifying nested preemptions was added by Cristian Klein. 3.0.8 – 2015-06-23 ================== - [NEW] Added a monitoring guide to the documentation. - [FIX] Improved packaging (thanks to Larissa Reis). - [FIX] Fixed and improved various test cases. 3.0.7 - 2015-03-01 ================== - [FIX] State of resources and requests were inconsistent before the request has been processed (`issue #62 `__). - [FIX] Empty conditions were never triggered (regression in 3.0.6, `issue #63 `__). - [FIX] ``Environment.run()`` will fail if the until event does not get triggered (`issue #64 `__). - [FIX] Callback modification during event processing is now prohibited (thanks to Andreas Beham). 3.0.6 - 2015-01-30 ================== - [NEW] Guide to SimPy resources. - [CHANGE] Improve performance of condition events. - [CHANGE] Improve performance of filter store (thanks to Christoph Körner). - [CHANGE] Exception tracebacks are now more compact. - [FIX] ``AllOf`` conditions handle already processed events correctly (`issue #52 `__). - [FIX] Add ``sync()`` to ``RealtimeEnvironment`` to reset its internal wall-clock reference time (`issue #42 `__). - [FIX] Only send copies of exceptions into processes to prevent traceback modifications. - [FIX] Documentation improvements. 3.0.5 – 2014-05-14 ================== - [CHANGE] Move interruption and all of the safety checks into a new event (`pull request #30`__) - [FIX] ``FilterStore.get()`` now behaves correctly (`issue #49`__). - [FIX] Documentation improvements. __ https://bitbucket.org/simpy/simpy/pull-request/30 __ https://bitbucket.org/simpy/simpy/issue/49 3.0.4 – 2014-04-07 ================== - [NEW] Verified, that SimPy works on Python 3.4. - [NEW] Guide to SimPy events - [CHANGE] The result dictionary for condition events (``AllOF`` / ``&`` and ``AnyOf`` / ``|``) now is an *OrderedDict* sorted in the same way as the original events list. - [CHANGE] Condition events now also except processed events. - [FIX] ``Resource.request()`` directly after ``Resource.release()`` no longer successful. The process now has to wait as supposed to. - [FIX] ``Event.fail()`` now accept all exceptions derived from ``BaseException`` instead of only ``Exception``. 3.0.3 – 2014-03-06 ================== - [NEW] Guide to SimPy basics. - [NEW] Guide to SimPy Environments. - [FIX] Timing problems with real time simulation on Windows (issue #46). - [FIX] Installation problems on Windows due to Unicode errors (issue #41). - [FIX] Minor documentation issues. 3.0.2 – 2013-10-24 ================== - [FIX] The default capacity for ``Container`` and ``FilterStore`` is now also ``inf``. 3.0.1 – 2013-10-24 ================== - [FIX] Documentation and default parameters of ``Store`` didn't match. Its default capacity is now ``inf``. 3.0 – 2013-10-11 ================ SimPy 3 has been completely rewritten from scratch. Our main goals were to simplify the API and code base as well as making SimPy more flexible and extensible. Some of the most important changes are: - Stronger focus on events. Processes yield event instances and are suspended until the event is triggered. An example for an event is a *timeout* (formerly known as *hold*), but even processes are now events, too (you can wait until a process terminates). - Events can be combined with ``&`` (and) and ``|`` (or) to create *condition events*. - Process can now be defined by any generator function. You don't have to subclass ``Process`` anymore. - No more global simulation state. Every simulation stores its state in an *environment* which is comparable to the old ``Simulation`` class. - Improved resource system with newly added resource types. - Removed plotting and GUI capabilities. `Pyside`__ and `matplotlib`__ are much better with this. - Greatly improved test suite. Its cleaner, and the tests are shorter and more numerous. - Completely overhauled documentation. There is a `guide for porting from SimPy 2 to SimPy 3`__. If you want to stick to SimPy 2 for a while, change your requirements to ``'SimPy>=2.3,<3'``. All in all, SimPy has become a framework for asynchronous programming based on coroutines. It brings more than ten years of experience and scientific know-how in the field of event-discrete simulation to the world of asynchronous programming and should thus be a solid foundation for everything based on an event loop. You can find information about older versions on the `history page`__ __ http://qt-project.org/wiki/PySide __ http://matplotlib.org/ __ https://simpy.readthedocs.io/en/latest/topical_guides/porting_from_simpy2.html __ https://simpy.readthedocs.io/en/latest/about/history.html 2.3.1 – 2012-01-28 ================== - [NEW] More improvements on the documentation. - [FIX] Syntax error in tkconsole.py when installing on Py3.2. - [FIX] Added *mock* to the dep. list in SimPy.test(). 2.3 – 2011-12-24 ================ - [NEW] Support for Python 3.2. Support for Python <= 2.5 has been dropped. - [NEW] SimPy.test() method to run the tests on the installed version of SimPy. - [NEW] Tutorials/examples were integrated into the test suite. - [CHANGE] Even more code clean-up (e.g., removed prints throughout the code, removed if-main-blocks, ...). - [CHANGE] Many documentation improvements. 2.2 – 2011-09-27 ================ - [CHANGE] Restructured package layout to be conform to the `Hitchhiker’s Guide to packaging `_ - [CHANGE] Tests have been ported to pytest. - [CHANGE] Documentation improvements and clean-ups. - [FIX] Fixed incorrect behavior of Store._put, thanks to Johannes Koomer for the fix. 2.1 – 2010-06-03 ================ - [NEW] A function *step* has been added to the API. When called, it executes the next scheduled event. (*step* is actually a method of *Simulation*.) - [NEW] Another new function is *peek*. It returns the time of the next event. By using peek and step together, one can easily write e.g. an interactive program to step through a simulation event by event. - [NEW] A simple interactive debugger ``stepping.py`` has been added. It allows stepping through a simulation, with options to skip to a certain time, skip to the next event of a given process, or viewing the event list. - [NEW] Versions of the Bank tutorials (documents and programs) using the advanced- [NEW] object-oriented API have been added. - [NEW] A new document describes tools for gaining insight into and debugging SimPy models. - [CHANGE] Major re-structuring of SimPy code, resulting in much less SimPy code – great for the maintainers. - [CHANGE] Checks have been added which test whether entities belong to the same Simulation instance. - [CHANGE] The Monitor and Tally methods timeAverage and timeVariance now calculate only with the observed time-series. No value is assumed for the period prior to the first observation. - [CHANGE] Changed class Lister so that circular references between objects no longer lead to stack overflow and crash. - [FIX] Functions *allEventNotices* and *allEventTimes* are working again. - [FIX] Error messages for methods in SimPy.Lib work again. 2.0.1 – 2009-04-06 ================== - [NEW] Tests for real time behavior (testRT_Behavior.py and testRT_Behavior_OO.py in folder SimPy). - [FIX] Repaired a number of coding errors in several models in the SimPyModels folder. - [FIX] Repaired SimulationRT.py bug introduced by recoding for the OO API. - [FIX] Repaired errors in sample programs in documents: - Simulation with SimPy - In Depth Manual - SimPy’s Object Oriented API Manual - Simulation With Real Time Synchronization Manual - SimPlot Manual - Publication-quality Plot Production With Matplotlib Manual 2.0.0 – 2009-01-26 ================== This is a major release with changes to the SimPy application programming interface (API) and the formatting of the documentation. API changes ~~~~~~~~~~~~~~~ In addition to its existing API, SimPy now also has an object oriented API. The additional API - allows running SimPy in parallel on multiple processors or multi-core CPUs, - supports better structuring of SimPy programs, - allows subclassing of class *Simulation* and thus provides users with the capability of creating new simulation modes/libraries like SimulationTrace, and - reduces the total amount of SimPy code, thereby making it easier to maintain. Note that the OO API is **in addition** to the old API. SimPy 2.0 is fully backward compatible. Documentation format changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SimPy's documentation has been restructured and processed by the Sphinx documentation generation tool. This has generated one coherent, well structured document which can be easily browsed. A seach capability is included. March 2008: Version 1.9.1 ========================== This is a bug-fix release which cures the following bugs: - Excessive production of circular garbage, due to a circular reference between Process instances and event notices. This led to large memory requirements. - Runtime error for preempts of proceeses holding multiple Resource objects. It also adds a Short Manual, describing only the basic facilities of SimPy. December 2007: Version 1.9 ========================== This is a major release with added functionality/new user API calls and bug fixes. Major changes ~~~~~~~~~~~~~ - The event list handling has been changed to improve the runtime performance of large SimPy models (models with thousands of processes). The use of dictionaries for timestamps has been stopped. Thanks are due to Prof. Norm Matloff and a team of his students who did a study on improving SimPy performance. This was one of their recommendations. Thanks, Norm and guys! Furthermore, in version 1.9 the 'heapq' sorting package replaces 'bisect'. Finally, cancelling events no longer removes them, but rather marks them. When their event time comes, they are ignored. This was Tony Vignaux' idea! - The Manual has been edited and given an easier-to-read layout. - The Bank2 tutorial has been extended by models which use more advanced SimPy commands/constructs. Bug fixes ~~~~~~~~~ - The tracing of 'activate' statements has been enabled. Additions ~~~~~~~~~ - A method returning the time-weighted variance of observations has been added to classes Monitor and Tally. - A shortcut activation method called "start" has been added to class Process. January 2007: Version 1.8 ========================= Major Changes ~~~~~~~~~~~~~~ - SimPy 1.8 and future releases will not run under the obsolete Python 2.2 version. They require Python 2.3 or later. - The Manual has been thoroughly edited, restructured and rewritten. It is now also provided in PDF format. - The Cheatsheet has been totally rewritten in a tabular format. It is provided in both XLS (MS Excel spreadsheet) and PDF format. - The version of SimPy.Simulation(RT/Trace/Step) is now accessible by the variable 'version'. - The *__str__* method of Histogram was changed to return a table format. Bug fixes ~~~~~~~~~~~~ - Repaired a bug in *yield waituntil* runtime code. - Introduced check for *capacity* parameter of a Level or a Store being a number > 0. - Added code so that self.eventsFired gets set correctly after an event fires in a compound yield get/put with a waitevent clause (reneging case). - Repaired a bug in prettyprinting of Store objects. Additions ~~~~~~~~~~ - New compound yield statements support time-out or event-based reneging in get and put operations on Store and Level instances. - *yield get* on a Store instance can now have a filter function. - All Monitor and Tally instances are automatically registered in list *allMonitors* and *allTallies*, respectively. - The new function *startCollection* allows activation of Monitors and Tallies at a specified time. - A *printHistogram* method was added to Tally and Monitor which generates a table-form histogram. - In SimPy.SimulationRT: A function for allowing changing the ratio wall clock time to simulation time has been added. June 2006: Version 1.7.1 ============================== This is a maintenance release. The API has not been changed/added to. - Repair of a bug in the _get methods of Store and Level which could lead to synchronization problems (blocking of producer processes, despite space being available in the buffer). - Repair of Level __init__ method to allow initialBuffered to be of either float or int type. - Addition of type test for Level get parameter 'nrToGet' to limit it to positive int or float. - To improve pretty-printed output of 'Level' objects, changed attribute '_nrBuffered' to 'nrBuffered' (synonym for 'amount' property). - To improve pretty-printed output of 'Store' objects, added attribute 'buffered' (which refers to '_theBuffer' attribute). February 2006: Version 1.7 =============================== This is a major release. - Addition of an abstract class Buffer, with two sub-classes *Store* and *Level* Buffers are used for modelling inter-process synchronization in producer/ consumer and multi-process cooperation scenarios. - Addition of two new *yield* statements: + *yield put* for putting items into a buffer, and + *yield get* for getting items from a buffer. - The Manual has undergone a major re-write/edit. - All scripts have been restructured for compatibility with IronPython 1 beta2. This was doen by moving all *import* statements to the beginning of the scripts. After the removal of the first (shebang) line, all scripts (with the exception of plotting and GUI scripts) can run successfully under this new Python implementation. September 2005: Version 1.6.1 ================================= This is a minor release. - Addition of Tally data collection class as alternative to Monitor. It is intended for collecting very large data sets more efficiently in storage space and time than Monitor. - Change of Resource to work with Tally (new Resource API is backwards-compatible with 1.6). - Addition of function setHistogram to class Monitor for initializing histograms. - New function allEventNotices() for debugging/teaching purposes. It returns a prettyprinted string with event times and names of process instances. - Addition of function allEventTimes (returns event times of all scheduled events). 15 June 2005: Version 1.6 ============================== - Addition of two compound yield statement forms to support the modelling of processes reneging from resource queues. - Addition of two test/demo files showing the use of the new reneging statements. - Addition of test for prior simulation initialization in method activate(). - Repair of bug in monitoring thw waitQ of a resource when preemption occurs. - Major restructuring/editing to Manual and Cheatsheet. 1 February 2005: Version 1.5.1 ================================== - MAJOR LICENSE CHANGE: Starting with this version 1.5.1, SimPy is being release under the GNU Lesser General Public License (LGPL), instead of the GNU GPL. This change has been made to encourage commercial firms to use SimPy in for-profit work. - Minor re-release - No additional/changed functionality - Includes unit test file'MonitorTest.py' which had been accidentally deleted from 1.5 - Provides updated version of 'Bank.html' tutorial. - Provides an additional tutorial ('Bank2.html') which shows how to use the new synchronization constructs introduced in SimPy 1.5. - More logical, cleaner version numbering in files. 1 December 2004: Version 1.5 ================================ - No new functionality/API changes relative to 1.5 alpha - Repaired bug related to waiting/queuing for multiple events - SimulationRT: Improved synchronization with wallclock time on Unix/Linux 25 September 2004: Version 1.5alpha =================================== - New functionality/API additions * SimEvents and signalling synchronization constructs, with 'yield waitevent' and 'yield queueevent' commands. * A general "wait until" synchronization construct, with the 'yield waituntil' command. - No changes to 1.4.x API, i.e., existing code will work as before. 19 May 2004: Version 1.4.2 ========================== - Sub-release to repair two bugs: * The unittest for monitored Resource queues does not fail anymore. * SimulationTrace now works correctly with "yield hold,self" form. - No functional or API changes 29 February 2004: Version 1.4.1 =============================== - Sub-release to repair two bugs: * The (optional) monitoring of the activeQ in Resource now works correctly. * The "cellphone.py" example is now implemented correctly. - No functional or API changes 1 February 2004: Version 1.4 ============================ - Released on SourceForge.net 22 December 2003: Version 1.4 alpha =================================== - New functionality/API changes * All classes in the SimPy API are now new style classes, i.e., they inherit from *object* either directly or indirectly. * Module *Monitor.py* has been merged into module *Simulation.py* and all *SimulationXXX.py* modules. Import of *Simulation* or any *SimulationXXX* module now also imports *Monitor*. * Some *Monitor* methods/attributes have changed. See Manual! * *Monitor* now inherits from *list*. * A class *Histogram* has been added to *Simulation.py* and all *SimulationXXX.py* modules. * A module *SimulationRT* has been added which allows synchronization between simulated and wallclock time. * A moduleSimulationStep which allows the execution of a simulation model event-by-event, with the facility to execute application code after each event. * A Tk/Tkinter-based module *SimGUI* has been added which provides a SimPy GUI framework. * A Tk/Tkinter-based module *SimPlot* has been added which provides for plot output from SimPy programs. 22 June 2003: Version 1.3 ========================= - No functional or API changes - Reduction of sourcecode linelength in Simulation.py to <= 80 characters June 2003: Version 1.3 alpha ============================ - Significantly improved performance - Significant increase in number of quasi-parallel processes SimPy can handle - New functionality/API changes: * Addition of SimulationTrace, an event trace utility * Addition of Lister, a prettyprinter for instance attributes * No API changes - Internal changes: * Implementation of a proposal by Simon Frost: storing the keys of the event set dictionary in a binary search tree using bisect. Thank you, Simon! SimPy 1.3 is dedicated to you! - Update of Manual to address tracing. - Update of Interfacing doc to address output visualization using Scientific Python gplt package. 29 April 2003: Version 1.2 ========================== - No changes in API. - Internal changes: * Defined "True" and "False" in Simulation.py to support Python 2.2. 22 October 2002 =============== - Re-release of 0.5 Beta on SourceForge.net to replace corrupted file __init__.py. - No code changes whatever! 18 October 2002 =============== - Version 0.5 Beta-release, intended to get testing by application developers and system integrators in preparation of first full (production) release. Released on SourceForge.net on 20 October 2002. - More models - Documentation enhanced by a manual, a tutorial ("The Bank") and installation instructions. - Major changes to the API: * Introduced 'simulate(until=0)' instead of 'scheduler(till=0)'. Left 'scheduler()' in for backward compatibility, but marked as deprecated. * Added attribute "name" to class Process. Process constructor is now:: def __init__(self,name="a_process") Backward compatible if keyword parameters used. * Changed Resource constructor to:: def __init__(self,capacity=1,name="a_resource",unitName="units") Backward compatible if keyword parameters used. 27 September 2002 ================= * Version 0.2 Alpha-release, intended to attract feedback from users * Extended list of models * Upodated documentation 17 September 2002 ================= * Version 0.1.2 published on SourceForge; fully working, pre-alpha code * Implements simulation, shared resources with queuing (FIFO), and monitors for data gathering/analysis. * Contains basic documentation (cheatsheet) and simulation models for test and demonstration. simpy-simpy-a59c5bcfade4/docs/about/index.rst0000644000000000000000000000051213323151071017436 0ustar 00000000000000.. _about: =========== About SimPy =========== This sections is all about the non-technical stuff. How did SimPy evolve? Who was responsible for it? And what the heck were they thinking when they made it? .. toctree:: :maxdepth: 1 history acknowledgements ports defense_of_design release_process license simpy-simpy-a59c5bcfade4/docs/about/license.rst0000644000000000000000000000007713323151071017757 0ustar 00000000000000======= License ======= .. literalinclude:: ../../LICENSE.txt simpy-simpy-a59c5bcfade4/docs/about/ports.rst0000644000000000000000000000062613323151071017504 0ustar 00000000000000.. _ports: ============================== Ports and comparable libraries ============================== Reimplementations of SimPy and libraries similar to SimPy are available in the following languages: - C#: `SimSharp `_ (written by Andreas Beham) - Julia: `SimJulia `_ - R: `Simmer `_ simpy-simpy-a59c5bcfade4/docs/about/release_process.rst0000644000000000000000000001046613323151071021516 0ustar 00000000000000=============== Release Process =============== This process describes the steps to execute in order to release a new version of SimPy. Preparations ============ #. Close all `tickets for the next version `_. #. Update the *minium* required versions of dependencies in :file:`setup.py`. Update the *exact* version of all entries in :file:`requirements.txt`. #. Run :command:`tox` from the project root. All tests for all supported versions must pass: .. code-block:: bash $ tox [...] ________ summary ________ py27: commands succeeded py32: commands succeeded py33: commands succeeded pypy: commands succeeded congratulations :) .. note:: Tox will use the :file:`requirements.txt` to setup the venvs, so make sure you've updated it! #. Build the docs (HTML is enough). Make sure there are no errors and undefined references. .. code-block:: bash $ cd docs/ $ make clean html $ cd .. #. Check if all authors are listed in :file:`AUTHORS.txt`. #. Update the change logs (:file:`CHANGES.txt` and :file:`docs/about/history.rst`). Only keep changes for the current major release in :file:`CHANGES.txt` and reference the history page from there. #. Commit all changes: .. code-block:: bash $ hg ci -m 'Updated change log for the upcoming release.' #. Update the version number in :file:`simpy/__init__.py` and :file:`setup.py` and commit: .. code-block:: bash $ hg ci -m 'Bump version from x.y.z to a.b.c' .. warning:: Do not yet tag and push the changes so that you can safely do a rollback if one of the next step fails and you need change something! #. Write a draft for the announcement mail with a list of changes, acknowledgements and installation instructions. Everyone in the team should agree with it. Build and release ================= #. Test the release process. Build a source distribution and a `wheel `_ package and test them: .. code-block:: bash $ python setup.py sdist bdist_wheel $ ls dist/ simpy-a.b.c-py2.py3-none-any.whl simpy-a.b.c.tar.gz Try installing them: .. code-block:: bash $ rm -rf /tmp/simpy-sdist # ensure clean state if ran repeatedly $ virtualenv /tmp/simpy-sdist $ /tmp/simpy-sdist/bin/pip install dist/simpy-a.b.c.tar.gz and .. code-block:: bash $ rm -rf /tmp/simpy-wheel # ensure clean state if ran repeatedly $ virtualenv /tmp/simpy-wheel $ /tmp/simpy-wheel/bin/pip install dist/simpy-a.b.c-py2.py3-none-any.whl #. Create or check your accounts for the `test server ` and `PyPI `_. Update your :file:`~/.pypirc` with your current credentials: .. code-block:: ini [distutils] index-servers = pypi test [test] repository = https://testpypi.python.org/pypi username = password = [pypi] repository = http://pypi.python.org/pypi username = password = #. Upload the distributions for the new version to the test server and test the installation again: .. code-block:: bash $ twine upload -r test dist/simpy*a.b.c* $ pip install -i https://testpypi.python.org/pypi simpy #. Check if the package is displayed correctly: https://testpypi.python.org/pypi/simpy #. Finally upload the package to PyPI and test its installation one last time: .. code-block:: bash $ twine upload -r pypi dist/simpy*a.b.c* $ pip install -U simpy #. Check if the package is displayed correctly: https://pypi.python.org/pypi/simpy Post release ============ #. Push your changes: .. code-block:: bash $ hg tag a.b.c $ hg push ssh://hg@bitbucket.org/simpy/simpy #. Activate the `documentation build `_ for the new version. #. Send the prepared email to the mailing list and post it on Google+. #. Update `Wikipedia `_ entries. #. Update `Python Wiki `_ #. Post something to Planet Python (e.g., via Stefan's blog). simpy-simpy-a59c5bcfade4/docs/api_reference/index.rst0000644000000000000000000000053113323151071021114 0ustar 00000000000000.. _api: ============= API Reference ============= The API reference provides detailed descriptions of SimPy's classes and functions. It should be helpful if you plan to extend SimPy with custom components. .. toctree:: :maxdepth: 1 simpy simpy.core simpy.exceptions simpy.events simpy.resources simpy.rt simpy.util simpy-simpy-a59c5bcfade4/docs/api_reference/simpy.core.rst0000644000000000000000000000227713323151071022106 0ustar 00000000000000========================================== ``simpy.core`` --- SimPy's core components ========================================== .. automodule:: simpy.core .. autoclass:: BaseEnvironment :members: .. autoclass:: Environment .. autoattribute:: now .. autoattribute:: active_process .. method:: process(generator) Create a new :class:`~simpy.events.Process` instance for *generator*. .. method:: timeout(delay, value=None) Return a new :class:`~simpy.events.Timeout` event with a *delay* and, optionally, a *value*. .. method:: event() Return a new :class:`~simpy.events.Event` instance. Yielding this event suspends a process until another process triggers the event. .. method:: all_of(events) Return a new :class:`~simpy.events.AllOf` condition for a list of *events*. .. method:: any_of(events) Return a new :class:`~simpy.events.AnyOf` condition for a list of *events*. .. automethod:: exit .. automethod:: schedule .. automethod:: peek .. automethod:: step .. automethod:: run .. autoclass:: BoundClass :members: .. autoclass:: EmptySchedule .. autodata:: Infinity simpy-simpy-a59c5bcfade4/docs/api_reference/simpy.events.rst0000644000000000000000000000146513323151071022460 0ustar 00000000000000===================================== ``simpy.events`` --- Core event types ===================================== .. automodule:: simpy.events .. autodata:: PENDING :annotation: = object() .. autodata:: URGENT .. autodata:: NORMAL .. autoclass:: Event :inherited-members: .. autoclass:: Timeout :inherited-members: .. autoclass:: Initialize :inherited-members: .. autoclass:: Interruption :inherited-members: .. autoclass:: Process :inherited-members: .. autoclass:: Condition :inherited-members: .. autoclass:: AllOf :inherited-members: :exclude-members: all_events, any_events .. autoclass:: AnyOf :inherited-members: :exclude-members: all_events, any_events .. autoclass:: ConditionValue :members: simpy-simpy-a59c5bcfade4/docs/api_reference/simpy.exceptions.rst0000644000000000000000000000032413323151071023326 0ustar 00000000000000====================================================== ``simpy.exceptions`` --- Exception types used by SimPy ====================================================== .. automodule:: simpy.exceptions :members: simpy-simpy-a59c5bcfade4/docs/api_reference/simpy.resources.rst0000644000000000000000000000303413323151071023160 0ustar 00000000000000================================================== ``simpy.resources`` --- Shared resource primitives ================================================== .. automodule:: simpy.resources Resources --- ``simpy.resources.resource`` ========================================== .. automodule:: simpy.resources.resource .. autoclass:: Resource :members: .. autoclass:: PriorityResource :members: .. autoclass:: PreemptiveResource :members: .. autoclass:: Preempted :members: .. autoclass:: Request :members: .. autoclass:: PriorityRequest :members: .. autoclass:: Release :members: .. autoclass:: SortedQueue :members: Containers --- ``simpy.resources.container`` ============================================ .. automodule:: simpy.resources.container .. autoclass:: Container :members: .. autoclass:: ContainerPut :members: .. autoclass:: ContainerGet :members: Stores --- ``simpy.resources.store`` ==================================== .. automodule:: simpy.resources.store .. autoclass:: Store :members: .. autoclass:: PriorityItem(priority, item) :members: priority, item .. autoclass:: PriorityStore :members: .. autoclass:: FilterStore :members: .. autoclass:: StorePut :members: .. autoclass:: StoreGet :members: .. autoclass:: FilterStoreGet :members: Base classes --- ``simpy.resources.base`` ========================================= .. automodule:: simpy.resources.base .. autoclass:: BaseResource :members: .. autoclass:: Put :members: .. autoclass:: Get :members: simpy-simpy-a59c5bcfade4/docs/api_reference/simpy.rst0000644000000000000000000000006513323151071021150 0ustar 00000000000000========= ``simpy`` ========= .. automodule:: simpy simpy-simpy-a59c5bcfade4/docs/api_reference/simpy.rt.rst0000644000000000000000000000217613323151071021601 0ustar 00000000000000===================================== ``simpy.rt`` --- Real-time simulation ===================================== .. automodule:: simpy.rt .. autoclass:: RealtimeEnvironment .. autoattribute:: now .. autoattribute:: active_process .. autoattribute:: factor .. autoattribute:: strict .. method:: process(generator) Create a new :class:`~simpy.events.Process` instance for *generator*. .. method:: timeout(delay, value=None) Return a new :class:`~simpy.events.Timeout` event with a *delay* and, optionally, a *value*. .. method:: event() Return a new :class:`~simpy.events.Event` instance. Yielding this event suspends a process until another process triggers the event. .. method:: all_of(events) Return a new :class:`~simpy.events.AllOf` condition for a list of *events*. .. method:: any_of(events) Return a new :class:`~simpy.events.AnyOf` condition for a list of *events*. .. automethod:: exit .. automethod:: schedule .. automethod:: peek .. automethod:: step .. automethod:: sync .. automethod:: run simpy-simpy-a59c5bcfade4/docs/api_reference/simpy.util.rst0000644000000000000000000000033113323151071022120 0ustar 00000000000000============================================== ``simpy.util`` --- Utility functions for SimPy ============================================== .. automodule:: simpy.util :members: :exclude-members: subscribe_at simpy-simpy-a59c5bcfade4/docs/conf.py0000644000000000000000000001725013323151071015771 0ustar 00000000000000# -*- coding: utf-8 -*- # # SimPy documentation build configuration file, created by # sphinx-quickstart on Fri Aug 12 23:31:06 2011. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import datetime import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join('..', 'src'))) import simpy # noqa # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'contents' # General information about the project. authors = ['Team SimPy'] project = 'SimPy' copyright = u'2002–%s, %s' % (datetime.datetime.now().year, ', '.join(authors)) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '.'.join(simpy.__version__.split('.')[0:2]) # The full version, including alpha/beta/rc tags. release = simpy.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'friendly' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. try: import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] except ImportError: html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = '_static/simpy-logo-small.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None html_favicon = '_static/favicon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'SimPydoc' # -- Options for LaTeX output ------------------------------------------------- latex_elements = { 'classoptions': ',openany,oneside', 'babel': '\\usepackage[english]{babel}' } # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples (source start # file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('contents', 'SimPy.tex', 'SimPy Documentation', ', '.join(authors), 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. latex_logo = '_static/simpy-logo-small.png' # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'simpy', 'SimPy Documentation', [', '.join(authors)], 1), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'http://docs.python.org/3/': None, } # Autodoc autodoc_member_order = 'bysource' simpy-simpy-a59c5bcfade4/docs/conftest.py0000644000000000000000000001003313323151071016661 0ustar 00000000000000""" This module scans all ``*.rst`` files below ``docs/`` for example code. Example code is discoved by checking for lines containing the ``.. literalinclude:: `` directives. An example consists of two consecutive literalinclude directives. The first must include a ``*.py`` file and the second a ``*.out`` file. The ``*.py`` file consists of the example code which is executed in a separate process. The output of this process is compared to the contents of the ``*.out`` file. """ import subprocess import sys from _pytest.assertion.util import _diff_text from py._code.code import TerminalRepr import pytest def pytest_collect_file(path, parent): """Checks if the file is a rst file and creates an :class:`ExampleFile` instance.""" if path.ext == '.py' and path.dirname.endswith('code'): return ExampleFile(path, parent) class ExampleFile(pytest.File): """Represents an example ``.py`` and its output ``.out``.""" def collect(self): pyfile = self.fspath # Python 2's random number generator produces different numbers # than Python 3's. Use a separate out-file for examples using # random numbers. if sys.version_info[0] < 3 and pyfile.new(ext='.out2').check(): outfile = pyfile.new(ext='.out2') else: outfile = pyfile.new(ext='.out') if outfile.check(): yield ExampleItem(pyfile, outfile, self) class ExampleItem(pytest.Item): """Executes an example found in a rst-file.""" def __init__(self, pyfile, outfile, parent): pytest.Item.__init__(self, str(pyfile), parent) self.pyfile = pyfile self.outfile = outfile def runtest(self): # Read expected output. with self.outfile.open() as f: expected = f.read() output = subprocess.check_output([sys.executable, str(self.pyfile)], stderr=subprocess.STDOUT, universal_newlines=True) if output != expected: # Hijack the ValueError exception to identify mismatching output. raise ValueError(expected, output) def repr_failure(self, exc_info): if exc_info.errisinstance(ValueError): # Output is mismatching. Create a nice diff as failure description. expected, output = exc_info.value.args message = _diff_text(output, expected) return ReprFailExample(self.pyfile, self.outfile, message) elif exc_info.errisinstance(subprocess.CalledProcessError): # Something wrent wrong while executing the example. return ReprErrorExample(self.pyfile, exc_info) else: # Something went terribly wrong :( return pytest.Item.repr_failure(self, exc_info) def reportinfo(self): return self.fspath, None, '%s example' % self.pyfile.purebasename class ReprFailExample(TerminalRepr): """Reports output mismatches in a nice and informative representation.""" Markup = { '+': dict(green=True), '-': dict(red=True), '?': dict(bold=True), } """Colorization codes for the diff markup.""" def __init__(self, pyfile, outfile, message): self.pyfile = pyfile self.outfile = outfile self.message = message def toterminal(self, tw): for line in self.message: markup = ReprFailExample.Markup.get(line[0], {}) tw.line(line, **markup) tw.line('') tw.line('%s: Unexpected output' % (self.pyfile)) class ReprErrorExample(TerminalRepr): """Reports failures in the execution of an example.""" def __init__(self, pyfile, exc_info): self.pyfile = pyfile self.exc_info = exc_info def toterminal(self, tw): tw.line('Execution of %s failed. Captured output:' % self.pyfile.basename, red=True, bold=True) tw.sep('-') tw.line(self.exc_info.value.output) tw.line('%s: Example failed (exitcode=%d)' % (self.pyfile, self.exc_info.value.returncode)) simpy-simpy-a59c5bcfade4/docs/contents.rst0000644000000000000000000000055013323151071017054 0ustar 00000000000000.. _contents: ======================= Documentation for SimPy ======================= .. only:: html Contents: .. toctree:: :maxdepth: 2 index simpy_intro/index topical_guides/index examples/index api_reference/index about/index Indices and tables ================== * :ref:`genindex` * :ref:`search` simpy-simpy-a59c5bcfade4/docs/examples/bank_renege.rst0000644000000000000000000000122713323151071021277 0ustar 00000000000000=========== Bank Renege =========== Covers: - Resources: Resource - Condition events A counter with a random service time and customers who renege. Based on the program bank08.py from TheBank tutorial of SimPy 2. (KGM) This example models a bank counter and customers arriving t random times. Each customer has a certain patience. It waits to get to the counter until she’s at the end of her tether. If she gets to the counter, she uses it for a while before releasing it. New customers are created by the ``source`` process every few time steps. .. literalinclude:: code/bank_renege.py The simulation's output: .. literalinclude:: code/bank_renege.out simpy-simpy-a59c5bcfade4/docs/examples/carwash.rst0000644000000000000000000000154413323151071020471 0ustar 00000000000000======= Carwash ======= Covers: - Waiting for other processes - Resources: Resource The *Carwash* example is a simulation of a carwash with a limited number of machines and a number of cars that arrive at the carwash to get cleaned. The carwash uses a :class:`~simpy.resources.resource.Resource` to model the limited number of washing machines. It also defines a process for washing a car. When a car arrives at the carwash, it requests a machine. Once it got one, it starts the carwash's *wash* processes and waits for it to finish. It finally releases the machine and leaves. The cars are generated by a *setup* process. After creating an intial amount of cars it creates new *car* processes after a random time interval as long as the simulation continues. .. literalinclude:: code/carwash.py The simulation's output: .. literalinclude:: code/carwash.out simpy-simpy-a59c5bcfade4/docs/examples/code/README.txt0000644000000000000000000000035013323151071020711 0ustar 00000000000000The ``*.out`` files contain the output for the corresponding ``*.py`` files. Since Python 2 produces different random numbers (given the same seed) than Python 3, there are extra ``*.out2`` files for examples using random numbers. simpy-simpy-a59c5bcfade4/docs/examples/code/bank_renege.out0000644000000000000000000000070713323151071022212 0ustar 00000000000000Bank renege 0.0000 Customer00: Here I am 0.0000 Customer00: Waited 0.000 3.8595 Customer00: Finished 10.2006 Customer01: Here I am 10.2006 Customer01: Waited 0.000 12.7265 Customer02: Here I am 13.9003 Customer02: RENEGED after 1.174 23.7507 Customer01: Finished 34.9993 Customer03: Here I am 34.9993 Customer03: Waited 0.000 37.9599 Customer03: Finished 40.4798 Customer04: Here I am 40.4798 Customer04: Waited 0.000 43.1401 Customer04: Finished simpy-simpy-a59c5bcfade4/docs/examples/code/bank_renege.out20000644000000000000000000000070713323151071022274 0ustar 00000000000000Bank renege 0.0000 Customer00: Here I am 0.0000 Customer00: Waited 0.000 3.8595 Customer00: Finished 10.2006 Customer01: Here I am 10.2006 Customer01: Waited 0.000 12.7265 Customer02: Here I am 13.9003 Customer02: RENEGED after 1.174 23.7507 Customer01: Finished 34.9993 Customer03: Here I am 34.9993 Customer03: Waited 0.000 37.9599 Customer03: Finished 40.4798 Customer04: Here I am 40.4798 Customer04: Waited 0.000 43.1401 Customer04: Finished simpy-simpy-a59c5bcfade4/docs/examples/code/bank_renege.py0000644000000000000000000000347213323151071022035 0ustar 00000000000000""" Bank renege example Covers: - Resources: Resource - Condition events Scenario: A counter with a random service time and customers who renege. Based on the program bank08.py from TheBank tutorial of SimPy 2. (KGM) """ import random import simpy RANDOM_SEED = 42 NEW_CUSTOMERS = 5 # Total number of customers INTERVAL_CUSTOMERS = 10.0 # Generate new customers roughly every x seconds MIN_PATIENCE = 1 # Min. customer patience MAX_PATIENCE = 3 # Max. customer patience def source(env, number, interval, counter): """Source generates customers randomly""" for i in range(number): c = customer(env, 'Customer%02d' % i, counter, time_in_bank=12.0) env.process(c) t = random.expovariate(1.0 / interval) yield env.timeout(t) def customer(env, name, counter, time_in_bank): """Customer arrives, is served and leaves.""" arrive = env.now print('%7.4f %s: Here I am' % (arrive, name)) with counter.request() as req: patience = random.uniform(MIN_PATIENCE, MAX_PATIENCE) # Wait for the counter or abort at the end of our tether results = yield req | env.timeout(patience) wait = env.now - arrive if req in results: # We got to the counter print('%7.4f %s: Waited %6.3f' % (env.now, name, wait)) tib = random.expovariate(1.0 / time_in_bank) yield env.timeout(tib) print('%7.4f %s: Finished' % (env.now, name)) else: # We reneged print('%7.4f %s: RENEGED after %6.3f' % (env.now, name, wait)) # Setup and start the simulation print('Bank renege') random.seed(RANDOM_SEED) env = simpy.Environment() # Start processes and run counter = simpy.Resource(env, capacity=1) env.process(source(env, NEW_CUSTOMERS, INTERVAL_CUSTOMERS, counter)) env.run() simpy-simpy-a59c5bcfade4/docs/examples/code/carwash.out0000644000000000000000000000176213323151071021404 0ustar 00000000000000Carwash Check out http://youtu.be/fXXmeP9TvBg while simulating ... ;-) Car 0 arrives at the carwash at 0.00. Car 1 arrives at the carwash at 0.00. Car 2 arrives at the carwash at 0.00. Car 3 arrives at the carwash at 0.00. Car 0 enters the carwash at 0.00. Car 1 enters the carwash at 0.00. Car 4 arrives at the carwash at 5.00. Carwash removed 97% of Car 0's dirt. Carwash removed 67% of Car 1's dirt. Car 0 leaves the carwash at 5.00. Car 1 leaves the carwash at 5.00. Car 2 enters the carwash at 5.00. Car 3 enters the carwash at 5.00. Car 5 arrives at the carwash at 10.00. Carwash removed 64% of Car 2's dirt. Carwash removed 58% of Car 3's dirt. Car 2 leaves the carwash at 10.00. Car 3 leaves the carwash at 10.00. Car 4 enters the carwash at 10.00. Car 5 enters the carwash at 10.00. Carwash removed 97% of Car 4's dirt. Carwash removed 56% of Car 5's dirt. Car 4 leaves the carwash at 15.00. Car 5 leaves the carwash at 15.00. Car 6 arrives at the carwash at 16.00. Car 6 enters the carwash at 16.00. simpy-simpy-a59c5bcfade4/docs/examples/code/carwash.out20000644000000000000000000000165013323151071021462 0ustar 00000000000000Carwash Check out http://youtu.be/fXXmeP9TvBg while simulating ... ;-) Car 0 arrives at the carwash at 0.00. Car 1 arrives at the carwash at 0.00. Car 2 arrives at the carwash at 0.00. Car 3 arrives at the carwash at 0.00. Car 0 enters the carwash at 0.00. Car 1 enters the carwash at 0.00. Carwash removed 51% of Car 0's dirt. Carwash removed 63% of Car 1's dirt. Car 0 leaves the carwash at 5.00. Car 1 leaves the carwash at 5.00. Car 2 enters the carwash at 5.00. Car 3 enters the carwash at 5.00. Car 4 arrives at the carwash at 8.00. Carwash removed 86% of Car 2's dirt. Carwash removed 83% of Car 3's dirt. Car 2 leaves the carwash at 10.00. Car 3 leaves the carwash at 10.00. Car 4 enters the carwash at 10.00. Car 5 arrives at the carwash at 14.00. Car 5 enters the carwash at 14.00. Carwash removed 54% of Car 4's dirt. Car 4 leaves the carwash at 15.00. Carwash removed 71% of Car 5's dirt. Car 5 leaves the carwash at 19.00. simpy-simpy-a59c5bcfade4/docs/examples/code/carwash.py0000644000000000000000000000547113323151071021226 0ustar 00000000000000""" Carwash example. Covers: - Waiting for other processes - Resources: Resource Scenario: A carwash has a limited number of washing machines and defines a washing processes that takes some (random) time. Car processes arrive at the carwash at a random time. If one washing machine is available, they start the washing process and wait for it to finish. If not, they wait until they an use one. """ import random import simpy RANDOM_SEED = 42 NUM_MACHINES = 2 # Number of machines in the carwash WASHTIME = 5 # Minutes it takes to clean a car T_INTER = 7 # Create a car every ~7 minutes SIM_TIME = 20 # Simulation time in minutes class Carwash(object): """A carwash has a limited number of machines (``NUM_MACHINES``) to clean cars in parallel. Cars have to request one of the machines. When they got one, they can start the washing processes and wait for it to finish (which takes ``washtime`` minutes). """ def __init__(self, env, num_machines, washtime): self.env = env self.machine = simpy.Resource(env, num_machines) self.washtime = washtime def wash(self, car): """The washing processes. It takes a ``car`` processes and tries to clean it.""" yield self.env.timeout(WASHTIME) print("Carwash removed %d%% of %s's dirt." % (random.randint(50, 99), car)) def car(env, name, cw): """The car process (each car has a ``name``) arrives at the carwash (``cw``) and requests a cleaning machine. It then starts the washing process, waits for it to finish and leaves to never come back ... """ print('%s arrives at the carwash at %.2f.' % (name, env.now)) with cw.machine.request() as request: yield request print('%s enters the carwash at %.2f.' % (name, env.now)) yield env.process(cw.wash(name)) print('%s leaves the carwash at %.2f.' % (name, env.now)) def setup(env, num_machines, washtime, t_inter): """Create a carwash, a number of initial cars and keep creating cars approx. every ``t_inter`` minutes.""" # Create the carwash carwash = Carwash(env, num_machines, washtime) # Create 4 initial cars for i in range(4): env.process(car(env, 'Car %d' % i, carwash)) # Create more cars while the simulation is running while True: yield env.timeout(random.randint(t_inter - 2, t_inter + 2)) i += 1 env.process(car(env, 'Car %d' % i, carwash)) # Setup and start the simulation print('Carwash') print('Check out http://youtu.be/fXXmeP9TvBg while simulating ... ;-)') random.seed(RANDOM_SEED) # This helps reproducing the results # Create an environment and start the setup process env = simpy.Environment() env.process(setup(env, NUM_MACHINES, WASHTIME, T_INTER)) # Execute! env.run(until=SIM_TIME) simpy-simpy-a59c5bcfade4/docs/examples/code/gas_station_refuel.out0000644000000000000000000000137413323151071023630 0ustar 00000000000000Gas Station refuelling Car 0 arriving at gas station at 87.0 Car 0 finished refueling in 18.5 seconds. Car 1 arriving at gas station at 129.0 Car 1 finished refueling in 19.0 seconds. Car 2 arriving at gas station at 284.0 Car 2 finished refueling in 21.0 seconds. Car 3 arriving at gas station at 385.0 Car 3 finished refueling in 13.5 seconds. Car 4 arriving at gas station at 459.0 Calling tank truck at 460 Car 4 finished refueling in 22.0 seconds. Car 5 arriving at gas station at 705.0 Car 6 arriving at gas station at 750.0 Tank truck arriving at time 760 Tank truck refuelling 188.0 liters. Car 6 finished refueling in 29.0 seconds. Car 5 finished refueling in 76.5 seconds. Car 7 arriving at gas station at 891.0 Car 7 finished refueling in 13.0 seconds. simpy-simpy-a59c5bcfade4/docs/examples/code/gas_station_refuel.out20000644000000000000000000000156413323151071023713 0ustar 00000000000000Gas Station refuelling Car 0 arriving at gas station at 203.0 Calling tank truck at 210 Car 0 finished refueling in 20.0 seconds. Car 1 arriving at gas station at 239.0 Car 1 finished refueling in 15.0 seconds. Car 2 arriving at gas station at 329.0 Car 2 finished refueling in 13.0 seconds. Tank truck arriving at time 510 Tank truck refuelling 97.0 liters. Car 3 arriving at gas station at 542.0 Calling tank truck at 550 Car 3 finished refueling in 18.0 seconds. Car 4 arriving at gas station at 595.0 Car 4 finished refueling in 20.0 seconds. Car 5 arriving at gas station at 633.0 Car 5 finished refueling in 22.0 seconds. Car 6 arriving at gas station at 799.0 Car 6 finished refueling in 16.0 seconds. Tank truck arriving at time 850 Tank truck refuelling 155.0 liters. Car 7 arriving at gas station at 882.0 Calling tank truck at 890 Car 7 finished refueling in 20.0 seconds. simpy-simpy-a59c5bcfade4/docs/examples/code/gas_station_refuel.py0000644000000000000000000000671713323151071023457 0ustar 00000000000000""" Gas Station Refueling example Covers: - Resources: Resource - Resources: Container - Waiting for other processes Scenario: A gas station has a limited number of gas pumps that share a common fuel reservoir. Cars randomly arrive at the gas station, request one of the fuel pumps and start refueling from that reservoir. A gas station control process observes the gas station's fuel level and calls a tank truck for refueling if the station's level drops below a threshold. """ import itertools import random import simpy RANDOM_SEED = 42 GAS_STATION_SIZE = 200 # liters THRESHOLD = 10 # Threshold for calling the tank truck (in %) FUEL_TANK_SIZE = 50 # liters FUEL_TANK_LEVEL = [5, 25] # Min/max levels of fuel tanks (in liters) REFUELING_SPEED = 2 # liters / second TANK_TRUCK_TIME = 300 # Seconds it takes the tank truck to arrive T_INTER = [30, 300] # Create a car every [min, max] seconds SIM_TIME = 1000 # Simulation time in seconds def car(name, env, gas_station, fuel_pump): """A car arrives at the gas station for refueling. It requests one of the gas station's fuel pumps and tries to get the desired amount of gas from it. If the stations reservoir is depleted, the car has to wait for the tank truck to arrive. """ fuel_tank_level = random.randint(*FUEL_TANK_LEVEL) print('%s arriving at gas station at %.1f' % (name, env.now)) with gas_station.request() as req: start = env.now # Request one of the gas pumps yield req # Get the required amount of fuel liters_required = FUEL_TANK_SIZE - fuel_tank_level yield fuel_pump.get(liters_required) # The "actual" refueling process takes some time yield env.timeout(liters_required / REFUELING_SPEED) print('%s finished refueling in %.1f seconds.' % (name, env.now - start)) def gas_station_control(env, fuel_pump): """Periodically check the level of the *fuel_pump* and call the tank truck if the level falls below a threshold.""" while True: if fuel_pump.level / fuel_pump.capacity * 100 < THRESHOLD: # We need to call the tank truck now! print('Calling tank truck at %d' % env.now) # Wait for the tank truck to arrive and refuel the station yield env.process(tank_truck(env, fuel_pump)) yield env.timeout(10) # Check every 10 seconds def tank_truck(env, fuel_pump): """Arrives at the gas station after a certain delay and refuels it.""" yield env.timeout(TANK_TRUCK_TIME) print('Tank truck arriving at time %d' % env.now) ammount = fuel_pump.capacity - fuel_pump.level print('Tank truck refuelling %.1f liters.' % ammount) yield fuel_pump.put(ammount) def car_generator(env, gas_station, fuel_pump): """Generate new cars that arrive at the gas station.""" for i in itertools.count(): yield env.timeout(random.randint(*T_INTER)) env.process(car('Car %d' % i, env, gas_station, fuel_pump)) # Setup and start the simulation print('Gas Station refuelling') random.seed(RANDOM_SEED) # Create environment and start processes env = simpy.Environment() gas_station = simpy.Resource(env, 2) fuel_pump = simpy.Container(env, GAS_STATION_SIZE, init=GAS_STATION_SIZE) env.process(gas_station_control(env, fuel_pump)) env.process(car_generator(env, gas_station, fuel_pump)) # Execute! env.run(until=SIM_TIME) simpy-simpy-a59c5bcfade4/docs/examples/code/latency.out0000644000000000000000000000151613323151071021410 0ustar 00000000000000Event Latency Received this at 15 while Sender sent this at 5 Received this at 20 while Sender sent this at 10 Received this at 25 while Sender sent this at 15 Received this at 30 while Sender sent this at 20 Received this at 35 while Sender sent this at 25 Received this at 40 while Sender sent this at 30 Received this at 45 while Sender sent this at 35 Received this at 50 while Sender sent this at 40 Received this at 55 while Sender sent this at 45 Received this at 60 while Sender sent this at 50 Received this at 65 while Sender sent this at 55 Received this at 70 while Sender sent this at 60 Received this at 75 while Sender sent this at 65 Received this at 80 while Sender sent this at 70 Received this at 85 while Sender sent this at 75 Received this at 90 while Sender sent this at 80 Received this at 95 while Sender sent this at 85 simpy-simpy-a59c5bcfade4/docs/examples/code/latency.out20000644000000000000000000000151613323151071021472 0ustar 00000000000000Event Latency Received this at 15 while Sender sent this at 5 Received this at 20 while Sender sent this at 10 Received this at 25 while Sender sent this at 15 Received this at 30 while Sender sent this at 20 Received this at 35 while Sender sent this at 25 Received this at 40 while Sender sent this at 30 Received this at 45 while Sender sent this at 35 Received this at 50 while Sender sent this at 40 Received this at 55 while Sender sent this at 45 Received this at 60 while Sender sent this at 50 Received this at 65 while Sender sent this at 55 Received this at 70 while Sender sent this at 60 Received this at 75 while Sender sent this at 65 Received this at 80 while Sender sent this at 70 Received this at 85 while Sender sent this at 75 Received this at 90 while Sender sent this at 80 Received this at 95 while Sender sent this at 85 simpy-simpy-a59c5bcfade4/docs/examples/code/latency.py0000644000000000000000000000306113323151071021226 0ustar 00000000000000""" Event Latency example Covers: - Resources: Store Scenario: This example shows how to separate the time delay of events between processes from the processes themselves. When Useful: When modeling physical things such as cables, RF propagation, etc. it better encapsulation to keep this propagation mechanism outside of the sending and receiving processes. Can also be used to interconnect processes sending messages Example by: Keith Smith """ import simpy SIM_DURATION = 100 class Cable(object): """This class represents the propagation through a cable.""" def __init__(self, env, delay): self.env = env self.delay = delay self.store = simpy.Store(env) def latency(self, value): yield self.env.timeout(self.delay) self.store.put(value) def put(self, value): self.env.process(self.latency(value)) def get(self): return self.store.get() def sender(env, cable): """A process which randomly generates messages.""" while True: # wait for next transmission yield env.timeout(5) cable.put('Sender sent this at %d' % env.now) def receiver(env, cable): """A process which consumes messages.""" while True: # Get event for message pipe msg = yield cable.get() print('Received this at %d while %s' % (env.now, msg)) # Setup and start the simulation print('Event Latency') env = simpy.Environment() cable = Cable(env, 10) env.process(sender(env, cable)) env.process(receiver(env, cable)) env.run(until=SIM_DURATION) simpy-simpy-a59c5bcfade4/docs/examples/code/machine_shop.out0000644000000000000000000000047613323151071022412 0ustar 00000000000000Machine shop Machine shop results after 4 weeks Machine 0 made 3251 parts. Machine 1 made 3273 parts. Machine 2 made 3242 parts. Machine 3 made 3343 parts. Machine 4 made 3387 parts. Machine 5 made 3244 parts. Machine 6 made 3269 parts. Machine 7 made 3185 parts. Machine 8 made 3302 parts. Machine 9 made 3279 parts. simpy-simpy-a59c5bcfade4/docs/examples/code/machine_shop.out20000644000000000000000000000047613323151071022474 0ustar 00000000000000Machine shop Machine shop results after 4 weeks Machine 0 made 3251 parts. Machine 1 made 3273 parts. Machine 2 made 3242 parts. Machine 3 made 3343 parts. Machine 4 made 3387 parts. Machine 5 made 3244 parts. Machine 6 made 3269 parts. Machine 7 made 3185 parts. Machine 8 made 3302 parts. Machine 9 made 3279 parts. simpy-simpy-a59c5bcfade4/docs/examples/code/machine_shop.py0000644000000000000000000001056713323151071022235 0ustar 00000000000000""" Machine shop example Covers: - Interrupts - Resources: PreemptiveResource Scenario: A workshop has *n* identical machines. A stream of jobs (enough to keep the machines busy) arrives. Each machine breaks down periodically. Repairs are carried out by one repairman. The repairman has other, less important tasks to perform, too. Broken machines preempt theses tasks. The repairman continues them when he is done with the machine repair. The workshop works continuously. """ import random import simpy RANDOM_SEED = 42 PT_MEAN = 10.0 # Avg. processing time in minutes PT_SIGMA = 2.0 # Sigma of processing time MTTF = 300.0 # Mean time to failure in minutes BREAK_MEAN = 1 / MTTF # Param. for expovariate distribution REPAIR_TIME = 30.0 # Time it takes to repair a machine in minutes JOB_DURATION = 30.0 # Duration of other jobs in minutes NUM_MACHINES = 10 # Number of machines in the machine shop WEEKS = 4 # Simulation time in weeks SIM_TIME = WEEKS * 7 * 24 * 60 # Simulation time in minutes def time_per_part(): """Return actual processing time for a concrete part.""" return random.normalvariate(PT_MEAN, PT_SIGMA) def time_to_failure(): """Return time until next failure for a machine.""" return random.expovariate(BREAK_MEAN) class Machine(object): """A machine produces parts and my get broken every now and then. If it breaks, it requests a *repairman* and continues the production after the it is repaired. A machine has a *name* and a numberof *parts_made* thus far. """ def __init__(self, env, name, repairman): self.env = env self.name = name self.parts_made = 0 self.broken = False # Start "working" and "break_machine" processes for this machine. self.process = env.process(self.working(repairman)) env.process(self.break_machine()) def working(self, repairman): """Produce parts as long as the simulation runs. While making a part, the machine may break multiple times. Request a repairman when this happens. """ while True: # Start making a new part done_in = time_per_part() while done_in: try: # Working on the part start = self.env.now yield self.env.timeout(done_in) done_in = 0 # Set to 0 to exit while loop. except simpy.Interrupt: self.broken = True done_in -= self.env.now - start # How much time left? # Request a repairman. This will preempt its "other_job". with repairman.request(priority=1) as req: yield req yield self.env.timeout(REPAIR_TIME) self.broken = False # Part is done. self.parts_made += 1 def break_machine(self): """Break the machine every now and then.""" while True: yield self.env.timeout(time_to_failure()) if not self.broken: # Only break the machine if it is currently working. self.process.interrupt() def other_jobs(env, repairman): """The repairman's other (unimportant) job.""" while True: # Start a new job done_in = JOB_DURATION while done_in: # Retry the job until it is done. # It's priority is lower than that of machine repairs. with repairman.request(priority=2) as req: yield req try: start = env.now yield env.timeout(done_in) done_in = 0 except simpy.Interrupt: done_in -= env.now - start # Setup and start the simulation print('Machine shop') random.seed(RANDOM_SEED) # This helps reproducing the results # Create an environment and start the setup process env = simpy.Environment() repairman = simpy.PreemptiveResource(env, capacity=1) machines = [Machine(env, 'Machine %d' % i, repairman) for i in range(NUM_MACHINES)] env.process(other_jobs(env, repairman)) # Execute! env.run(until=SIM_TIME) # Analyis/results print('Machine shop results after %s weeks' % WEEKS) for machine in machines: print('%s made %d parts.' % (machine.name, machine.parts_made)) simpy-simpy-a59c5bcfade4/docs/examples/code/movie_renege.out0000644000000000000000000000063113323151071022412 0ustar 00000000000000Movie renege Movie "Python Unchained" sold out 38.0 minutes after ticket counter opening. Number of people leaving queue when film sold out: 16 Movie "Kill Process" sold out 43.0 minutes after ticket counter opening. Number of people leaving queue when film sold out: 5 Movie "Pulp Implementation" sold out 28.0 minutes after ticket counter opening. Number of people leaving queue when film sold out: 5 simpy-simpy-a59c5bcfade4/docs/examples/code/movie_renege.out20000644000000000000000000000063013323151071022473 0ustar 00000000000000Movie renege Movie "Python Unchained" sold out 26.5 minutes after ticket counter opening. Number of people leaving queue when film sold out: 7 Movie "Kill Process" sold out 39.0 minutes after ticket counter opening. Number of people leaving queue when film sold out: 5 Movie "Pulp Implementation" sold out 41.0 minutes after ticket counter opening. Number of people leaving queue when film sold out: 1 simpy-simpy-a59c5bcfade4/docs/examples/code/movie_renege.py0000644000000000000000000000651613323151071022243 0ustar 00000000000000""" Movie renege example Covers: - Resources: Resource - Condition events - Shared events Scenario: A movie theatre has one ticket counter selling tickets for three movies (next show only). When a movie is sold out, all people waiting to buy tickets for that movie renege (leave queue). """ import collections import random import simpy RANDOM_SEED = 42 TICKETS = 50 # Number of tickets per movie SIM_TIME = 120 # Simulate until def moviegoer(env, movie, num_tickets, theater): """A moviegoer tries to by a number of tickets (*num_tickets*) for a certain *movie* in a *theater*. If the movie becomes sold out, she leaves the theater. If she gets to the counter, she tries to buy a number of tickets. If not enough tickets are left, she argues with the teller and leaves. If at most one ticket is left after the moviegoer bought her tickets, the *sold out* event for this movie is triggered causing all remaining moviegoers to leave. """ with theater.counter.request() as my_turn: # Wait until its our turn or until the movie is sold out result = yield my_turn | theater.sold_out[movie] # Check if it's our turn of if movie is sold out if my_turn not in result: theater.num_renegers[movie] += 1 env.exit() # Check if enough tickets left. if theater.available[movie] < num_tickets: # Moviegoer leaves after some discussion yield env.timeout(0.5) env.exit() # Buy tickets theater.available[movie] -= num_tickets if theater.available[movie] < 2: # Trigger the "sold out" event for the movie theater.sold_out[movie].succeed() theater.when_sold_out[movie] = env.now theater.available[movie] = 0 yield env.timeout(1) def customer_arrivals(env, theater): """Create new *moviegoers* until the sim time reaches 120.""" while True: yield env.timeout(random.expovariate(1 / 0.5)) movie = random.choice(theater.movies) num_tickets = random.randint(1, 6) if theater.available[movie]: env.process(moviegoer(env, movie, num_tickets, theater)) Theater = collections.namedtuple('Theater', 'counter, movies, available, ' 'sold_out, when_sold_out, ' 'num_renegers') # Setup and start the simulation print('Movie renege') random.seed(RANDOM_SEED) env = simpy.Environment() # Create movie theater counter = simpy.Resource(env, capacity=1) movies = ['Python Unchained', 'Kill Process', 'Pulp Implementation'] available = {movie: TICKETS for movie in movies} sold_out = {movie: env.event() for movie in movies} when_sold_out = {movie: None for movie in movies} num_renegers = {movie: 0 for movie in movies} theater = Theater(counter, movies, available, sold_out, when_sold_out, num_renegers) # Start process and run env.process(customer_arrivals(env, theater)) env.run(until=SIM_TIME) # Analysis/results for movie in movies: if theater.sold_out[movie]: print('Movie "%s" sold out %.1f minutes after ticket counter ' 'opening.' % (movie, theater.when_sold_out[movie])) print(' Number of people leaving queue when film sold out: %s' % theater.num_renegers[movie]) simpy-simpy-a59c5bcfade4/docs/examples/code/process_communication.out0000644000000000000000000000522013323151071024350 0ustar 00000000000000Process communication One-to-one pipe communication at time 6: Consumer A received message: Generator A says hello at 6. at time 12: Consumer A received message: Generator A says hello at 12. at time 19: Consumer A received message: Generator A says hello at 19. at time 26: Consumer A received message: Generator A says hello at 26. at time 36: Consumer A received message: Generator A says hello at 36. at time 46: Consumer A received message: Generator A says hello at 46. at time 52: Consumer A received message: Generator A says hello at 52. at time 58: Consumer A received message: Generator A says hello at 58. LATE Getting Message: at time 66: Consumer A received message: Generator A says hello at 65 at time 75: Consumer A received message: Generator A says hello at 75. at time 85: Consumer A received message: Generator A says hello at 85. at time 95: Consumer A received message: Generator A says hello at 95. One-to-many pipe communication at time 10: Consumer A received message: Generator A says hello at 10. at time 10: Consumer B received message: Generator A says hello at 10. at time 18: Consumer A received message: Generator A says hello at 18. at time 18: Consumer B received message: Generator A says hello at 18. at time 27: Consumer A received message: Generator A says hello at 27. at time 27: Consumer B received message: Generator A says hello at 27. at time 34: Consumer A received message: Generator A says hello at 34. at time 34: Consumer B received message: Generator A says hello at 34. at time 40: Consumer A received message: Generator A says hello at 40. LATE Getting Message: at time 41: Consumer B received message: Generator A says hello at 40 at time 46: Consumer A received message: Generator A says hello at 46. LATE Getting Message: at time 47: Consumer B received message: Generator A says hello at 46 at time 56: Consumer A received message: Generator A says hello at 56. at time 56: Consumer B received message: Generator A says hello at 56. at time 65: Consumer A received message: Generator A says hello at 65. at time 65: Consumer B received message: Generator A says hello at 65. at time 74: Consumer A received message: Generator A says hello at 74. at time 74: Consumer B received message: Generator A says hello at 74. at time 82: Consumer A received message: Generator A says hello at 82. at time 82: Consumer B received message: Generator A says hello at 82. at time 92: Consumer A received message: Generator A says hello at 92. at time 92: Consumer B received message: Generator A says hello at 92. at time 98: Consumer B received message: Generator A says hello at 98. at time 98: Consumer A received message: Generator A says hello at 98. simpy-simpy-a59c5bcfade4/docs/examples/code/process_communication.out20000644000000000000000000000544713323151071024445 0ustar 00000000000000Process communication One-to-one pipe communication at time 9: Consumer A received message: Generator A says hello at 9. at time 15: Consumer A received message: Generator A says hello at 15. at time 22: Consumer A received message: Generator A says hello at 22. at time 31: Consumer A received message: Generator A says hello at 31. at time 37: Consumer A received message: Generator A says hello at 37. at time 43: Consumer A received message: Generator A says hello at 43. at time 51: Consumer A received message: Generator A says hello at 51. LATE Getting Message: at time 58: Consumer A received message: Generator A says hello at 57 at time 65: Consumer A received message: Generator A says hello at 65. at time 73: Consumer A received message: Generator A says hello at 73. LATE Getting Message: at time 81: Consumer A received message: Generator A says hello at 79 at time 88: Consumer A received message: Generator A says hello at 88. LATE Getting Message: at time 96: Consumer A received message: Generator A says hello at 94 One-to-many pipe communication at time 6: Consumer A received message: Generator A says hello at 6. at time 6: Consumer B received message: Generator A says hello at 6. at time 16: Consumer A received message: Generator A says hello at 16. at time 16: Consumer B received message: Generator A says hello at 16. at time 25: Consumer A received message: Generator A says hello at 25. at time 25: Consumer B received message: Generator A says hello at 25. at time 32: Consumer A received message: Generator A says hello at 32. LATE Getting Message: at time 33: Consumer B received message: Generator A says hello at 32 at time 41: Consumer A received message: Generator A says hello at 41. at time 41: Consumer B received message: Generator A says hello at 41. at time 50: Consumer A received message: Generator A says hello at 50. at time 50: Consumer B received message: Generator A says hello at 50. at time 57: Consumer A received message: Generator A says hello at 57. at time 57: Consumer B received message: Generator A says hello at 57. at time 63: Consumer A received message: Generator A says hello at 63. LATE Getting Message: at time 64: Consumer B received message: Generator A says hello at 63 at time 70: Consumer A received message: Generator A says hello at 70. at time 70: Consumer B received message: Generator A says hello at 70. at time 77: Consumer B received message: Generator A says hello at 77. LATE Getting Message: at time 78: Consumer A received message: Generator A says hello at 77 at time 86: Consumer A received message: Generator A says hello at 86. at time 86: Consumer B received message: Generator A says hello at 86. at time 92: Consumer A received message: Generator A says hello at 92. LATE Getting Message: at time 94: Consumer B received message: Generator A says hello at 92 simpy-simpy-a59c5bcfade4/docs/examples/code/process_communication.py0000644000000000000000000001061313323151071024173 0ustar 00000000000000""" Process communication example Covers: - Resources: Store Scenario: This example shows how to interconnect simulation model elements together using :class:`~simpy.resources.store.Store` for one-to-one, and many-to-one asynchronous processes. For one-to-many a simple BroadCastPipe class is constructed from Store. When Useful: When a consumer process does not always wait on a generating process and these processes run asynchronously. This example shows how to create a buffer and also tell is the consumer process was late yielding to the event from a generating process. This is also useful when some information needs to be broadcast to many receiving processes Finally, using pipes can simplify how processes are interconnected to each other in a simulation model. Example By: Keith Smith """ import random import simpy RANDOM_SEED = 42 SIM_TIME = 100 class BroadcastPipe(object): """A Broadcast pipe that allows one process to send messages to many. This construct is useful when message consumers are running at different rates than message generators and provides an event buffering to the consuming processes. The parameters are used to create a new :class:`~simpy.resources.store.Store` instance each time :meth:`get_output_conn()` is called. """ def __init__(self, env, capacity=simpy.core.Infinity): self.env = env self.capacity = capacity self.pipes = [] def put(self, value): """Broadcast a *value* to all receivers.""" if not self.pipes: raise RuntimeError('There are no output pipes.') events = [store.put(value) for store in self.pipes] return self.env.all_of(events) # Condition event for all "events" def get_output_conn(self): """Get a new output connection for this broadcast pipe. The return value is a :class:`~simpy.resources.store.Store`. """ pipe = simpy.Store(self.env, capacity=self.capacity) self.pipes.append(pipe) return pipe def message_generator(name, env, out_pipe): """A process which randomly generates messages.""" while True: # wait for next transmission yield env.timeout(random.randint(6, 10)) # messages are time stamped to later check if the consumer was # late getting them. Note, using event.triggered to do this may # result in failure due to FIFO nature of simulation yields. # (i.e. if at the same env.now, message_generator puts a message # in the pipe first and then message_consumer gets from pipe, # the event.triggered will be True in the other order it will be # False msg = (env.now, '%s says hello at %d' % (name, env.now)) out_pipe.put(msg) def message_consumer(name, env, in_pipe): """A process which consumes messages.""" while True: # Get event for message pipe msg = yield in_pipe.get() if msg[0] < env.now: # if message was already put into pipe, then # message_consumer was late getting to it. Depending on what # is being modeled this, may, or may not have some # significance print('LATE Getting Message: at time %d: %s received message: %s' % (env.now, name, msg[1])) else: # message_consumer is synchronized with message_generator print('at time %d: %s received message: %s.' % (env.now, name, msg[1])) # Process does some other work, which may result in missing messages yield env.timeout(random.randint(4, 8)) # Setup and start the simulation print('Process communication') random.seed(RANDOM_SEED) env = simpy.Environment() # For one-to-one or many-to-one type pipes, use Store pipe = simpy.Store(env) env.process(message_generator('Generator A', env, pipe)) env.process(message_consumer('Consumer A', env, pipe)) print('\nOne-to-one pipe communication\n') env.run(until=SIM_TIME) # For one-to many use BroadcastPipe # (Note: could also be used for one-to-one,many-to-one or many-to-many) env = simpy.Environment() bc_pipe = BroadcastPipe(env) env.process(message_generator('Generator A', env, bc_pipe)) env.process(message_consumer('Consumer A', env, bc_pipe.get_output_conn())) env.process(message_consumer('Consumer B', env, bc_pipe.get_output_conn())) print('\nOne-to-many pipe communication\n') env.run(until=SIM_TIME) simpy-simpy-a59c5bcfade4/docs/examples/gas_station_refuel.rst0000644000000000000000000000177513323151071022724 0ustar 00000000000000===================== Gas Station Refueling ===================== Covers: - Resources: Resource - Resources: Container - Waiting for other processes This examples models a gas station and cars that arrive at the station for refueling. The gas station has a limited number of fuel pumps and a fuel tank that is shared between the fuel pumps. The gas station is thus modeled as :class:`~simpy.resources.resource.Resource`. The shared fuel tank is modeled with a :class:`~simpy.resources.container.Container`. Vehicles arriving at the gas station first request a fuel pump from the station. Once they acquire one, they try to take the desired amount of fuel from the fuel pump. They leave when they are done. The gas stations fuel level is regularly monitored by *gas station control*. When the level drops below a certain threshold, a *tank truck* is called to refuel the gas station itself. .. literalinclude:: code/gas_station_refuel.py The simulation's output: .. literalinclude:: code/gas_station_refuel.out simpy-simpy-a59c5bcfade4/docs/examples/index.rst0000644000000000000000000000241113323151071020142 0ustar 00000000000000.. _examples: ======== Examples ======== All theory is grey. In this section, we present various practical examples that demonstrate how to uses SimPy's features. Here is a list of examples grouped by the features they demonstrate. Condition events ================ - :doc:`bank_renege` - :doc:`movie_renege` Interrupts ========== - :doc:`machine_shop` Monitoring ========== Resources: Container ==================== - :doc:`gas_station_refuel` Resources: Preemptive Resource ============================== - :doc:`machine_shop` Resources: Resource =================== - :doc:`bank_renege` - :doc:`carwash` - :doc:`gas_station_refuel` - :doc:`movie_renege` Resources: Store ================ - :doc:`latency` - :doc:`process_communication` Shared events ============= - :doc:`movie_renege` Waiting for other processes =========================== - :doc:`carwash` - :doc:`gas_station_refuel` All examples ============ .. toctree:: bank_renege carwash machine_shop movie_renege gas_station_refuel process_communication latency You have ideas for better examples? Please send them to our `mailing list `_ or make a pull request on `bitbucket `_. simpy-simpy-a59c5bcfade4/docs/examples/latency.rst0000644000000000000000000000107013323151071020472 0ustar 00000000000000============= Event Latency ============= Covers: - Resources: Store This example shows how to separate the time delay of events between processes from the processes themselves. When Useful: When modeling physical things such as cables, RF propagation, etc. it better encapsulation to keep this propagation mechanism outside of the sending and receiving processes. Can also be used to interconnect processes sending messages Example by: Keith Smith .. literalinclude:: code/latency.py The simulation's output: .. literalinclude:: code/latency.out simpy-simpy-a59c5bcfade4/docs/examples/machine_shop.rst0000644000000000000000000000215613323151071021476 0ustar 00000000000000============ Machine Shop ============ Covers: - Interrupts - Resources: PreemptiveResource This example comprises a workshop with *n* identical machines. A stream of jobs (enough to keep the machines busy) arrives. Each machine breaks down periodically. Repairs are carried out by one repairman. The repairman has other, less important tasks to perform, too. Broken machines preempt theses tasks. The repairman continues them when he is done with the machine repair. The workshop works continuously. A machine has two processes: *working* implements the actual behaviour of the machine (producing parts). *break_machine* periodically interrupts the *working* process to simulate the machine failure. The repairman's other job is also a process (implemented by *other_job*). The repairman itself is a :class:`~simpy.resources.resource.PreemptiveResource` with a capacity of *1*. The machine repairing has a priority of *1*, while the other job has a priority of *2* (the smaller the number, the higher the priority). .. literalinclude:: code/machine_shop.py The simulation's output: .. literalinclude:: code/machine_shop.out simpy-simpy-a59c5bcfade4/docs/examples/movie_renege.rst0000644000000000000000000000245013323151071021502 0ustar 00000000000000============ Movie Renege ============ Covers: - Resources: Resource - Condition events - Shared events This examples models a movie theater with one ticket counter selling tickets for three movies (next show only). People arrive at random times and triy to buy a random number (1--6) tickets for a random movie. When a movie is sold out, all people waiting to buy a ticket for that movie renege (leave the queue). The movie theater is just a container for all the related data (movies, the counter, tickets left, collected data, ...). The counter is a :class:`~simpy.resources.resource.Resource` with a capacity of one. The *moviegoer* process starts waiting until either it's his turn (it acquires the counter resource) or until the *sold out* signal is triggered. If the latter is the case it reneges (leaves the queue). If it gets to the counter, it tries to buy some tickets. This might not be successful, e.g. if the process tries to buy 5 tickets but only 3 are left. If less then two tickets are left after the ticket purchase, the *sold out* signal is triggered. Moviegoers are generated by the *customer arrivals* process. It also chooses a movie and the number of tickets for the moviegoer. .. literalinclude:: code/movie_renege.py The simulation's output: .. literalinclude:: code/movie_renege.out simpy-simpy-a59c5bcfade4/docs/examples/process_communication.rst0000644000000000000000000000167113323151071023445 0ustar 00000000000000===================== Process Communication ===================== Covers: - Resources: Store This example shows how to interconnect simulation model elements together using "resources.Store" for one-to-one, and many-to-one asynchronous processes. For one-to-many a simple BroadCastPipe class is constructed from Store. When Useful: When a consumer process does not always wait on a generating process and these processes run asynchronously. This example shows how to create a buffer and also tell is the consumer process was late yielding to the event from a generating process. This is also useful when some information needs to be broadcast to many receiving processes Finally, using pipes can simplify how processes are interconnected to each other in a simulation model. Example By: Keith Smith .. literalinclude:: code/process_communication.py The simulation's output: .. literalinclude:: code/process_communication.out simpy-simpy-a59c5bcfade4/docs/index.rst0000644000000000000000000000640613323151071016334 0ustar 00000000000000.. only:: html .. figure:: _static/simpy-logo-small.png :align: center Discrete event simulation for Python `News `_ | `PyPI `_ | `Bitbucket `_ | `Issues `_ | `Mailing list `_ ======== Overview ======== .. only:: html .. sidebar:: Documentation :ref:`Tutorial ` learn the basics of SimPy in just a couple of minutes :ref:`Topical Guides ` guides covering various features of SimPy in-depth :ref:`Examples ` usage examples for SimPy :ref:`API Reference ` detailed description of SimPy’s API :ref:`Contents ` for a complete overview :ref:`About ` non-technical stuff (history, change logs, ports, …) SimPy is a process-based discrete-event simulation framework based on standard Python. Processes in SimPy are defined by Python `generator functions `_ and may, for example, be used to model active components like customers, vehicles or agents. SimPy also provides various types of :ref:`shared resources ` to model limited capacity congestion points (like servers, checkout counters and tunnels). Simulations can be performed :ref:`“as fast as possible” `, in :ref:`real time ` (wall clock time) or by manually :ref:`stepping ` through the events. Though it is theoretically possible to do continuous simulations with SimPy, it has no features that help you with that. On the other hand, SimPy is overkill for simulations with a fixed step size where your processes don’t interact with each other or with shared resources. A short example simulating two clocks ticking in different time intervals looks like this: >>> import simpy >>> >>> def clock(env, name, tick): ... while True: ... print(name, env.now) ... yield env.timeout(tick) ... >>> env = simpy.Environment() >>> env.process(clock(env, 'fast', 0.5)) >>> env.process(clock(env, 'slow', 1)) >>> env.run(until=2) fast 0 slow 0 fast 0.5 slow 1 fast 1.0 fast 1.5 The documentation contains a :ref:`tutorial `, :ref:`several guides ` explaining key concepts, a number of :ref:`examples ` and the :ref:`API reference `. SimPy is released under the MIT License. Simulation model developers are encouraged to share their SimPy modeling techniques with the SimPy community. Please post a message to the `SimPy mailing list `_. There is an introductory talk that explains SimPy’s concepts and provides some examples: `watch the video `_ or `get the slides `_. SimPy has also been reimplemented in other programming languages. See the :ref:`list of ports ` for more details. simpy-simpy-a59c5bcfade4/docs/make.bat0000644000000000000000000001063513323151071016077 0ustar 00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SimPy.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SimPy.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end simpy-simpy-a59c5bcfade4/docs/simpy_intro/basic_concepts.rst0000644000000000000000000001014713323151071022555 0ustar 00000000000000.. _basic_concepts: .. currentmodule:: simpy.core ============== Basic Concepts ============== SimPy is a discrete-event simulation library. The behavior of active components (like vehicles, customers or messages) is modeled with *processes*. All processes live in an *environment*. They interact with the environment and with each other via *events*. Processes are described by simple Python `generators `_. You can call them *process function* or *process method*, depending on whether it is a normal function or method of a class. During their lifetime, they create events and ``yield`` them in order to wait for them to be triggered. When a process yields an event, the process gets *suspended*. SimPy *resumes* the process, when the event occurs (we say that the event is *triggered*). Multiple processes can wait for the same event. SimPy resumes them in the same order in which they yielded that event. An important event type is the :class:`~simpy.events.Timeout`. Events of this type are triggered after a certain amount of (simulated) time has passed. They allow a process to sleep (or hold its state) for the given time. A :class:`~simpy.events.Timeout` and all other events can be created by calling the appropriate method of the :class:`Environment` that the process lives in (:meth:`Environment.timeout()` for example). Our First Process ================= Our first example will be a *car* process. The car will alternately drive and park for a while. When it starts driving (or parking), it will print the current simulation time. So let's start:: >>> def car(env): ... while True: ... print('Start parking at %d' % env.now) ... parking_duration = 5 ... yield env.timeout(parking_duration) ... ... print('Start driving at %d' % env.now) ... trip_duration = 2 ... yield env.timeout(trip_duration) Our *car* process requires a reference to an :class:`Environment` (``env``) in order to create new events. The *car*'s behavior is described in an infinite loop. Remember, this function is a generator. Though it will never terminate, it will pass the control flow back to the simulation once a ``yield`` statement is reached. Once the yielded event is triggered ("it occurs"), the simulation will resume the function at this statement. As I said before, our car switches between the states *parking* and *driving*. It announces its new state by printing a message and the current simulation time (as returned by the :attr:`Environment.now` property). It then calls the :meth:`Environment.timeout()` factory function to create a :class:`~simpy.events.Timeout` event. This event describes the point in time the car is done *parking* (or *driving*, respectively). By yielding the event, it signals the simulation that it wants to wait for the event to occur. Now that the behavior of our car has been modeled, lets create an instance of it and see how it behaves:: >>> import simpy >>> env = simpy.Environment() >>> env.process(car(env)) >>> env.run(until=15) Start parking at 0 Start driving at 5 Start parking at 7 Start driving at 12 Start parking at 14 The first thing we need to do is to create an instance of :class:`Environment`. This instance is passed into our *car* process function. Calling it creates a *process generator* that needs to be started and added to the environment via :meth:`Environment.process()`. Note, that at this time, none of the code of our process function is being executed. Its execution is merely scheduled at the current simulation time. The :class:`~simpy.events.Process` returned by :meth:`~Environment.process()` can be used for process interactions (we will cover that in the next section, so we will ignore it for now). Finally, we start the simulation by calling :meth:`~Environment.run()` and passing an end time to it. What's Next? ============ You should now be familiar with SimPy's terminology and basic concepts. In the :doc:`next section `, we will cover process interaction. simpy-simpy-a59c5bcfade4/docs/simpy_intro/how_to_proceed.rst0000644000000000000000000000103013323151071022565 0ustar 00000000000000============== How to Proceed ============== If you are not certain yet if SimPy fulfills your requirements or if you want to see more features in action, you should take a look at the various :doc:`examples <../examples/index>` we provide. If you are looking for a more detailed description of a certain aspect or feature of SimPy, the :doc:`Topical Guides <../topical_guides/index>` section might help you. Finally, there is an :doc:`API Reference <../api_reference/index>` that describes all functions and classes in full detail. simpy-simpy-a59c5bcfade4/docs/simpy_intro/index.rst0000644000000000000000000000077613323151071020714 0ustar 00000000000000.. _intro: =================== SimPy in 10 Minutes =================== In this section, you'll learn the basics of SimPy in just a few minutes. Afterwards, you will be able to implement a simple simulation using SimPy and you'll be able to make an educated decision if SimPy is what you need. We'll also give you some hints on how to proceed to implement more complex simulations. .. toctree:: :maxdepth: 1 installation basic_concepts process_interaction shared_resources how_to_proceed simpy-simpy-a59c5bcfade4/docs/simpy_intro/installation.rst0000644000000000000000000000222513323151071022275 0ustar 00000000000000============ Installation ============ SimPy is implemented in pure Python and has no dependencies. SimPy runs on Python 2 (>= 2.7) and Python 3 (>= 3.2). PyPy is also supported. If you have `pip `_ installed, just type .. code-block:: bash $ pip install simpy and you are done. Installing from source ====================== Alternatively, you can `download SimPy `_ and install it manually. Extract the archive, open a terminal window where you extracted SimPy and type: .. code-block:: bash $ python setup.py install You can now optionally run SimPy's tests to see if everything works fine. You need `pytest `_ for this. Run the following command within the source directory of SimPy: .. code-block:: bash $ py.test --pyargs simpy Upgrading from SimPy 2 ====================== If you are already familiar with SimPy 2, please read the Guide :ref:`porting_from_simpy2`. What's Next =========== Now that you've installed SimPy, you probably want to simulate something. The :ref:`next section ` will introduce you to SimPy's basic concepts. simpy-simpy-a59c5bcfade4/docs/simpy_intro/process_interaction.rst0000644000000000000000000001313513323151071023653 0ustar 00000000000000=================== Process Interaction =================== .. currentmodule:: simpy.core The :class:`~simpy.events.Process` instance that is returned by :meth:`Environment.process()` can be utilized for process interactions. The two most common examples for this are to wait for another process to finish and to interrupt another process while it is waiting for an event. Waiting for a Process ===================== As it happens, a SimPy :class:`~simpy.events.Process` can be used like an event (technically, a process actually *is* an event). If you yield it, you are resumed once the process has finished. Imagine a car-wash simulation where cars enter the car-wash and wait for the washing process to finish. Or an airport simulation where passengers have to wait until a security check finishes. Lets assume that the car from our last example magically became an electric vehicle. Electric vehicles usually take a lot of time charging their batteries after a trip. They have to wait until their battery is charged before they can start driving again. We can model this with an additional ``charge()`` process for our car. Therefore, we refactor our car to be a class with two process methods: ``run()`` (which is the original ``car()`` process function) and ``charge()``. The ``run`` process is automatically started when ``Car`` is instantiated. A new ``charge`` process is started every time the vehicle starts parking. By yielding the :class:`~simpy.events.Process` instance that :meth:`Environment.process()` returns, the ``run`` process starts waiting for it to finish:: >>> class Car(object): ... def __init__(self, env): ... self.env = env ... # Start the run process everytime an instance is created. ... self.action = env.process(self.run()) ... ... def run(self): ... while True: ... print('Start parking and charging at %d' % self.env.now) ... charge_duration = 5 ... # We yield the process that process() returns ... # to wait for it to finish ... yield self.env.process(self.charge(charge_duration)) ... ... # The charge process has finished and ... # we can start driving again. ... print('Start driving at %d' % self.env.now) ... trip_duration = 2 ... yield self.env.timeout(trip_duration) ... ... def charge(self, duration): ... yield self.env.timeout(duration) Starting the simulation is straightforward again: We create an environment, one (or more) cars and finally call :meth:`~simpy.core.Environment.run()`. :: >>> import simpy >>> env = simpy.Environment() >>> car = Car(env) >>> env.run(until=15) Start parking and charging at 0 Start driving at 5 Start parking and charging at 7 Start driving at 12 Start parking and charging at 14 Interrupting Another Process ============================ Imagine, you don't want to wait until your electric vehicle is fully charged but want to interrupt the charging process and just start driving instead. SimPy allows you to interrupt a running process by calling its :meth:`~simpy.events.Process.interrupt()` method:: >>> def driver(env, car): ... yield env.timeout(3) ... car.action.interrupt() The ``driver`` process has a reference to the car's ``action`` process. After waiting for 3 time steps, it interrupts that process. Interrupts are thrown into process functions as :exc:`~simpy.exceptions.Interrupt` exceptions that can (should) be handled by the interrupted process. The process can then decide what to do next (e.g., continuing to wait for the original event or yielding a new event):: >>> class Car(object): ... def __init__(self, env): ... self.env = env ... self.action = env.process(self.run()) ... ... def run(self): ... while True: ... print('Start parking and charging at %d' % self.env.now) ... charge_duration = 5 ... # We may get interrupted while charging the battery ... try: ... yield self.env.process(self.charge(charge_duration)) ... except simpy.Interrupt: ... # When we received an interrupt, we stop charging and ... # switch to the "driving" state ... print('Was interrupted. Hope, the battery is full enough ...') ... ... print('Start driving at %d' % self.env.now) ... trip_duration = 2 ... yield self.env.timeout(trip_duration) ... ... def charge(self, duration): ... yield self.env.timeout(duration) When you compare the output of this simulation with the previous example, you'll notice that the car now starts driving at time ``3`` instead of ``5``:: >>> env = simpy.Environment() >>> car = Car(env) >>> env.process(driver(env, car)) >>> env.run(until=15) Start parking and charging at 0 Was interrupted. Hope, the battery is full enough ... Start driving at 3 Start parking and charging at 5 Start driving at 10 Start parking and charging at 12 What's Next =========== We just demonstrated two basic methods for process interactions---waiting for a process and interrupting a process. Take a look at the :doc:`../topical_guides/index` or the :class:`~simpy.events.Process` API reference for more details. In the :doc:`next section ` we will cover the basic usage of shared resources. simpy-simpy-a59c5bcfade4/docs/simpy_intro/shared_resources.rst0000644000000000000000000000717713323151071023147 0ustar 00000000000000================ Shared Resources ================ SimPy offers three types of :mod:`~simpy.resources` that help you modeling problems, where multiple processes want to use a resource of limited capacity (e.g., cars at a fuel station with a limited number of fuel pumps) or classical producer-consumer problems. In this section, we'll briefly introduce SimPy's :class:`~simpy.resources.resource.Resource` class. Basic Resource Usage ==================== We'll slightly modify our electric vehicle process ``car`` that we introduced in the last sections. The car will now drive to a *battery charging station (BCS)* and request one of its two *charging spots*. If both of these spots are currently in use, it waits until one of them becomes available again. It then starts charging its battery and leaves the station afterwards:: >>> def car(env, name, bcs, driving_time, charge_duration): ... # Simulate driving to the BCS ... yield env.timeout(driving_time) ... ... # Request one of its charging spots ... print('%s arriving at %d' % (name, env.now)) ... with bcs.request() as req: ... yield req ... ... # Charge the battery ... print('%s starting to charge at %s' % (name, env.now)) ... yield env.timeout(charge_duration) ... print('%s leaving the bcs at %s' % (name, env.now)) The resource's :meth:`~simpy.resources.resource.Resource.request()` method generates an event that lets you wait until the resource becomes available again. If you are resumed, you "own" the resource until you *release* it. If you use the resource with the ``with`` statement as shown above, the resource is automatically being released. If you call ``request()`` without ``with``, you are responsible to call :meth:`~simpy.resources.resource.Resource.release()` once you are done using the resource. When you release a resource, the next waiting process is resumed and now "owns" one of the resource's slots. The basic :class:`~simpy.resources.resource.Resource` sorts waiting processes in a *FIFO (first in---first out)* way. A resource needs a reference to an :class:`~simpy.core.Environment` and a *capacity* when it is created:: >>> import simpy >>> env = simpy.Environment() >>> bcs = simpy.Resource(env, capacity=2) We can now create the ``car`` processes and pass a reference to our resource as well as some additional parameters to them:: >>> for i in range(4): ... env.process(car(env, 'Car %d' % i, bcs, i*2, 5)) Finally, we can start the simulation. Since the car processes all terminate on their own in this simulation, we don't need to specify an *until* time---the simulation will automatically stop when there are no more events left:: >>> env.run() Car 0 arriving at 0 Car 0 starting to charge at 0 Car 1 arriving at 2 Car 1 starting to charge at 2 Car 2 arriving at 4 Car 0 leaving the bcs at 5 Car 2 starting to charge at 5 Car 3 arriving at 6 Car 1 leaving the bcs at 7 Car 3 starting to charge at 7 Car 2 leaving the bcs at 10 Car 3 leaving the bcs at 12 Note that the first two cars can start charging immediately after they arrive at the BCS, while cars 2 an 3 have to wait. What's Next =========== .. The last part of this tutorial will demonstrate, how you can collect data from .. your simulation. You should now be familiar with SimPy's basic concepts. The :doc:`next section ` shows you how you can proceed with using SimPy from here on. simpy-simpy-a59c5bcfade4/docs/topical_guides/environments.rst0000644000000000000000000001544713323151071022754 0ustar 00000000000000============ Environments ============ .. currentmodule:: simpy.core A simulation environment manages the simulation time as well as the scheduling and processing of events. It also provides means to step through or execute the simulation. The base class for all environments is :class:`~simpy.core.BaseEnvironment`. "Normal" simulations usually use its subclass :class:`~simpy.core.Environment`. For real-time simulations, SimPy provides a :class:`~simpy.rt.RealtimeEnvironment` (more on that in :doc:`real-time-simulations`). .. _simulation-control: Simulation control ================== SimPy is very flexible in terms of simulation execution. You can run your simulation until there are no more events, until a certain simulation time is reached, or until a certain event is triggered. You can also step through the simulation event by event. Furthermore, you can mix these things as you like. For example, you could run your simulation until an interesting event occurs. You could then step through the simulation event by event for a while; and finally run the simulation until there are no more events left and your processes have all terminated. The most important method here is :meth:`Environment.run()`: - If you call it without any argument (``env.run()``), it steps through the simulation until there are no more events left. .. warning:: If your processes run forever (``while True: yield env.timeout(1)``), this method will never terminate (unless you kill your script by e.g., pressing :kbd:`Ctrl-C`). - In most cases it is advisable to stop your simulation when it reaches a certain simulation time. Therefore, you can pass the desired time via the *until* parameter, e.g.: ``env.run(until=10)``. The simulation will then stop when the internal clock reaches 10 but will not process any events scheduled for time 10. This is similar to a new environment where the clock is 0 but (obviously) no events have yet been processed. If you want to integrate your simulation in a GUI and want to draw a process bar, you can repeatedly call this function with increasing *until* values and update your progress bar after each call: .. code-block:: python for i in range(100): env.run(until=i) progressbar.update(i) - Instead of passing a number to ``run()``, you can also pass any event to it. ``run()`` will then return when the event has been processed. Assuming that the current time is 0, ``env.run(until=env.timeout(5))`` is equivalent to ``env.run(until=5)``. You can also pass other types of events (remember, that a :class:`~simpy.events.Process` is an event, too):: >>> import simpy >>> >>> def my_proc(env): ... yield env.timeout(1) ... return 'Monty Python’s Flying Circus' >>> >>> env = simpy.Environment() >>> proc = env.process(my_proc(env)) >>> env.run(until=proc) 'Monty Python’s Flying Circus' .. _simulation-step: To step through the simulation event by event, the environment offers :meth:`~Environment.peek()` and :meth:`~Environment.step()`. ``peek()`` returns the time of the next scheduled event or *infinity* (``float('inf')``) if no future events are scheduled. ``step()`` processes the next scheduled event. It raises an :class:`EmptySchedule` exception if no event is available. In a typical use case, you use these methods in a loop like: .. code-block:: python until = 10 while env.peek() < until: env.step() State access ============ The environment allows you to get the current simulation time via the :attr:`Environment.now` property. The simulation time is a number without unit and is increased via :class:`~simpy.events.Timeout` events. By default, ``now`` starts at 0, but you can pass an ``initial_time`` to the :class:`Environment` to use something else. .. note:: Although the simulation time is technically unitless, you can pretend that it is, for example, in seconds and use it like a timestamp returned by :func:`time.time()` to calculate a date or the day of the week. The property :attr:`Environment.active_process` is comparable to :func:`os.getpid()` and is either ``None`` or pointing at the currently active :class:`~simpy.events.Process`. A process is *active* when its process function is being executed. It becomes *inactive* (or suspended) when it yields an event. Thus, it only makes sense to access this property from within a process function or a function that is called by your process function:: >>> def subfunc(env): ... print(env.active_process) # will print "p1" >>> >>> def my_proc(env): ... while True: ... print(env.active_process) # will print "p1" ... subfunc(env) ... yield env.timeout(1) >>> >>> env = simpy.Environment() >>> p1 = env.process(my_proc(env)) >>> env.active_process # None >>> env.step() >>> env.active_process # None An exemplary use case for this is the resource system: If a process function calls :meth:`~simpy.resources.resource.Resource.request()` to request a resource, the resource determines the requesting process via ``env.active_process``. Take a `look at the code`__ to see how we do this :-). __ https://bitbucket.org/simpy/simpy/src/3.0.2/simpy/resources/base.py#cl-35 Event creation ============== To create events, you normally have to import :mod:`simpy.events`, instantiate the event class and pass a reference to the environment to it. To reduce the amount of typing, the :class:`Environment` provides some shortcuts for event creation. For example, :meth:`Environment.event()` is equivalent to ``simpy.events.Event(env)``. Other shortcuts are: - :meth:`Environment.process()` - :meth:`Environment.timeout()` - :meth:`Environment.all_of()` - :meth:`Environment.any_of()` More details on what the events do can be found in the :doc:`guide to events `. Miscellaneous ============= Since Python 3.3, a generator function can have a return value: .. code-block:: python def my_proc(env): yield env.timeout(1) return 42 In SimPy, this can be used to provide return values for processes that can be used by other processes: .. code-block:: python def other_proc(env): ret_val = yield env.process(my_proc(env)) assert ret_val == 42 Internally, Python passes the return value as parameter to the :exc:`StopIteration` exception that it raises when a generator is exhausted. So in Python 2.7 and 3.2 you could replace the ``return 42`` with a ``raise StopIteration(42)`` to achieve the same result. To keep your code more readable, the environment provides the method :meth:`~Environment.exit()` to do exactly this: .. code-block:: python def my_proc(env): yield env.timeout(1) env.exit(42) # Py2 equivalent to "return 42" simpy-simpy-a59c5bcfade4/docs/topical_guides/events.rst0000644000000000000000000002434213323151071021523 0ustar 00000000000000====== Events ====== .. currentmodule:: simpy.events SimPy includes an extensive set of event types for various purposes. All of them inherit :class:`simpy.events.Event`. The listing below shows the hierarchy of events built into SimPy:: events.Event | +— events.Timeout | +— events.Initialize | +— events.Process | +— events.Condition | | | +— events.AllOf | | | +— events.AnyOf . . . This is the set of basic events. Events are extensible and resources, for example, define additional events. In this guide, we'll focus on the events in the :mod:`simpy.events` module. The :doc:`guide to resources ` describes the various resource events. Event basics ============ SimPy events are very similar – if not identical — to deferreds, futures or promises. Instances of the class :class:`Event` are used to describe any kind of events. Events can be in one of the following states. An event - might happen (not triggered), - is going to happen (triggered) or - has happened (processed). They traverse these states exactly once in that order. Events are also tightly bound to time and time causes events to advance their state. Initially, events are not triggered and just objects in memory. If an event gets triggered, it is scheduled at a given time and inserted into SimPy's event queue. The property :attr:`Event.triggered` becomes ``True``. As long as the event is not *processed*, you can add *callbacks* to an event. Callbacks are callables that accept an event as parameter and are stored in the :attr:`Event.callbacks` list. An event becomes *processed* when SimPy pops it from the event queue and calls all of its callbacks. It is now no longer possible to add callbacks. The property :attr:`Event.processed` becomes ``True``. Events also have a *value*. The value can be set before or when the event is triggered and can be retrieved via :attr:`Event.value` or, within a process, by yielding the event (``value = yield event``). Adding callbacks to an event ---------------------------- "What? Callbacks? I've never seen no callbacks!", you might think if you have worked your way through the :doc:`tutorial <../simpy_intro/index>`. That's on purpose. The most common way to add a callback to an event is yielding it from your process function (``yield event``). This will add the process' *_resume()* method as a callback. That's how your process gets resumed when it yielded an event. However, you can add any callable object (function) to the list of callbacks as long as it accepts an event instance as its single parameter: .. code-block:: python >>> import simpy >>> >>> def my_callback(event): ... print('Called back from', event) ... >>> env = simpy.Environment() >>> event = env.event() >>> event.callbacks.append(my_callback) >>> event.callbacks [] If an event has been *processed*, all of its :attr:`Event.callbacks` have been executed and the attribute is set to ``None``. This is to prevent you from adding more callbacks – these would of course never get called because the event has already happened. Processes are smart about this, though. If you yield a processed event, *_resume()* will immediately resume your process with the value of the event (because there is nothing to wait for). Triggering events ----------------- When events are triggered, they can either *succeed* or *fail*. For example, if an event is to be triggered at the end of a computation and everything works out fine, the event will *succeed*. If an exceptions occurs during that computation, the event will *fail*. To trigger an event and mark it as successful, you can use ``Event.succeed(value=None)``. You can optionally pass a *value* to it (e.g., the results of a computation). To trigger an event and mark it as failed, call ``Event.fail(exception)`` and pass an :class:`Exception` instance to it (e.g., the exception you caught during your failed computation). There is also a generic way to trigger an event: ``Event.trigger(event)``. This will take the value and outcome (success or failure) of the event passed to it. All three methods return the event instance they are bound to. This allows you to do things like ``yield Event(env).succeed()``. Example usages for ``Event`` ============================ The simple mechanics outlined above provide a great flexibility in the way events (even the basic :class:`Event`) can be used. One example for this is that events can be shared. They can be created by a process or outside of the context of a process. They can be passed to other processes and chained: .. code-block:: python >>> class School: ... def __init__(self, env): ... self.env = env ... self.class_ends = env.event() ... self.pupil_procs = [env.process(self.pupil()) for i in range(3)] ... self.bell_proc = env.process(self.bell()) ... ... def bell(self): ... for i in range(2): ... yield self.env.timeout(45) ... self.class_ends.succeed() ... self.class_ends = self.env.event() ... print() ... ... def pupil(self): ... for i in range(2): ... print(' \o/', end='') ... yield self.class_ends ... >>> school = School(env) >>> env.run() \o/ \o/ \o/ \o/ \o/ \o/ This can also be used like the *passivate / reactivate* known from SimPy 2. The pupils *passivate* when class begins and are *reactivated* when the bell rings. Let time pass by: the ``Timeout`` ================================= To actually let time pass in a simulation, there is the *timeout* event. A timeout has two parameters: a *delay* and an optional *value*: ``Timeout(delay, value=None)``. It triggers itself during its creation and schedules itself at ``now + delay``. Thus, the ``succeed()`` and ``fail()`` methods cannot be called again and you have to pass the event value to it when you create the timeout. The delay can be any kind of number, usually an *int* or *float* as long as it supports comparison and addition. Processes are events, too ========================= SimPy processes (as created by :class:`Process` or ``env.process()``) have the nice property of being events, too. That means, that a process can yield another process. It will then be resumed when the other process ends. The event's value will be the return value of that process: .. code-block:: python >>> def sub(env): ... yield env.timeout(1) ... return 23 ... >>> def parent(env): ... ret = yield env.process(sub(env)) ... return ret ... >>> env.run(env.process(parent(env))) 23 The example above will only work in Python >= 3.3. As a workaround for older Python versions, you can use ``env.exit(23)`` with the same effect. When a process is created, it schedules an :class:`Initialize` event which will start the execution of the process when triggered. You usually won't have to deal with this type of event. If you don't want a process to start immediately but after a certain delay, you can use :func:`simpy.util.start_delayed()`. This method returns a helper process that uses a *timeout* before actually starting a process. The example from above, but with a delayed start of ``sub()``: .. code-block:: python >>> from simpy.util import start_delayed >>> >>> def sub(env): ... yield env.timeout(1) ... return 23 ... >>> def parent(env): ... start = env.now ... sub_proc = yield start_delayed(env, sub(env), delay=3) ... assert env.now - start == 3 ... ... ret = yield sub_proc ... return ret ... >>> env.run(env.process(parent(env))) 23 .. _waiting_for_multiple_events_at_once: Waiting for multiple events at once =================================== Sometimes, you want to wait for more than one event at the same time. For example, you may want to wait for a resource, but not for an unlimited amount of time. Or you may want to wait until all a set of events has happened. SimPy therefore offers the :class:`AnyOf` and :class:`AllOf` events which both are a :class:`Condition` event. Both take a list of events as an argument and are triggered if at least one or all of them of them are triggered. .. code-block:: python >>> from simpy.events import AnyOf, AllOf, Event >>> events = [Event(env) for i in range(3)] >>> a = AnyOf(env, events) # Triggers if at least one of "events" is triggered. >>> b = AllOf(env, events) # Triggers if all each of "events" is triggered. The value of a condition event is an ordered dictionary with an entry for every triggered event. In the case of ``AllOf``, the size of that dictionary will always be the same as the length of the event list. The value dict of ``AnyOf`` will have at least one entry. In both cases, the event instances are used as keys and the event values will be the values. As a shorthand for ``AllOf`` and ``AnyOf``, you can also use the logical operators ``&`` (and) and ``|`` (or): .. code-block:: python >>> def test_condition(env): ... t1, t2 = env.timeout(1, value='spam'), env.timeout(2, value='eggs') ... ret = yield t1 | t2 ... assert ret == {t1: 'spam'} ... ... t1, t2 = env.timeout(1, value='spam'), env.timeout(2, value='eggs') ... ret = yield t1 & t2 ... assert ret == {t1: 'spam', t2: 'eggs'} ... ... # You can also concatenate & and | ... e1, e2, e3 = [env.timeout(i) for i in range(3)] ... yield (e1 | e2) & e3 ... assert all(e.processed for e in [e1, e2, e3]) ... >>> proc = env.process(test_condition(env)) >>> env.run() The order of condition results is identical to the order in which the condition events were specified. This allows the following idiom for conveniently fetching the values of multiple events specified in an *and* condition (including ``AllOf``): .. code-block:: python >>> def fetch_values_of_multiple_events(env): ... t1, t2 = env.timeout(1, value='spam'), env.timeout(2, value='eggs') ... r1, r2 = (yield t1 & t2).values() ... assert r1 == 'spam' and r2 == 'eggs' ... >>> proc = env.process(fetch_values_of_multiple_events(env)) >>> env.run() simpy-simpy-a59c5bcfade4/docs/topical_guides/index.rst0000644000000000000000000000077213323151071021327 0ustar 00000000000000.. _guides: ============== Topical Guides ============== This sections covers various aspects of SimPy more in-depth. It assumes that you have a basic understanding of SimPy's capabilities and that you know what you are looking for. .. toctree:: :maxdepth: 1 simpy_basics environments events process_interaction resources real-time-simulations monitoring time_and_scheduling porting_from_simpy2 .. - analyzing results (numpy, matploblib, ...) .. - integration with guis simpy-simpy-a59c5bcfade4/docs/topical_guides/monitoring.rst0000644000000000000000000002330413323151071022401 0ustar 00000000000000========== Monitoring ========== Monitoring is a relatively complex topic with a lot of different use-cases and lots of variations. This guide presents some of the more common and more interesting ones. It’s purpose is to give you some hints and ideas how you can implement simulation monitoring tailored to your use-cases. So, before you start, you need to define them: *What* do you want to monitor? - :ref:`Your processes `? - :ref:`Resource usage `? - :ref:`Trace all events of the simulation `? *When* do you want to monitor? - Regularly in defined intervals? - When something happens? *How* do you want to store the collected data? - Store it in a simple list? - Log it to a file? - Write it to a database? The following sections discuss these questions and provide some example code to help you. .. _monitoring-your-processes: Monitoring your processes ------------------------- Monitoring your own processes is relatively easy, because *you* control the code. From our experience, the most common thing you might want to do is monitor the value of one or more state variables every time they change or at discrete intervals and store it somewhere (in memory, in a database, or in a file, for example). In the simples case, you just use a list and append the required value(s) every time they change: .. code-block:: python >>> import simpy >>> >>> data = [] # This list will hold all collected data >>> >>> def test_process(env, data): ... val = 0 ... for i in range(5): ... val += env.now ... data.append(val) # Collect data ... yield env.timeout(1) >>> >>> env = simpy.Environment() >>> p = env.process(test_process(env, data)) >>> env.run(p) >>> print('Collected', data) # Lets see what we got Collected [0, 1, 3, 6, 10] If you want to monitor multiple variables, you can append (named)tuples to your data list. If you want to store the data in a NumPy array or a database, you can often increase performance if you buffer the data in a plain Python list and only write larger chunks (or the complete dataset) to the database. .. _resource-usage: Resource usage -------------- The use-cases for resource monitoring are numerous, for example you might want to monitor: - Utilization of a resource over time and on average, that is, - the number of processes that are using the resource at a time - the level of a container - the amount of items in a store This can be monitored either in discrete time steps or every time there is a change. - Number of processes in the (put|get)queue over time (and the average). Again, this could be monitored at discrete time steps or every time there is a change. - For *PreemptiveResource*, you may want to measure how often preemption occurs over time. In contrast to your processes, you don't have direct access to the code of the built-in resource classes. But this doesn't prevent you from monitoring them. Monkey-patching some of a resource's methods allows you to gather all the data you need. Here is an example that demonstrate how you can add callbacks to a resource that get called just before or after a *get / request* or a *put / release* event: .. code-block:: python >>> from functools import partial, wraps >>> import simpy >>> >>> def patch_resource(resource, pre=None, post=None): ... """Patch *resource* so that it calls the callable *pre* before each ... put/get/request/release operation and the callable *post* after each ... operation. The only argument to these functions is the resource ... instance. ... ... """ ... def get_wrapper(func): ... # Generate a wrapper for put/get/request/release ... @wraps(func) ... def wrapper(*args, **kwargs): ... # This is the actual wrapper ... # Call "pre" callback ... if pre: ... pre(resource) ... ... # Perform actual operation ... ret = func(*args, **kwargs) ... ... # Call "post" callback ... if post: ... post(resource) ... ... return ret ... return wrapper ... ... # Replace the original operations with our wrapper ... for name in ['put', 'get', 'request', 'release']: ... if hasattr(resource, name): ... setattr(resource, name, get_wrapper(getattr(resource, name))) >>> >>> def monitor(data, resource): ... """This is our monitoring callback.""" ... item = ( ... resource._env.now, # The current simulation time ... resource.count, # The number of users ... len(resource.queue), # The number of queued processes ... ) ... data.append(item) >>> >>> def test_process(env, res): ... with res.request() as req: ... yield req ... yield env.timeout(1) >>> >>> env = simpy.Environment() >>> >>> res = simpy.Resource(env, capacity=1) >>> data = [] >>> # Bind *data* as first argument to monitor() >>> # see https://docs.python.org/3/library/functools.html#functools.partial >>> monitor = partial(monitor, data) >>> patch_resource(res, post=monitor) # Patches (only) this resource instance >>> >>> p = env.process(test_process(env, res)) >>> env.run(p) >>> >>> print(data) [(0, 1, 0), (1, 0, 0)] The example above is a very generic but also very flexible way to monitor all aspects of all kinds of resources. The other extreme would be to fit the monitoring to exactly one use case. Imagine, for example, you only want to know how many processes are waiting for a ``Resource`` at a time: .. code-block:: python >>> import simpy >>> >>> class MonitoredResource(simpy.Resource): ... def __init__(self, *args, **kwargs): ... super().__init__(*args, **kwargs) ... self.data = [] ... ... def request(self, *args, **kwargs): ... self.data.append((self._env.now, len(self.queue))) ... return super().request(*args, **kwargs) ... ... def release(self, *args, **kwargs): ... self.data.append((self._env.now, len(self.queue))) ... return super().release(*args, **kwargs) >>> >>> def test_process(env, res): ... with res.request() as req: ... yield req ... yield env.timeout(1) >>> >>> env = simpy.Environment() >>> >>> res = MonitoredResource(env, capacity=1) >>> p1 = env.process(test_process(env, res)) >>> p2 = env.process(test_process(env, res)) >>> env.run() >>> >>> print(res.data) [(0, 0), (0, 0), (1, 1), (2, 0)] In contrast to the first example, we now haven't patched a single resource instance but the whole class. It also removed all of the first example's flexibility: We only monitor ``Resource`` typed resources, we only collect data *before* the actual requests are made and we only collect the time and queue length. At the same time, you need less than half of the code. .. _event-tracing: Event tracing ------------- .. currentmodule:: simpy.core In order to debug or visualize a simulation, you might want to trace when events are created, triggered and processed. Maybe you also want to trace which process created an event and which processes waited for an event. The two most interesting functions for these use-cases are :meth:`Environment.step()`, where all events get processed, and :meth:`Environment.schedule()`, where all events get scheduled and inserted into SimPy's event queue. Here is an example that shows how :meth:`Environment.step()` can be patched in order to trace all processed events: .. code-block:: python >>> from functools import partial, wraps >>> import simpy >>> >>> def trace(env, callback): ... """Replace the ``step()`` method of *env* with a tracing function ... that calls *callbacks* with an events time, priority, ID and its ... instance just before it is processed. ... ... """ ... def get_wrapper(env_step, callback): ... """Generate the wrapper for env.step().""" ... @wraps(env_step) ... def tracing_step(): ... """Call *callback* for the next event if one exist before ... calling ``env.step()``.""" ... if len(env._queue): ... t, prio, eid, event = env._queue[0] ... callback(t, prio, eid, event) ... return env_step() ... return tracing_step ... ... env.step = get_wrapper(env.step, callback) >>> >>> def monitor(data, t, prio, eid, event): ... data.append((t, eid, type(event))) >>> >>> def test_process(env): ... yield env.timeout(1) >>> >>> data = [] >>> # Bind *data* as first argument to monitor() >>> # see https://docs.python.org/3/library/functools.html#functools.partial >>> monitor = partial(monitor, data) >>> >>> env = simpy.Environment() >>> trace(env, monitor) >>> >>> p = env.process(test_process(env)) >>> env.run(until=p) >>> >>> for d in data: ... print(d) (0, 0, ) (1, 1, ) (1, 2, ) The example above is inspired by a pull request from Steve Pothier. Using the same concepts, you can also patch :meth:`Environment.schedule()`. This would give you central access to the information when which event is scheduled for what time. In addition to that, you could also patch some or all of SimPy's event classes, e.g., their `__init__()` method in order to trace when and how an event is initially being created. simpy-simpy-a59c5bcfade4/docs/topical_guides/porting_from_simpy2.rst0000644000000000000000000002331613323151071024227 0ustar 00000000000000.. _porting_from_simpy2: ========================= Porting from SimPy 2 to 3 ========================= Porting from SimPy 2 to SimPy 3 is not overly complicated. A lot of changes merely comprise copy/paste. This guide describes the conceptual and API changes between both SimPy versions and shows you how to change your code for SimPy 3. Imports ======= In SimPy 2, you had to decide at import-time whether you wanted to use a normal simulation (``SimPy.Simulation``), a real-time simulation (``SimPy.SimulationRT``) or something else. You usually had to import ``Simulation`` (or ``SimulationRT``), ``Process`` and some of the SimPy keywords (``hold`` or ``passivate``, for example) from that package. In SimPy 3, you usually need to import much less classes and modules (for example, all keywords are gone). In most use cases you will now only need to import :mod:`simpy`. **SimPy 2** .. code-block:: python from Simpy.Simulation import Simulation, Process, hold **SimPy 3** .. code-block:: python import simpy The ``Simulation*`` classes =========================== SimPy 2 encapsulated the simulation state in a ``Simulation*`` class (e.g., ``Simulation``, ``SimulationRT`` or ``SimulationTrace``). This class also had a ``simulate()`` method that executed a normal simulation, a real-time simulation or something else (depending on the particular class). There was a global ``Simulation`` instance that was automatically created when you imported SimPy. You could also instantiate it on your own to uses SimPy's object-orient API. This led to some confusion and problems, because you had to pass the ``Simulation`` instance around when you were using the object-oriented API but not if you were using the procedural API. In SimPy 3, an :class:`~simpy.core.Environment` replaces ``Simulation`` and :class:`~simpy.rt.RealtimeEnvironment` replaces ``SimulationRT``. You always need to instantiate an environment. There's no more global state. To execute a simulation, you call the environment's :meth:`~simpy.core.Environment.run()` method. **SimPy 2** .. code-block:: python # Procedural API from SimPy.Simulation import initialize, simulate initialize() # Start processes simulate(until=10) .. code-block:: python # Object-oriented API from SimPy.Simulation import Simulation sim = Simulation() # Start processes sim.simulate(until=10) **SimPy3** .. code-block:: python import simpy env = simpy.Environment() # Start processes env.run(until=10) Defining a Process ================== Processes had to inherit the ``Process`` base class in SimPy 2. Subclasses had to implement at least a so called *Process Execution Method (PEM)* (which is basically a generator function) and in most cases ``__init__()``. Each process needed to know the ``Simulation`` instance it belonged to. This reference was passed implicitly in the procedural API and had to be passed explicitly in the object-oriented API. Apart from some internal problems, this made it quite cumbersome to define a simple process. Processes were started by passing the ``Process`` and a generator instance created by the generator function to either the global ``activate()`` function or the corresponding ``Simulation`` method. A process in SimPy 3 is a Python generator (no matter if it’s defined on module level or as an instance method) wrapped in a :class:`~simpy.events.Process` instance. The generator usually requires a reference to a :class:`~simpy.core.Environment` to interact with, but this is completely optional. Processes are can be started by creating a :class:`~simpy.events.Process` instance and passing the generator to it. The environment provides a shortcut for this: :meth:`~simpy.core.Environment.process()`. **SimPy 2** .. code-block:: python # Procedural API from Simpy.Simulation import Process class MyProcess(Process): def __init__(self, another_param): super().__init__() self.another_param = another_param def generator_function(self): """Implement the process' behavior.""" yield something initialize() proc = Process('Spam') activate(proc, proc.generator_function()) .. code-block:: python # Object-oriented API from SimPy.Simulation import Simulation, Process class MyProcess(Process): def __init__(self, sim, another_param): super().__init__(sim=sim) self.another_param = another_param def generator_function(self): """Implement the process' behaviour.""" yield something sim = Simulation() proc = Process(sim, 'Spam') sim.activate(proc, proc.generator_function()) **SimPy 3** .. code-block:: python import simpy def generator_function(env, another_param): """Implement the process' behavior.""" yield something env = simpy.Environment() proc = env.process(generator_function(env, 'Spam')) SimPy Keywords (``hold`` etc.) ============================== In SimPy 2, processes created new events by yielding a *SimPy Keyword* and some additional parameters (at least ``self``). These keywords had to be imported from ``SimPy.Simulation*`` if they were used. Internally, the keywords were mapped to a function that generated the according event. In SimPy 3, you directly yield :mod:`~simpy.events` if you want to wait for an event to occur. You can instantiate an event directly or use the shortcuts provided by :class:`~simpy.core.Environment`. Generally, whenever a process yields an event, the execution of the process is suspended and resumed once the event has been triggered. To motivate this understanding, some of the events were renamed. For example, the ``hold`` keyword meant to wait until some time has passed. In terms of events this means that a timeout has happened. Therefore ``hold`` has been replaced by a :class:`~simpy.events.Timeout` event. .. note:: :class:`~simpy.events.Process` is also an :class:`~simpy.events.Event`. If you want to wait for a process to finish, simply yield it. **SimPy 2** .. code-block:: python yield hold, self, duration yield passivate, self yield request, self, resource yield release, self, resource yield waitevent, self, event yield waitevent, self, [event_a, event_b, event_c] yield queueevent, self, event_list yield get, self, level, amount yield put, self, level, amount **SimPy 3** .. code-block:: python yield env.timeout(duration) # hold: renamed yield env.event() # passivate: renamed yield resource.request() # Request is now bound to class Resource resource.release() # Release no longer needs to be yielded yield event # waitevent: just yield the event yield env.all_of([event_a, event_b, event_c]) # waitvent yield env.any_of([event_a, event_b, event_c]) # queuevent yield container.get(amount) # Level is now called Container yield container.put(amount) yield event_a | event_b # Wait for either a or b. This is new. yield event_a & event_b # Wait for a and b. This is new. yield env.process(calculation(env)) # Wait for the process calculation to # to finish. Partially supported features ---------------------------- The following ``waituntil`` keyword is not completely supported anymore: .. code-block:: python yield waituntil, self, cond_func SimPy 2 was evaluating ``cond_func`` after *every* event, which was computationally very expensive. One possible workaround is for example the following process, which evaluates ``cond_func`` periodically: .. code-block:: python def waituntil(env, cond_func, delay=1): while not cond_func(): yield env.timeout(delay) # Usage: yield waituntil(env, cond_func) Interrupts ========== In SimPy 2, ``interrupt()`` was a method of the interrupting process. The victim of the interrupt had to be passed as an argument. The victim was not directly notified of the interrupt but had to check if the ``interrupted`` flag was set. Afterwards, it had to reset the interrupt via ``interruptReset()``. You could manually set the ``interruptCause`` attribute of the victim. Explicitly checking for an interrupt is obviously error prone as it is too easy to be forgotten. In SimPy 3, you call :meth:`~simpy.events.Process.interrupt()` on the victim process. You can optionally supply a cause. An :exc:`~simpy.exceptions.Interrupt` is then thrown into the victim process, which has to handle the interrupt via ``try: ... except Interrupt: ...``. **SimPy 2** .. code-block:: python class Interrupter(Process): def __init__(self, victim): super().__init__() self.victim = victim def run(self): yield hold, self, 1 self.interrupt(self.victim_proc) self.victim_proc.interruptCause = 'Spam' class Victim(Process): def run(self): yield hold, self, 10 if self.interrupted: cause = self.interruptCause self.interruptReset() **SimPy 3** .. code-block:: python def interrupter(env, victim_proc): yield env.timeout(1) victim_proc.interrupt('Spam') def victim(env): try: yield env.timeout(10) except Interrupt as interrupt: cause = interrupt.cause Conclusion ========== This guide is by no means complete. If you run into problems, please have a look at the other :doc:`guides `, the :doc:`examples <../examples/index>` or the :doc:`../api_reference/index`. You are also very welcome to submit improvements. Just create a pull request at `bitbucket `_. simpy-simpy-a59c5bcfade4/docs/topical_guides/process_interaction.rst0000644000000000000000000001640313323151071024273 0ustar 00000000000000=================== Process Interaction =================== Discrete event simulation is only made interesting by interactions between processes. So this guide is about: * :ref:`sleep-until-woken-up` (passivate/reactivate) * :ref:`waiting-for-another-process-to-terminate` * :ref:`interrupting-another-process` The first two items were already covered in the :doc:`events` guide, but we'll also include them here for the sake of completeness. Another possibility for processes to interact are resources. They are discussed in a :doc:`separate guide `. .. _sleep-until-woken-up: Sleep until woken up ==================== Imagine you want to model an electric vehicle with an intelligent battery-charging controller. While the vehicle is driving, the controller can be passive but needs to be reactivate once the vehicle is connected to the power grid in order to charge the battery. In SimPy 2, this pattern was known as *passivate / reactivate*. In SimPy 3, you can accomplish that with a simple, shared :class:`~simpy.events.Event`: .. code-block:: python >>> from random import seed, randint >>> seed(23) >>> >>> import simpy >>> >>> class EV: ... def __init__(self, env): ... self.env = env ... self.drive_proc = env.process(self.drive(env)) ... self.bat_ctrl_proc = env.process(self.bat_ctrl(env)) ... self.bat_ctrl_reactivate = env.event() ... ... def drive(self, env): ... while True: ... # Drive for 20-40 min ... yield env.timeout(randint(20, 40)) ... ... # Park for 1–6 hours ... print('Start parking at', env.now) ... self.bat_ctrl_reactivate.succeed() # "reactivate" ... self.bat_ctrl_reactivate = env.event() ... yield env.timeout(randint(60, 360)) ... print('Stop parking at', env.now) ... ... def bat_ctrl(self, env): ... while True: ... print('Bat. ctrl. passivating at', env.now) ... yield self.bat_ctrl_reactivate # "passivate" ... print('Bat. ctrl. reactivated at', env.now) ... ... # Intelligent charging behavior here … ... yield env.timeout(randint(30, 90)) ... >>> env = simpy.Environment() >>> ev = EV(env) >>> env.run(until=150) Bat. ctrl. passivating at 0 Start parking at 29 Bat. ctrl. reactivated at 29 Bat. ctrl. passivating at 60 Stop parking at 131 Since ``bat_ctrl()`` just waits for a normal event, we no longer call this pattern *passivate / reactivate* in SimPy 3. .. _waiting-for-another-process-to-terminate: Waiting for another process to terminate ======================================== The example above has a problem: it may happen that the vehicles wants to park for a shorter duration than it takes to charge the battery (this is the case if both, charging and parking would take 60 to 90 minutes). To fix this problem we have to slightly change our model. A new ``bat_ctrl()`` will be started every time the EV starts parking. The EV then waits until the parking duration is over *and* until the charging has stopped: .. code-block:: python >>> class EV: ... def __init__(self, env): ... self.env = env ... self.drive_proc = env.process(self.drive(env)) ... ... def drive(self, env): ... while True: ... # Drive for 20-40 min ... yield env.timeout(randint(20, 40)) ... ... # Park for 1–6 hours ... print('Start parking at', env.now) ... charging = env.process(self.bat_ctrl(env)) ... parking = env.timeout(randint(60, 360)) ... yield charging & parking ... print('Stop parking at', env.now) ... ... def bat_ctrl(self, env): ... print('Bat. ctrl. started at', env.now) ... # Intelligent charging behavior here … ... yield env.timeout(randint(30, 90)) ... print('Bat. ctrl. done at', env.now) ... >>> env = simpy.Environment() >>> ev = EV(env) >>> env.run(until=310) Start parking at 29 Bat. ctrl. started at 29 Bat. ctrl. done at 83 Stop parking at 305 Again, nothing new (if you've read the :doc:`events` guide) and special is happening. SimPy processes are events, too, so you can yield them and will thus wait for them to get triggered. You can also wait for two events at the same time by concatenating them with ``&`` (see :ref:`waiting_for_multiple_events_at_once`). .. _interrupting-another-process: Interrupting another process ============================ As usual, we now have another problem: Imagine, a trip is very urgent, but with the current implementation, we always need to wait until the battery is fully charged. If we could somehow interrupt that ... Fortunate coincidence, there is indeed a way to do exactly this. You can call ``interrupt()`` on a :class:`~simpy.events.Process`. This will throw an :class:`~simpy.exceptions.Interrupt` exception into that process, resuming it immediately: .. code-block:: python >>> class EV: ... def __init__(self, env): ... self.env = env ... self.drive_proc = env.process(self.drive(env)) ... ... def drive(self, env): ... while True: ... # Drive for 20-40 min ... yield env.timeout(randint(20, 40)) ... ... # Park for 1 hour ... print('Start parking at', env.now) ... charging = env.process(self.bat_ctrl(env)) ... parking = env.timeout(60) ... yield charging | parking ... if not charging.triggered: ... # Interrupt charging if not already done. ... charging.interrupt('Need to go!') ... print('Stop parking at', env.now) ... ... def bat_ctrl(self, env): ... print('Bat. ctrl. started at', env.now) ... try: ... yield env.timeout(randint(60, 90)) ... print('Bat. ctrl. done at', env.now) ... except simpy.Interrupt as i: ... # Onoes! Got interrupted before the charging was done. ... print('Bat. ctrl. interrupted at', env.now, 'msg:', ... i.cause) ... >>> env = simpy.Environment() >>> ev = EV(env) >>> env.run(until=100) Start parking at 31 Bat. ctrl. started at 31 Stop parking at 91 Bat. ctrl. interrupted at 91 msg: Need to go! What ``process.interrupt()`` actually does is scheduling an :class:`~simpy.events.Interruption` event for immediate execution. If this event is executed it will remove the victim process' ``_resume()`` method from the callbacks of the event that it is currently waiting for (see :attr:`~simpy.events.Process.target`). Following that it will throw the ``Interrupt`` exception into the process. Since we don't do anything special to the original target event of the process, the interrupted process can yield the same event again after catching the ``Interrupt`` – Imagine someone waiting for a shop to open. The person may get interrupted by a phone call. After finishing the call, he or she checks if the shop already opened and either enters or continues to wait. simpy-simpy-a59c5bcfade4/docs/topical_guides/real-time-simulations.rst0000644000000000000000000000604713323151071024445 0ustar 00000000000000.. _realtime: ===================== Real-time simulations ===================== Sometimes, you might not want to perform a simulation as fast as possible but synchronous to the wall-clock time. This kind of simulation is also called *real-time simulation*. Real-time simulations may be necessary - if you have hardware-in-the-loop, - if there is human interaction with your simulation, or - if you want to analyze the real-time behavior of an algorithm. To convert a simulation into a real-time simulation, you only need to replace SimPy's default :class:`~simpy.core.Environment` with a :class:`simpy.rt.RealtimeEnvironment`. Apart from the *initial_time* argument, there are two additional parameters: *factor* and *strict*: ``RealtimeEnvironment(initial_time=0, factor=1.0, strict=True)``. The *factor* defines how much *real time* passes with each step of simulation time. By default, this is one second. If you set ``factor=0.1``, a unit of simulation time will only take a tenth of a second; if you set ``factor=60``, it will take a minute. Here is a simple example for converting a normal simulation to a real-time simulation with a duration of one tenth of a second per simulation time unit: .. code-block:: python >>> import time >>> import simpy >>> >>> def example(env): ... start = time.perf_counter() ... yield env.timeout(1) ... end = time.perf_counter() ... print('Duration of one simulation time unit: %.2fs' % (end - start)) >>> >>> env = simpy.Environment() >>> proc = env.process(example(env)) >>> env.run(until=proc) Duration of one simulation time unit: 0.00s >>> >>> import simpy.rt >>> env = simpy.rt.RealtimeEnvironment(factor=0.1) >>> proc = env.process(example(env)) >>> env.run(until=proc) Duration of one simulation time unit: 0.10s If the *strict* parameter is set to ``True`` (the default), the ``step()`` and ``run()`` methods will raise a ``RuntimeError`` if the computation within a simulation time step take more time than the real-time factor allows. In the following example, a process will perform a task that takes 0.02 seconds within a real-time environment with a time factor of 0.01 seconds: .. code-block:: python >>> import time >>> import simpy.rt >>> >>> def slow_proc(env): ... time.sleep(0.02) # Heavy computation :-) ... yield env.timeout(1) >>> >>> env = simpy.rt.RealtimeEnvironment(factor=0.01) >>> proc = env.process(slow_proc(env)) >>> try: ... env.run(until=proc) ... print('Everything alright') ... except RuntimeError: ... print('Simulation is too slow') Simulation is too slow To suppress the error, simply set ``strict=False``: .. code-block:: python >>> env = simpy.rt.RealtimeEnvironment(factor=0.01, strict=False) >>> proc = env.process(slow_proc(env)) >>> try: ... env.run(until=proc) ... print('Everything alright') ... except RuntimeError: ... print('Simulation is too slow') Everything alright That's it. Real-time simulations are that simple with SimPy! simpy-simpy-a59c5bcfade4/docs/topical_guides/resources.rst0000644000000000000000000004535713323151071022242 0ustar 00000000000000.. _shared-resources: ================ Shared Resources ================ Shared resources are another way to model :doc:`process_interaction`. They form a congestion point where processes queue up in order to use them. SimPy defines three categories of resources: - :ref:`res_type_resource` – Resources that can be used by a limited number of processes at a time (e.g., a gas station with a limited number of fuel pumps). - :ref:`res_type_container` – Resources that model the production and consumption of a homogeneous, undifferentiated bulk. It may either be continuous (like water) or discrete (like apples). - :ref:`res_type_store` – Resources that allow the production and consumption of Python objects. The basic concept of resources ============================== All resources share the same basic concept: The resource itself is some kind of a container with a, usually limited, *capacity*. Processes can either try to *put* something into the resource or try to *get* something out. If the resource is full or empty, they have to *queue* up and wait. This is roughly how every resource looks:: BaseResource(capacity): put_queue get_queue put(): event get(): event Every resource has a maximum capacity and two queues: one for processes that want to put something into it and one for processes that want to get something out. The ``put()`` and ``get()`` methods both return an event that is triggered when the corresponding action was successful. Resources and interrupts ------------------------ While a process is waiting for a put or get event to succeed, it may be :ref:`interrupted ` by another process. After catching the interrupt, the process has two possibilities: 1. It may continue to wait for the request (by yielding the event again). 2. It may stop waiting for the request. In this case, it has to call the event's ``cancel()`` method. Since you can easily forget this, all resources events are *context managers* (see the `Python docs `_ for details). --------- The resource system is modular and extensible. Resources can, for example, use specialized queues and event types. This allows them to use sorted queues, to add priorities to events, or to offer preemption. .. _res_type_resource: Resources ========= .. currentmodule:: simpy.resources.resource Resources can be used by a limited number of processes at a time (e.g., a gas station with a limited number of fuel pumps). Processes *request* these resources to become a user (or to "own" them) and have to *release* them once they are done (e.g., vehicles arrive at the gas station, use a fuel-pump, if one is available, and leave when they are done). Requesting a resource is modeled as "putting a process' token into the resource" and releasing a resource correspondingly as "getting a process' token out of the resource". Thus, calling ``request()``/``release()`` is equivalent to calling ``put()``/``get()``. Releasing a resource will always succeed immediately. SimPy implements three *resource* types: 1. :class:`Resource` 2. :class:`PriorityResource`, where queueing processes are sorted by priority 3. :class:`PreemptiveResource`, where processes additionally may preempt other processes with a lower priority Resource -------- The ``Resource`` is conceptually a *semaphore*. Its only parameter – apart from the obligatory reference to an :class:`~simpy.core.Environment` – is its *capacity*. It must be a positive number and defaults to 1: ``Resource(env, capacity=1)``. Instead of just counting its current users, it stores the request event as an "access token" for each user. This is, for example, useful for adding preemption (see below). Here is a basic example for using a resource: .. code-block:: python >>> import simpy >>> >>> def resource_user(env, resource): ... request = resource.request() # Generate a request event ... yield request # Wait for access ... yield env.timeout(1) # Do something ... resource.release(request) # Release the resource ... >>> env = simpy.Environment() >>> res = simpy.Resource(env, capacity=1) >>> user = env.process(resource_user(env, res)) >>> env.run() Note, that you have to release the resource under all conditions; for example, if you got interrupted while waiting for or using the resource. In order to help you with that and to avoid too many ``try: ... finally: ...`` constructs, request events can be used as context manager: .. code-block:: python >>> def resource_user(env, resource): ... with resource.request() as req: # Generate a request event ... yield req # Wait for access ... yield env.timeout(1) # Do something ... # Resource released automatically >>> user = env.process(resource_user(env, res)) >>> env.run() Resources allow you to retrieve lists of the current users or queued users, the number of current users and the resource's capacity: .. code-block:: python >>> res = simpy.Resource(env, capacity=1) >>> >>> def print_stats(res): ... print('%d of %d slots are allocated.' % (res.count, res.capacity)) ... print(' Users:', res.users) ... print(' Queued events:', res.queue) >>> >>> >>> def user(res): ... print_stats(res) ... with res.request() as req: ... yield req ... print_stats(res) ... print_stats(res) >>> >>> procs = [env.process(user(res)), env.process(user(res))] >>> env.run() 0 of 1 slots are allocated. Users: [] Queued events: [] 1 of 1 slots are allocated. Users: [] Queued events: [] 1 of 1 slots are allocated. Users: [] Queued events: [] 0 of 1 slots are allocated. Users: [] Queued events: [] 1 of 1 slots are allocated. Users: [] Queued events: [] 0 of 1 slots are allocated. Users: [] Queued events: [] PriorityResource ---------------- As you may know from the real world, not every one is equally important. To map that to SimPy, there's the *PriorityResource*. This subclass of *Resource* lets requesting processes provide a priority for each request. More important requests will gain access to the resource earlier than less important ones. Priority is expressed by integer numbers; smaller numbers mean a higher priority. Apart from that, it works like a normal *Resource*: .. code-block:: python >>> def resource_user(name, env, resource, wait, prio): ... yield env.timeout(wait) ... with resource.request(priority=prio) as req: ... print('%s requesting at %s with priority=%s' % (name, env.now, prio)) ... yield req ... print('%s got resource at %s' % (name, env.now)) ... yield env.timeout(3) ... >>> env = simpy.Environment() >>> res = simpy.PriorityResource(env, capacity=1) >>> p1 = env.process(resource_user(1, env, res, wait=0, prio=0)) >>> p2 = env.process(resource_user(2, env, res, wait=1, prio=0)) >>> p3 = env.process(resource_user(3, env, res, wait=2, prio=-1)) >>> env.run() 1 requesting at 0 with priority=0 1 got resource at 0 2 requesting at 1 with priority=0 3 requesting at 2 with priority=-1 3 got resource at 3 2 got resource at 6 Although *p3* requested the resource later than *p2*, it could use it earlier because its priority was higher. PreemptiveResource ------------------ Sometimes, new requests are so important that queue-jumping is not enough and they need to kick existing users out of the resource (this is called *preemption*). The *PreemptiveResource* allows you to do exactly this: .. code-block:: python >>> def resource_user(name, env, resource, wait, prio): ... yield env.timeout(wait) ... with resource.request(priority=prio) as req: ... print('%s requesting at %s with priority=%s' % (name, env.now, prio)) ... yield req ... print('%s got resource at %s' % (name, env.now)) ... try: ... yield env.timeout(3) ... except simpy.Interrupt as interrupt: ... by = interrupt.cause.by ... usage = env.now - interrupt.cause.usage_since ... print('%s got preempted by %s at %s after %s' % ... (name, by, env.now, usage)) ... >>> env = simpy.Environment() >>> res = simpy.PreemptiveResource(env, capacity=1) >>> p1 = env.process(resource_user(1, env, res, wait=0, prio=0)) >>> p2 = env.process(resource_user(2, env, res, wait=1, prio=0)) >>> p3 = env.process(resource_user(3, env, res, wait=2, prio=-1)) >>> env.run() 1 requesting at 0 with priority=0 1 got resource at 0 2 requesting at 1 with priority=0 3 requesting at 2 with priority=-1 1 got preempted by at 2 after 2 3 got resource at 2 2 got resource at 5 *PreemptiveResource* inherits from *PriorityResource* and adds a *preempt* flag (that defaults to ``True``) to ``request()``. By setting this to ``False`` (``resource.request(priority=x, preempt=False)``), a process can decide to not preempt another resource user. It will still be put in the queue according to its priority, though. The implementation of *PreemptiveResource* values priorities higher than preemption. That means preempt requests are not allowed to cheat and jump over a higher prioritized request. The following example shows that preemptive low priority requests cannot queue-jump over high priority requests: .. code-block:: python >>> def user(name, env, res, prio, preempt): ... with res.request(priority=prio, preempt=preempt) as req: ... try: ... print('%s requesting at %d' % (name, env.now)) ... yield req ... print('%s got resource at %d' % (name, env.now)) ... yield env.timeout(3) ... except simpy.Interrupt: ... print('%s got preempted at %d' % (name, env.now)) >>> >>> env = simpy.Environment() >>> res = simpy.PreemptiveResource(env, capacity=1) >>> A = env.process(user('A', env, res, prio=0, preempt=True)) >>> env.run(until=1) # Give A a head start A requesting at 0 A got resource at 0 >>> B = env.process(user('B', env, res, prio=-2, preempt=False)) >>> C = env.process(user('C', env, res, prio=-1, preempt=True)) >>> env.run() B requesting at 1 C requesting at 1 B got resource at 3 C got resource at 6 1. Process *A* requests the resource with priority 0. It immediately becomes a user. 2. Process *B* requests the resource with priority -2 but sets *preempt* to ``False``. It will queue up and wait. 3. Process *C* requests the resource with priority -1 but leaves *preempt* ``True``. Normally, it would preempt *A* but in this case, *B* is queued up before *C* and prevents *C* from preempting *A*. *C* can also not preempt *B* since its priority is not high enough. Thus, the behavior in the example is the same as if no preemption was used at all. Be careful when using mixed preemption! Due to the higher priority of process *B*, no preemption occurs in this example. Note that an additional request with a priority of -3 would be able to preempt *A*. If your use-case requires a different behaviour, for example queue-jumping or valuing preemption over priorities, you can subclass *PreemptiveResource* and override the default behaviour. .. _res_type_container: Containers ========== .. currentmodule:: simpy.resources.container Containers help you modelling the production and consumption of a homogeneous, undifferentiated bulk. It may either be continuous (like water) or discrete (like apples). You can use this, for example, to model the gas / petrol tank of a gas station. Tankers increase the amount of gasoline in the tank while cars decrease it. The following example is a very simple model of a gas station with a limited number of fuel dispensers (modeled as ``Resource``) and a tank modeled as ``Container``: .. code-block:: python >>> class GasStation: ... def __init__(self, env): ... self.fuel_dispensers = simpy.Resource(env, capacity=2) ... self.gas_tank = simpy.Container(env, init=100, capacity=1000) ... self.mon_proc = env.process(self.monitor_tank(env)) ... ... def monitor_tank(self, env): ... while True: ... if self.gas_tank.level < 100: ... print('Calling tanker at %s' % env.now) ... env.process(tanker(env, self)) ... yield env.timeout(15) >>> >>> >>> def tanker(env, gas_station): ... yield env.timeout(10) # Need 10 Minutes to arrive ... print('Tanker arriving at %s' % env.now) ... amount = gas_station.gas_tank.capacity - gas_station.gas_tank.level ... yield gas_station.gas_tank.put(amount) >>> >>> >>> def car(name, env, gas_station): ... print('Car %s arriving at %s' % (name, env.now)) ... with gas_station.fuel_dispensers.request() as req: ... yield req ... print('Car %s starts refueling at %s' % (name, env.now)) ... yield gas_station.gas_tank.get(40) ... yield env.timeout(5) ... print('Car %s done refueling at %s' % (name, env.now)) >>> >>> >>> def car_generator(env, gas_station): ... for i in range(4): ... env.process(car(i, env, gas_station)) ... yield env.timeout(5) >>> >>> >>> env = simpy.Environment() >>> gas_station = GasStation(env) >>> car_gen = env.process(car_generator(env, gas_station)) >>> env.run(35) Car 0 arriving at 0 Car 0 starts refueling at 0 Car 1 arriving at 5 Car 0 done refueling at 5 Car 1 starts refueling at 5 Car 2 arriving at 10 Car 1 done refueling at 10 Car 2 starts refueling at 10 Calling tanker at 15 Car 3 arriving at 15 Car 3 starts refueling at 15 Tanker arriving at 25 Car 2 done refueling at 30 Car 3 done refueling at 30 Containers allow you to retrieve their current ``level`` as well as their ``capacity`` (see ``GasStation.monitor_tank()`` and ``tanker()``). You can also access the list of waiting events via the ``put_queue`` and ``get_queue`` attributes (similar to ``Resource.queue``). .. _res_type_store: Stores ====== .. currentmodule:: simpy.resources.store Using Stores you can model the production and consumption of concrete objects (in contrast to the rather abstract "amount" stored in containers). A single Store can even contain multiple types of objects. Beside :class:`Store`, there is a :class:`FilterStore` that lets you use a custom function to filter the objects you get out of the store and :class:`PriorityStore` where items come out of the store in priority order. Here is a simple example modelling a generic producer/consumer scenario: .. code-block:: python >>> def producer(env, store): ... for i in range(100): ... yield env.timeout(2) ... yield store.put('spam %s' % i) ... print('Produced spam at', env.now) >>> >>> >>> def consumer(name, env, store): ... while True: ... yield env.timeout(1) ... print(name, 'requesting spam at', env.now) ... item = yield store.get() ... print(name, 'got', item, 'at', env.now) >>> >>> >>> env = simpy.Environment() >>> store = simpy.Store(env, capacity=2) >>> >>> prod = env.process(producer(env, store)) >>> consumers = [env.process(consumer(i, env, store)) for i in range(2)] >>> >>> env.run(until=5) 0 requesting spam at 1 1 requesting spam at 1 Produced spam at 2 0 got spam 0 at 2 0 requesting spam at 3 Produced spam at 4 1 got spam 1 at 4 As with the other resource types, you can get a store's capacity via the ``capacity`` attribute. The attribute ``items`` points to the list of items currently available in the store. The put and get queues can be accessed via the ``put_queue`` and ``get_queue`` attributes. *FilterStore* can, for example, be used to model machine shops where machines have varying attributes. This can be useful if the homogeneous slots of a *Resource* are not what you need: .. code-block:: python >>> from collections import namedtuple >>> >>> Machine = namedtuple('Machine', 'size, duration') >>> m1 = Machine(1, 2) # Small and slow >>> m2 = Machine(2, 1) # Big and fast >>> >>> env = simpy.Environment() >>> machine_shop = simpy.FilterStore(env, capacity=2) >>> machine_shop.items = [m1, m2] # Pre-populate the machine shop >>> >>> def user(name, env, ms, size): ... machine = yield ms.get(lambda machine: machine.size == size) ... print(name, 'got', machine, 'at', env.now) ... yield env.timeout(machine.duration) ... yield ms.put(machine) ... print(name, 'released', machine, 'at', env.now) >>> >>> >>> users = [env.process(user(i, env, machine_shop, (i % 2) + 1)) ... for i in range(3)] >>> env.run() 0 got Machine(size=1, duration=2) at 0 1 got Machine(size=2, duration=1) at 0 1 released Machine(size=2, duration=1) at 1 0 released Machine(size=1, duration=2) at 2 2 got Machine(size=1, duration=2) at 2 2 released Machine(size=1, duration=2) at 4 With a :class:`PriorityStore`, we can model items of differing priorities. In the following example, an inspector process finds and logs issues that a separate maintainer process repairs in priority order. .. code-block:: python >>> env = simpy.Environment() >>> issues = simpy.PriorityStore(env) >>> >>> def inspector(env, issues): ... for issue in [simpy.PriorityItem('P2', '#0000'), ... simpy.PriorityItem('P0', '#0001'), ... simpy.PriorityItem('P3', '#0002'), ... simpy.PriorityItem('P1', '#0003')]: ... yield env.timeout(1) ... print(env.now, 'log', issue) ... yield issues.put(issue) >>> >>> def maintainer(env, issues): ... while True: ... yield env.timeout(3) ... issue = yield issues.get() ... print(env.now, 'repair', issue) >>> >>> _ = env.process(inspector(env, issues)) >>> _ = env.process(maintainer(env, issues)) >>> env.run() 1 log PriorityItem(priority='P2', item='#0000') 2 log PriorityItem(priority='P0', item='#0001') 3 log PriorityItem(priority='P3', item='#0002') 3 repair PriorityItem(priority='P0', item='#0001') 4 log PriorityItem(priority='P1', item='#0003') 6 repair PriorityItem(priority='P1', item='#0003') 9 repair PriorityItem(priority='P2', item='#0000') 12 repair PriorityItem(priority='P3', item='#0002') simpy-simpy-a59c5bcfade4/docs/topical_guides/simpy_basics.rst0000644000000000000000000001056313323151071022704 0ustar 00000000000000============ SimPy basics ============ This guide describes the basic concepts of SimPy: How does it work? What are processes, events and the environment? What can I do with them? How SimPy works =============== If you break SimPy down, it is just an asynchronous event dispatcher. You generate events and schedule them at a given simulation time. Events are sorted by priority, simulation time, and an increasing event id. An event also has a list of callbacks, which are executed when the event is triggered and processed by the event loop. Events may also have a return value. The components involved in this are the :class:`~simpy.core.Environment`, :mod:`~simpy.events` and the process functions that you write. Process functions implement your simulation model, that is, they define the behavior of your simulation. They are plain Python generator functions that yield instances of :class:`~simpy.events.Event`. The environment stores these events in its event list and keeps track of the current simulation time. If a process function yields an event, SimPy adds the process to the event's callbacks and suspends the process until the event is triggered and processed. When a process waiting for an event is resumed, it will also receive the event's value. Here is a very simple example that illustrates all this; the code is more verbose than it needs to be to make things extra clear. You find a compact version of it at the end of this section:: >>> import simpy >>> >>> def example(env): ... event = simpy.events.Timeout(env, delay=1, value=42) ... value = yield event ... print('now=%d, value=%d' % (env.now, value)) >>> >>> env = simpy.Environment() >>> example_gen = example(env) >>> p = simpy.events.Process(env, example_gen) >>> >>> env.run() now=1, value=42 The ``example()`` process function above first creates a :class:`~simpy.events.Timeout` event. It passes the environment, a delay, and a value to it. The Timeout schedules itself at ``now + delay`` (that's why the environment is required); other event types usually schedule themselves at the current simulation time. The process function then yields the event and thus gets suspended. It is resumed, when SimPy processes the Timeout event. The process function also receives the event's value (42) -- this is, however, optional, so ``yield event`` would have been okay if the you were not interested in the value or if the event had no value at all. Finally, the process function prints the current simulation time (that is accessible via the environment's :attr:`~simpy.core.Environment.now` attribute) and the Timeout's value. If all required process functions are defined, you can instantiate all objects for your simulation. In most cases, you start by creating an instance of :class:`~simpy.core.Environment`, because you'll need to pass it around a lot when creating everything else. Starting a process function involves two things: 1. You have to call the process function to create a generator object. (This will not execute any code of that function yet. Please read `The Python yield keyword explained `_, to understand why this is the case.) 2. You then create an instance of :class:`~simpy.events.Process` and pass the environment and the generator object to it. This will schedule an :class:`~simpy.events.Initialize` event at the current simulation time which starts the execution of the process function. The process instance is also an event that is triggered when the process function returns. The :doc:`guide to events ` explains why this is handy. Finally, you can start SimPy's event loop. By default, it will run as long as there are events in the event list, but you can also let it stop earlier by providing an ``until`` argument (see :ref:`simulation-control`). The following guides describe the environment and its interactions with events and process functions in more detail. "Best practice" version of the example above ============================================ :: >>> import simpy >>> >>> def example(env): ... value = yield env.timeout(1, value=42) ... print('now=%d, value=%d' % (env.now, value)) >>> >>> env = simpy.Environment() >>> p = env.process(example(env)) >>> env.run() now=1, value=42 simpy-simpy-a59c5bcfade4/docs/topical_guides/time_and_scheduling.rst0000644000000000000000000000770113323151071024204 0ustar 00000000000000=================== Time and Scheduling =================== The aim of this section is to give you a deeper understanding of how time passes in SimPy and how it schedules and processes events. What is time? ============= *Time* itself is not easy to grasp. The `wikipedians describe it `_ this way: *«Time is the indefinite continued progress of existence and events that occur in apparently irreversible succession from the past through the present to the future. Time is a component quantity of various measurements used to sequence events, to compare the duration of events or the intervals between them, and to quantify rates of change of quantities in material reality or in the conscious experience. Time is often referred to as the fourth dimension, along with the three spatial dimensions.»* What's the problem with it? =========================== Often, events (in the real world) appear to happen "at the same time", when they are in fact happening at slightly different times. Here is an obvious example: Alice and Bob have birthday on the same day. If your time scale is in days, both birthday events happen at the same time. If you increase the resolution of you clock, e.g. to minutes, you may realise that Alice was actually born at 0:42 in the morning and Bob at 11:14 and that there's quite a difference between the time of both events. Doing simulation on computers suffers from similar problems. Integers (`and floats, too `_) are discrete numbers with a lot of void in between them. Thus, events that would occur after each other in the real world (e.g., at *t*:sub:`1` = 0.1 and *t*:sub:`2` = 0.2) might occur at the "same" time if mapped to an integer scale (e.g., at *t* = 0). On the other hand, SimPy is (like most simulation frameworks) a single-threaded, deterministic library. It processes events sequentially – one after another. If two events are scheduled at the same time, the one that is scheduled first will also be the processed first (FIFO). That is very important for you to understand. The processes in your modeled/simulated world may run "in parallel", but when the simulation runs on your CPU, all events are processed sequentially and deterministically. If you run your simulation multiple times (and if you don't use :mod:`random` ;-)), you will *always* get the same results. So keep this in mind: - In the real world, there's usually no *at the same time*. - Discretization of the time scale can make events appear to be *at the same time*. - SimPy processes events *one after another*, even if they have the *same time*. SimPy Events and time ===================== Before we continue, let's recap the states an event can be in (see :doc:`events` for details): - untriggered: not known to the event queue - triggered: scheduled at a time *t* and inserted into the event queue - processed: removed from the event queue SimPy's event queue is implemented as a `heap queue `_: "Heaps are binary trees for which every parent node has a value less than or equal to any of its children." So if we insert events as tuples *(t, event)* (with *t* being the scheduled time) into it, the first element in the queue will by definition always be the one with the smallest *t* and the next one to be processed. However, storing *(t, event)* tuples will not work if two events are scheduled at the same time because events are not comparable. To fix this, we also store a strictly increasing event ID with them: *(t, eid, event)*. That way, if two events get scheduled for the same time, the one scheduled first will always be processed first. .. Remove determinism by "jittering" timeouts .. ========================================== .. .. .. TODO: .. A relatively easy way to remove the determinism from your simulation is to .. add "jitter" to simpy-simpy-a59c5bcfade4/requirements.txt0000644000000000000000000000123013323151071017015 0ustar 00000000000000alabaster==0.7.11 atomicwrites==1.1.5 attrs==18.1.0 Babel==2.6.0 certifi==2018.4.16 chardet==3.0.4 coverage==4.5.1 docutils==0.14 flake8==3.5.0 idna==2.7 imagesize==1.0.0 Jinja2==2.10 MarkupSafe==1.0 mccabe==0.6.1 more-itertools==4.2.0 packaging==17.1 pkginfo==1.4.2 pluggy==0.6.0 py==1.5.4 pycodestyle==2.3.1 py-cpuinfo==4.0.0 pyflakes==1.6.0 Pygments==2.2.0 pyparsing==2.2.0 pytest==3.6.3 pytest-benchmark==3.1.1 pytest-cov==2.5.1 pytz==2018.5 requests==2.19.1 requests-toolbelt==0.8.0 six==1.11.0 snowballstemmer==1.2.1 Sphinx==1.7.5 sphinxcontrib-websupport==1.1.0 sphinx-rtd-theme==0.4.0 tox==3.1.2 tqdm==4.23.4 twine==1.11.0 urllib3==1.23 virtualenv==16.0.0 simpy-simpy-a59c5bcfade4/setup.cfg0000644000000000000000000000033013323151071015352 0ustar 00000000000000[flake8] ignore = max-line-length = 79 max-complexity = 11 [tool:pytest] addopts = --doctest-glob="*.rst" -m "not benchmark" [coverage:run] branch = True include = src/simpy/*,docs/*,tests/* [wheel] universal = 1 simpy-simpy-a59c5bcfade4/setup.py0000644000000000000000000000321013323151071015243 0ustar 00000000000000# encoding: utf-8 from setuptools import setup, find_packages setup( name='simpy', version='3.0.11', author='Ontje Lünsdorf, Stefan Scherfke', author_email='the_com@gmx.de, stefan@sofa-rockers.org', description='Event discrete, process based simulation for Python.', long_description='\n\n'.join( open(f, 'rb').read().decode('utf-8') for f in ['README.txt', 'CHANGES.txt', 'AUTHORS.txt']), url='https://simpy.readthedocs.io', license='MIT License', install_requires=[], packages=find_packages(where='src'), package_dir={'': 'src'}, include_package_data=True, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: Education', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Scientific/Engineering', ], ) simpy-simpy-a59c5bcfade4/src/simpy/__init__.py0000644000000000000000000000346113323151071017602 0ustar 00000000000000""" The ``simpy`` module aggregates SimPy's most used components into a single namespace. This is purely for convenience. You can of course also access everything (and more!) via their actual submodules. The following tables list all of the available components in this module. {toc} """ from pkgutil import extend_path from simpy.core import Environment from simpy.rt import RealtimeEnvironment from simpy.exceptions import SimPyException, Interrupt, StopProcess from simpy.events import Event, Timeout, Process, AllOf, AnyOf from simpy.resources.resource import ( Resource, PriorityResource, PreemptiveResource) from simpy.resources.container import Container from simpy.resources.store import ( Store, PriorityItem, PriorityStore, FilterStore) def compile_toc(entries, section_marker='='): """Compiles a list of sections with objects into sphinx formatted autosummary directives.""" toc = '' for section, objs in entries: toc += '\n\n%s\n%s\n\n' % (section, section_marker * len(section)) toc += '.. autosummary::\n\n' for obj in objs: toc += ' ~%s.%s\n' % (obj.__module__, obj.__name__) return toc toc = ( ('Environments', ( Environment, RealtimeEnvironment, )), ('Events', ( Event, Timeout, Process, AllOf, AnyOf, Interrupt, )), ('Resources', ( Resource, PriorityResource, PreemptiveResource, Container, Store, PriorityItem, PriorityStore, FilterStore, )), ('Exceptions', ( SimPyException, Interrupt, StopProcess, )), ) # Use the toc to keep the documentation and the implementation in sync. if __doc__: __doc__ = __doc__.format(toc=compile_toc(toc)) __all__ = [obj.__name__ for section, objs in toc for obj in objs] __path__ = extend_path(__path__, __name__) __version__ = '3.0.11' simpy-simpy-a59c5bcfade4/src/simpy/_compat.py0000644000000000000000000000433013323151071017461 0ustar 00000000000000""" Compatibility helpers for older Python versions. """ import sys PY2 = sys.version_info[0] == 2 if PY2: # NOQA # Python 2.x does not report exception chains. To emulate the behaviour of # Python 3 traceback.format_exception and traceback.print_exception are # overwritten with the custom functions. The original functions # are stored in _format_exception and _print_exception. import traceback from collections import deque _print_exception = traceback.print_exception _format_exception = traceback.format_exception def print_exception(etype, value, tb, limit=None, file=None): if file is None: file = sys.stderr # Build the exception chain. chain = deque() cause = value while True: cause = cause.__dict__.get('__cause__', None) if cause is None: break chain.appendleft(cause) # Print the exception chain. for cause in chain: _print_exception(type(cause), cause, cause.__dict__.get('__traceback__', None), limit, file) traceback._print(file, '\nThe above exception was the direct ' 'cause of the following exception:\n') _print_exception(etype, value, tb, limit, file) traceback.print_exception = print_exception def format_exception(etype, value, tb, limit=None): # Build the exception chain. chain = deque() cause = value while True: cause = cause.__dict__.get('__cause__', None) if cause is None: break chain.appendleft(cause) # Format the exception chain. lines = [] for cause in chain: lines.extend(_format_exception( type(cause), cause, cause.__dict__.get('__traceback__', None), limit)) lines.append('\nThe above exception was the direct ' 'cause of the following exception:\n\n') lines.extend(_format_exception(etype, value, tb, limit)) return lines traceback.format_exception = format_exception sys.excepthook = print_exception simpy-simpy-a59c5bcfade4/src/simpy/core.py0000644000000000000000000001713713323151071017000 0ustar 00000000000000""" Core components for event-discrete simulation environments. """ import types from heapq import heappush, heappop from itertools import count from simpy.exceptions import StopProcess from simpy.events import (AllOf, AnyOf, Event, Process, Timeout, URGENT, NORMAL) Infinity = float('inf') #: Convenience alias for infinity class BoundClass(object): """Allows classes to behave like methods. The ``__get__()`` descriptor is basically identical to ``function.__get__()`` and binds the first argument of the ``cls`` to the descriptor instance. """ def __init__(self, cls): self.cls = cls def __get__(self, obj, type=None): if obj is None: return self.cls return types.MethodType(self.cls, obj) @staticmethod def bind_early(instance): """Bind all :class:`BoundClass` attributes of the *instance's* class to the instance itself to increase performance.""" cls = type(instance) for name, obj in cls.__dict__.items(): if type(obj) is BoundClass: bound_class = getattr(instance, name) setattr(instance, name, bound_class) class EmptySchedule(Exception): """Thrown by an :class:`Environment` if there are no further events to be processed.""" pass class StopSimulation(Exception): """Indicates that the simulation should stop now.""" @classmethod def callback(cls, event): """Used as callback in :meth:`BaseEnvironment.run()` to stop the simulation when the *until* event occurred.""" if event.ok: raise cls(event.value) else: raise event.value class BaseEnvironment(object): """Base class for event processing environments. An implementation must at least provide the means to access the current time of the environment (see :attr:`now`) and to schedule (see :meth:`schedule()`) events as well as processing them (see :meth:`step()`. The class is meant to be subclassed for different execution environments. For example, SimPy defines a :class:`Environment` for simulations with a virtual time and and a :class:`~simpy.rt.RealtimeEnvironment` that schedules and executes events in real (e.g., wallclock) time. """ @property def now(self): """The current time of the environment.""" raise NotImplementedError(self) @property def active_process(self): """The currently active process of the environment.""" raise NotImplementedError(self) def schedule(self, event, priority=NORMAL, delay=0): """Schedule an *event* with a given *priority* and a *delay*. There are two default priority values, :data:`~simpy.events.URGENT` and :data:`~simpy.events.NORMAL`. """ raise NotImplementedError(self) def step(self): """Processes the next event.""" raise NotImplementedError(self) def run(self, until=None): """Executes :meth:`step()` until the given criterion *until* is met. - If it is ``None`` (which is the default), this method will return when there are no further events to be processed. - If it is an :class:`~simpy.events.Event`, the method will continue stepping until this event has been triggered and will return its value. Raises a :exc:`RuntimeError` if there are no further events to be processed and the *until* event was not triggered. - If it is a number, the method will continue stepping until the environment's time reaches *until*. """ if until is not None: if not isinstance(until, Event): # Assume that *until* is a number if it is not None and # not an event. Create a Timeout(until) in this case. at = float(until) if at <= self.now: raise ValueError('until(=%s) should be > the current ' 'simulation time.' % at) # Schedule the event before all regular timeouts. until = Event(self) until._ok = True until._value = None self.schedule(until, URGENT, at - self.now) elif until.callbacks is None: # Until event has already been processed. return until.value until.callbacks.append(StopSimulation.callback) try: while True: self.step() except StopSimulation as exc: return exc.args[0] # == until.value except EmptySchedule: if until is not None: assert not until.triggered raise RuntimeError('No scheduled events left but "until" ' 'event was not triggered: %s' % until) def exit(self, value=None): """Stop the current process, optionally providing a ``value``. This is a convenience function provided for Python versions prior to 3.3. From Python 3.3, you can instead use ``return value`` in a process. """ raise StopProcess(value) class Environment(BaseEnvironment): """Execution environment for an event-based simulation. The passing of time is simulated by stepping from event to event. You can provide an *initial_time* for the environment. By default, it starts at ``0``. This class also provides aliases for common event types, for example :attr:`process`, :attr:`timeout` and :attr:`event`. """ def __init__(self, initial_time=0): self._now = initial_time self._queue = [] # The list of all currently scheduled events. self._eid = count() # Counter for event IDs self._active_proc = None # Bind all BoundClass instances to "self" to improve performance. BoundClass.bind_early(self) @property def now(self): """The current simulation time.""" return self._now @property def active_process(self): """The currently active process of the environment.""" return self._active_proc process = BoundClass(Process) timeout = BoundClass(Timeout) event = BoundClass(Event) all_of = BoundClass(AllOf) any_of = BoundClass(AnyOf) def schedule(self, event, priority=NORMAL, delay=0): """Schedule an *event* with a given *priority* and a *delay*.""" heappush(self._queue, (self._now + delay, priority, next(self._eid), event)) def peek(self): """Get the time of the next scheduled event. Return :data:`~simpy.core.Infinity` if there is no further event.""" try: return self._queue[0][0] except IndexError: return Infinity def step(self): """Process the next event. Raise an :exc:`EmptySchedule` if no further events are available. """ try: self._now, _, _, event = heappop(self._queue) except IndexError: raise EmptySchedule() # Process callbacks of the event. Set the events callbacks to None # immediately to prevent concurrent modifications. callbacks, event.callbacks = event.callbacks, None for callback in callbacks: callback(event) if not event._ok and not hasattr(event, '_defused'): # The event has failed and has not been defused. Crash the # environment. # Create a copy of the failure exception with a new traceback. exc = type(event._value)(*event._value.args) exc.__cause__ = event._value raise exc simpy-simpy-a59c5bcfade4/src/simpy/events.py0000644000000000000000000005277213323151071017360 0ustar 00000000000000""" This module contains the basic event types used in SimPy. The base class for all events is :class:`Event`. Though it can be directly used, there are several specialized subclasses of it. .. autosummary:: ~simpy.events.Event ~simpy.events.Timeout ~simpy.events.Process ~simpy.events.AnyOf ~simpy.events.AllOf """ from simpy._compat import PY2 from simpy.exceptions import Interrupt, StopProcess if PY2: import sys PENDING = object() """Unique object to identify pending values of events.""" URGENT = 0 """Priority of interrupts and process initialization events.""" NORMAL = 1 """Default priority used by events.""" class Event(object): """An event that may happen at some point in time. An event - may happen (:attr:`triggered` is ``False``), - is going to happen (:attr:`triggered` is ``True``) or - has happened (:attr:`processed` is ``True``). Every event is bound to an environment *env* and is initially not triggered. Events are scheduled for processing by the environment after they are triggered by either :meth:`succeed`, :meth:`fail` or :meth:`trigger`. These methods also set the *ok* flag and the *value* of the event. An event has a list of :attr:`callbacks`. A callback can be any callable. Once an event gets processed, all callbacks will be invoked with the event as the single argument. Callbacks can check if the event was successful by examining *ok* and do further processing with the *value* it has produced. Failed events are never silently ignored and will raise an exception upon being processed. If a callback handles an exception, it must set :attr:`defused` to ``True`` to prevent this. This class also implements ``__and__()`` (``&``) and ``__or__()`` (``|``). If you concatenate two events using one of these operators, a :class:`Condition` event is generated that lets you wait for both or one of them. """ def __init__(self, env): self.env = env """The :class:`~simpy.core.Environment` the event lives in.""" self.callbacks = [] """List of functions that are called when the event is processed.""" self._value = PENDING def __repr__(self): """Return the description of the event (see :meth:`_desc`) with the id of the event.""" return '<%s object at 0x%x>' % (self._desc(), id(self)) def _desc(self): """Return a string *Event()*.""" return '%s()' % self.__class__.__name__ @property def triggered(self): """Becomes ``True`` if the event has been triggered and its callbacks are about to be invoked.""" return self._value is not PENDING @property def processed(self): """Becomes ``True`` if the event has been processed (e.g., its callbacks have been invoked).""" return self.callbacks is None @property def ok(self): """Becomes ``True`` when the event has been triggered successfully. A "successful" event is one triggered with :meth:`succeed()`. :raises AttributeError: if accessed before the event is triggered. """ return self._ok @property def defused(self): """Becomes ``True`` when the failed event's exception is "defused". When an event fails (i.e. with :meth:`fail()`), the failed event's `value` is an exception that will be re-raised when the :class:`~simpy.core.Environment` processes the event (i.e. in :meth:`~simpy.core.Environment.step()`). It is also possible for the failed event's exception to be defused by setting :attr:`defused` to ``True`` from an event callback. Doing so prevents the event's exception from being re-raised when the event is processed by the :class:`~simpy.core.Environment`. """ return hasattr(self, '_defused') @defused.setter def defused(self, value): self._defused = True @property def value(self): """The value of the event if it is available. The value is available when the event has been triggered. Raises :exc:`AttributeError` if the value is not yet available. """ if self._value is PENDING: raise AttributeError('Value of %s is not yet available' % self) return self._value def trigger(self, event): """Trigger the event with the state and value of the provided *event*. Return *self* (this event instance). This method can be used directly as a callback function to trigger chain reactions. """ self._ok = event._ok self._value = event._value self.env.schedule(self) def succeed(self, value=None): """Set the event's value, mark it as successful and schedule it for processing by the environment. Returns the event instance. Raises :exc:`RuntimeError` if this event has already been triggerd. """ if self._value is not PENDING: raise RuntimeError('%s has already been triggered' % self) self._ok = True self._value = value self.env.schedule(self) return self def fail(self, exception): """Set *exception* as the events value, mark it as failed and schedule it for processing by the environment. Returns the event instance. Raises :exc:`ValueError` if *exception* is not an :exc:`Exception`. Raises :exc:`RuntimeError` if this event has already been triggered. """ if self._value is not PENDING: raise RuntimeError('%s has already been triggered' % self) if not isinstance(exception, BaseException): raise ValueError('%s is not an exception.' % exception) self._ok = False self._value = exception self.env.schedule(self) return self def __and__(self, other): """Return a :class:`~simpy.events.Condition` that will be triggered if both, this event and *other*, have been processed.""" return Condition(self.env, Condition.all_events, [self, other]) def __or__(self, other): """Return a :class:`~simpy.events.Condition` that will be triggered if either this event or *other* have been processed (or even both, if they happened concurrently).""" return Condition(self.env, Condition.any_events, [self, other]) class Timeout(Event): """A :class:`~simpy.events.Event` that gets triggered after a *delay* has passed. This event is automatically triggered when it is created. """ def __init__(self, env, delay, value=None): if delay < 0: raise ValueError('Negative delay %s' % delay) # NOTE: The following initialization code is inlined from # Event.__init__() for performance reasons. self.env = env self.callbacks = [] self._value = value self._delay = delay self._ok = True env.schedule(self, NORMAL, delay) def _desc(self): """Return a string *Timeout(delay[, value=value])*.""" return '%s(%s%s)' % (self.__class__.__name__, self._delay, '' if self._value is None else (', value=%s' % self._value)) class Initialize(Event): """Initializes a process. Only used internally by :class:`Process`. This event is automatically triggered when it is created. """ def __init__(self, env, process): # NOTE: The following initialization code is inlined from # Event.__init__() for performance reasons. self.env = env self.callbacks = [process._resume] self._value = None # The initialization events needs to be scheduled as urgent so that it # will be handled before interrupts. Otherwise a process whose # generator has not yet been started could be interrupted. self._ok = True env.schedule(self, URGENT) class Interruption(Event): """Immediately schedules an :class:`~simpy.exceptions.Interrupt` exception with the given *cause* to be thrown into *process*. This event is automatically triggered when it is created. """ def __init__(self, process, cause): # NOTE: The following initialization code is inlined from # Event.__init__() for performance reasons. self.env = process.env self.callbacks = [self._interrupt] self._value = Interrupt(cause) self._ok = False self._defused = True if process._value is not PENDING: raise RuntimeError('%s has terminated and cannot be interrupted.' % process) if process is self.env.active_process: raise RuntimeError('A process is not allowed to interrupt itself.') self.process = process self.env.schedule(self, URGENT) def _interrupt(self, event): # Ignore dead processes. Multiple concurrently scheduled interrupts # cause this situation. If the process dies while handling the first # one, the remaining interrupts must be ignored. if self.process._value is not PENDING: return # A process never expects an interrupt and is always waiting for a # target event. Remove the process from the callbacks of the target. self.process._target.callbacks.remove(self.process._resume) self.process._resume(self) class Process(Event): """Process an event yielding generator. A generator (also known as a coroutine) can suspend its execution by yielding an event. ``Process`` will take care of resuming the generator with the value of that event once it has happened. The exception of failed events is thrown into the generator. ``Process`` itself is an event, too. It is triggered, once the generator returns or raises an exception. The value of the process is the return value of the generator or the exception, respectively. .. note:: Python version prior to 3.3 do not support return statements in generators. You can use :meth:~simpy.core.Environment.exit() as a workaround. Processes can be interrupted during their execution by :meth:`interrupt`. """ def __init__(self, env, generator): if not hasattr(generator, 'throw'): # Implementation note: Python implementations differ in the # generator types they provide. Cython adds its own generator type # in addition to the CPython type, which renders a type check # impractical. To workaround this issue, we check for attribute # name instead of type and optimistically assume that all objects # with a ``throw`` attribute are generators (the more intuitive # name ``__next__`` cannot be used because it was renamed from # ``next`` in Python 2). # Remove this workaround if it causes issues in production! raise ValueError('%s is not a generator.' % generator) # NOTE: The following initialization code is inlined from # Event.__init__() for performance reasons. self.env = env self.callbacks = [] self._value = PENDING self._generator = generator # Schedule the start of the execution of the process. self._target = Initialize(env, self) def _desc(self): """Return a string *Process(process_func_name)*.""" return '%s(%s)' % (self.__class__.__name__, self._generator.__name__) @property def target(self): """The event that the process is currently waiting for. Returns ``None`` if the process is dead or it is currently being interrupted. """ return self._target @property def is_alive(self): """``True`` until the process generator exits.""" return self._value is PENDING def interrupt(self, cause=None): """Interupt this process optionally providing a *cause*. A process cannot be interrupted if it already terminated. A process can also not interrupt itself. Raise a :exc:`RuntimeError` in these cases. """ Interruption(self, cause) def _resume(self, event): """Resumes the execution of the process with the value of *event*. If the process generator exits, the process itself will get triggered with the return value or the exception of the generator.""" # Mark the current process as active. self.env._active_proc = self while True: # Get next event from process try: if event._ok: event = self._generator.send(event._value) else: # The process has no choice but to handle the failed event # (or fail itself). event._defused = True # Create an exclusive copy of the exception for this # process to prevent traceback modifications by other # processes. exc = type(event._value)(*event._value.args) exc.__cause__ = event._value if PY2: if hasattr(event._value, '__traceback__'): exc.__traceback__ = event._value.__traceback__ event = self._generator.throw(exc) except (StopIteration, StopProcess) as e: # Process has terminated. event = None self._ok = True self._value = e.args[0] if len(e.args) else None self.env.schedule(self) break except BaseException as e: # Process has failed. event = None self._ok = False tb = e.__traceback__ if not PY2 else sys.exc_info()[2] # Strip the frame of this function from the traceback as it # does not add any useful information. e.__traceback__ = tb.tb_next self._value = e self.env.schedule(self) break # Process returned another event to wait upon. try: # Be optimistic and blindly access the callbacks attribute. if event.callbacks is not None: # The event has not yet been triggered. Register callback # to resume the process if that happens. event.callbacks.append(self._resume) break except AttributeError: # Our optimism didn't work out, figure out what went wrong and # inform the user. if not hasattr(event, 'callbacks'): msg = 'Invalid yield value "%s"' % event descr = _describe_frame(self._generator.gi_frame) error = RuntimeError('\n%s%s' % (descr, msg)) # Drop the AttributeError as the cause for this exception. error.__cause__ = None raise error self._target = event self.env._active_proc = None class ConditionValue(object): """Result of a :class:`~simpy.events.Condition`. It supports convenient dict-like access to the triggered events and their values. The events are ordered by their occurences in the condition.""" def __init__(self): self.events = [] def __getitem__(self, key): if key not in self.events: raise KeyError(str(key)) return key._value def __contains__(self, key): return key in self.events def __eq__(self, other): if type(other) is ConditionValue: return self.events == other.events return self.todict() == other def __repr__(self): return '' % self.todict() def __iter__(self): return self.keys() def keys(self): return (event for event in self.events) def values(self): return (event._value for event in self.events) def items(self): return ((event, event._value) for event in self.events) def todict(self): return dict((event, event._value) for event in self.events) class Condition(Event): """An event that gets triggered once the condition function *evaluate* returns ``True`` on the given list of *events*. The value of the condition event is an instance of :class:`ConditionValue` which allows convenient access to the input events and their values. The :class:`ConditionValue` will only contain entries for those events that occurred before the condition is processed. If one of the events fails, the condition also fails and forwards the exception of the failing event. The *evaluate* function receives the list of target events and the number of processed events in this list: ``evaluate(events, processed_count)``. If it returns ``True``, the condition is triggered. The :func:`Condition.all_events()` and :func:`Condition.any_events()` functions are used to implement *and* (``&``) and *or* (``|``) for events. Condition events can be nested. """ def __init__(self, env, evaluate, events): super(Condition, self).__init__(env) self._evaluate = evaluate self._events = events if type(events) is tuple else tuple(events) self._count = 0 if not self._events: # Immediately succeed if no events are provided. self.succeed(ConditionValue()) return # Check if events belong to the same environment. for event in self._events: if self.env != event.env: raise ValueError('It is not allowed to mix events from ' 'different environments') # Check if the condition is met for each processed event. Attach # _check() as a callback otherwise. for event in self._events: if event.callbacks is None: self._check(event) else: event.callbacks.append(self._check) # Register a callback which will build the value of this condition # after it has been triggered. self.callbacks.append(self._build_value) def _desc(self): """Return a string *Condition(evaluate, [events])*.""" return '%s(%s, %s)' % (self.__class__.__name__, self._evaluate.__name__, self._events) def _populate_value(self, value): """Populate the *value* by recursively visiting all nested conditions.""" for event in self._events: if isinstance(event, Condition): event._populate_value(value) elif event.callbacks is None: value.events.append(event) def _build_value(self, event): """Build the value of this condition.""" self._remove_check_callbacks() if event._ok: self._value = ConditionValue() self._populate_value(self._value) def _remove_check_callbacks(self): """Remove _check() callbacks from events recursively. Once the condition has triggered, the condition's events no longer need to have _check() callbacks. Removing the _check() callbacks is important to break circular references between the condition and untriggered events. """ for event in self._events: if event.callbacks and self._check in event.callbacks: event.callbacks.remove(self._check) if isinstance(event, Condition): event._remove_check_callbacks() def _check(self, event): """Check if the condition was already met and schedule the *event* if so.""" if self._value is not PENDING: return self._count += 1 if not event._ok: # Abort if the event has failed. event._defused = True self.fail(event._value) elif self._evaluate(self._events, self._count): # The condition has been met. The _build_value() callback will # populate the ConditionValue once this condition is processed. self.succeed() @staticmethod def all_events(events, count): """An evaluation function that returns ``True`` if all *events* have been triggered.""" return len(events) == count @staticmethod def any_events(events, count): """An evaluation function that returns ``True`` if at least one of *events* has been triggered.""" return count > 0 or len(events) == 0 class AllOf(Condition): """A :class:`~simpy.events.Condition` event that is triggered if all of a list of *events* have been successfully triggered. Fails immediately if any of *events* failed. """ def __init__(self, env, events): super(AllOf, self).__init__(env, Condition.all_events, events) class AnyOf(Condition): """A :class:`~simpy.events.Condition` event that is triggered if any of a list of *events* has been successfully triggered. Fails immediately if any of *events* failed. """ def __init__(self, env, events): super(AnyOf, self).__init__(env, Condition.any_events, events) def _describe_frame(frame): """Print filename, line number and function name of a stack frame.""" filename, name = frame.f_code.co_filename, frame.f_code.co_name lineno = frame.f_lineno with open(filename) as f: for no, line in enumerate(f): if no + 1 == lineno: break return ' File "%s", line %d, in %s\n %s\n' % (filename, lineno, name, line.strip()) simpy-simpy-a59c5bcfade4/src/simpy/exceptions.py0000644000000000000000000000270413323151071020223 0ustar 00000000000000""" SimPy specific exeptions. """ class SimPyException(Exception): """Base class for all SimPy specific exceptions.""" class Interrupt(SimPyException): """Exception thrown into a process if it is interrupted (see :func:`~simpy.events.Process.interrupt()`). :attr:`cause` provides the reason for the interrupt, if any. If a process is interrupted concurrently, all interrupts will be thrown into the process in the same order as they occurred. """ def __init__(self, cause): super(Interrupt, self).__init__(cause) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.cause) @property def cause(self): """The cause of the interrupt or ``None`` if no cause was provided.""" return self.args[0] class StopProcess(SimPyException): """Raised to stop a SimPy process (similar to :exc:`StopIteration`). In Python 2, a ``return value`` inside generator functions is not allowed. The fall-back was raising ``StopIteration(value)`` instead. However, this is deprecated_ now, so we need a custom exception type for this. .. _deprecated: https://www.python.org/dev/peps/pep-0479/ """ def __init__(self, value): super(StopProcess, self).__init__(value) def __str__(self): return '%s(%r)' % (self.__class__.__name__, self.value) @property def value(self): """The process' return value.""" return self.args[0] simpy-simpy-a59c5bcfade4/src/simpy/resources/__init__.py0000644000000000000000000000060613323151071021612 0ustar 00000000000000""" SimPy implements three types of resources that can be used to synchronize processes or to model congestion points: .. currentmodule:: simpy.resources .. autosummary:: resource container store They are derived from the base classes defined in the :mod:`~simpy.resources.base` module. These classes are also meant to support the implementation of custom resource types. """ simpy-simpy-a59c5bcfade4/src/simpy/resources/base.py0000644000000000000000000002007313323151071020765 0ustar 00000000000000""" Base classes of for SimPy's shared resource types. :class:`BaseResource` defines the abstract base resource. It supports *get* and *put* requests, which return :class:`Put` and :class:`Get` events respectively. These events are triggered once the request has been completed. """ from simpy.core import BoundClass from simpy.events import Event class Put(Event): """Generic event for requesting to put something into the *resource*. This event (and all of its subclasses) can act as context manager and can be used with the :keyword:`with` statement to automatically cancel the request if an exception (like an :class:`simpy.exceptions.Interrupt` for example) occurs: .. code-block:: python with res.put(item) as request: yield request """ def __init__(self, resource): super(Put, self).__init__(resource._env) self.resource = resource self.proc = self.env.active_process resource.put_queue.append(self) self.callbacks.append(resource._trigger_get) resource._trigger_put(None) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.cancel() def cancel(self): """Cancel this put request. This method has to be called if the put request must be aborted, for example if a process needs to handle an exception like an :class:`~simpy.exceptions.Interrupt`. If the put request was created in a :keyword:`with` statement, this method is called automatically. """ if not self.triggered: self.resource.put_queue.remove(self) class Get(Event): """Generic event for requesting to get something from the *resource*. This event (and all of its subclasses) can act as context manager and can be used with the :keyword:`with` statement to automatically cancel the request if an exception (like an :class:`simpy.exceptions.Interrupt` for example) occurs: .. code-block:: python with res.get() as request: item = yield request """ def __init__(self, resource): super(Get, self).__init__(resource._env) self.resource = resource self.proc = self.env.active_process resource.get_queue.append(self) self.callbacks.append(resource._trigger_put) resource._trigger_get(None) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.cancel() def cancel(self): """Cancel this get request. This method has to be called if the get request must be aborted, for example if a process needs to handle an exception like an :class:`~simpy.exceptions.Interrupt`. If the get request was created in a :keyword:`with` statement, this method is called automatically. """ if not self.triggered: self.resource.get_queue.remove(self) class BaseResource(object): """Abstract base class for a shared resource. You can :meth:`put()` something into the resources or :meth:`get()` something out of it. Both methods return an event that is triggered once the operation is completed. If a :meth:`put()` request cannot complete immediately (for example if the resource has reached a capacity limit) it is enqueued in the :attr:`put_queue` for later processing. Likewise for :meth:`get()` requests. Subclasses can customize the resource by: - providing custom :attr:`PutQueue` and :attr:`GetQueue` types, - providing custom :class:`Put` respectively :class:`Get` events, - and implementing the request processing behaviour through the methods ``_do_get()`` and ``_do_put()``. """ PutQueue = list """The type to be used for the :attr:`put_queue`. It is a plain :class:`list` by default. The type must support index access (e.g. ``__getitem__()`` and ``__len__()``) as well as provide ``append()`` and ``pop()`` operations.""" GetQueue = list """The type to be used for the :attr:`get_queue`. It is a plain :class:`list` by default. The type must support index access (e.g. ``__getitem__()`` and ``__len__()``) as well as provide ``append()`` and ``pop()`` operations.""" def __init__(self, env, capacity): self._env = env self._capacity = capacity self.put_queue = self.PutQueue() """Queue of pending *put* requests.""" self.get_queue = self.GetQueue() """Queue of pending *get* requests.""" # Bind event constructors as methods BoundClass.bind_early(self) @property def capacity(self): """Maximum capacity of the resource.""" return self._capacity put = BoundClass(Put) """Request to put something into the resource and return a :class:`Put` event, which gets triggered once the request succeeds.""" get = BoundClass(Get) """Request to get something from the resource and return a :class:`Get` event, which gets triggered once the request succeeds.""" def _do_put(self, event): """Perform the *put* operation. This method needs to be implemented by subclasses. If the conditions for the put *event* are met, the method must trigger the event (e.g. call :meth:`Event.succeed()` with an apropriate value). This method is called by :meth:`_trigger_put` for every event in the :attr:`put_queue`, as long as the return value does not evaluate ``False``. """ raise NotImplementedError(self) def _trigger_put(self, get_event): """This method is called once a new put event has been created or a get event has been processed. The method iterates over all put events in the :attr:`put_queue` and calls :meth:`_do_put` to check if the conditions for the event are met. If :meth:`_do_put` returns ``False``, the iteration is stopped early. """ # Maintain queue invariant: All put requests must be untriggered. # This code is not very pythonic because the queue interface should be # simple (only append(), pop(), __getitem__() and __len__() are # required). idx = 0 while idx < len(self.put_queue): put_event = self.put_queue[idx] proceed = self._do_put(put_event) if not put_event.triggered: idx += 1 elif self.put_queue.pop(idx) != put_event: raise RuntimeError('Put queue invariant violated') if not proceed: break def _do_get(self, event): """Perform the *get* operation. This method needs to be implemented by subclasses. If the conditions for the get *event* are met, the method must trigger the event (e.g. call :meth:`Event.succeed()` with an apropriate value). This method is called by :meth:`_trigger_get` for every event in the :attr:`get_queue`, as long as the return value does not evaluate ``False``. """ raise NotImplementedError(self) def _trigger_get(self, put_event): """Trigger get events. This method is called once a new get event has been created or a put event has been processed. The method iterates over all get events in the :attr:`get_queue` and calls :meth:`_do_get` to check if the conditions for the event are met. If :meth:`_do_get` returns ``False``, the iteration is stopped early. """ # Maintain queue invariant: All get requests must be untriggered. # This code is not very pythonic because the queue interface should be # simple (only append(), pop(), __getitem__() and __len__() are # required). idx = 0 while idx < len(self.get_queue): get_event = self.get_queue[idx] proceed = self._do_get(get_event) if not get_event.triggered: idx += 1 elif self.get_queue.pop(idx) != get_event: raise RuntimeError('Get queue invariant violated') if not proceed: break simpy-simpy-a59c5bcfade4/src/simpy/resources/container.py0000644000000000000000000000605713323151071022043 0ustar 00000000000000""" Resource for sharing homogeneous matter between processes, either continuous (like water) or discrete (like apples). A :class:`Container` can be used to model the fuel tank of a gasoline station. Tankers increase and refuelled cars decrease the amount of gas in the station's fuel tanks. """ from simpy.core import BoundClass from simpy.resources import base class ContainerPut(base.Put): """Request to put *amount* of matter into the *container*. The request will be triggered once there is enough space in the *container* available. Raise a :exc:`ValueError` if ``amount <= 0``. """ def __init__(self, container, amount): if amount <= 0: raise ValueError('amount(=%s) must be > 0.' % amount) self.amount = amount """The amount of matter to be put into the container.""" super(ContainerPut, self).__init__(container) class ContainerGet(base.Get): """Request to get *amount* of matter from the *container*. The request will be triggered once there is enough matter available in the *container*. Raise a :exc:`ValueError` if ``amount <= 0``. """ def __init__(self, container, amount): if amount <= 0: raise ValueError('amount(=%s) must be > 0.' % amount) self.amount = amount """The amount of matter to be taken out of the container.""" super(ContainerGet, self).__init__(container) class Container(base.BaseResource): """Resource containing up to *capacity* of matter which may either be continuous (like water) or discrete (like apples). It supports requests to put or get matter into/from the container. The *env* parameter is the :class:`~simpy.core.Environment` instance the container is bound to. The *capacity* defines the size of the container. By default, a container is of unlimited size. The initial amount of matter is specified by *init* and defaults to ``0``. Raise a :exc:`ValueError` if ``capacity <= 0``, ``init < 0`` or ``init > capacity``. """ def __init__(self, env, capacity=float('inf'), init=0): if capacity <= 0: raise ValueError('"capacity" must be > 0.') if init < 0: raise ValueError('"init" must be >= 0.') if init > capacity: raise ValueError('"init" must be <= "capacity".') super(Container, self).__init__(env, capacity) self._level = init @property def level(self): """The current amount of the matter in the container.""" return self._level put = BoundClass(ContainerPut) """Request to put *amount* of matter into the container.""" get = BoundClass(ContainerGet) """Request to get *amount* of matter out of the container.""" def _do_put(self, event): if self._capacity - self._level >= event.amount: self._level += event.amount event.succeed() return True def _do_get(self, event): if self._level >= event.amount: self._level -= event.amount event.succeed() return True simpy-simpy-a59c5bcfade4/src/simpy/resources/resource.py0000644000000000000000000002052313323151071021702 0ustar 00000000000000""" Shared resources supporting priorities and preemption. These resources can be used to limit the number of processes using them concurrently. A process needs to *request* the usage right to a resource. Once the usage right is not needed anymore it has to be *released*. A gas station can be modelled as a resource with a limited amount of fuel-pumps. Vehicles arrive at the gas station and request to use a fuel-pump. If all fuel-pumps are in use, the vehicle needs to wait until one of the users has finished refueling and releases its fuel-pump. These resources can be used by a limited number of processes at a time. Processes *request* these resources to become a user and have to *release* them once they are done. For example, a gas station with a limited number of fuel pumps can be modeled with a `Resource`. Arriving vehicles request a fuel-pump. Once one is available they refuel. When they are done, the release the fuel-pump and leave the gas station. Requesting a resource is modelled as "putting a process' token into the resources" and releasing a resources correspondingly as "getting a process' token out of the resource". Thus, calling ``request()``/``release()`` is equivalent to calling ``put()``/``get()``. Note, that releasing a resource will always succeed immediately, no matter if a process is actually using a resource or not. Besides :class:`Resource`, there is a :class:`PriorityResource`, where processes can define a request priority, and a :class:`PreemptiveResource` whose resource users can be preempted by requests with a higher priority. """ from simpy.core import BoundClass from simpy.resources import base class Preempted(object): """Cause of an preemption :class:`~simpy.exceptions.Interrupt` containing information about the preemption. """ def __init__(self, by, usage_since, resource): self.by = by """The preempting :class:`simpy.events.Process`.""" self.usage_since = usage_since """The simulation time at which the preempted process started to use the resource.""" self.resource = resource """The resource which was lost, i.e., caused the preemption.""" class Request(base.Put): """Request usage of the *resource*. The event is triggered once access is granted. Subclass of :class:`simpy.resources.base.Put`. If the maximum capacity of users has not yet been reached, the request is triggered immediately. If the maximum capacity has been reached, the request is triggered once an earlier usage request on the resource is released. The request is automatically released when the request was created within a :keyword:`with` statement. """ def __exit__(self, exc_type, value, traceback): super(Request, self).__exit__(exc_type, value, traceback) # Don't release the resource on generator cleanups. This seems to # create unclaimable circular references otherwise. if exc_type is not GeneratorExit: self.resource.release(self) class Release(base.Get): """Releases the usage of *resource* granted by *request*. This event is triggered immediately. Subclass of :class:`simpy.resources.base.Get`. """ def __init__(self, resource, request): self.request = request """The request (:class:`Request`) that is to be released.""" super(Release, self).__init__(resource) class PriorityRequest(Request): """Request the usage of *resource* with a given *priority*. If the *resource* supports preemption and *preempt* is ``True`` other usage requests of the *resource* may be preempted (see :class:`PreemptiveResource` for details). This event type inherits :class:`Request` and adds some additional attributes needed by :class:`PriorityResource` and :class:`PreemptiveResource` """ def __init__(self, resource, priority=0, preempt=True): self.priority = priority """The priority of this request. A smaller number means higher priority.""" self.preempt = preempt """Indicates whether the request should preempt a resource user or not (:class:`PriorityResource` ignores this flag).""" self.time = resource._env.now """The time at which the request was made.""" self.usage_since = None """The time at which the request succeeded.""" self.key = (self.priority, self.time, not self.preempt) """Key for sorting events. Consists of the priority (lower value is more important), the time at which the request was made (earlier requests are more important) and finally the preemption flag (preempt requests are more important).""" super(PriorityRequest, self).__init__(resource) class SortedQueue(list): """Queue for sorting events by their :attr:`~PriorityRequest.key` attribute. """ def __init__(self, maxlen=None): super(SortedQueue, self).__init__() self.maxlen = maxlen """Maximum length of the queue.""" def append(self, item): """Sort *item* into the queue. Raise a :exc:`RuntimeError` if the queue is full. """ if self.maxlen is not None and len(self) >= self.maxlen: raise RuntimeError('Cannot append event. Queue is full.') super(SortedQueue, self).append(item) super(SortedQueue, self).sort(key=lambda e: e.key) class Resource(base.BaseResource): """Resource with *capacity* of usage slots that can be requested by processes. If all slots are taken, requests are enqueued. Once a usage request is released, a pending request will be triggered. The *env* parameter is the :class:`~simpy.core.Environment` instance the resource is bound to. """ def __init__(self, env, capacity=1): if capacity <= 0: raise ValueError('"capacity" must be > 0.') super(Resource, self).__init__(env, capacity) self.users = [] """List of :class:`Request` events for the processes that are currently using the resource.""" self.queue = self.put_queue """Queue of pending :class:`Request` events. Alias of :attr:`~simpy.resources.base.BaseResource.put_queue`. """ @property def count(self): """Number of users currently using the resource.""" return len(self.users) request = BoundClass(Request) """Request a usage slot.""" release = BoundClass(Release) """Release a usage slot.""" def _do_put(self, event): if len(self.users) < self.capacity: self.users.append(event) event.usage_since = self._env.now event.succeed() def _do_get(self, event): try: self.users.remove(event.request) except ValueError: pass event.succeed() class PriorityResource(Resource): """A :class:`~simpy.resources.resource.Resource` supporting prioritized requests. Pending requests in the :attr:`~Resource.queue` are sorted in ascending order by their *priority* (that means lower values are more important). """ PutQueue = SortedQueue """Type of the put queue. See :attr:`~simpy.resources.base.BaseResource.put_queue` for details.""" GetQueue = list """Type of the get queue. See :attr:`~simpy.resources.base.BaseResource.get_queue` for details.""" def __init__(self, env, capacity=1): super(PriorityResource, self).__init__(env, capacity) request = BoundClass(PriorityRequest) """Request a usage slot with the given *priority*.""" release = BoundClass(Release) """Release a usage slot.""" class PreemptiveResource(PriorityResource): """A :class:`~simpy.resources.resource.PriorityResource` with preemption. If a request is preempted, the process of that request will receive an :class:`~simpy.exceptions.Interrupt` with a :class:`Preempted` instance as cause. """ def _do_put(self, event): if len(self.users) >= self.capacity and event.preempt: # Check if we can preempt another process preempt = sorted(self.users, key=lambda e: e.key)[-1] if preempt.key > event.key: self.users.remove(preempt) preempt.proc.interrupt(Preempted( by=event.proc, usage_since=preempt.usage_since, resource=self)) return super(PreemptiveResource, self)._do_put(event) simpy-simpy-a59c5bcfade4/src/simpy/resources/store.py0000644000000000000000000001221613323151071021207 0ustar 00000000000000""" Shared resources for storing a possibly unlimited amount of objects supporting requests for specific objects. The :class:`Store` operates in a FIFO (first-in, first-out) order. Objects are retrieved from the store in the order they were put in. The *get* requests of a :class:`FilterStore` can be customized by a filter to only retrieve objects matching a given criterion. """ from heapq import heappush, heappop from collections import namedtuple from simpy.core import BoundClass from simpy.resources import base class StorePut(base.Put): """Request to put *item* into the *store*. The request is triggered once there is space for the item in the store. """ def __init__(self, store, item): self.item = item """The item to put into the store.""" super(StorePut, self).__init__(store) class StoreGet(base.Get): """Request to get an *item* from the *store*. The request is triggered once there is an item available in the store. """ pass class FilterStoreGet(StoreGet): """Request to get an *item* from the *store* matching the *filter*. The request is triggered once there is such an item available in the store. *filter* is a function receiving one item. It should return ``True`` for items matching the filter criterion. The default function returns ``True`` for all items, which makes the request to behave exactly like :class:`StoreGet`. """ def __init__(self, resource, filter=lambda item: True): self.filter = filter """The filter function to filter items in the store.""" super(FilterStoreGet, self).__init__(resource) class Store(base.BaseResource): """Resource with *capacity* slots for storing arbitrary objects. By default, the *capacity* is unlimited and objects are put and retrieved from the store in a first-in first-out order. The *env* parameter is the :class:`~simpy.core.Environment` instance the container is bound to. """ def __init__(self, env, capacity=float('inf')): if capacity <= 0: raise ValueError('"capacity" must be > 0.') super(Store, self).__init__(env, capacity) self.items = [] """List of the items available in the store.""" put = BoundClass(StorePut) """Request to put *item* into the store.""" get = BoundClass(StoreGet) """Request to get an *item* out of the store.""" def _do_put(self, event): if len(self.items) < self._capacity: self.items.append(event.item) event.succeed() def _do_get(self, event): if self.items: event.succeed(self.items.pop(0)) class PriorityItem(namedtuple('PriorityItem', 'priority item')): """Wrap an arbitrary *item* with an orderable *priority*. Pairs a *priority* with an arbitrary *item*. Comparisons of *PriorityItem* instances only consider the *priority* attribute, thus supporting use of unorderable items in a :class:`PriorityStore` instance. """ def __lt__(self, other): return self.priority < other.priority class PriorityStore(Store): """Resource with *capacity* slots for storing objects in priority order. Unlike :class:`Store` which provides first-in first-out discipline, :class:`PriorityStore` maintains items in sorted order such that the smallest items value are retreived first from the store. All items in a *PriorityStore* instance must be orderable; which is to say that items must implement :meth:`~object.__lt__()`. To use unorderable items with *PriorityStore*, use :class:`PriorityItem`. """ def _do_put(self, event): if len(self.items) < self._capacity: heappush(self.items, event.item) event.succeed() def _do_get(self, event): if self.items: event.succeed(heappop(self.items)) class FilterStore(Store): """Resource with *capacity* slots for storing arbitrary objects supporting filtered get requests. Like the :class:`Store`, the *capacity* is unlimited by default and objects are put and retrieved from the store in a first-in first-out order. Get requests can be customized with a filter function to only trigger for items for which said filter function returns ``True``. .. note:: In contrast to :class:`Store`, get requests of a :class:`FilterStore` won't necessarily be triggered in the same order they were issued. *Example:* The store is empty. *Process 1* tries to get an item of type *a*, *Process 2* an item of type *b*. Another process puts one item of type *b* into the store. Though *Process 2* made his request after *Process 1*, it will receive that new item because *Process 1* doesn't want it. """ put = BoundClass(StorePut) """Request a to put *item* into the store.""" get = BoundClass(FilterStoreGet) """Request a to get an *item*, for which *filter* returns ``True``, out of the store.""" def _do_get(self, event): for item in self.items: if event.filter(item): self.items.remove(item) event.succeed(item) break return True simpy-simpy-a59c5bcfade4/src/simpy/rt.py0000644000000000000000000000564713323151071016500 0ustar 00000000000000"""Execution environment for events that synchronizes passing of time with the real-time (aka *wall-clock time*). """ try: # Python >= 3.3 from time import monotonic as time, sleep except ImportError: # Python < 3.3 from time import time, sleep from simpy.core import Environment, EmptySchedule, Infinity class RealtimeEnvironment(Environment): """Execution environment for an event-based simulation which is synchronized with the real-time (also known as wall-clock time). A time step will take *factor* seconds of real time (one second by default). A step from ``0`` to ``3`` with a ``factor=0.5`` will, for example, take at least 1.5 seconds. The :meth:`step()` method will raise a :exc:`RuntimeError` if a time step took too long to compute. This behaviour can be disabled by setting *strict* to ``False``. """ def __init__(self, initial_time=0, factor=1.0, strict=True): Environment.__init__(self, initial_time) self.env_start = initial_time self.real_start = time() self._factor = factor self._strict = strict @property def factor(self): """Scaling factor of the real-time.""" return self._factor @property def strict(self): """Running mode of the environment. :meth:`step()` will raise a :exc:`RuntimeError` if this is set to ``True`` and the processing of events takes too long.""" return self._strict def sync(self): """Synchronize the internal time with the current wall-clock time. This can be useful to prevent :meth:`step()` from raising an error if a lot of time passes between creating the RealtimeEnvironment and calling :meth:`run()` or :meth:`step()`. """ self.real_start = time() def step(self): """Process the next event after enough real-time has passed for the event to happen. The delay is scaled according to the real-time :attr:`factor`. With :attr:`strict` mode enabled, a :exc:`RuntimeError` will be raised, if the event is processed too slowly. """ evt_time = self.peek() if evt_time is Infinity: raise EmptySchedule() real_time = self.real_start + (evt_time - self.env_start) * self.factor if self.strict and time() - real_time > self.factor: # Events scheduled for time *t* may take just up to *t+1* # for their computation, before an error is raised. raise RuntimeError('Simulation too slow for real time (%.3fs).' % ( time() - real_time)) # Sleep in a loop to fix inaccuracies of windows (see # http://stackoverflow.com/a/15967564 for details) and to ignore # interrupts. while True: delta = real_time - time() if delta <= 0: break sleep(delta) return Environment.step(self) simpy-simpy-a59c5bcfade4/src/simpy/util.py0000644000000000000000000000330513323151071017015 0ustar 00000000000000""" A collection of utility functions: .. autosummary:: start_delayed """ def start_delayed(env, generator, delay): """Return a helper process that starts another process for *generator* after a certain *delay*. :meth:`~simpy.core.Environment.process()` starts a process at the current simulation time. This helper allows you to start a process after a delay of *delay* simulation time units:: >>> from simpy import Environment >>> from simpy.util import start_delayed >>> def my_process(env, x): ... print('%s, %s' % (env.now, x)) ... yield env.timeout(1) ... >>> env = Environment() >>> proc = start_delayed(env, my_process(env, 3), 5) >>> env.run() 5, 3 Raise a :exc:`ValueError` if ``delay <= 0``. """ if delay <= 0: raise ValueError('delay(=%s) must be > 0.' % delay) def starter(): yield env.timeout(delay) proc = env.process(generator) env.exit(proc) return env.process(starter()) def subscribe_at(event): """Register at the *event* to receive an interrupt when it occurs. The most common use case for this is to pass a :class:`~simpy.events.Process` to get notified when it terminates. Raise a :exc:`RuntimeError` if ``event`` has already occurred. """ env = event.env subscriber = env.active_process def signaller(signaller, receiver): result = yield signaller if receiver.is_alive: receiver.interrupt((signaller, result)) if event.callbacks is not None: env.process(signaller(event, subscriber)) else: raise RuntimeError('%s has already terminated.' % event) simpy-simpy-a59c5bcfade4/tests/__init__.py0000644000000000000000000000000013323151071016776 0ustar 00000000000000simpy-simpy-a59c5bcfade4/tests/conftest.py0000644000000000000000000000020313323151071017071 0ustar 00000000000000import pytest import simpy @pytest.fixture def log(): return [] @pytest.fixture def env(): return simpy.Environment() simpy-simpy-a59c5bcfade4/tests/test_benchmark.py0000644000000000000000000001006313323151071020242 0ustar 00000000000000""" Performance benchmark tests using the `pytest-benchmark` package. Benchmarks are divided into three groups: *frequent*, *targeted*, and *simulation*. The *frequent* group benchmarks various simpy functions expected to be called frequently in normal simulations. The *targeted* group benchmarks singular behaviors run by the environment. The *simulation* group benchmarks complete simulations using processes and resources. """ import random import pytest import simpy @pytest.mark.benchmark(group='frequent') def test_event_init(env, benchmark): benchmark(env.event) @pytest.mark.benchmark(group='frequent') def test_timeout_init(env, benchmark): benchmark(env.timeout, 1) @pytest.mark.benchmark(group='frequent') def test_process_init(env, benchmark): def g(): yield env.timeout(1) benchmark(env.process, g()) @pytest.mark.benchmark(group='frequent') def test_environment_step(env, benchmark): def g(env): while True: yield env.timeout(1) env.process(g(env)) benchmark(env.step) @pytest.mark.benchmark(group='targeted') def test_condition_events(env, benchmark): def cond_proc(env): yield (env.timeout(0) & (env.timeout(2) | env.timeout(1))) def sim(): for _ in range(20): env.process(cond_proc(env)) env.run() benchmark(sim) @pytest.mark.benchmark(group='targeted') def test_condition_wait(env, benchmark): def cond_proc(env): yield env.all_of(env.timeout(i) for i in range(10)) def sim(): for _ in range(10): env.process(cond_proc(env)) env.run() benchmark(sim) @pytest.mark.benchmark(group='targeted') def test_wait_for_proc(env, benchmark): r = random.Random(1234) def child(env): yield env.timeout(r.randint(1, 1000)) def parent(env): children = [env.process(child(env)) for _ in range(10)] for proc in children: if not proc.triggered: yield proc def sim(env): for _ in range(5): env.process(parent(env)) env.run() benchmark(sim, env) @pytest.mark.benchmark(group='simulation') def test_store_sim(benchmark): def producer(env, store, n): for i in range(n): yield env.timeout(1) yield store.put(i) def consumer(env, store): while True: yield store.get() yield env.timeout(2) def sim(): env = simpy.Environment() store = simpy.Store(env, capacity=5) for _ in range(2): env.process(producer(env, store, 10)) for _ in range(3): env.process(consumer(env, store)) env.run() return next(env._eid) num_events = benchmark(sim) assert num_events == 87 @pytest.mark.benchmark(group='simulation') def test_resource_sim(benchmark): def worker(env, resource): while True: with resource.request() as req: yield req yield env.timeout(1) def sim(): env = simpy.Environment() resource = simpy.Resource(env, capacity=2) for _ in range(5): env.process(worker(env, resource)) env.run(until=15) return next(env._eid) num_events = benchmark(sim) assert num_events == 94 @pytest.mark.benchmark(group='simulation') def test_container_sim(benchmark): def producer(env, container, full_event): while True: yield container.put(1) if container.level == container.capacity: full_event.succeed() yield env.timeout(1) def consumer(env, container): while True: yield container.get(1) yield env.timeout(3) def sim(): env = simpy.Environment() container = simpy.Container(env, capacity=10) full_event = env.event() env.process(producer(env, container, full_event)) for _ in range(2): env.process(consumer(env, container)) env.run(until=full_event) return next(env._eid) num_events = benchmark(sim) assert num_events == 104 simpy-simpy-a59c5bcfade4/tests/test_condition.py0000644000000000000000000002043413323151071020301 0ustar 00000000000000import pytest def test_operator_and(env): def process(env): timeout = [env.timeout(delay, value=delay) for delay in range(3)] results = yield timeout[0] & timeout[1] & timeout[2] assert results == { timeout[0]: 0, timeout[1]: 1, timeout[2]: 2, } env.process(process(env)) env.run() def test_operator_and_blocked(env): def process(env): timeout = env.timeout(1) event = env.event() yield env.timeout(1) condition = timeout & event assert not condition.triggered env.process(process(env)) env.run() def test_operator_or(env): def process(env): timeout = [env.timeout(delay, value=delay) for delay in range(3)] results = yield timeout[0] | timeout[1] | timeout[2] assert results == { timeout[0]: 0, } env.process(process(env)) env.run() def test_operator_nested_and(env): def process(env): timeout = [env.timeout(delay, value=delay) for delay in range(3)] results = yield (timeout[0] & timeout[2]) | timeout[1] assert results == { timeout[0]: 0, timeout[1]: 1, } assert env.now == 1 env.process(process(env)) env.run() def test_operator_nested_or(env): def process(env): timeout = [env.timeout(delay, value=delay) for delay in range(3)] results = yield (timeout[0] | timeout[1]) & timeout[2] assert results == { timeout[0]: 0, timeout[1]: 1, timeout[2]: 2, } assert env.now == 2 env.process(process(env)) env.run() def test_nested_cond_with_error(env): def explode(env): yield env.timeout(1) raise ValueError('Onoes!') def process(env): try: yield env.process(explode(env)) & env.timeout(1) pytest.fail('The condition should have raised a ValueError') except ValueError as err: assert err.args == ('Onoes!',) env.process(process(env)) env.run() def test_cond_with_error(env): def explode(env, delay): yield env.timeout(delay) raise ValueError('Onoes, failed after %d!' % delay) def process(env): try: yield env.process(explode(env, 0)) | env.timeout(1) pytest.fail('The condition should have raised a ValueError') except ValueError as err: assert err.args == ('Onoes, failed after 0!',) env.process(process(env)) env.run() def test_cond_with_nested_error(env): def explode(env, delay): yield env.timeout(delay) raise ValueError('Onoes, failed after %d!' % delay) def process(env): try: yield (env.process(explode(env, 0)) & env.timeout(1) | env.timeout(1)) pytest.fail('The condition should have raised a ValueError') except ValueError as err: assert err.args == ('Onoes, failed after 0!',) env.process(process(env)) env.run() def test_cond_with_uncaught_error(env): """Errors that happen after the condition has been triggered will not be handled by the condition and cause the simulation to crash.""" def explode(env, delay): yield env.timeout(delay) raise ValueError('Onoes, failed after %d!' % delay) def process(env): yield env.timeout(1) | env.process(explode(env, 2)) env.process(process(env)) try: env.run() assert False, 'There should have been an exception.' except ValueError: pass assert env.now == 2 def test_iand_with_and_cond(env): def process(env): cond = env.timeout(1, value=1) & env.timeout(2, value=2) orig = cond cond &= env.timeout(0, value=0) assert cond is not orig results = yield cond assert list(results.values()) == [1, 2, 0] env.process(process(env)) env.run() def test_iand_with_or_cond(env): def process(env): cond = env.timeout(1, value=1) | env.timeout(2, value=2) orig = cond cond &= env.timeout(0, value=0) assert cond is not orig results = yield cond assert list(results.values()) == [1, 0] env.process(process(env)) env.run() def test_ior_with_or_cond(env): def process(env): cond = env.timeout(1, value=1) | env.timeout(2, value=2) orig = cond cond |= env.timeout(0, value=0) assert cond is not orig results = yield cond assert list(results.values()) == [0] env.process(process(env)) env.run() def test_ior_with_and_cond(env): def process(env): cond = env.timeout(1, value=1) & env.timeout(2, value=2) orig = cond cond |= env.timeout(0, value=0) assert cond is not orig results = yield cond assert list(results.values()) == [0] env.process(process(env)) env.run() def test_immutable_results(env): """Results of conditions should not change after they have been triggered.""" def process(env): timeout = [env.timeout(delay, value=delay) for delay in range(3)] # The or condition in this expression will trigger immediately. The and # condition will trigger later on. condition = timeout[0] | (timeout[1] & timeout[2]) results = yield condition assert results == {timeout[0]: 0} # Make sure that the results of condition were frozen. The results of # the nested and condition do not become visible afterwards. yield env.timeout(2) assert results == {timeout[0]: 0} env.process(process(env)) env.run() def test_shared_and_condition(env): timeout = [env.timeout(delay, value=delay) for delay in range(3)] c1 = timeout[0] & timeout[1] c2 = c1 & timeout[2] def p1(env, condition): results = yield condition assert results == {timeout[0]: 0, timeout[1]: 1} def p2(env, condition): results = yield condition assert results == {timeout[0]: 0, timeout[1]: 1, timeout[2]: 2} env.process(p1(env, c1)) env.process(p2(env, c2)) env.run() def test_shared_or_condition(env): timeout = [env.timeout(delay, value=delay) for delay in range(3)] c1 = timeout[0] | timeout[1] c2 = c1 | timeout[2] def p1(env, condition): results = yield condition assert results == {timeout[0]: 0} def p2(env, condition): results = yield condition assert results == {timeout[0]: 0} env.process(p1(env, c1)) env.process(p2(env, c2)) env.run() def test_condition_value(env): """The value of a condition behaves like a readonly dictionary.""" timeouts = list([env.timeout(delay, value=delay) for delay in range(3)]) def p(env, timeouts): results = yield env.all_of(timeouts) assert list(results) == timeouts assert list(results.keys()) == timeouts assert list(results.values()) == [0, 1, 2] assert list(results.items()) == list(zip(timeouts, [0, 1, 2])) assert timeouts[0] in results assert results[timeouts[0]] == 0 assert results == results assert results == results.todict() env.process(p(env, timeouts)) env.run() def test_result_order(env): """The order of a conditions result is based on the order in which the events have been specified.""" timeouts = list(reversed([env.timeout(delay) for delay in range(3)])) def p(env, timeouts): results = yield env.all_of(timeouts) assert list(results.keys()) == timeouts env.process(p(env, timeouts)) env.run() def test_nested_result_order(env): """The order of a conditions result is based on the order in which the events have been specified (even if nested).""" timeouts = [env.timeout(delay) for delay in range(3)] condition = (timeouts[0] | timeouts[1]) & timeouts[2] def p(env, timeouts): results = yield condition assert list(results.keys()) == timeouts env.process(p(env, timeouts)) env.run() def test_all_of_empty_list(env): """AllOf with an empty list should immediately be triggered.""" evt = env.all_of([]) assert evt.triggered def test_any_of_empty_list(env): """AnyOf with an empty list should immediately be triggered.""" evt = env.any_of([]) assert evt.triggered simpy-simpy-a59c5bcfade4/tests/test_environment.py0000644000000000000000000000400113323151071020647 0ustar 00000000000000""" General test for the the `simpy.core.Environment`. """ # Pytest gets the parameters "env" and "log" from the *conftest.py* file import pytest def test_event_queue_empty(env, log): """The simulation should stop if there are no more events, that means, no more active process.""" def pem(env, log): while env.now < 2: log.append(env.now) yield env.timeout(1) env.process(pem(env, log)) env.run(10) assert log == [0, 1] def test_run_negative_until(env): """Test passing a negative time to run.""" pytest.raises(ValueError, env.run, -3) def test_run_resume(env): """Stopped simulation can be resumed.""" events = [env.timeout(t) for t in (5, 10, 15)] assert env.now == 0 assert not any(event.processed for event in events) env.run(until=10) assert env.now == 10 assert all(event.processed for event in events[:1]) assert not any(event.processed for event in events[1:]) env.run(until=15) assert env.now == 15 assert all(event.processed for event in events[:2]) assert not any(event.processed for event in events[2:]) env.run() assert env.now == 15 assert all(event.processed for event in events) def test_run_until_value(env): """Anything that can be converted to a float is a valid until value.""" env.run(until='3.141592') assert env.now == 3.141592 def test_run_with_processed_event(env): """An already processed event may also be passed as until value.""" timeout = env.timeout(1, value='spam') assert env.run(until=timeout) == 'spam' assert env.now == 1 # timeout has been processed, calling run again will return its value # again. assert env.run(until=timeout) == 'spam' assert env.now == 1 def test_run_with_untriggered_event(env): excinfo = pytest.raises(RuntimeError, env.run, until=env.event()) assert str(excinfo.value).startswith('No scheduled events left but "until"' ' event was not triggered:') simpy-simpy-a59c5bcfade4/tests/test_event.py0000644000000000000000000000724013323151071017434 0ustar 00000000000000""" Tests for ``simpy.events.Event``. """ # Pytest gets the parameters "env" and "log" from the *conftest.py* file import re import pytest def test_succeed(env): """Test for the Environment.event() helper function.""" def child(env, event): value = yield event assert value == 'ohai' assert env.now == 5 def parent(env): event = env.event() env.process(child(env, event)) yield env.timeout(5) event.succeed('ohai') env.process(parent(env)) env.run() def test_fail(env): """Test for the Environment.event() helper function.""" def child(env, event): try: yield event pytest.fail('Should not get here.') except ValueError as err: assert err.args[0] == 'ohai' assert env.now == 5 def parent(env): event = env.event() env.process(child(env, event)) yield env.timeout(5) event.fail(ValueError('ohai')) env.process(parent(env)) env.run() def test_names(env): def pem(): yield env.exit() assert re.match(r'', str(env.event())) assert re.match(r'', str(env.timeout(1))) assert re.match(r'', str(env.timeout(1, value=2))) assert re.match(r', ' r'\)\) object at 0x.*>', str(env.event() & env.event())) assert re.match(r'', str(env.process(pem()))) def test_value(env): """After an event has been triggered, its value becomes accessible.""" event = env.timeout(0, 'I am the value') env.run() assert event.value == 'I am the value' def test_unavailable_value(env): """If an event has not yet been triggered, its value is not availabe and trying to access it will result in a AttributeError.""" event = env.event() try: event.value assert False, 'Expected an exception' except AttributeError as e: assert e.args[0].endswith('is not yet available') def test_triggered(env): def pem(env, event): value = yield event env.exit(value) event = env.event() event.succeed('i was already done') result = env.run(env.process(pem(env, event))) assert result == 'i was already done' def test_callback_modification(env): """The callbacks of an event will get set to None before actually invoking the callbacks. This prevents concurrent modifications.""" def callback(event): assert event.callbacks is None event = env.event() event.callbacks.append(callback) event.succeed() env.run(until=event) def test_condition_callback_removal(env): """A condition will remove all outstanding callbacks from its events.""" a, b = env.event(), env.event() a.succeed() env.run(until=a | b) # The condition has removed its callback from event b. assert not a.callbacks and not b.callbacks def test_condition_nested_callback_removal(env): """A condition will remove all outstanding callbacks from its events (even if nested).""" a, b, c = env.event(), env.event(), env.event() b_and_c = b & c a_or_b_and_c = a | b_and_c a.succeed() env.run(until=a_or_b_and_c) # Callbacks from nested conditions are also removed. assert not a.callbacks assert not b.callbacks assert not c.callbacks for cb in b_and_c.callbacks: # b_and_c may have a _build_value callback. assert cb.__name__ != '_check' assert not a_or_b_and_c.callbacks simpy-simpy-a59c5bcfade4/tests/test_exceptions.py0000644000000000000000000001635013323151071020476 0ustar 00000000000000""" Tests for forwarding exceptions from child to parent processes. """ import platform import re import textwrap import traceback import pytest def test_error_forwarding(env): """Exceptions are forwarded from child to parent processes if there are any. """ def child(env): raise ValueError('Onoes!') yield env.timeout(1) def parent(env): try: yield env.process(child(env)) pytest.fail('We should not have gotten here ...') except ValueError as err: assert err.args[0] == 'Onoes!' env.process(parent(env)) env.run() def test_no_parent_process(env): """Exceptions should be normally raised if there are no processes waiting for the one that raises something. """ def child(env): raise ValueError('Onoes!') yield env.timeout(1) def parent(env): try: env.process(child(env)) yield env.timeout(1) except Exception as err: pytest.fail('There should be no error (%s).' % err) env.process(parent(env)) pytest.raises(ValueError, env.run) def test_crashing_child_traceback(env): def panic(env): yield env.timeout(1) raise RuntimeError('Oh noes, roflcopter incoming... BOOM!') def root(env): try: yield env.process(panic(env)) pytest.fail("Hey, where's the roflcopter?") except RuntimeError: # The current frame must be visible in the stacktrace. stacktrace = traceback.format_exc() assert 'yield env.process(panic(env))' in stacktrace assert 'raise RuntimeError(\'Oh noes,' in stacktrace env.process(root(env)) env.run() def test_exception_chaining(env): """Unhandled exceptions pass through the entire event stack. This must be visible in the stacktrace of the exception. """ def child(env): yield env.timeout(1) raise RuntimeError('foo') def parent(env): child_proc = env.process(child(env)) yield child_proc def grandparent(env): parent_proc = env.process(parent(env)) yield parent_proc env.process(grandparent(env)) try: env.run() pytest.fail('There should have been an exception') except RuntimeError: trace = traceback.format_exc() expected = re.escape(textwrap.dedent("""\ Traceback (most recent call last): File "{path}tests/test_exceptions.py", line {line}, in child raise RuntimeError('foo') RuntimeError: foo The above exception was the direct cause of the following exception: Traceback (most recent call last): File "{path}tests/test_exceptions.py", line {line}, in parent yield child_proc RuntimeError: foo The above exception was the direct cause of the following exception: Traceback (most recent call last): File "{path}tests/test_exceptions.py", line {line}, in grandparent yield parent_proc RuntimeError: foo The above exception was the direct cause of the following exception: Traceback (most recent call last): File "{path}tests/test_exceptions.py", line {line}, in test_exception_chaining env.run() File "{path}simpy/core.py", line {line}, in run self.step() File "{path}simpy/core.py", line {line}, in step raise exc RuntimeError: foo """)).replace(r'\{line\}', r'\d+').replace(r'\{path\}', r'.*') # NOQA if platform.system() == 'Windows': expected = expected.replace(r'\/', r'\\') assert re.match(expected, trace), 'Traceback mismatch' def test_invalid_event(env): """Invalid yield values will cause the simulation to fail.""" def root(env): yield None env.process(root(env)) try: env.run() pytest.fail('Hey, this is not allowed!') except RuntimeError as err: assert err.args[0].endswith('Invalid yield value "None"') def test_exception_handling(env): """If failed events are not defused (which is the default) the simulation crashes.""" event = env.event() event.fail(RuntimeError()) try: env.run(until=1) assert False, 'There must be a RuntimeError!' except RuntimeError: pass def test_callback_exception_handling(env): """Callbacks of events may handle exception by setting the ``defused`` attribute of ``event`` to ``True``.""" def callback(event): event.defused = True event = env.event() event.callbacks.append(callback) event.fail(RuntimeError()) assert not event.defused, 'Event has been defused immediately' env.run(until=1) assert event.defused, 'Event has not been defused' def test_process_exception_handling(env): """Processes can't ignore failed events and auto-handle execeptions.""" def pem(env, event): try: yield event assert False, 'Hey, the event should fail!' except RuntimeError: pass event = env.event() env.process(pem(env, event)) event.fail(RuntimeError()) assert not event.defused, 'Event has been defused immediately' env.run(until=1) assert event.defused, 'Event has not been defused' def test_process_exception_chaining(env): """Because multiple processes can be waiting for an event, exceptions of failed events are copied before being thrown into a process. Otherwise, the traceback of the exception gets modified by a process. See https://bitbucket.org/simpy/simpy/issue/60 for more details.""" import traceback def process_a(event): try: yield event except RuntimeError: stacktrace = traceback.format_exc() assert 'process_b' not in stacktrace def process_b(event): try: yield event except RuntimeError: stacktrace = traceback.format_exc() assert 'process_a' not in stacktrace event = env.event() event.fail(RuntimeError('foo')) env.process(process_a(event)) env.process(process_b(event)) env.run() def test_sys_excepthook(env): """Check that the default exception hook reports exception chains.""" def process_a(event): yield event def process_b(event): yield event event = env.event() event.fail(RuntimeError('foo')) env.process(process_b(env.process(process_a(event)))) try: env.run() except BaseException: # Let the default exception hook print the traceback to the redirected # standard error channel. import sys from simpy._compat import PY2 if PY2: from io import BytesIO stderr, sys.stderr = sys.stderr, BytesIO() else: from io import StringIO stderr, sys.stderr = sys.stderr, StringIO() sys.excepthook(*sys.exc_info()) if PY2: traceback = sys.stderr.getvalue().decode() else: traceback = sys.stderr.getvalue() sys.stderr = stderr # Check if frames of process_a and process_b are visible in the # tracebabck. assert 'process_a' in traceback assert 'process_b' in traceback simpy-simpy-a59c5bcfade4/tests/test_interrupts.py0000644000000000000000000001254313323151071020534 0ustar 00000000000000""" Test asynchronous interrupts. """ import re import pytest import simpy def test_interruption(env): """Processes can be interrupted while waiting for other events.""" def interruptee(env): try: yield env.timeout(10) pytest.fail('Expected an interrupt') except simpy.Interrupt as interrupt: assert interrupt.cause == 'interrupt!' def interruptor(env): child_process = env.process(interruptee(env)) yield env.timeout(5) child_process.interrupt('interrupt!') env.process(interruptor(env)) env.run() def test_concurrent_interrupts(env, log): """Concurrent interrupts are scheduled in the order in which they occurred. """ def fox(env, log): while True: try: yield env.timeout(10) except simpy.Interrupt as interrupt: log.append((env.now, interrupt.cause)) def farmer(env, name, fox): fox.interrupt(name) yield env.timeout(1) fantastic_mr_fox = env.process(fox(env, log)) for name in ('boggis', 'bunce', 'beans'): env.process(farmer(env, name, fantastic_mr_fox)) env.run(20) assert log == [(0, 'boggis'), (0, 'bunce'), (0, 'beans')] def test_concurrent_interrupts_and_events(env, log): """Interrupts interrupt a process while waiting for an event. Even if the event has happened concurrently with the interrupt.""" def fox(env, coup, log): while True: try: yield coup log.append('coup completed at %d' % env.now) env.exit() except simpy.Interrupt: log.append('coup interrupted at %d' % env.now) def master_plan(env, fox, coup): yield env.timeout(1) # Succeed and interrupt concurrently. coup.succeed() fox.interrupt() coup = env.event() fantastic_mr_fox = env.process(fox(env, coup, log)) env.process(master_plan(env, fantastic_mr_fox, coup)) env.run(5) assert log == ['coup interrupted at 1', 'coup completed at 1'] def test_init_interrupt(env): """An interrupt should always be executed after the Initialize event at the same time.""" def child(env): try: yield env.timeout(10) pytest.fail('Should have been interrupted.') except simpy.Interrupt: assert env.now == 0 def root(env): child_proc = env.process(child(env)) child_proc.interrupt() yield env.timeout(1) env.process(root(env)) env.run() def test_interrupt_terminated_process(env): """Dead processes cannot be interrupted.""" def child(env): yield env.timeout(1) def parent(env): child_proc = env.process(child(env)) # Wait long enough so that child_proc terminates. yield env.timeout(2) ei = pytest.raises(RuntimeError, child_proc.interrupt) assert re.match(r' has terminated ' r'and cannot be interrupted.', ei.value.args[0]) yield env.timeout(1) env.process(parent(env)) env.run() def test_multiple_interrupts(env): """Interrupts on dead processes are discarded. If there are multiple concurrent interrupts on a process and the latter dies after handling the first interrupt, the remaining ones are silently ignored. """ def child(env): try: yield env.timeout(1) except simpy.Interrupt as i: env.exit(i.cause) def parent(env): c = env.process(child(env)) yield env.timeout(0) c.interrupt(1) c.interrupt(2) result = yield c assert result == 1 env.process(parent(env)) env.run() def test_interrupt_self(env): """A process should not be able to interrupt itself.""" def pem(env): pytest.raises(RuntimeError, env.active_process.interrupt) yield env.timeout(0) env.process(pem(env)) env.run() def test_immediate_interrupt(env, log): """Processes are immediately interruptable.""" def child(env, log): try: yield env.event() except simpy.Interrupt: log.append(env.now) def parent(env, log): child_proc = env.process(child(env, log)) child_proc.interrupt() yield env.exit() env.process(parent(env, log)) env.run() # Confirm that child has been interrupted immediately at timestep 0. assert log == [0] def test_interrupt_event(env): """A process should be interruptable while waiting for an Event.""" def child(env): try: yield env.event() except simpy.Interrupt: assert env.now == 5 def parent(env): child_proc = env.process(child(env)) yield env.timeout(5) child_proc.interrupt() env.process(parent(env)) env.run() def test_concurrent_behaviour(env): def proc_a(env): timeouts = [env.timeout(0) for i in range(2)] while timeouts: try: yield timeouts.pop(0) assert False, 'Expected an interrupt' except simpy.Interrupt: pass def proc_b(env, proc_a): for i in range(2): proc_a.interrupt() yield env.exit() proc_a = env.process(proc_a(env)) env.process(proc_b(env, proc_a)) env.run() simpy-simpy-a59c5bcfade4/tests/test_process.py0000644000000000000000000001153213323151071017770 0ustar 00000000000000""" Tests for the ``simpy.events.Process``. """ # Pytest gets the parameters "env" and "log" from the *conftest.py* file import pytest from simpy import Interrupt def test_start_non_process(env): """Check that you cannot start a normal function.""" def foo(): pass pytest.raises(ValueError, env.process, foo) def test_get_state(env): """A process is alive until it's generator has not terminated.""" def pem_a(env): yield env.timeout(3) def pem_b(env, pem_a): yield env.timeout(1) assert pem_a.is_alive yield env.timeout(3) assert not pem_a.is_alive proc_a = env.process(pem_a(env)) env.process(pem_b(env, proc_a)) env.run() def test_target(env): def pem(env, event): yield event event = env.timeout(5) proc = env.process(pem(env, event)) # Wait until "proc" is initialized and yielded the event while env.peek() < 5: env.step() assert proc.target is event proc.interrupt() def test_wait_for_proc(env): """A process can wait until another process finishes.""" def finisher(env): yield env.timeout(5) def waiter(env, finisher): proc = env.process(finisher(env)) yield proc # Waits until "proc" finishes assert env.now == 5 env.process(waiter(env, finisher)) env.run() def test_exit(env): """Processes can set a return value via an ``exit()`` function, comparable to ``sys.exit()``. """ def child(env): yield env.timeout(1) env.exit(env.now) def parent(env): result1 = yield env.process(child(env)) result2 = yield env.process(child(env)) assert [result1, result2] == [1, 2] env.process(parent(env)) env.run() @pytest.mark.skipif('sys.version_info[:2] < (3, 3)') def test_return_value(env): """Processes can set a return value.""" # Python < 3.2 would raise a SyntaxError if this was real code ... code = """def child(env): yield env.timeout(1) return env.now """ globs, locs = {}, {} code = compile(code, '', 'exec') eval(code, globs, locs) child = locs['child'] def parent(env): result1 = yield env.process(child(env)) result2 = yield env.process(child(env)) assert [result1, result2] == [1, 2] env.process(parent(env)) env.run() def test_child_exception(env): """A child catches an exception and sends it to its parent.""" def child(env): try: yield env.timeout(1) raise RuntimeError('Onoes!') except RuntimeError as err: env.exit(err) def parent(env): result = yield env.process(child(env)) assert isinstance(result, Exception) env.process(parent(env)) env.run() def test_interrupted_join(env): """Interrupts remove a process from the callbacks of its target.""" def interruptor(env, process): yield env.timeout(1) process.interrupt() def child(env): yield env.timeout(2) def parent(env): child_proc = env.process(child(env)) try: yield child_proc pytest.fail('Did not receive an interrupt.') except Interrupt: assert env.now == 1 assert child_proc.is_alive # We should not get resumed when child terminates. yield env.timeout(5) assert env.now == 6 parent_proc = env.process(parent(env)) env.process(interruptor(env, parent_proc)) env.run() def test_interrupted_join_and_rejoin(env): """Tests that interrupts are raised while the victim is waiting for another process. The victim tries to join again. """ def interruptor(env, process): yield env.timeout(1) process.interrupt() def child(env): yield env.timeout(2) def parent(env): child_proc = env.process(child(env)) try: yield child_proc pytest.fail('Did not receive an interrupt.') except Interrupt: assert env.now == 1 assert child_proc.is_alive yield child_proc assert env.now == 2 parent_proc = env.process(parent(env)) env.process(interruptor(env, parent_proc)) env.run() def test_error_and_interrupted_join(env): def child_a(env, process): process.interrupt() env.exit() yield # Dummy yield def child_b(env): raise AttributeError('spam') yield # Dummy yield def parent(env): env.process(child_a(env, env.active_process)) b = env.process(child_b(env)) try: yield b # This interrupt unregisters me from b so I won't receive its # AttributeError except Interrupt: pass yield env.timeout(0) env.process(parent(env)) pytest.raises(AttributeError, env.run) simpy-simpy-a59c5bcfade4/tests/test_resources.py0000644000000000000000000005150413323151071020327 0ustar 00000000000000""" Theses test cases demonstrate the API for shared resources. """ # Pytest gets the parameters "env" and "log" from the *conftest.py* file import pytest import simpy # # Tests for Resource # def test_resource(env, log): """A *resource* is something with a limited numer of slots that need to be requested before and released after the usage (e.g., gas pumps at a gas station). """ def pem(env, name, resource, log): req = resource.request() yield req assert resource.count == 1 yield env.timeout(1) resource.release(req) log.append((name, env.now)) resource = simpy.Resource(env, capacity=1) assert resource.capacity == 1 assert resource.count == 0 env.process(pem(env, 'a', resource, log)) env.process(pem(env, 'b', resource, log)) env.run() assert log == [('a', 1), ('b', 2)] def test_resource_capacity(env): pytest.raises(ValueError, simpy.Resource, env, 0) def test_resource_context_manager(env, log): """The event that ``Resource.request()`` returns can be used as Context Manager.""" def pem(env, name, resource, log): with resource.request() as request: yield request yield env.timeout(1) log.append((name, env.now)) resource = simpy.Resource(env, capacity=1) env.process(pem(env, 'a', resource, log)) env.process(pem(env, 'b', resource, log)) env.run() assert log == [('a', 1), ('b', 2)] def test_resource_slots(env, log): def pem(env, name, resource, log): with resource.request() as req: yield req log.append((name, env.now)) yield env.timeout(1) resource = simpy.Resource(env, capacity=3) for i in range(9): env.process(pem(env, str(i), resource, log)) env.run() assert log == [('0', 0), ('1', 0), ('2', 0), ('3', 1), ('4', 1), ('5', 1), ('6', 2), ('7', 2), ('8', 2)] def test_resource_continue_after_interrupt(env): """A process may be interrupted while waiting for a resource but should be able to continue waiting afterwards.""" def pem(env, res): with res.request() as req: yield req yield env.timeout(1) def victim(env, res): try: evt = res.request() yield evt pytest.fail('Should not have gotten the resource.') except simpy.Interrupt: yield evt res.release(evt) assert env.now == 1 def interruptor(env, proc): proc.interrupt() yield env.exit(0) res = simpy.Resource(env, 1) env.process(pem(env, res)) proc = env.process(victim(env, res)) env.process(interruptor(env, proc)) env.run() def test_resource_release_after_interrupt(env): """A process needs to release a resource, even it it was interrupted and does not continue to wait for it.""" def blocker(env, res): with res.request() as req: yield req yield env.timeout(1) def victim(env, res): try: evt = res.request() yield evt pytest.fail('Should not have gotten the resource.') except simpy.Interrupt: # Dont wait for the resource res.release(evt) assert env.now == 0 env.exit() def interruptor(env, proc): proc.interrupt() yield env.exit(0) res = simpy.Resource(env, 1) env.process(blocker(env, res)) victim_proc = env.process(victim(env, res)) env.process(interruptor(env, victim_proc)) env.run() def test_resource_immediate_requests(env): """A process must not acquire a resource if it releases it and immediately requests it again while there are already other requesting processes.""" def child(env, res): result = [] for i in range(3): with res.request() as req: yield req result.append(env.now) yield env.timeout(1) env.exit(result) def parent(env): res = simpy.Resource(env, 1) child_a = env.process(child(env, res)) child_b = env.process(child(env, res)) a_acquire_times = yield child_a b_acquire_times = yield child_b assert a_acquire_times == [0, 2, 4] assert b_acquire_times == [1, 3, 5] env.process(parent(env)) env.run() def test_resource_cm_exception(env, log): """Resource with context manager receives an exception.""" def process(env, resource, log, raise_): try: with resource.request() as req: yield req yield env.timeout(1) log.append(env.now) if raise_: raise ValueError('Foo') except ValueError as err: assert err.args == ('Foo',) resource = simpy.Resource(env, 1) env.process(process(env, resource, log, True)) # The second process is used to check if it was able to access the # resource: env.process(process(env, resource, log, False)) env.run() assert log == [1, 2] def test_resource_with_condition(env): def process(env, resource): with resource.request() as res_event: result = yield res_event | env.timeout(1) assert res_event in result resource = simpy.Resource(env, 1) env.process(process(env, resource)) env.run() def test_resource_with_priority_queue(env): def process(env, delay, resource, priority, res_time): yield env.timeout(delay) req = resource.request(priority=priority) yield req assert env.now == res_time yield env.timeout(5) resource.release(req) resource = simpy.PriorityResource(env, capacity=1) env.process(process(env, 0, resource, 2, 0)) env.process(process(env, 2, resource, 3, 10)) env.process(process(env, 2, resource, 3, 15)) # Test equal priority env.process(process(env, 4, resource, 1, 5)) env.run() def test_sorted_queue_maxlen(env): """Requests must fail if more than *maxlen* requests happen concurrently.""" resource = simpy.PriorityResource(env, capacity=1) resource.put_queue.maxlen = 1 def process(env, resource): # The first request immediately triggered and does not enter the queue. resource.request(priority=1) # The second request is enqueued. resource.request(priority=1) try: # The third request will now fail. resource.request(priority=1) pytest.fail('Expected a RuntimeError') except RuntimeError as e: assert e.args[0] == 'Cannot append event. Queue is full.' yield env.timeout(0) env.process(process(env, resource)) env.run() def test_get_users(env): def process(env, resource): with resource.request() as req: yield req yield env.timeout(1) resource = simpy.Resource(env, 1) procs = [env.process(process(env, resource)) for i in range(3)] env.run(until=1) assert [evt.proc for evt in resource.users] == procs[0:1] assert [evt.proc for evt in resource.queue] == procs[1:] env.run(until=2) assert [evt.proc for evt in resource.users] == procs[1:2] assert [evt.proc for evt in resource.queue] == procs[2:] # # Tests for PreemptiveResource # def test_preemptive_resource(env): """Processes with a higher priority may preempt requests of lower priority processes. Note that higher priorities are indicated by a lower number value.""" def proc_a(env, resource, prio): try: with resource.request(priority=prio) as req: yield req pytest.fail('Should have received an interrupt/preemption.') except simpy.Interrupt: pass def proc_b(env, resource, prio): with resource.request(priority=prio) as req: yield req resource = simpy.PreemptiveResource(env, 1) env.process(proc_a(env, resource, 1)) env.process(proc_b(env, resource, 0)) env.run() def test_preemptive_resource_timeout_0(env): def proc_a(env, resource, prio): with resource.request(priority=prio) as req: try: yield req yield env.timeout(1) pytest.fail('Should have received an interrupt/preemption.') except simpy.Interrupt: pass yield env.event() def proc_b(env, resource, prio): with resource.request(priority=prio) as req: yield req resource = simpy.PreemptiveResource(env, 1) env.process(proc_a(env, resource, 1)) env.process(proc_b(env, resource, 0)) env.run() def test_mixed_preemption(env, log): def p(id, env, res, delay, prio, preempt, log): yield env.timeout(delay) with res.request(priority=prio, preempt=preempt) as req: try: yield req yield env.timeout(2) log.append((env.now, id)) except simpy.Interrupt as ir: log.append((env.now, id, (ir.cause.by, ir.cause.usage_since))) res = simpy.PreemptiveResource(env, 1) # p0: First user: env.process(p(0, env, res, delay=0, prio=2, preempt=True, log=log)) # p1: Waits (cannot preempt): env.process(p(1, env, res, delay=0, prio=2, preempt=True, log=log)) # p2: Waits later, but has a higher prio: env.process(p(2, env, res, delay=1, prio=1, preempt=False, log=log)) # p3: Preempt the above proc: p3 = env.process(p(3, env, res, delay=3, prio=0, preempt=True, log=log)) # p4: Wait again: env.process(p(4, env, res, delay=4, prio=3, preempt=True, log=log)) env.run() assert log == [ (2, 0), # p0 done (3, 2, (p3, 2)), # p2 got it next, but got interrupted by p3 (5, 3), # p3 done (7, 1), # p1 done (finally got the resource) (9, 4), # p4 done ] def test_nested_preemption(env, log): def process(id, env, res, delay, prio, preempt, log): yield env.timeout(delay) with res.request(priority=prio, preempt=preempt) as req: try: yield req yield env.timeout(5) log.append((env.now, id)) except simpy.Interrupt as ir: log.append((env.now, id, (ir.cause.by, ir.cause.usage_since))) def process2(id, env, res0, res1, delay, prio, preempt, log): yield env.timeout(delay) with res0.request(priority=prio, preempt=preempt) as req0: try: yield req0 with res1.request(priority=prio, preempt=preempt) as req1: try: yield req1 yield env.timeout(5) log.append((env.now, id)) except simpy.Interrupt as ir: log.append((env.now, id, (ir.cause.by, ir.cause.usage_since, ir.cause.resource))) except simpy.Interrupt as ir: log.append((env.now, id, (ir.cause.by, ir.cause.usage_since, ir.cause.resource))) res0 = simpy.PreemptiveResource(env, 1) res1 = simpy.PreemptiveResource(env, 1) env.process(process2(0, env, res0, res1, 0, -1, True, log)) p1 = env.process(process(1, env, res1, 1, -2, True, log)) env.process(process2(2, env, res0, res1, 20, -1, True, log)) p3 = env.process(process(3, env, res0, 21, -2, True, log)) env.process(process2(4, env, res0, res1, 21, -1, True, log)) env.run() assert log == [ (1, 0, (p1, 0, res1)), (6, 1), (21, 2, (p3, 20, res0)), (26, 3), (31, 4), ] # # Tests for Container # def test_container(env, log): """A *container* is a resource (of optinally limited capacity) where you can put in our take out a discrete or continuous amount of things (e.g., a box of lump sugar or a can of milk). The *put* and *get* operations block if the buffer is to full or to empty. If they return, the process nows that the *put* or *get* operation was successfull. """ def putter(env, buf, log): yield env.timeout(1) while True: yield buf.put(2) log.append(('p', env.now)) yield env.timeout(1) def getter(env, buf, log): yield buf.get(1) log.append(('g', env.now)) yield env.timeout(1) yield buf.get(1) log.append(('g', env.now)) buf = simpy.Container(env, init=0, capacity=2) env.process(putter(env, buf, log)) env.process(getter(env, buf, log)) env.run(until=5) assert log == [('p', 1), ('g', 1), ('g', 2), ('p', 2)] def test_container_get_queued(env): def proc(env, wait, container, what): yield env.timeout(wait) with getattr(container, what)(1) as req: yield req container = simpy.Container(env, 1) p0 = env.process(proc(env, 0, container, 'get')) env.process(proc(env, 1, container, 'put')) env.process(proc(env, 1, container, 'put')) p3 = env.process(proc(env, 1, container, 'put')) env.run(until=1) assert [ev.proc for ev in container.put_queue] == [] assert [ev.proc for ev in container.get_queue] == [p0] env.run(until=2) assert [ev.proc for ev in container.put_queue] == [p3] assert [ev.proc for ev in container.get_queue] == [] def test_initial_container_capacity(env): container = simpy.Container(env) assert container.capacity == float('inf') def test_container_get_put_bounds(env): container = simpy.Container(env) pytest.raises(ValueError, container.get, -13) pytest.raises(ValueError, container.put, -13) @pytest.mark.parametrize(('error', 'args'), [ (None, [2, 1]), # normal case (None, [1, 1]), # init == capacity should be valid (None, [1, 0]), # init == 0 should be valid (ValueError, [1, 2]), # init > capcity (ValueError, [0]), # capacity == 0 (ValueError, [-1]), # capacity < 0 (ValueError, [1, -1]), # init < 0 ]) def test_container_init_capacity(env, error, args): args.insert(0, env) if error: pytest.raises(error, simpy.Container, *args) else: simpy.Container(*args) # # Tests fore Store # def test_store(env): """A store models the production and consumption of concrete python objects (in contrast to containers, where you only now if the *put* or *get* operations were successfull but don't get concrete objects). """ def putter(env, store, item): yield store.put(item) def getter(env, store, orig_item): item = yield store.get() assert item is orig_item store = simpy.Store(env, capacity=2) item = object() # NOTE: Does the start order matter? Need to test this. env.process(putter(env, store, item)) env.process(getter(env, store, item)) env.run() @pytest.mark.parametrize('Store', [ simpy.Store, simpy.FilterStore, ]) def test_initial_store_capacity(env, Store): store = Store(env) assert store.capacity == float('inf') def test_store_capacity(env): pytest.raises(ValueError, simpy.Store, env, 0) pytest.raises(ValueError, simpy.Store, env, -1) capacity = 2 store = simpy.Store(env, capacity) env.process((store.put(i) for i in range(capacity + 1))) env.run() # Ensure store is filled to capacity assert len(store.items) == capacity def test_store_cancel(env): store = simpy.Store(env, capacity=1) def acquire_implicit_cancel(): with store.get(): yield env.timeout(1) # implicit cancel() when exiting with-block env.process(acquire_implicit_cancel()) env.run() def test_priority_store_item_priority(env): pstore = simpy.PriorityStore(env, 3) log = [] def getter(wait): yield env.timeout(wait) item = yield pstore.get() log.append(item) # Do not specify priority; the items themselves will be compared to # determine priority. env.process((pstore.put(s) for s in 'bcadefg')) env.process(getter(1)) env.process(getter(2)) env.process(getter(3)) env.run() assert log == ['a', 'b', 'c'] def test_priority_store_stable_order(env): pstore = simpy.PriorityStore(env, 3) log = [] def getter(wait): yield env.timeout(wait) _, item = yield pstore.get() log.append(item) items = [object() for _ in range(3)] # Unorderable items are inserted with same priority. env.process((pstore.put(simpy.PriorityItem(0, item)) for item in items)) env.process(getter(1)) env.process(getter(2)) env.process(getter(3)) env.run() # Since the priorities were the same for all items, ensure that items are # retrieved in insertion order. assert log == items def test_filter_store(env): def pem(env): store = simpy.FilterStore(env, capacity=2) get_event = store.get(lambda item: item == 'b') yield store.put('a') assert not get_event.triggered yield store.put('b') assert get_event.triggered env.process(pem(env)) env.run() def test_filter_store_get_after_mismatch(env): """Regression test for issue #49. Triggering get-events after a put in FilterStore wrongly breaks after the first mismatch. """ def putter(env, store): # The order of putting 'spam' before 'eggs' is important here. yield store.put('spam') yield env.timeout(1) yield store.put('eggs') def getter(store): # The order of requesting 'eggs' before 'spam' is important here. eggs = store.get(lambda i: i == 'eggs') spam = store.get(lambda i: i == 'spam') ret = yield spam | eggs assert spam in ret and eggs not in ret assert env.now == 0 yield eggs assert env.now == 1 store = simpy.FilterStore(env, capacity=2) env.process(getter(store)) env.process(putter(env, store)) env.run() def test_filter_calls_best_case(env): """The filter function is called every item in the store until a match is found. In the best case the first item already matches.""" log = [] def log_filter(item): log.append('check %s' % item) return True store = simpy.FilterStore(env) store.items = [1, 2, 3] def getter(store): log.append('get %s' % (yield store.get(log_filter))) log.append('get %s' % (yield store.get(log_filter))) log.append('get %s' % (yield store.get(log_filter))) env.process(getter(store)) env.run() assert log == ['check 1', 'get 1', 'check 2', 'get 2', 'check 3', 'get 3'] def test_filter_calls_worst_case(env): """In the worst case the filter function is being called for items multiple times.""" log = [] store = simpy.FilterStore(env) def putter(store): for i in range(4): log.append('put %s' % i) yield store.put(i) def log_filter(item): log.append('check %s' % item) return item >= 3 def getter(store): log.append('get %s' % (yield store.get(log_filter))) env.process(getter(store)) env.process(putter(store)) env.run() # The filter function is repeatedly called for every item in the store # until a match is found. assert log == [ 'put 0', 'check 0', 'put 1', 'check 0', 'check 1', 'put 2', 'check 0', 'check 1', 'check 2', 'put 3', 'check 0', 'check 1', 'check 2', 'check 3', 'get 3', ] def test_immediate_put_request(env): """Put requests that can be fulfilled immediately do not enter the put queue.""" resource = simpy.Resource(env, capacity=1) assert len(resource.users) == 0 assert len(resource.queue) == 0 # The resource is empty, the first request will succeed immediately without # entering the queue. request = resource.request() assert request.triggered assert len(resource.users) == 1 assert len(resource.queue) == 0 # A second request will get enqueued however. request = resource.request() assert not request.triggered assert len(resource.users) == 1 assert len(resource.queue) == 1 def test_immediate_get_request(env): """Get requests that can be fulfilled immediately do not enter the get queue.""" container = simpy.Container(env) # Put something in the container, this request is triggered immediately # without entering the queue. request = container.put(1) assert request.triggered assert container.level == 1 assert len(container.put_queue) == 0 # The first get request will succeed immediately without entering the # queue. request = container.get(1) assert request.triggered assert container.level == 0 assert len(container.get_queue) == 0 # A second get request will get enqueued. request = container.get(1) assert not request.triggered assert len(container.get_queue) == 1 simpy-simpy-a59c5bcfade4/tests/test_rt.py0000644000000000000000000000566113323151071016745 0ustar 00000000000000""" Tests for SimPy's real-time behavior. """ import time try: # Python >= 3.3 from time import monotonic except ImportError: # Python < 3.3 from time import time as monotonic import pytest from simpy.rt import RealtimeEnvironment def process(env, log, sleep, timeout=1): """Test process.""" while True: time.sleep(sleep) yield env.timeout(timeout) log.append(env.now) def check_duration(real, expected): return expected <= real < (expected + 0.02) @pytest.mark.parametrize('factor', [0.1, 0.05, 0.15]) def test_rt(log, factor): """Basic tests for run().""" start = monotonic() env = RealtimeEnvironment(factor=factor) env.process(process(env, log, 0.01, 1)) env.process(process(env, log, 0.02, 1)) env.run(2) duration = monotonic() - start assert check_duration(duration, 2 * factor) assert log == [1, 1] def test_rt_multiple_call(log): """Test multiple calls to run().""" start = monotonic() env = RealtimeEnvironment(factor=0.05) env.process(process(env, log, 0.01, 2)) env.process(process(env, log, 0.01, 3)) env.run(5) duration = monotonic() - start # assert almost_equal(duration, 0.2) assert check_duration(duration, 5 * 0.05) assert log == [2, 3, 4] env.run(12) duration = monotonic() - start assert check_duration(duration, 12 * 0.05) assert log == [2, 3, 4, 6, 6, 8, 9, 10] def test_rt_slow_sim_default_behavior(log): """By default, SimPy should raise an error if a simulation is too slow for the selected real-time factor.""" env = RealtimeEnvironment(factor=0.05) env.process(process(env, log, 0.1, 1)) err = pytest.raises(RuntimeError, env.run, 3) assert 'Simulation too slow for real time' in str(err.value) assert log == [] def test_rt_slow_sim_no_error(log): """Test ignoring slow simulations.""" start = monotonic() env = RealtimeEnvironment(factor=0.05, strict=False) env.process(process(env, log, 0.1, 1)) env.run(2) duration = monotonic() - start assert check_duration(duration, 2 * 0.1) assert log == [1] def test_rt_illegal_until(): """Test illegal value for *until*.""" env = RealtimeEnvironment() err = pytest.raises(ValueError, env.run, -1) assert str(err.value) == ('until(=-1.0) should be > the current ' 'simulation time.') def test_rt_sync(log): """Test resetting the internal wall-clock reference time.""" env = RealtimeEnvironment(factor=0.05) env.process(process(env, log, 0.01)) time.sleep(0.06) # Simulate massiv workload :-) env.sync() env.run(3) def test_run_with_untriggered_event(env): env = RealtimeEnvironment(factor=0.05) excinfo = pytest.raises(RuntimeError, env.run, until=env.event()) assert str(excinfo.value).startswith('No scheduled events left but "until"' ' event was not triggered:') simpy-simpy-a59c5bcfade4/tests/test_timeout.py0000644000000000000000000000333413323151071020001 0ustar 00000000000000""" Tests for ``simpy.events.Timeout``. """ # Pytest gets the parameters "env" and "log" from the *conftest.py* file import pytest def test_discrete_time_steps(env, log): """envple envulation with discrete time steps.""" def pem(env, log): while True: log.append(env.now) yield env.timeout(delay=1) env.process(pem(env, log)) env.run(until=3) assert log == [0, 1, 2] def test_negative_timeout(env): """Don't allow negative timeout times.""" def pem(env): yield env.timeout(-1) env.process(pem(env)) pytest.raises(ValueError, env.run) def test_timeout_value(env): """You can pass an additional *value* to *timeout* which will be directly yielded back into the PEM. This is useful to implement some kinds of resources or other additions. See :class:`envpy.resources.Store` for an example. """ def pem(env): val = yield env.timeout(1, 'ohai') assert val == 'ohai' env.process(pem(env)) env.run() def test_shared_timeout(env, log): def child(env, timeout, id, log): yield timeout log.append((id, env.now)) timeout = env.timeout(1) for i in range(3): env.process(child(env, timeout, i, log)) env.run() assert log == [(0, 1), (1, 1), (2, 1)] def test_triggered_timeout(env): def process(env): def child(env, event): value = yield event env.exit(value) event = env.timeout(1, 'i was already done') # Start the child after the timeout has already happened. yield env.timeout(2) value = yield env.process(child(env, event)) assert value == 'i was already done' env.run(env.process(process(env))) simpy-simpy-a59c5bcfade4/tests/test_util.py0000644000000000000000000002137013323151071017270 0ustar 00000000000000""" Tests for the utility functions from :mod:`simpy.util`. """ import pytest from simpy import Interrupt from simpy.events import ConditionValue from simpy.util import start_delayed, subscribe_at def test_start_delayed(env): def pem(env): assert env.now == 5 yield env.timeout(1) start_delayed(env, pem(env), delay=5) env.run() def test_start_delayed_error(env): """Check if delayed() raises an error if you pass a negative dt.""" def pem(env): yield env.timeout(1) pytest.raises(ValueError, start_delayed, env, pem(env), delay=-1) def test_subscribe(env): """Check async. interrupt if a process terminates.""" def child(env): yield env.timeout(3) env.exit('ohai') def parent(env): child_proc = env.process(child(env)) subscribe_at(child_proc) try: yield env.event() except Interrupt as interrupt: assert interrupt.cause[0] is child_proc assert interrupt.cause[1] == 'ohai' assert env.now == 3 env.process(parent(env)) env.run() def test_subscribe_terminated_proc(env): """subscribe() proc should send a singal immediatly if "other" has already terminated. """ def child(env): yield env.timeout(1) def parent(env): child_proc = env.process(child(env)) yield env.timeout(2) pytest.raises(RuntimeError, subscribe_at, child_proc) env.process(parent(env)) env.run() def test_subscribe_with_join(env): """Test that subscribe() works if a process waits for another one.""" def child(env, i): yield env.timeout(i) def parent(env): child_proc1 = env.process(child(env, 1)) child_proc2 = env.process(child(env, 2)) try: subscribe_at(child_proc1) yield child_proc2 except Interrupt as interrupt: assert env.now == 1 assert interrupt.cause[0] is child_proc1 assert child_proc2.is_alive env.process(parent(env)) env.run() def test_subscribe_at_timeout(env): """You should be able to subscribe at arbitrary events.""" def pem(env): to = env.timeout(2) subscribe_at(to) try: yield env.timeout(10) except Interrupt as interrupt: assert interrupt.cause == (to, None) assert env.now == 2 env.process(pem(env)) env.run() def test_subscribe_at_timeout_with_value(env): """An event's value should be accessible via the interrupt cause.""" def pem(env): val = 'ohai' to = env.timeout(2, value=val) subscribe_at(to) try: yield env.timeout(10) except Interrupt as interrupt: assert interrupt.cause == (to, val) assert env.now == 2 env.process(pem(env)) env.run() def test_all_of(env): """Wait for all events to be triggered.""" def parent(env): # Start 10 events. events = [env.timeout(i, value=i) for i in range(10)] results = yield env.all_of(events) assert results == {events[i]: i for i in range(10)} assert env.now == 9 env.process(parent(env)) env.run() def test_all_of_generator(env): """Conditions also work with generators.""" def parent(env): # Start 10 events. events = (env.timeout(i, value=i) for i in range(10)) results = yield env.all_of(events) assert list(results.values()) == [i for i in range(10)] assert env.now == 9 env.process(parent(env)) env.run() def test_wait_for_all_with_errors(env): """On default AllOf should fail immediately if one of its events fails.""" def child_with_error(env, value): yield env.timeout(value) raise RuntimeError('crashing') def parent(env): events = [env.timeout(1, value=1), env.process(child_with_error(env, 2)), env.timeout(3, value=3)] try: condition = env.all_of(events) yield condition assert False, 'There should have been an exception' except RuntimeError as e: assert e.args[0] == 'crashing' # Although the condition has failed, interim values are available. assert condition._events[0].value == 1 assert condition._events[1].value.args[0] == 'crashing' # The last child has not terminated yet. assert not events[2].processed env.process(parent(env)) env.run() def test_all_of_chaining(env): """If a wait_for_all condition A is chained to a wait_for_all condition B, B will be merged into A.""" def parent(env): condition_A = env.all_of([env.timeout(i, value=i) for i in range(2)]) condition_B = env.all_of([env.timeout(i, value=i) for i in range(2)]) condition_A &= condition_B results = yield condition_A assert list(results.values()) == [0, 1, 0, 1] env.process(parent(env)) env.run() def test_all_of_chaining_intermediate_results(env): """If a wait_for_all condition A with intermediate results is merged into another wait_for_all condition B, the results are copied into condition A.""" def parent(env): condition_A = env.all_of([env.timeout(i, value=i) for i in range(2)]) condition_B = env.all_of([env.timeout(i, value=i) for i in range(2)]) yield env.timeout(0) condition = condition_A & condition_B result = ConditionValue() condition._populate_value(result) assert list(result.values()) == [0, 0] results = yield condition assert list(results.values()) == [0, 1, 0, 1] env.process(parent(env)) env.run() def test_all_of_with_triggered_events(env): """Processed events can be added to a condition. Confirm this with all_of.""" def parent(env): events = [env.timeout(0, value='spam'), env.timeout(1, value='eggs')] yield env.timeout(2) values = list((yield env.all_of(events)).values()) assert values == ['spam', 'eggs'] env.process(parent(env)) env.run() def test_any_of(env): """Wait for any event to be triggered.""" def parent(env): # Start 10 events. events = [env.timeout(i, value=i) for i in range(10)] results = yield env.any_of(events) assert results == {events[0]: 0} assert env.now == 0 env.process(parent(env)) env.run() def test_any_of_with_errors(env): """On default any_of should fail if the event has failed too.""" def child_with_error(env, value): yield env.timeout(value) raise RuntimeError('crashing') def parent(env): events = [env.process(child_with_error(env, 1)), env.timeout(2, value=2)] try: condition = env.any_of(events) yield condition assert False, 'There should have been an exception' except RuntimeError as e: assert e.args[0] == 'crashing' assert condition._events[0].value.args[0] == 'crashing' # The last event has not terminated yet. assert not events[1].processed env.process(parent(env)) env.run() def test_any_of_chaining(env): """If a any_of condition A is chained to a any_of condition B, B will be merged into A.""" def parent(env): condition_A = env.any_of([env.timeout(2, value='a')]) condition_B = env.any_of([env.timeout(1, value='b')]) condition_A |= condition_B results = yield condition_A assert list(results.values()) == ['b'] env.process(parent(env)) env.run() def test_any_of_with_triggered_events(env): """Processed events can be added to a condition. Confirm this with all_of.""" def parent(env): events = [env.timeout(0, value='spam'), env.timeout(1, value='eggs')] yield env.timeout(2) values = list((yield env.any_of(events)).values()) assert values == ['spam', 'eggs'] env.process(parent(env)) env.run() def test_empty_any_of(env): """AnyOf will triggered immediately if there are no events.""" def parent(env): results = yield env.any_of([]) assert results == {} env.process(parent(env)) env.run() def test_empty_all_of(env): """AllOf will triggered immediately if there are no events.""" def parent(env): results = yield env.all_of([]) assert results == {} env.process(parent(env)) env.run() def test_all_of_expansion(env): """The result of AllOf is an OrderedDict, which allows to expand its values directly into variables.""" def p(env): timeouts = [env.timeout(d, d) for d in [3, 2, 1]] a, b, c = (yield env.all_of(timeouts)).values() assert a == 3 and b == 2 and c == 1 env.process(p(env)) env.run() simpy-simpy-a59c5bcfade4/tox.ini0000644000000000000000000000065113323151071015052 0ustar 00000000000000[tox] envlist = py27,py33,py34,py35,py36,py37,pypy,pypy3,docs,flake8 [testenv] deps = pytest commands = py.test --doctest-glob="" tests {posargs} [testenv:docs] commands = py.test --doctest-glob="*.rst" --doctest-glob="README.txt" README.txt docs/ [testenv:benchmark] deps = pytest-benchmark commands = py.test -m benchmark [testenv:flake8] skip_install = True deps = flake8 commands= flake8